import { and, eq } from 'drizzle-orm'; import { db } from '@/lib/db'; import { user, userProfiles, userPortRoles, roles } from '@/lib/db/schema'; import { auth } from '@/lib/auth'; import { createAuditLog } from '@/lib/audit'; import { ConflictError, NotFoundError, ValidationError } from '@/lib/errors'; import { emitToRoom } from '@/lib/socket/server'; import type { CreateUserInput, UpdateUserInput } from '@/lib/validators/users'; interface AuditMeta { userId: string; portId: string; ipAddress: string; userAgent: string; } export async function listUsers(portId: string) { const rows = await db .select({ userId: userPortRoles.userId, displayName: userProfiles.displayName, email: user.email, phone: userProfiles.phone, isActive: userProfiles.isActive, isSuperAdmin: userProfiles.isSuperAdmin, lastLoginAt: userProfiles.lastLoginAt, roleId: roles.id, roleName: roles.name, assignedAt: userPortRoles.createdAt, }) .from(userPortRoles) .innerJoin(userProfiles, eq(userPortRoles.userId, userProfiles.userId)) .innerJoin(user, eq(userPortRoles.userId, user.id)) .innerJoin(roles, eq(userPortRoles.roleId, roles.id)) .where(eq(userPortRoles.portId, portId)) .orderBy(userProfiles.displayName); return rows.map((row) => ({ userId: row.userId, displayName: row.displayName, email: row.email, phone: row.phone, isActive: row.isActive, isSuperAdmin: row.isSuperAdmin, lastLoginAt: row.lastLoginAt, role: { id: row.roleId, name: row.roleName }, assignedAt: row.assignedAt, })); } export async function getUser(userId: string, portId: string) { const profile = await db.query.userProfiles.findFirst({ where: eq(userProfiles.userId, userId), }); if (!profile) throw new NotFoundError('User'); const authUser = await db.query.user.findFirst({ where: eq(user.id, userId), }); const portRole = await db.query.userPortRoles.findFirst({ where: and(eq(userPortRoles.userId, userId), eq(userPortRoles.portId, portId)), with: { role: true }, }); if (!portRole) throw new NotFoundError('User not assigned to this port'); return { userId: profile.userId, displayName: profile.displayName, email: authUser?.email ?? '', phone: profile.phone, isActive: profile.isActive, isSuperAdmin: profile.isSuperAdmin, lastLoginAt: profile.lastLoginAt, avatarUrl: profile.avatarUrl, preferences: profile.preferences, role: { id: portRole.role.id, name: portRole.role.name }, createdAt: profile.createdAt, }; } export async function createUser(portId: string, data: CreateUserInput, meta: AuditMeta) { // Check email uniqueness const existingUser = await db.query.user.findFirst({ where: eq(user.email, data.email.toLowerCase()), }); if (existingUser) { throw new ConflictError('A user with this email already exists'); } // Validate role exists const role = await db.query.roles.findFirst({ where: eq(roles.id, data.roleId), }); if (!role) throw new ValidationError('Invalid role ID'); // Create Better Auth user const authResult = await auth.api.signUpEmail({ body: { email: data.email.toLowerCase(), password: data.password, name: data.name, }, }); const newUserId = authResult.user.id; // Create CRM profile await db.insert(userProfiles).values({ userId: newUserId, displayName: data.displayName, phone: data.phone ?? null, }); // Assign to port with role await db.insert(userPortRoles).values({ userId: newUserId, portId, roleId: data.roleId, assignedBy: meta.userId, }); void createAuditLog({ userId: meta.userId, portId, action: 'create', entityType: 'user', entityId: newUserId, newValue: { email: data.email, displayName: data.displayName, role: role.name }, ipAddress: meta.ipAddress, userAgent: meta.userAgent, }); emitToRoom(`port:${portId}`, 'system:alert', { alertType: 'user:created', message: `User "${data.displayName}" added`, severity: 'info', }); return getUser(newUserId, portId); } export async function updateUser( userId: string, portId: string, data: UpdateUserInput, meta: AuditMeta, ) { const profile = await db.query.userProfiles.findFirst({ where: eq(userProfiles.userId, userId), }); if (!profile) throw new NotFoundError('User'); const portRole = await db.query.userPortRoles.findFirst({ where: and(eq(userPortRoles.userId, userId), eq(userPortRoles.portId, portId)), }); if (!portRole) throw new NotFoundError('User not assigned to this port'); // Update profile fields const profileUpdates: Record = { updatedAt: new Date() }; if (data.displayName !== undefined) profileUpdates.displayName = data.displayName; if (data.phone !== undefined) profileUpdates.phone = data.phone; if (data.isActive !== undefined) profileUpdates.isActive = data.isActive; if (Object.keys(profileUpdates).length > 1) { await db.update(userProfiles).set(profileUpdates).where(eq(userProfiles.userId, userId)); } // Update role assignment if (data.roleId && data.roleId !== portRole.roleId) { const newRole = await db.query.roles.findFirst({ where: eq(roles.id, data.roleId), }); if (!newRole) throw new ValidationError('Invalid role ID'); await db .update(userPortRoles) .set({ roleId: data.roleId, assignedBy: meta.userId }) .where(and(eq(userPortRoles.userId, userId), eq(userPortRoles.portId, portId))); } void createAuditLog({ userId: meta.userId, portId, action: 'update', entityType: 'user', entityId: userId, oldValue: { displayName: profile.displayName, isActive: profile.isActive, roleId: portRole.roleId, }, newValue: data, ipAddress: meta.ipAddress, userAgent: meta.userAgent, }); emitToRoom(`port:${portId}`, 'system:alert', { alertType: 'user:updated', message: `User "${data.displayName ?? profile.displayName}" updated`, severity: 'info', }); return getUser(userId, portId); } export async function removeUserFromPort(userId: string, portId: string, meta: AuditMeta) { const portRole = await db.query.userPortRoles.findFirst({ where: and(eq(userPortRoles.userId, userId), eq(userPortRoles.portId, portId)), }); if (!portRole) throw new NotFoundError('User not assigned to this port'); // Prevent removing yourself if (userId === meta.userId) { throw new ValidationError('Cannot remove yourself from the port'); } await db .delete(userPortRoles) .where(and(eq(userPortRoles.userId, userId), eq(userPortRoles.portId, portId))); const profile = await db.query.userProfiles.findFirst({ where: eq(userProfiles.userId, userId), }); void createAuditLog({ userId: meta.userId, portId, action: 'delete', entityType: 'user', entityId: userId, oldValue: { displayName: profile?.displayName, roleId: portRole.roleId }, ipAddress: meta.ipAddress, userAgent: meta.userAgent, }); emitToRoom(`port:${portId}`, 'system:alert', { alertType: 'user:removed', message: `User "${profile?.displayName}" removed from port`, severity: 'info', }); }