make auth working

This commit is contained in:
Wuesteon 2025-11-26 01:30:51 +01:00
parent 28d167a978
commit 7a1f1e9aef
10 changed files with 302 additions and 52 deletions

View file

@ -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

View file

@ -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"]

View file

@ -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!

View file

@ -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

View file

@ -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",

View file

@ -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<string>('jwt.privateKey');
const privateKeyRaw = this.configService.get<string>('jwt.privateKey');
if (!privateKeyRaw) {
throw new Error('JWT private key not configured');
}
const privateKey: string = privateKeyRaw;
const accessTokenExpiry = this.configService.get<string>('jwt.accessTokenExpiry') || '15m';
const refreshTokenExpiry = this.configService.get<string>('jwt.refreshTokenExpiry') || '7d';
const issuer = this.configService.get<string>('jwt.issuer');
const audience = this.configService.get<string>('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<string, unknown> = {
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<string>('jwt.publicKey');
if (!publicKey) {
throw new Error('JWT public key not configured');
}
const audience = this.configService.get<string>('jwt.audience');
const issuer = this.configService.get<string>('jwt.issuer');

View file

@ -16,6 +16,9 @@ export class JwtAuthGuard implements CanActivate {
try {
const publicKey = this.configService.get<string>('jwt.publicKey');
if (!publicKey) {
throw new UnauthorizedException('JWT configuration error');
}
const audience = this.configService.get<string>('jwt.audience');
const issuer = this.configService.get<string>('jwt.issuer');

View file

@ -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;

View file

@ -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() {

View file

@ -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/**/*"],