import { BadRequestException, ConflictException, Injectable, Logger, NotFoundException, } from "@nestjs/common"; import { Prisma, WorkspaceMemberRole } from "@prisma/client"; import { randomUUID } from "node:crypto"; import { PrismaService } from "../prisma/prisma.service"; import type { InviteUserDto } from "./dto/invite-user.dto"; import type { UpdateUserDto } from "./dto/update-user.dto"; import type { CreateWorkspaceDto } from "./dto/create-workspace.dto"; import type { AdminUserResponse, AdminWorkspaceResponse, InvitationResponse, PaginatedResponse, } from "./types/admin.types"; @Injectable() export class AdminService { private readonly logger = new Logger(AdminService.name); constructor(private readonly prisma: PrismaService) {} async listUsers(page = 1, limit = 50): Promise> { const skip = (page - 1) * limit; const [users, total] = await Promise.all([ this.prisma.user.findMany({ include: { workspaceMemberships: { include: { workspace: { select: { id: true, name: true } }, }, }, }, orderBy: { createdAt: "desc" }, skip, take: limit, }), this.prisma.user.count(), ]); return { data: users.map((user) => ({ id: user.id, name: user.name, email: user.email, emailVerified: user.emailVerified, image: user.image, createdAt: user.createdAt, deactivatedAt: user.deactivatedAt, isLocalAuth: user.isLocalAuth, invitedAt: user.invitedAt, invitedBy: user.invitedBy, workspaceMemberships: user.workspaceMemberships.map((m) => ({ workspaceId: m.workspaceId, workspaceName: m.workspace.name, role: m.role, joinedAt: m.joinedAt, })), })), meta: { total, page, limit, totalPages: Math.ceil(total / limit), }, }; } async inviteUser(dto: InviteUserDto, inviterId: string): Promise { const existing = await this.prisma.user.findUnique({ where: { email: dto.email }, }); if (existing) { throw new ConflictException(`User with email ${dto.email} already exists`); } if (dto.workspaceId) { const workspace = await this.prisma.workspace.findUnique({ where: { id: dto.workspaceId }, }); if (!workspace) { throw new NotFoundException(`Workspace ${dto.workspaceId} not found`); } } const invitationToken = randomUUID(); const now = new Date(); const user = await this.prisma.$transaction(async (tx) => { const created = await tx.user.create({ data: { email: dto.email, name: dto.name ?? dto.email.split("@")[0] ?? dto.email, emailVerified: false, invitedBy: inviterId, invitationToken, invitedAt: now, }, }); if (dto.workspaceId) { await tx.workspaceMember.create({ data: { workspaceId: dto.workspaceId, userId: created.id, role: dto.role ?? WorkspaceMemberRole.MEMBER, }, }); } return created; }); this.logger.log(`User invited: ${user.email} by ${inviterId}`); return { userId: user.id, invitationToken, email: user.email, invitedAt: now, }; } async updateUser(id: string, dto: UpdateUserDto): Promise { const existing = await this.prisma.user.findUnique({ where: { id } }); if (!existing) { throw new NotFoundException(`User ${id} not found`); } const data: Prisma.UserUpdateInput = {}; if (dto.name !== undefined) { data.name = dto.name; } if (dto.emailVerified !== undefined) { data.emailVerified = dto.emailVerified; } if (dto.preferences !== undefined) { data.preferences = dto.preferences as Prisma.InputJsonValue; } if (dto.deactivatedAt !== undefined) { data.deactivatedAt = dto.deactivatedAt ? new Date(dto.deactivatedAt) : null; } const user = await this.prisma.user.update({ where: { id }, data, include: { workspaceMemberships: { include: { workspace: { select: { id: true, name: true } }, }, }, }, }); this.logger.log(`User updated: ${id}`); return { id: user.id, name: user.name, email: user.email, emailVerified: user.emailVerified, image: user.image, createdAt: user.createdAt, deactivatedAt: user.deactivatedAt, isLocalAuth: user.isLocalAuth, invitedAt: user.invitedAt, invitedBy: user.invitedBy, workspaceMemberships: user.workspaceMemberships.map((m) => ({ workspaceId: m.workspaceId, workspaceName: m.workspace.name, role: m.role, joinedAt: m.joinedAt, })), }; } async deactivateUser(id: string): Promise { const existing = await this.prisma.user.findUnique({ where: { id } }); if (!existing) { throw new NotFoundException(`User ${id} not found`); } if (existing.deactivatedAt) { throw new BadRequestException(`User ${id} is already deactivated`); } const [user] = await this.prisma.$transaction([ this.prisma.user.update({ where: { id }, data: { deactivatedAt: new Date() }, include: { workspaceMemberships: { include: { workspace: { select: { id: true, name: true } }, }, }, }, }), this.prisma.session.deleteMany({ where: { userId: id } }), ]); this.logger.log(`User deactivated and sessions invalidated: ${id}`); return { id: user.id, name: user.name, email: user.email, emailVerified: user.emailVerified, image: user.image, createdAt: user.createdAt, deactivatedAt: user.deactivatedAt, isLocalAuth: user.isLocalAuth, invitedAt: user.invitedAt, invitedBy: user.invitedBy, workspaceMemberships: user.workspaceMemberships.map((m) => ({ workspaceId: m.workspaceId, workspaceName: m.workspace.name, role: m.role, joinedAt: m.joinedAt, })), }; } async createWorkspace(dto: CreateWorkspaceDto): Promise { const owner = await this.prisma.user.findUnique({ where: { id: dto.ownerId } }); if (!owner) { throw new NotFoundException(`User ${dto.ownerId} not found`); } const workspace = await this.prisma.$transaction(async (tx) => { const created = await tx.workspace.create({ data: { name: dto.name, ownerId: dto.ownerId, settings: dto.settings ? (dto.settings as Prisma.InputJsonValue) : {}, }, }); await tx.workspaceMember.create({ data: { workspaceId: created.id, userId: dto.ownerId, role: WorkspaceMemberRole.OWNER, }, }); return created; }); this.logger.log(`Workspace created: ${workspace.id} with owner ${dto.ownerId}`); return { id: workspace.id, name: workspace.name, ownerId: workspace.ownerId, settings: workspace.settings as Record, createdAt: workspace.createdAt, updatedAt: workspace.updatedAt, memberCount: 1, }; } async updateWorkspace( id: string, dto: { name?: string; settings?: Record } ): Promise { const existing = await this.prisma.workspace.findUnique({ where: { id } }); if (!existing) { throw new NotFoundException(`Workspace ${id} not found`); } const data: Prisma.WorkspaceUpdateInput = {}; if (dto.name !== undefined) { data.name = dto.name; } if (dto.settings !== undefined) { data.settings = dto.settings as Prisma.InputJsonValue; } const workspace = await this.prisma.workspace.update({ where: { id }, data, include: { _count: { select: { members: true } }, }, }); this.logger.log(`Workspace updated: ${id}`); return { id: workspace.id, name: workspace.name, ownerId: workspace.ownerId, settings: workspace.settings as Record, createdAt: workspace.createdAt, updatedAt: workspace.updatedAt, memberCount: workspace._count.members, }; } }