import React from 'react'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; 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 } from '../../services/api'; import { useAuth } from '../../auth/AuthProvider'; import { useRbac } from '../../auth/rbac'; import { CreateUserRequest, Role, Tenant } from '../../types'; const createUserSchema = z.object({ tenantId: z.string().min(1, 'Tenant is required'), idNumber: z .string() .min(5, 'Identification Number must be at least 5 characters') .max(30, 'Identification Number is too long') .regex(/^[a-zA-Z0-9_-]+$/, 'ID Number must be alphanumeric (Hex-encodable)'), email: z.string().email('Please enter a valid email address'), userName: z.string().optional(), phoneNumber: z.string().optional(), initialRoleId: z.string().optional(), lockoutEnabled: z.boolean().default(false), twoFactorEnabled: z.boolean().default(false), }); type CreateUserFormValues = z.infer; interface CreateUserModalProps { isOpen: boolean; onClose: () => void; defaultTenantId?: number | string | null; } export const CreateUserModal: React.FC = ({ isOpen, onClose, defaultTenantId, }) => { const { tenantId: userTenantId, canManageTenants } = useAuth(); const { canAdd, canView, isUnrestricted } = useRbac(); const canCreate = canAdd('settings_users') || canAdd('tenant_self_service') || canAdd('tenant_management') || isUnrestricted; const hasTenantSelfHelpAccess = canAdd('tenant_self_service') || canView('tenant_self_service'); const queryClient = useQueryClient(); const { success, error: toastError } = useToast(); const effectiveDefaultTenant = defaultTenantId ? String(defaultTenantId) : userTenantId ? String(userTenantId) : '1'; const { register, handleSubmit, reset, watch, setValue, formState: { errors }, } = useForm({ resolver: zodResolver(createUserSchema), defaultValues: { tenantId: effectiveDefaultTenant, idNumber: '', email: '', userName: '', phoneNumber: '', initialRoleId: '', lockoutEnabled: false, twoFactorEnabled: false, }, }); React.useEffect(() => { if (isOpen) { reset({ tenantId: effectiveDefaultTenant, idNumber: '', email: '', userName: '', phoneNumber: '', initialRoleId: '', lockoutEnabled: false, twoFactorEnabled: false, }); } }, [isOpen, effectiveDefaultTenant, reset]); // System roles for assignment const { data: systemRoles = [], isLoading: loadingRoles } = useQuery({ queryKey: ['system-roles'], queryFn: getSystemRoles, enabled: isOpen, }); // 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'; if (isRoleAdmin && hasTenantSelfHelpAccess) { return false; } return true; }); }, [systemRoles, hasTenantSelfHelpAccess]); // Tenants list for super-admins const { data: tenantsList = [] } = useQuery({ queryKey: ['tenants'], queryFn: getTenants, enabled: isOpen && canManageTenants, }); const createUserMutation = useMutation({ 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')) { throw new Error('Users who have access to tenant self-help are restricted from creating Admin users.'); } } const payload: CreateUserRequest = { email: values.email.trim(), userName: values.userName?.trim() || values.email.split('@')[0], emailConfirmed: true, phoneNumber: values.phoneNumber?.trim() || null, phoneNumberConfirmed: false, LockoutEnabled: values.lockoutEnabled, twoFactorEnabled: values.twoFactorEnabled, accessFailedCount: 0, }; await createUser(values.tenantId, values.idNumber.trim(), payload); // If an initial role was chosen and we have a newly generated user, // invalidate query cache to refresh directory return values; }, onSuccess: (values) => { success('User Created', `User ${values.email} provisioned under Tenant #${values.tenantId}.`); queryClient.invalidateQueries({ queryKey: ['users'] }); queryClient.invalidateQueries({ queryKey: ['users-directory'] }); onClose(); }, onError: (err: any) => { toastError('User Creation Failed', err); }, }); const onSubmit = (data: CreateUserFormValues) => { createUserMutation.mutate(data); }; return (
{/* Tenant Scope Selector */}
{canManageTenants && tenantsList.length > 0 ? ( ) : ( )} {errors.tenantId &&

{errors.tenantId.message}

}
{/* ID Number with Hashids Hex guidance */}

Standard South African National ID (13 digits) or alphanumeric identifier. The client automatically converts and hashes this via Hashids Hex.

{errors.idNumber &&

{errors.idNumber.message}

}
{/* Email & Username */}
{errors.email &&

{errors.email.message}

}
{/* Phone & Initial Role */}
{hasTenantSelfHelpAccess && (

Tenant Self-Help Policy: Admin user creation is restricted.

)}
{/* Security Flags */}
Security & Authentication Flags
{!canCreate && (
User provisioning is restricted by your RBAC claim assignment.
)} {/* Form Actions */}
); };