diff --git a/.env.example b/.env.example index 52e10cb..475dc96 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,27 @@ -PORT=3000 +NODE_ENV=development +PORT=3001 # Supabase (CEO-OS project, Coolify) -DATABASE_URL=postgresql://postgres:CHANGE_ME@supabase-db:5432/postgres -SUPABASE_URL=http://supabasekong-to6mg3yxqit4ac9nl0otnvav.152.53.112.35.sslip.io:8000 +DATABASE_URL= +SUPABASE_URL= +SUPABASE_ANON_KEY= SUPABASE_SERVICE_ROLE_KEY= # Redis (ceo-os-redis, Coolify) -REDIS_URL=redis://default:CHANGE_ME@ceo-os-redis:6379/0 +REDIS_URL= + +APP_ENCRYPTION_KEY= +JWT_SECRET= +COOKIE_SECRET= + +AI_GATEWAY_URL= +AI_GATEWAY_API_KEY= + +N8N_BASE_URL= +N8N_WEBHOOK_SECRET= + +SMTP_HOST= +SMTP_PORT= +SMTP_USER= +SMTP_PASSWORD= +SMTP_FROM= diff --git a/README.md b/README.md index 9270145..c439f86 100644 --- a/README.md +++ b/README.md @@ -10,4 +10,15 @@ cp .env.example .env npm run start:dev ``` -`GET /health` returneaza statusul serviciului. +`GET /health` returneaza statusul serviciului. `GET /docs` expune Swagger UI. + +## Stack + +NestJS, Drizzle ORM (`src/db`), Supabase Admin SDK (`src/supabase`), BullMQ/Redis, class-validator/Zod, Helmet, rate limiting (Throttler), logging Pino. + +## Migratii + +```bash +npm run db:generate +npm run db:migrate +``` diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..eedc2d2 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + schema: './src/db/schema.ts', + out: './drizzle', + dialect: 'postgresql', + dbCredentials: { url: process.env.DATABASE_URL! }, +}); diff --git a/package.json b/package.json index 32e9470..36b4501 100644 --- a/package.json +++ b/package.json @@ -9,19 +9,37 @@ "start": "node dist/main", "start:dev": "nest start --watch", "lint": "eslint \"src/**/*.ts\"", - "test": "jest" + "test": "jest", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate" }, "dependencies": { + "@nestjs/bullmq": "^11.0.0", "@nestjs/common": "^11.0.0", "@nestjs/config": "^4.0.0", "@nestjs/core": "^11.0.0", "@nestjs/platform-express": "^11.0.0", + "@nestjs/swagger": "^8.0.0", + "@nestjs/throttler": "^6.2.0", + "@supabase/supabase-js": "^2.47.0", + "bullmq": "^5.28.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "drizzle-orm": "^0.36.0", + "helmet": "^8.0.0", + "ioredis": "^5.4.1", + "nestjs-pino": "^4.1.0", + "pg": "^8.13.0", + "pino-http": "^10.3.0", "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "zod": "^3.23.8" }, "devDependencies": { "@nestjs/cli": "^11.0.0", "@types/node": "^22.0.0", + "@types/pg": "^8.11.10", + "drizzle-kit": "^0.28.0", "typescript": "^5.6.0" } } diff --git a/src/app.module.ts b/src/app.module.ts index c4818a8..8d18e99 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,9 +1,15 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; +import { ThrottlerModule } from '@nestjs/throttler'; +import { LoggerModule } from 'nestjs-pino'; import { HealthController } from './health/health.controller'; @Module({ - imports: [ConfigModule.forRoot({ isGlobal: true })], + imports: [ + ConfigModule.forRoot({ isGlobal: true }), + LoggerModule.forRoot(), + ThrottlerModule.forRoot([{ ttl: 60000, limit: 100 }]), + ], controllers: [HealthController], }) export class AppModule {} diff --git a/src/db/client.ts b/src/db/client.ts new file mode 100644 index 0000000..1c950c2 --- /dev/null +++ b/src/db/client.ts @@ -0,0 +1,7 @@ +import { drizzle } from 'drizzle-orm/node-postgres'; +import { Pool } from 'pg'; +import * as schema from './schema'; + +const pool = new Pool({ connectionString: process.env.DATABASE_URL }); + +export const db = drizzle(pool, { schema }); diff --git a/src/db/schema.ts b/src/db/schema.ts new file mode 100644 index 0000000..e85c111 --- /dev/null +++ b/src/db/schema.ts @@ -0,0 +1,7 @@ +import { pgTable, uuid, text, timestamp } from 'drizzle-orm/pg-core'; + +export const organizations = pgTable('organizations', { + id: uuid('id').defaultRandom().primaryKey(), + name: text('name').notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), +}); diff --git a/src/main.ts b/src/main.ts index f075bb6..89410b5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,11 +1,24 @@ import 'reflect-metadata'; +import helmet from 'helmet'; import { NestFactory } from '@nestjs/core'; +import { Logger } from 'nestjs-pino'; +import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { AppModule } from './app.module'; async function bootstrap() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule, { bufferLogs: true }); + app.useLogger(app.get(Logger)); + app.use(helmet()); app.enableCors(); - const port = process.env.PORT ?? 3000; + + const config = new DocumentBuilder() + .setTitle('CEO OS API') + .setVersion('0.1.0') + .build(); + const document = SwaggerModule.createDocument(app, config); + SwaggerModule.setup('docs', app, document); + + const port = process.env.PORT ?? 3001; await app.listen(port); } diff --git a/src/supabase/supabase.client.ts b/src/supabase/supabase.client.ts new file mode 100644 index 0000000..1dba456 --- /dev/null +++ b/src/supabase/supabase.client.ts @@ -0,0 +1,6 @@ +import { createClient } from '@supabase/supabase-js'; + +export const supabaseAdmin = createClient( + process.env.SUPABASE_URL ?? '', + process.env.SUPABASE_SERVICE_ROLE_KEY ?? '', +);