Implemented notification strategy

This commit is contained in:
2026-10-22 22:59:33 +02:00
parent 7071c404f0
commit 46c6ddcc80
11 changed files with 784 additions and 71 deletions
@@ -48,7 +48,7 @@ const instrumentSchema = z.object({
name: z.string().min(3, 'Funding instrument title must be at least 3 characters'),
code: z.string().optional(),
description: z.string().min(5, 'Description is required (minimum 5 characters)'),
status: z.coerce.number().default(ActivityStatus.Active),
status: z.coerce.number(),
});
type InstrumentFormValues = z.infer<typeof instrumentSchema>;
@@ -46,8 +46,8 @@ const ruleSchema = z.object({
programmeId: z.coerce.number().min(1, 'Please select an associated programme from the dropdown'),
name: z.string().min(3, 'Rule name must be at least 3 characters'),
description: z.string().min(5, 'Description is required (minimum 5 characters)'),
version: z.coerce.number().min(1, 'Version must be at least 1').default(1),
status: z.coerce.number().default(ActivityStatus.Active),
version: z.coerce.number().min(1, 'Version must be at least 1'),
status: z.coerce.number(),
});
type RuleFormValues = z.infer<typeof ruleSchema>;
@@ -898,7 +898,7 @@ export const ContractsManagement: React.FC<ContractsManagementProps> = ({
className="space-y-4"
>
{/* ProblemDetails Error Alert */}
{createApiError && (
{Boolean(createApiError) && (
<ProblemDetailsAlert
error={createApiError}
onDismiss={() => setCreateApiError(null)}
@@ -1126,7 +1126,7 @@ export const ContractsManagement: React.FC<ContractsManagementProps> = ({
}}
className="space-y-4"
>
{uploadApiError && (
{Boolean(uploadApiError) && (
<ProblemDetailsAlert
error={uploadApiError}
onDismiss={() => setUploadApiError(null)}
@@ -1228,7 +1228,7 @@ export const ContractsManagement: React.FC<ContractsManagementProps> = ({
}}
className="space-y-4"
>
{updateApiError && (
{Boolean(updateApiError) && (
<ProblemDetailsAlert
error={updateApiError}
onDismiss={() => setUpdateApiError(null)}
@@ -83,7 +83,7 @@ export const RiskHeatmapMatrix: React.FC<RiskHeatmapMatrixProps> = ({
const projectMap = useMemo(() => {
const map: Record<string, string> = {};
projects.forEach((p) => {
map[String(p.id)] = p.name;
map[String(p.id)] = p.name || '';
});
return map;
}, [projects]);
@@ -6,10 +6,17 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { UserPlus, Shield, Hash, Mail, Phone, Lock, Building, CheckCircle2 } from 'lucide-react';
import { Modal } from '../ui/Modal';
import { useToast } from '../ui/Toast';
import { createUser, getSystemRoles, assignUserRole, getTenants, getUsers } from '../../services/api';
import {
createUser,
getSystemRoles,
assignUserRole,
getTenants,
getUsers,
notifyNewUserCreated,
} from '../../services/api';
import { useAuth } from '../../auth/AuthProvider';
import { useRbac } from '../../auth/rbac';
import { CreateUserRequest, Role, Tenant } from '../../types';
import { CreateUserRequest, Role, Tenant, User } from '../../types';
const createUserSchema = z.object({
tenantId: z.string().min(1, 'Tenant is required'),
@@ -22,8 +29,8 @@ const createUserSchema = z.object({
userName: z.string().optional(),
phoneNumber: z.string().optional(),
initialRoleId: z.string().optional(),
lockoutEnabled: z.boolean().default(false),
twoFactorEnabled: z.boolean().default(false),
lockoutEnabled: z.boolean(),
twoFactorEnabled: z.boolean(),
});
type CreateUserFormValues = z.infer<typeof createUserSchema>;
@@ -98,7 +105,9 @@ export const CreateUserModal: React.FC<CreateUserModalProps> = ({
// Filter out Admin role if user has access to tenant self-help (Requirement 1)
const assignableRoles = React.useMemo(() => {
return systemRoles.filter((r) => {
const isRoleAdmin = r.name.toLowerCase() === 'admin' || r.id === '1';
const rName = r.name || r.Name || '';
const rId = r.id || r.Id || '';
const isRoleAdmin = rName.toLowerCase() === 'admin' || rId === '1';
if (isRoleAdmin && hasTenantSelfHelpAccess) {
return false;
}
@@ -117,8 +126,10 @@ export const CreateUserModal: React.FC<CreateUserModalProps> = ({
mutationFn: async (values: CreateUserFormValues) => {
// Security enforcement: Users with tenant self-help access cannot create admin users
if (values.initialRoleId && hasTenantSelfHelpAccess) {
const selectedRole = systemRoles.find((r) => r.id === values.initialRoleId);
if (selectedRole && (selectedRole.name.toLowerCase() === 'admin' || selectedRole.id === '1')) {
const selectedRole = systemRoles.find((r) => (r.id || r.Id) === values.initialRoleId);
const selName = selectedRole?.name || selectedRole?.Name || '';
const selId = selectedRole?.id || selectedRole?.Id || '';
if (selectedRole && (selName.toLowerCase() === 'admin' || selId === '1')) {
throw new Error('Users who have access to tenant self-help are restricted from creating Admin users.');
}
}
@@ -141,22 +152,40 @@ export const CreateUserModal: React.FC<CreateUserModalProps> = ({
await createUser(values.tenantId, values.idNumber.trim(), payload);
if (values.initialRoleId) {
try {
await queryClient.invalidateQueries({ queryKey: ['users-directory'] });
await queryClient.invalidateQueries({ queryKey: ['users'] });
const freshUsers = await getUsers(1, 1000, { email: values.email.trim() });
const createdUser = freshUsers.find(
(u) => u.email && u.email.toLowerCase() === values.email.trim().toLowerCase()
);
if (createdUser) {
await assignUserRole(createdUser.id, values.initialRoleId);
}
} catch {
// Graceful fallback
let createdUser: User | undefined;
try {
await queryClient.invalidateQueries({ queryKey: ['users-directory'] });
await queryClient.invalidateQueries({ queryKey: ['users'] });
const freshUsers = await getUsers(1, 1000, { email: values.email.trim() });
createdUser = freshUsers.find(
(u) => u.email && u.email.toLowerCase() === values.email.trim().toLowerCase()
);
if (createdUser && values.initialRoleId) {
await assignUserRole(createdUser.id, values.initialRoleId);
}
} catch {
// Graceful fallback
}
// RULE 3: Issue InApp and Email message directing new user to appropriate portal
const selectedRole = systemRoles.find((r) => (r.id || r.Id) === values.initialRoleId);
const roleName = selectedRole?.name || selectedRole?.Name || '';
const isPartner =
roleName.toLowerCase().trim() === 'partner' ||
String(values.initialRoleId || '').toLowerCase().trim() === 'partner';
const targetUserId = createdUser?.id || values.idNumber.trim();
await notifyNewUserCreated({
userId: targetUserId,
email: rawEmail,
isPartner,
roleName,
tenantId: values.tenantId,
}).catch(console.warn);
queryClient.invalidateQueries({ queryKey: ['inapp-notifications'] });
queryClient.invalidateQueries({ queryKey: ['notifications'] });
return values;
},
onSuccess: (values) => {
@@ -286,11 +315,15 @@ export const CreateUserModal: React.FC<CreateUserModalProps> = ({
className="w-full bg-slate-900 border border-slate-800 rounded-xl px-3 py-2 text-white focus:outline-none focus:border-blue-500 disabled:opacity-50"
>
<option value="">Select Initial Role (Optional)</option>
{assignableRoles.map((r) => (
<option key={r.id} value={r.id}>
{r.name}
</option>
))}
{assignableRoles.map((r) => {
const roleId = r.id || r.Id || '';
const roleLabel = r.name || r.Name || '';
return (
<option key={roleId} value={roleId}>
{roleLabel}
</option>
);
})}
</select>
{hasTenantSelfHelpAccess && (
<p className="text-[10px] text-amber-400 mt-1">