Implemented notification strategy
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user