diff --git a/mana-core-auth/.env.example b/mana-core-auth/.env.example index 0c793c8c4..3a98a8cea 100644 --- a/mana-core-auth/.env.example +++ b/mana-core-auth/.env.example @@ -12,8 +12,9 @@ REDIS_PORT=6379 REDIS_PASSWORD= # JWT Configuration -JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nYOUR_PUBLIC_KEY_HERE\n-----END PUBLIC KEY-----" -JWT_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----\nYOUR_PRIVATE_KEY_HERE\n-----END RSA PRIVATE KEY-----" +JWT_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDGRsOXROB4lprw\n9oXaOIt+cwHe3UxBOoiWiUXcpFuXwb+kBWn/LyjeCIOXtefOwE0S10JEodK+6foe\naqGHanq86qAmmkb4a8sjj5LAxXkHL35sJo8HaYcx5NkJQLxQSRHpTfdfxsKsKwxa\n4R4uqrvToqdo6tl/VMsGDPS8L7KzaiKaSdGugvlVtXWgV1soeXSUPyPwpyAXQg7h\nY4CkTSkJAplrs77RLdj8u6jbHKR3F7QkwiU1JocjhM1GP/suKiqXRu8omLFnu45C\ns09SNSRsOpNY5csrKA4PZ2LCks9VHH7HafFvB+BbRw4+Ssr6myOysAztqi3bZMRW\nLTakWpBbAgMBAAECggEAF5zi0IzaghHxhtkyYfrSRgSynX9+WYBRNu2ch8/SZqAj\neghOXMkZgAPEjtiSMDGqRsr4ReMoYtB2Qea8sOX8kwC1gj4Po1Mhtez0cwexclUf\nebLH3X/y9/1YiZJk5YImOMIuaoC/ELDvFOhIEhJcMbKREbIc+oiMcH6HgN0vViVh\nJptgHTnqnGHNARkEpf+xnxqJJxEgrEMz50b4fApKpoZsWXNnZ3Atc/i2ziGew5z4\npnGJxs9TWSukBZaQvl9iluBBvqmPkCOId+L7CmB44bNURpqQOm8gxEgLcdn06y5j\nIKee3Z4H6OTseFvSIYYqBqCyyyZWHICBZXUCDQKUbQKBgQDnFe+O+pQc5looLFiF\nxuYsfDtJqvoMgQ0BaVAo6wVpPe6w+1NA6ZxghcM0+8zyc70jZvdMXINhdsfWD5Gi\nJ/NEDI8EXJJKMfnFQ7F1Ad5NyTnnn/TsLda4GIGQznPRS6uxUP4ljFtxmU9G8Diz\nUQ47XsLjwzzbTedMTSYoQ46kdwKBgQDbp0dIq047o4A72/BBttKdZbgQmjFmqCXF\n8YRUquIDXh/CJ4OQwOIaOvk2398Rg53c3MsV+XCJaMmWYqnJ4BdITLsqeGKsczoS\nI0DMehDr++aOoX/f29r1c+7J/fV5jtAEUcwIEOR1vyAM+WdiWnnTvdpMPVUDsgaT\ntuH0E8WgPQKBgQCCINci87Z+Q7VXVAmRY7zwJhEY3eArNGzHc6+BKz+D0S1dmll6\nf1LhA9I2PuldSpGiovP1m08cjk/gGipPXyHdGxlaQmravyPA0urWUfQGZ59k8K1y\nZim4x4wGqEuN+4e2tT44lL5VzRhYgSPcznMuOaGTsrjNYiQy0mr/V3O25wKBgHvV\nryaVDaIp553XvXgO7ma2djNF+xv5KHKUWxqwzINBiX4YcOAnHlHTdbUuOcDSByoB\ngK1+16dgYGZccYTSxc2JFOw4usimndKj9WBSYT/p4G4BNuqqNKO1HKbceoxxq20E\nAJd7jpGjkxo9cb/Nammp22yoF0niEDsvG+xTSVOxAoGBAMfxHYCMdPc625upCbqG\nkPSJJGYREKGad80OtXilYXLvBPzV65q32k2YZGjaicPKRAzj72KO4nfIu9SY6bfO\nBvXCtIcvllZQuxyd3Cd8MirujJodKwThLTMd4bAYYMXGz1/W6R6pzunZs5KEpgEr\nczy9Gk9WNp0t8vfzyZZ9aago\n-----END PRIVATE KEY-----\n" + +JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxkbDl0TgeJaa8PaF2jiL\nfnMB3t1MQTqIlolF3KRbl8G/pAVp/y8o3giDl7XnzsBNEtdCRKHSvun6Hmqhh2p6\nvOqgJppG+GvLI4+SwMV5By9+bCaPB2mHMeTZCUC8UEkR6U33X8bCrCsMWuEeLqq7\n06KnaOrZf1TLBgz0vC+ys2oimknRroL5VbV1oFdbKHl0lD8j8KcgF0IO4WOApE0p\nCQKZa7O+0S3Y/Luo2xykdxe0JMIlNSaHI4TNRj/7Lioql0bvKJixZ7uOQrNPUjUk\nbDqTWOXLKygOD2diwpLPVRx+x2nxbwfgW0cOPkrK+psjsrAM7aot22TEVi02pFqQ\nWwIDAQAB\n-----END PUBLIC KEY-----\n" JWT_ACCESS_TOKEN_EXPIRY=15m JWT_REFRESH_TOKEN_EXPIRY=7d JWT_ISSUER=manacore diff --git a/mana-core-auth/Dockerfile b/mana-core-auth/Dockerfile index 8ba95ae18..08ec88080 100644 --- a/mana-core-auth/Dockerfile +++ b/mana-core-auth/Dockerfile @@ -6,18 +6,18 @@ RUN npm install -g pnpm@9.15.0 WORKDIR /app -# Copy package files -COPY package.json pnpm-lock.yaml* pnpm-workspace.yaml* ./ -COPY mana-core-auth/package.json ./mana-core-auth/ +# Copy package files for mana-core-auth only (standalone build) +COPY mana-core-auth/package.json ./ -# Install dependencies -RUN pnpm install --frozen-lockfile +# Install all dependencies (including devDependencies for build) +RUN pnpm install # Copy source code -COPY mana-core-auth ./mana-core-auth +COPY mana-core-auth/src ./src +COPY mana-core-auth/tsconfig*.json ./ +COPY mana-core-auth/nest-cli.json ./ # Build the application -WORKDIR /app/mana-core-auth RUN pnpm build # Production stage @@ -29,18 +29,19 @@ RUN npm install -g pnpm@9.15.0 WORKDIR /app # Copy package files -COPY --from=builder /app/package.json /app/pnpm-lock.yaml* /app/pnpm-workspace.yaml* ./ -COPY --from=builder /app/mana-core-auth/package.json ./mana-core-auth/ +COPY --from=builder /app/package.json ./ -# Install production dependencies only -RUN pnpm install --prod --frozen-lockfile +# Install production dependencies + tsx for migrations +RUN pnpm install --prod && pnpm add tsx # Copy built application -COPY --from=builder /app/mana-core-auth/dist ./mana-core-auth/dist -COPY --from=builder /app/mana-core-auth/src/db ./mana-core-auth/src/db +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/src/db ./src/db +COPY mana-core-auth/drizzle.config.ts ./ +COPY mana-core-auth/docker-entrypoint.sh ./ -# Set working directory to the app -WORKDIR /app/mana-core-auth +# Make entrypoint executable +RUN chmod +x ./docker-entrypoint.sh # Create non-root user RUN addgroup -g 1001 -S nodejs && \ @@ -59,5 +60,5 @@ EXPOSE 3001 HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \ CMD node -e "require('http').get('http://localhost:3001/api/v1/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" -# Start the application -CMD ["node", "dist/main.js"] +# Start the application with entrypoint that runs migrations +ENTRYPOINT ["./docker-entrypoint.sh"] diff --git a/mana-core-auth/MIGRATIONS.md b/mana-core-auth/MIGRATIONS.md new file mode 100644 index 000000000..a412b6116 --- /dev/null +++ b/mana-core-auth/MIGRATIONS.md @@ -0,0 +1,218 @@ +# Database Migrations - Mana Core Auth + +## Overview + +This project uses **Drizzle ORM** for database schema management with automatic migration support in Docker. + +## Automatic Migration System + +### 🐳 Docker (Production) + +When you run `docker-compose up`, migrations are **automatically applied** before the service starts: + +1. The `docker-entrypoint.sh` script runs `pnpm db:push --force` +2. This syncs the Drizzle schema to PostgreSQL +3. The application starts only after migrations succeed + +**No manual intervention needed!** + +### 💻 Local Development + +For local development, you have two options: + +#### Option 1: Automatic Schema Sync (Recommended) +```bash +# Sync schema to database (creates/updates tables) +pnpm db:push +``` + +This is the **fastest** way during development. It pushes your schema changes directly to the database without generating migration files. + +#### Option 2: Generated Migrations (Production-style) +```bash +# 1. Generate migration files from schema changes +pnpm migration:generate + +# 2. Apply migrations to database +pnpm migration:run +``` + +Use this approach when you want explicit migration files for version control. + +## Commands Reference + +| Command | Description | +|---------|-------------| +| `pnpm db:push` | Sync schema to database (no migration files) | +| `pnpm db:studio` | Open Drizzle Studio to view/edit data | +| `pnpm migration:generate` | Generate migration files from schema | +| `pnpm migration:run` | Apply pending migrations | + +## How It Works + +### Schema Location +All database tables are defined in TypeScript: +``` +src/db/schema/ +├── auth.schema.ts # Users, sessions, passwords, etc. +├── credits.schema.ts # Credit system tables +└── index.ts # Export all schemas +``` + +### Migration Flow + +```mermaid +graph LR + A[Edit Schema] --> B{Environment?} + B -->|Development| C[pnpm db:push] + B -->|Production| D[pnpm migration:generate] + D --> E[pnpm migration:run] + C --> F[Tables Updated] + E --> F +``` + +### Docker Entrypoint Script + +The `docker-entrypoint.sh` script ensures migrations run before the app starts: + +```bash +#!/bin/sh +set -e + +echo "🔄 Running database migrations..." +pnpm db:push --force +echo "✅ Migrations complete" + +echo "🚀 Starting Mana Core Auth..." +exec node dist/main.js +``` + +## First-Time Setup + +When starting fresh: + +1. **Start PostgreSQL**: + ```bash + docker compose up postgres -d + ``` + +2. **Apply Schema**: + ```bash + pnpm db:push + ``` + +3. **Start Service**: + ```bash + pnpm start:dev + ``` + +## Production Deployment + +When deploying with Docker Compose: + +```bash +# Migrations run automatically on container startup +docker compose up -d mana-core-auth +``` + +The service will: +1. Wait for PostgreSQL to be healthy (`depends_on`) +2. Run migrations via entrypoint script +3. Start the NestJS application + +## Troubleshooting + +### "relation does not exist" +**Problem**: Schema not synced to database + +**Solution**: +```bash +pnpm db:push +``` + +### "schema already exists" +**Problem**: Partial migration state + +**Solution**: +```bash +# Option 1: Force push +pnpm db:push --force + +# Option 2: Reset database (⚠️ deletes all data) +docker compose down -v +docker compose up postgres -d +pnpm db:push +``` + +### Migration fails in Docker +**Problem**: Database credentials or connection + +**Solution**: +Check `docker-compose.yml` environment variables: +- `DATABASE_URL` +- `POSTGRES_PASSWORD` + +## Best Practices + +### Development +- ✅ Use `pnpm db:push` for fast iteration +- ✅ Use Drizzle Studio to inspect data: `pnpm db:studio` +- ❌ Don't commit generated migration files during active development + +### Production +- ✅ Let Docker handle migrations automatically +- ✅ Monitor container logs for migration success +- ✅ Ensure DATABASE_URL is correct in environment + +### Schema Changes +- ✅ Make schema changes in `src/db/schema/*.ts` +- ✅ Test locally with `pnpm db:push` +- ✅ Commit schema changes to git +- ✅ Docker will auto-apply on deployment + +## Migration Strategy + +This project uses **"push-based migrations"** rather than explicit migration files: + +| Approach | When to Use | +|----------|-------------| +| **Push (`db:push`)** | Development, Docker, quick iteration | +| **Generated Migrations** | When you need explicit SQL files, audit trail | + +The push-based approach is **simpler** and **faster** for most use cases, which is why it's used in the Docker entrypoint. + +## Environment Variables + +Required for migrations: + +```env +DATABASE_URL=postgresql://user:password@host:5432/dbname +``` + +In Docker Compose, this is auto-configured: +```yaml +DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@pgbouncer:6432/${POSTGRES_DB} +``` + +## Health Checks + +The service won't start until: +1. ✅ PostgreSQL is healthy +2. ✅ Migrations complete successfully +3. ✅ Application boots without errors + +Check container logs: +```bash +docker logs manacore-auth +``` + +Look for: +``` +🔄 Running database migrations... +✅ Migrations complete +🚀 Starting Mana Core Auth... +``` + +--- + +**Status**: ✅ Automatic migrations configured and ready to use! diff --git a/mana-core-auth/docker-entrypoint.sh b/mana-core-auth/docker-entrypoint.sh new file mode 100755 index 000000000..844528896 --- /dev/null +++ b/mana-core-auth/docker-entrypoint.sh @@ -0,0 +1,13 @@ +#!/bin/sh +set -e + +echo "🔄 Running database migrations..." + +# Run actual migrations (creates schemas + tables) +pnpm migration:run + +echo "✅ Migrations complete" + +# Start the application +echo "🚀 Starting Mana Core Auth..." +exec node dist/main.js diff --git a/mana-core-auth/package.json b/mana-core-auth/package.json index 936eda023..10c537aa9 100644 --- a/mana-core-auth/package.json +++ b/mana-core-auth/package.json @@ -22,38 +22,39 @@ }, "dependencies": { "@nestjs/common": "^10.4.15", + "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.4.15", "@nestjs/platform-express": "^10.4.15", - "@nestjs/config": "^3.3.0", "@nestjs/throttler": "^6.2.1", - "better-auth": "^1.1.1", - "drizzle-orm": "^0.38.3", - "drizzle-kit": "^0.30.2", - "postgres": "^3.4.5", - "stripe": "^17.5.0", - "redis": "^4.7.0", "bcrypt": "^5.1.1", - "nanoid": "^5.0.9", - "zod": "^3.24.1", - "class-validator": "^0.14.1", + "better-auth": "^1.1.1", "class-transformer": "^0.5.1", - "jsonwebtoken": "^9.0.2", - "winston": "^3.17.0", - "helmet": "^8.0.0", + "class-validator": "^0.14.1", "cookie-parser": "^1.4.7", + "dotenv": "^16.4.7", + "drizzle-kit": "^0.30.2", + "drizzle-orm": "^0.38.3", + "helmet": "^8.0.0", + "jsonwebtoken": "^9.0.2", + "nanoid": "^5.0.9", + "postgres": "^3.4.5", + "redis": "^4.7.0", "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "stripe": "^17.5.0", + "winston": "^3.17.0", + "zod": "^3.24.1" }, "devDependencies": { "@nestjs/cli": "^11.0.0", "@nestjs/schematics": "^11.0.0", "@nestjs/testing": "^10.4.15", - "@types/express": "^5.0.0", - "@types/node": "^22.10.2", "@types/bcrypt": "^5.0.2", - "@types/jsonwebtoken": "^9.0.7", "@types/cookie-parser": "^1.4.7", + "@types/express": "^5.0.0", "@types/jest": "^29.5.14", + "@types/jsonwebtoken": "^9.0.7", + "@types/node": "^22.10.2", "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^8.18.2", "@typescript-eslint/parser": "^8.18.2", diff --git a/mana-core-auth/src/auth/auth.service.ts b/mana-core-auth/src/auth/auth.service.ts index cedce4e97..6ebd46991 100644 --- a/mana-core-auth/src/auth/auth.service.ts +++ b/mana-core-auth/src/auth/auth.service.ts @@ -1,15 +1,16 @@ import { Injectable, UnauthorizedException, ConflictException, BadRequestException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { eq, and } from 'drizzle-orm'; +import { eq, and, isNull } from 'drizzle-orm'; import * as bcrypt from 'bcrypt'; import * as jwt from 'jsonwebtoken'; import { nanoid } from 'nanoid'; +import { randomUUID } from 'crypto'; import { getDb } from '../db/connection'; import { users, passwords, sessions } from '../db/schema'; import { RegisterDto } from './dto/register.dto'; import { LoginDto } from './dto/login.dto'; -interface TokenPayload { +export interface TokenPayload { sub: string; email: string; role: string; @@ -136,7 +137,7 @@ export class AuthService { const [session] = await db .select() .from(sessions) - .where(and(eq(sessions.refreshToken, refreshToken), eq(sessions.revokedAt, null))) + .where(and(eq(sessions.refreshToken, refreshToken), isNull(sessions.revokedAt))) .limit(1); if (!session) { @@ -166,8 +167,8 @@ export class AuthService { user.id, user.email, user.role, - session.deviceId, - session.deviceName, + session.deviceId ?? undefined, + session.deviceName ?? undefined, ipAddress, userAgent, ); @@ -205,14 +206,18 @@ export class AuthService { ) { const db = this.getDb(); - const privateKey = this.configService.get('jwt.privateKey'); + const privateKeyRaw = this.configService.get('jwt.privateKey'); + if (!privateKeyRaw) { + throw new Error('JWT private key not configured'); + } + const privateKey: string = privateKeyRaw; const accessTokenExpiry = this.configService.get('jwt.accessTokenExpiry') || '15m'; const refreshTokenExpiry = this.configService.get('jwt.refreshTokenExpiry') || '7d'; const issuer = this.configService.get('jwt.issuer'); const audience = this.configService.get('jwt.audience'); - // Generate session ID - const sessionId = nanoid(); + // Generate session ID (must be UUID for database) + const sessionId = randomUUID(); // Create session record const refreshTokenString = nanoid(64); @@ -233,7 +238,7 @@ export class AuthService { }); // Generate JWT payload - const tokenPayload: TokenPayload = { + const tokenPayload: Record = { sub: userId, email, role, @@ -243,10 +248,10 @@ export class AuthService { // Sign access token const accessToken = jwt.sign(tokenPayload, privateKey, { - algorithm: 'RS256', - expiresIn: accessTokenExpiry, - issuer, - audience, + algorithm: 'RS256' as const, + expiresIn: accessTokenExpiry as jwt.SignOptions['expiresIn'], + ...(issuer && { issuer }), + ...(audience && { audience }), }); return { @@ -260,6 +265,9 @@ export class AuthService { async validateToken(token: string) { try { const publicKey = this.configService.get('jwt.publicKey'); + if (!publicKey) { + throw new Error('JWT public key not configured'); + } const audience = this.configService.get('jwt.audience'); const issuer = this.configService.get('jwt.issuer'); diff --git a/mana-core-auth/src/common/guards/jwt-auth.guard.ts b/mana-core-auth/src/common/guards/jwt-auth.guard.ts index bb77e98ee..254d11523 100644 --- a/mana-core-auth/src/common/guards/jwt-auth.guard.ts +++ b/mana-core-auth/src/common/guards/jwt-auth.guard.ts @@ -16,6 +16,9 @@ export class JwtAuthGuard implements CanActivate { try { const publicKey = this.configService.get('jwt.publicKey'); + if (!publicKey) { + throw new UnauthorizedException('JWT configuration error'); + } const audience = this.configService.get('jwt.audience'); const issuer = this.configService.get('jwt.issuer'); diff --git a/mana-core-auth/src/db/migrate.ts b/mana-core-auth/src/db/migrate.ts index 0266848c2..c807d91ba 100644 --- a/mana-core-auth/src/db/migrate.ts +++ b/mana-core-auth/src/db/migrate.ts @@ -1,6 +1,10 @@ +import { config } from 'dotenv'; import { migrate } from 'drizzle-orm/postgres-js/migrator'; import { getDb, closeConnection } from './connection'; +// Load environment variables +config(); + async function runMigrations() { const databaseUrl = process.env.DATABASE_URL; diff --git a/mana-core-auth/src/main.ts b/mana-core-auth/src/main.ts index 69dac5c5b..9424f6663 100644 --- a/mana-core-auth/src/main.ts +++ b/mana-core-auth/src/main.ts @@ -2,7 +2,7 @@ import { NestFactory } from '@nestjs/core'; import { ValidationPipe } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import helmet from 'helmet'; -import * as cookieParser from 'cookie-parser'; +import cookieParser from 'cookie-parser'; import { AppModule } from './app.module'; async function bootstrap() { diff --git a/mana-core-auth/tsconfig.json b/mana-core-auth/tsconfig.json index 258c7b7d6..20f54b655 100644 --- a/mana-core-auth/tsconfig.json +++ b/mana-core-auth/tsconfig.json @@ -9,7 +9,7 @@ "target": "ES2021", "sourceMap": true, "outDir": "./dist", - "baseUrl": "./", + "baseUrl": "../", "incremental": true, "skipLibCheck": true, "strictNullChecks": true, @@ -20,7 +20,8 @@ "esModuleInterop": true, "resolveJsonModule": true, "paths": { - "@/*": ["src/*"] + "@/*": ["mana-core-auth/src/*"], + "@manacore/*": ["packages/*/src"] } }, "include": ["src/**/*"],