chore: add Coolify deployment scaffolding (Dockerfiles, prod compose, git hygiene)
- apps/api/Dockerfile: build NestJS, run prisma migrate deploy on start - apps/web/Dockerfile + nginx.conf: build Vite, serve static, proxy /api -> api - docker-compose.coolify.yml: full prod stack (postgres, redis, minio, keycloak, api, web) - .dockerignore / .gitignore / .gitattributes Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import { AuditAction } from './audit.service';
|
||||
|
||||
export const AUDIT_META_KEY = 'audit_meta';
|
||||
|
||||
export interface AuditMeta {
|
||||
action: AuditAction;
|
||||
entity: string;
|
||||
}
|
||||
|
||||
export const Audit = (action: AuditAction, entity: string) =>
|
||||
SetMetadata(AUDIT_META_KEY, { action, entity } satisfies AuditMeta);
|
||||
@@ -0,0 +1,43 @@
|
||||
import {
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Observable, tap } from 'rxjs';
|
||||
import { AuditService } from './audit.service';
|
||||
import { AUDIT_META_KEY, AuditMeta } from './audit.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class AuditInterceptor implements NestInterceptor {
|
||||
constructor(
|
||||
private readonly auditService: AuditService,
|
||||
private readonly reflector: Reflector,
|
||||
) {}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
||||
const meta = this.reflector.get<AuditMeta>(
|
||||
AUDIT_META_KEY,
|
||||
context.getHandler(),
|
||||
);
|
||||
if (!meta) return next.handle();
|
||||
|
||||
const req = context.switchToHttp().getRequest();
|
||||
const user = req.user as { id: string; role: string } | undefined;
|
||||
|
||||
return next.handle().pipe(
|
||||
tap(() => {
|
||||
if (!user) return;
|
||||
void this.auditService.log({
|
||||
userId: user.id,
|
||||
userRole: user.role,
|
||||
ip: req.ip,
|
||||
action: meta.action,
|
||||
entity: meta.entity,
|
||||
entityId: req.params?.id ?? 'bulk',
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { AuditService } from './audit.service';
|
||||
import { AuditInterceptor } from './audit.interceptor';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [AuditService, AuditInterceptor],
|
||||
exports: [AuditService, AuditInterceptor],
|
||||
})
|
||||
export class AuditModule {}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
export type AuditAction = 'READ' | 'CREATE' | 'UPDATE' | 'DELETE' | 'EXPORT';
|
||||
|
||||
export interface AuditParams {
|
||||
userId: string;
|
||||
userRole: string;
|
||||
ip?: string;
|
||||
action: AuditAction;
|
||||
entity: string;
|
||||
entityId: string;
|
||||
field?: string;
|
||||
oldValue?: string;
|
||||
newValue?: string;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class AuditService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async log(params: AuditParams): Promise<void> {
|
||||
await this.prisma.auditLog.create({ data: params });
|
||||
}
|
||||
|
||||
async logRead(params: Omit<AuditParams, 'action'>): Promise<void> {
|
||||
await this.log({ ...params, action: 'READ' });
|
||||
}
|
||||
|
||||
async logChange(
|
||||
params: Omit<AuditParams, 'action'> & {
|
||||
action: 'CREATE' | 'UPDATE' | 'DELETE';
|
||||
},
|
||||
): Promise<void> {
|
||||
await this.log(params);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
|
||||
export type AppRole =
|
||||
| 'hr_admin'
|
||||
| 'hr_specialist'
|
||||
| 'nursing_director'
|
||||
| 'quality_auditor'
|
||||
| 'manager'
|
||||
| 'medic_familie'
|
||||
| 'employee';
|
||||
|
||||
export const ROLES_KEY = 'roles';
|
||||
export const Roles = (...roles: AppRole[]) => SetMetadata(ROLES_KEY, roles);
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { AppRole, ROLES_KEY } from '../decorators/roles.decorator';
|
||||
|
||||
@Injectable()
|
||||
export class RolesGuard implements CanActivate {
|
||||
constructor(private reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const required = this.reflector.getAllAndOverride<AppRole[]>(ROLES_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
if (!required?.length) return true;
|
||||
|
||||
const { user } = context.switchToHttp().getRequest<{ user: { role: AppRole } }>();
|
||||
return required.includes(user?.role);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { PrismaService } from './prisma.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [PrismaService],
|
||||
exports: [PrismaService],
|
||||
})
|
||||
export class PrismaModule {}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService
|
||||
extends PrismaClient
|
||||
implements OnModuleInit, OnModuleDestroy
|
||||
{
|
||||
async onModuleInit() {
|
||||
await this.$connect();
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
await this.$disconnect();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user