mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat: Enhance TeamComplianceAPI and TeamComplianceService to include user compliance statuses and improve type safety
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import UserMiddleware from "../Middleware/UserAuthorization";
|
||||
import TeamComplianceService, {
|
||||
TeamComplianceStatus,
|
||||
UserComplianceStatus,
|
||||
} from "../Services/TeamComplianceService";
|
||||
import {
|
||||
ExpressRequest,
|
||||
@@ -17,6 +18,7 @@ import Team from "../../Models/DatabaseModels/Team";
|
||||
import TeamService, {
|
||||
Service as TeamServiceType,
|
||||
} from "../Services/TeamService";
|
||||
import ComplianceRuleType from "../../Types/Team/ComplianceRuleType";
|
||||
|
||||
export default class TeamComplianceAPI extends BaseAPI<Team, TeamServiceType> {
|
||||
public constructor() {
|
||||
@@ -58,12 +60,30 @@ export default class TeamComplianceAPI extends BaseAPI<Team, TeamServiceType> {
|
||||
);
|
||||
|
||||
// Convert ObjectIDs to strings for JSON response
|
||||
const responseData = {
|
||||
const responseData: {
|
||||
teamId: string;
|
||||
teamName: string;
|
||||
complianceSettings: Array<{
|
||||
ruleType: ComplianceRuleType;
|
||||
enabled: boolean;
|
||||
}>;
|
||||
userComplianceStatuses: Array<{
|
||||
userId: string;
|
||||
userName: string;
|
||||
userEmail: string;
|
||||
userProfilePictureId: string | undefined;
|
||||
isCompliant: boolean;
|
||||
nonCompliantRules: Array<{
|
||||
ruleType: ComplianceRuleType;
|
||||
reason: string;
|
||||
}>;
|
||||
}>;
|
||||
} = {
|
||||
teamId: complianceStatus.teamId.toString(),
|
||||
teamName: complianceStatus.teamName,
|
||||
complianceSettings: complianceStatus.complianceSettings,
|
||||
userComplianceStatuses: complianceStatus.userComplianceStatuses.map(
|
||||
(user) => {
|
||||
(user: UserComplianceStatus) => {
|
||||
return {
|
||||
userId: user.userId.toString(),
|
||||
userName: user.userName,
|
||||
@@ -76,7 +96,7 @@ export default class TeamComplianceAPI extends BaseAPI<Team, TeamServiceType> {
|
||||
),
|
||||
};
|
||||
|
||||
return Response.sendJsonObjectResponse(req, res, responseData as any);
|
||||
return Response.sendJsonObjectResponse(req, res, responseData);
|
||||
} catch (e) {
|
||||
next(e);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,9 @@ import ComplianceRuleType from "../../Types/Team/ComplianceRuleType";
|
||||
import BadDataException from "../../Types/Exception/BadDataException";
|
||||
import Includes from "../../Types/BaseDatabase/Includes";
|
||||
import { LIMIT_PER_PROJECT } from "../../Types/Database/LimitMax";
|
||||
import Team from "../../Models/DatabaseModels/Team";
|
||||
import IncidentSeverity from "../../Models/DatabaseModels/IncidentSeverity";
|
||||
import AlertSeverity from "../../Models/DatabaseModels/AlertSeverity";
|
||||
|
||||
export interface UserComplianceStatus {
|
||||
userId: ObjectID;
|
||||
@@ -43,7 +46,7 @@ export default class TeamComplianceService {
|
||||
projectId: ObjectID,
|
||||
): Promise<TeamComplianceStatus> {
|
||||
// Get team details
|
||||
const team = await TeamService.findOneById({
|
||||
const team: Partial<Team> | null = await TeamService.findOneById({
|
||||
id: teamId,
|
||||
select: {
|
||||
name: true,
|
||||
@@ -59,7 +62,10 @@ export default class TeamComplianceService {
|
||||
}
|
||||
|
||||
// Get compliance settings for this team
|
||||
const complianceSettings = await TeamComplianceSettingService.findBy({
|
||||
const complianceSettings: Array<{
|
||||
ruleType?: ComplianceRuleType;
|
||||
enabled?: boolean;
|
||||
}> = await TeamComplianceSettingService.findBy({
|
||||
query: {
|
||||
teamId: teamId,
|
||||
projectId: projectId,
|
||||
@@ -76,30 +82,31 @@ export default class TeamComplianceService {
|
||||
});
|
||||
|
||||
// Get team members
|
||||
const teamMembers = await TeamMemberService.findBy({
|
||||
query: {
|
||||
teamId: teamId,
|
||||
projectId: projectId,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
_id: true,
|
||||
},
|
||||
limit: 100,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
const teamMembers: Array<{ userId?: ObjectID; _id?: string }> =
|
||||
await TeamMemberService.findBy({
|
||||
query: {
|
||||
teamId: teamId,
|
||||
projectId: projectId,
|
||||
},
|
||||
select: {
|
||||
userId: true,
|
||||
_id: true,
|
||||
},
|
||||
limit: 100,
|
||||
skip: 0,
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
});
|
||||
|
||||
const userIds = teamMembers
|
||||
.map((member) => {
|
||||
const userIds: Array<ObjectID> = teamMembers
|
||||
.map((member: { userId?: ObjectID; _id?: string }) => {
|
||||
return member.userId!;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
// Get user details
|
||||
const users = await UserService.findBy({
|
||||
const users: any = await UserService.findBy({
|
||||
query: {
|
||||
_id: new Includes(userIds),
|
||||
},
|
||||
@@ -120,15 +127,23 @@ export default class TeamComplianceService {
|
||||
const userComplianceStatuses: Array<UserComplianceStatus> = [];
|
||||
|
||||
for (const user of users) {
|
||||
const complianceStatus = await this.checkUserCompliance(
|
||||
const complianceStatus: {
|
||||
isCompliant: boolean;
|
||||
nonCompliantRules: Array<{
|
||||
ruleType: ComplianceRuleType;
|
||||
reason: string;
|
||||
}>;
|
||||
} = await this.checkUserCompliance(
|
||||
user.id!,
|
||||
projectId,
|
||||
complianceSettings.map((setting) => {
|
||||
return {
|
||||
ruleType: setting.ruleType!,
|
||||
enabled: setting.enabled || false,
|
||||
};
|
||||
}),
|
||||
complianceSettings.map(
|
||||
(setting: { ruleType?: ComplianceRuleType; enabled?: boolean }) => {
|
||||
return {
|
||||
ruleType: setting.ruleType!,
|
||||
enabled: setting.enabled || false,
|
||||
};
|
||||
},
|
||||
),
|
||||
);
|
||||
userComplianceStatuses.push({
|
||||
userId: user.id!,
|
||||
@@ -142,12 +157,14 @@ export default class TeamComplianceService {
|
||||
return {
|
||||
teamId: teamId,
|
||||
teamName: team.name || "Unknown Team",
|
||||
complianceSettings: complianceSettings.map((setting) => {
|
||||
return {
|
||||
ruleType: setting.ruleType!,
|
||||
enabled: setting.enabled || false,
|
||||
};
|
||||
}),
|
||||
complianceSettings: complianceSettings.map(
|
||||
(setting: { ruleType?: ComplianceRuleType; enabled?: boolean }) => {
|
||||
return {
|
||||
ruleType: setting.ruleType!,
|
||||
enabled: setting.enabled || false,
|
||||
};
|
||||
},
|
||||
),
|
||||
userComplianceStatuses,
|
||||
};
|
||||
}
|
||||
@@ -174,11 +191,8 @@ export default class TeamComplianceService {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isCompliant = await this.checkRuleCompliance(
|
||||
userId,
|
||||
projectId,
|
||||
setting.ruleType,
|
||||
);
|
||||
const isCompliant: { compliant: boolean; reason: string } =
|
||||
await this.checkRuleCompliance(userId, projectId, setting.ruleType);
|
||||
|
||||
if (!isCompliant.compliant) {
|
||||
nonCompliantRules.push({
|
||||
@@ -227,7 +241,7 @@ export default class TeamComplianceService {
|
||||
userId: ObjectID,
|
||||
projectId: ObjectID,
|
||||
): Promise<{ compliant: boolean; reason: string }> {
|
||||
const userEmails = await UserEmailService.findBy({
|
||||
const userEmails: Array<{ _id?: string }> = await UserEmailService.findBy({
|
||||
query: {
|
||||
userId: userId,
|
||||
projectId: projectId,
|
||||
@@ -243,7 +257,7 @@ export default class TeamComplianceService {
|
||||
skip: 0,
|
||||
});
|
||||
|
||||
const hasEmail = userEmails.length > 0;
|
||||
const hasEmail: boolean = userEmails.length > 0;
|
||||
return {
|
||||
compliant: hasEmail,
|
||||
reason: hasEmail
|
||||
@@ -256,7 +270,7 @@ export default class TeamComplianceService {
|
||||
userId: ObjectID,
|
||||
projectId: ObjectID,
|
||||
): Promise<{ compliant: boolean; reason: string }> {
|
||||
const userSMS = await UserSmsService.findBy({
|
||||
const userSMS: Array<{ _id?: string }> = await UserSmsService.findBy({
|
||||
query: {
|
||||
userId: userId,
|
||||
projectId: projectId,
|
||||
@@ -272,7 +286,7 @@ export default class TeamComplianceService {
|
||||
skip: 0,
|
||||
});
|
||||
|
||||
const hasSMS = userSMS.length > 0;
|
||||
const hasSMS: boolean = userSMS.length > 0;
|
||||
return {
|
||||
compliant: hasSMS,
|
||||
reason: hasSMS
|
||||
@@ -285,7 +299,7 @@ export default class TeamComplianceService {
|
||||
userId: ObjectID,
|
||||
projectId: ObjectID,
|
||||
): Promise<{ compliant: boolean; reason: string }> {
|
||||
const userCalls = await UserCallService.findBy({
|
||||
const userCalls: Array<{ _id?: string }> = await UserCallService.findBy({
|
||||
query: {
|
||||
userId: userId,
|
||||
projectId: projectId,
|
||||
@@ -301,7 +315,7 @@ export default class TeamComplianceService {
|
||||
skip: 0,
|
||||
});
|
||||
|
||||
const hasCall = userCalls.length > 0;
|
||||
const hasCall: boolean = userCalls.length > 0;
|
||||
return {
|
||||
compliant: hasCall,
|
||||
reason: hasCall
|
||||
@@ -314,7 +328,7 @@ export default class TeamComplianceService {
|
||||
userId: ObjectID,
|
||||
projectId: ObjectID,
|
||||
): Promise<{ compliant: boolean; reason: string }> {
|
||||
const userPush = await UserPushService.findBy({
|
||||
const userPush: Array<{ _id?: string }> = await UserPushService.findBy({
|
||||
query: {
|
||||
userId: userId,
|
||||
projectId: projectId,
|
||||
@@ -330,7 +344,7 @@ export default class TeamComplianceService {
|
||||
skip: 0,
|
||||
});
|
||||
|
||||
const hasPush = userPush.length > 0;
|
||||
const hasPush: boolean = userPush.length > 0;
|
||||
return {
|
||||
compliant: hasPush,
|
||||
reason: hasPush ? "" : "No verified push notification device configured",
|
||||
@@ -342,33 +356,42 @@ export default class TeamComplianceService {
|
||||
projectId: ObjectID,
|
||||
): Promise<{ compliant: boolean; reason: string }> {
|
||||
// Get all incident severities for the project
|
||||
const incidentSeverities = await IncidentSeverityService.findBy({
|
||||
query: {
|
||||
projectId: projectId,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
});
|
||||
const incidentSeverities: Array<Partial<IncidentSeverity>> =
|
||||
await IncidentSeverityService.findBy({
|
||||
query: {
|
||||
projectId: projectId,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
limit: LIMIT_PER_PROJECT,
|
||||
skip: 0,
|
||||
});
|
||||
|
||||
if (incidentSeverities.length === 0) {
|
||||
return { compliant: true, reason: "" }; // No incident severities configured
|
||||
}
|
||||
|
||||
// Check if user has notification rules for all incident severities
|
||||
const severityIds = incidentSeverities.map((severity) => {
|
||||
return severity._id!;
|
||||
});
|
||||
const severityIds: Array<string> = incidentSeverities.map(
|
||||
(severity: Partial<IncidentSeverity>) => {
|
||||
return severity._id!;
|
||||
},
|
||||
);
|
||||
const missingSeverities: Array<string> = [];
|
||||
|
||||
for (const severityId of severityIds) {
|
||||
const notificationRules = await UserNotificationRuleService.findBy({
|
||||
const notificationRules: Array<{
|
||||
_id?: string;
|
||||
userCallId?: ObjectID;
|
||||
userSmsId?: ObjectID;
|
||||
userEmailId?: ObjectID;
|
||||
userPushId?: ObjectID;
|
||||
}> = await UserNotificationRuleService.findBy({
|
||||
query: {
|
||||
userId: userId,
|
||||
projectId: projectId,
|
||||
@@ -389,20 +412,29 @@ export default class TeamComplianceService {
|
||||
});
|
||||
|
||||
// Check if user has at least one notification method configured for this severity
|
||||
const hasNotificationMethod = notificationRules.some((rule) => {
|
||||
return (
|
||||
rule.userCallId ||
|
||||
rule.userSmsId ||
|
||||
rule.userEmailId ||
|
||||
rule.userPushId
|
||||
);
|
||||
});
|
||||
const hasNotificationMethod: boolean = notificationRules.some(
|
||||
(rule: {
|
||||
_id?: string;
|
||||
userCallId?: ObjectID;
|
||||
userSmsId?: ObjectID;
|
||||
userEmailId?: ObjectID;
|
||||
userPushId?: ObjectID;
|
||||
}) => {
|
||||
return (
|
||||
rule.userCallId ||
|
||||
rule.userSmsId ||
|
||||
rule.userEmailId ||
|
||||
rule.userPushId
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (!hasNotificationMethod) {
|
||||
const severity = incidentSeverities.find((s) => {
|
||||
return s._id?.toString() === severityId.toString();
|
||||
});
|
||||
const severityName = severity?.name || severityId.toString();
|
||||
const severity: Partial<IncidentSeverity> | undefined =
|
||||
incidentSeverities.find((s: Partial<IncidentSeverity>) => {
|
||||
return s._id?.toString() === severityId.toString();
|
||||
});
|
||||
const severityName: string = severity?.name || severityId.toString();
|
||||
missingSeverities.push(severityName);
|
||||
}
|
||||
}
|
||||
@@ -425,33 +457,42 @@ export default class TeamComplianceService {
|
||||
projectId: ObjectID,
|
||||
): Promise<{ compliant: boolean; reason: string }> {
|
||||
// Get all alert severities for the project
|
||||
const alertSeverities = await AlertSeverityService.findBy({
|
||||
query: {
|
||||
projectId: projectId,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
limit: 100, // Assuming reasonable limit for severities
|
||||
skip: 0,
|
||||
});
|
||||
const alertSeverities: Array<Partial<AlertSeverity>> =
|
||||
await AlertSeverityService.findBy({
|
||||
query: {
|
||||
projectId: projectId,
|
||||
},
|
||||
select: {
|
||||
_id: true,
|
||||
name: true,
|
||||
},
|
||||
props: {
|
||||
isRoot: true,
|
||||
},
|
||||
limit: 100, // Assuming reasonable limit for severities
|
||||
skip: 0,
|
||||
});
|
||||
|
||||
if (alertSeverities.length === 0) {
|
||||
return { compliant: true, reason: "" }; // No alert severities configured
|
||||
}
|
||||
|
||||
// Check if user has notification rules for all alert severities
|
||||
const severityIds = alertSeverities.map((severity) => {
|
||||
return severity._id!;
|
||||
});
|
||||
const severityIds: Array<string> = alertSeverities.map(
|
||||
(severity: Partial<AlertSeverity>) => {
|
||||
return severity._id!;
|
||||
},
|
||||
);
|
||||
const missingSeverities: Array<string> = [];
|
||||
|
||||
for (const severityId of severityIds) {
|
||||
const notificationRules = await UserNotificationRuleService.findBy({
|
||||
const notificationRules: Array<{
|
||||
_id?: string;
|
||||
userCallId?: ObjectID;
|
||||
userSmsId?: ObjectID;
|
||||
userEmailId?: ObjectID;
|
||||
userPushId?: ObjectID;
|
||||
}> = await UserNotificationRuleService.findBy({
|
||||
query: {
|
||||
userId: userId,
|
||||
projectId: projectId,
|
||||
@@ -472,20 +513,29 @@ export default class TeamComplianceService {
|
||||
});
|
||||
|
||||
// Check if user has at least one notification method configured for this severity
|
||||
const hasNotificationMethod = notificationRules.some((rule) => {
|
||||
return (
|
||||
rule.userCallId ||
|
||||
rule.userSmsId ||
|
||||
rule.userEmailId ||
|
||||
rule.userPushId
|
||||
);
|
||||
});
|
||||
const hasNotificationMethod: boolean = notificationRules.some(
|
||||
(rule: {
|
||||
_id?: string;
|
||||
userCallId?: ObjectID;
|
||||
userSmsId?: ObjectID;
|
||||
userEmailId?: ObjectID;
|
||||
userPushId?: ObjectID;
|
||||
}) => {
|
||||
return (
|
||||
rule.userCallId ||
|
||||
rule.userSmsId ||
|
||||
rule.userEmailId ||
|
||||
rule.userPushId
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (!hasNotificationMethod) {
|
||||
const severity = alertSeverities.find((s) => {
|
||||
return s._id?.toString() === severityId.toString();
|
||||
});
|
||||
const severityName = severity?.name || severityId.toString();
|
||||
const severity: Partial<AlertSeverity> | undefined =
|
||||
alertSeverities.find((s: Partial<AlertSeverity>) => {
|
||||
return s._id?.toString() === severityId.toString();
|
||||
});
|
||||
const severityName: string = severity?.name || severityId.toString();
|
||||
missingSeverities.push(severityName);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,31 +57,34 @@ const TeamComplianceStatusTable: FunctionComponent<ComponentProps> = (
|
||||
fetchComplianceStatus();
|
||||
}, [props.teamId]);
|
||||
|
||||
const fetchComplianceStatus = async (): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const fetchComplianceStatus: () => Promise<void> =
|
||||
async (): Promise<void> => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
const response = await API.get<any>({
|
||||
url: URL.fromString(APP_API_URL.toString()).addRoute(
|
||||
`/team/compliance-status/${props.teamId.toString()}`,
|
||||
),
|
||||
headers: ModelAPI.getCommonHeaders(),
|
||||
});
|
||||
const response: any = await API.get<any>({
|
||||
url: URL.fromString(APP_API_URL.toString()).addRoute(
|
||||
`/team/compliance-status/${props.teamId.toString()}`,
|
||||
),
|
||||
headers: ModelAPI.getCommonHeaders(),
|
||||
});
|
||||
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
throw response;
|
||||
if (response instanceof HTTPErrorResponse) {
|
||||
throw response;
|
||||
}
|
||||
|
||||
setComplianceStatus(response.data as TeamComplianceStatus);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err as any));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
setComplianceStatus(response.data as TeamComplianceStatus);
|
||||
} catch (err) {
|
||||
setError(API.getFriendlyMessage(err as any));
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getRuleTypeLabel = (ruleType: string): string => {
|
||||
const getRuleTypeLabel: (ruleType: string) => string = (
|
||||
ruleType: string,
|
||||
): string => {
|
||||
const labels: Record<string, string> = {
|
||||
HasNotificationEmail: "Email Notification",
|
||||
HasNotificationSMS: "SMS Notification",
|
||||
@@ -130,16 +133,21 @@ const TeamComplianceStatusTable: FunctionComponent<ComponentProps> = (
|
||||
if (item.nonCompliantRules.length > 0) {
|
||||
return (
|
||||
<ul className="text-sm text-gray-900">
|
||||
{item.nonCompliantRules.map((rule, ruleIndex) => {
|
||||
return (
|
||||
<li key={ruleIndex} className="mb-1">
|
||||
<span className="font-medium">
|
||||
{getRuleTypeLabel(rule.ruleType)}:
|
||||
</span>{" "}
|
||||
{rule.reason}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
{item.nonCompliantRules.map(
|
||||
(
|
||||
rule: { ruleType: string; reason: string },
|
||||
ruleIndex: number,
|
||||
) => {
|
||||
return (
|
||||
<li key={ruleIndex} className="mb-1">
|
||||
<span className="font-medium">
|
||||
{getRuleTypeLabel(rule.ruleType)}:
|
||||
</span>{" "}
|
||||
{rule.reason}
|
||||
</li>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user