162 lines
5.2 KiB
TypeScript
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 };
|
|
}
|
|
}
|