import { ConflictException, Injectable, NotFoundException } from '@nestjs/common'; import * as bcrypt from 'bcrypt'; import { randomBytes } from 'node:crypto'; import { PlatformPrismaService } from '../prisma/platform-prisma.service'; import { createMysqlDatabase, databaseForSlug, pushTenantSchema } from '../tenancy/provision-database'; import { publicTenantView } from '../tenancy/public-tenant'; import { TenantConnectionService } from '../tenancy/tenant-connection.service'; import { normalizeSlug } from '../tenancy/tenant-url'; import type { CreateTenantDto } from './dto/create-tenant.dto'; import { buildPlatformOverview } from './platform-overview'; @Injectable() export class PlatformService { constructor( private readonly platform: PlatformPrismaService, private readonly tenants: TenantConnectionService, ) {} list() { return this.platform.tenant.findMany({ orderBy: { createdAt: 'asc' }, select: { id: true, name: true, slug: true, database: true, status: true, createdAt: true }, }); } async overview() { const tenants = await this.platform.tenant.findMany({ select: { status: true, database: true }, }); const users = ( await Promise.all(tenants.map((tenant) => this.tenants.getByDatabase(tenant.database).user.count())) ).reduce((sum, count) => sum + count, 0); return buildPlatformOverview(tenants, users); } async publicBySlug(raw: string) { let slug: string; try { slug = normalizeSlug(raw); } catch { throw new NotFoundException('Este cliente não existe.'); } const tenant = await this.platform.tenant.findUnique({ where: { slug }, select: { name: true, slug: true, status: true }, }); return publicTenantView(tenant); } async create(actorId: string, dto: CreateTenantDto, ip?: string) { const slug = normalizeSlug(dto.slug); const exists = await this.platform.tenant.findUnique({ where: { slug } }); if (exists) { throw new ConflictException('Já existe uma empresa com esse código.'); } const database = databaseForSlug(slug); await createMysqlDatabase(database); await pushTenantSchema(database); const tenant = await this.platform.tenant.create({ data: { name: dto.name.trim(), slug, database }, }); const adminEmail = `admin@${slug}.sinka`; const adminPassword = `Sinka-${randomBytes(3).toString('hex')}`; const db = this.tenants.getByDatabase(database); await db.user.create({ data: { name: 'Administrador', email: adminEmail, passwordHash: await bcrypt.hash(adminPassword, 10), issuedPassword: adminPassword, role: 'ADMIN', }, }); await this.platform.platformAuditLog.create({ data: { userId: actorId, action: 'tenant.create', entity: 'Tenant', entityId: tenant.id, ip, metadata: { slug, database }, }, }); return { ...tenant, access: { email: adminEmail, password: adminPassword, path: `/${slug}/login`, }, }; } async listUsers(actorId: string, tenantId: string, ip?: string) { const tenant = await this.platform.tenant.findUnique({ where: { id: tenantId } }); if (!tenant) { throw new NotFoundException('Cliente não encontrado.'); } const db = this.tenants.getByDatabase(tenant.database); const users = await db.user.findMany({ select: { id: true, name: true, email: true, role: true, issuedPassword: true }, orderBy: { createdAt: 'asc' }, }); await this.platform.platformAuditLog.create({ data: { userId: actorId, action: 'tenant.users', entity: 'Tenant', entityId: tenant.id, ip, metadata: { slug: tenant.slug, count: users.length }, }, }); return { tenant: { id: tenant.id, name: tenant.name, slug: tenant.slug }, users: users.map((user) => ({ id: user.id, name: user.name, email: user.email, role: user.role, password: user.issuedPassword, })), }; } async issuePassword(actorId: string, tenantId: string, userId: string, ip?: string) { const tenant = await this.platform.tenant.findUnique({ where: { id: tenantId } }); if (!tenant) { throw new NotFoundException('Cliente não encontrado.'); } const db = this.tenants.getByDatabase(tenant.database); const user = await db.user.findUnique({ where: { id: userId } }); if (!user) { throw new NotFoundException('Usuário não encontrado.'); } const password = `Sinka-${randomBytes(3).toString('hex')}`; await db.user.update({ where: { id: userId }, data: { passwordHash: await bcrypt.hash(password, 10), issuedPassword: password, }, }); await this.platform.platformAuditLog.create({ data: { userId: actorId, action: 'tenant.user.password', entity: 'User', entityId: userId, ip, metadata: { slug: tenant.slug, email: user.email }, }, }); return { id: user.id, email: user.email, password }; } }