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:
Danil Suhomlinov
2026-06-08 17:42:45 +03:00
commit 33800292aa
186 changed files with 30437 additions and 0 deletions
@@ -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',
});
}),
);
}
}
+10
View File
@@ -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);
+19
View File
@@ -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();
}
}