sinka/api/src/platform/platform.service.ts

162 lines
5.2 KiB
TypeScript

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 };
}
}