feat(phase1): add OIDC provider, MongoDB audit, and Docker infrastructure

- Add oidc-provider v9 for OIDC/OAuth2 authentication
- Add MongoDB client service for audit logging (sso_audit database)
- Add audit logger service with retry queue (up to 3 retries, 60s timeout)
- Add audit repository (insert, findByUserId, findByEventType)
- Add OIDC PostgreSQL adapter (grants storage)
- Add Handlebars views (login, register, consent)
- Add OIDC config service (issuer, TTL, cookie keys)
- Add oidc-grants and clients SQL migrations
- Update docker-compose: add PostgreSQL, MongoDB, rename containers
- Update .env.example: add OIDC, MongoDB, PostgreSQL variables
- Update package.json: add oidc-provider, mongodb, express-handlebars
- Update README with OIDC endpoints and architecture diagram
Co-authored-by: 's avatarCursor <cursoragent@cursor.com>
parent 55c1dc23
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
# Server # Server
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
# NODE_ENV: development (relaxed security), staging, production (strict security)
NODE_ENV=development NODE_ENV=development
PORT=3001 PORT=3001
BACKEND_URL=http://localhost:3001 BACKEND_URL=http://localhost:3001
# FRONTEND_URL: list of allowed origins (comma-separated)
FRONTEND_URL=http://localhost:3000,http://localhost:3001 FRONTEND_URL=http://localhost:3000,http://localhost:3001
PROJECT_NAME=Bekind Backend PROJECT_NAME=SSO VietProDev
PROJECT_VERSION=1.0.0 PROJECT_VERSION=1.0.0
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
# Database (SECRETS - keep in .env) # Database - PostgreSQL (SECRETS - keep in .env)
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
DB_HOST=your_db_host DB_HOST=localhost
DB_PORT=5432 DB_PORT=5432
DB_USER=your_db_user DB_USER=postgres
DB_PASSWORD=your_db_password DB_PASSWORD=postgres
DB_NAME=bekind DB_NAME=sso
DB_POOL_MAX=50 DB_POOL_MAX=50
DB_POOL_MIN=5 DB_POOL_MIN=5
DB_POOL_ACQUIRE=30000 DB_POOL_ACQUIRE=30000
DB_POOL_IDLE=10000 DB_POOL_IDLE=10000
DB_POOL_EVICT=60000 DB_POOL_EVICT=60000
# ─────────────────────────────────────────────
# MongoDB - Audit Logging (SECRETS - keep in .env)
# ─────────────────────────────────────────────
MONGODB_AUDIT_URL=mongodb://localhost:27017
MONGODB_AUDIT_DATABASE=sso_audit
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
# Redis (SECRETS - keep in .env) # Redis (SECRETS - keep in .env)
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
REDIS_ENABLED=false REDIS_ENABLED=true
REDIS_HOST=localhost REDIS_HOST=localhost
REDIS_PORT=6379 REDIS_PORT=6379
REDIS_PASSWORD= REDIS_PASSWORD=
...@@ -54,16 +58,15 @@ REDIS_B_DB=0 ...@@ -54,16 +58,15 @@ REDIS_B_DB=0
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
# Authentication & Security (SECRETS - keep in .env) # Authentication & Security (SECRETS - keep in .env)
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
# JWT secrets: use strong secrets in production (minimum 32 characters)
JWT_SECRET=change_this_to_strong_secret_min_32_chars JWT_SECRET=change_this_to_strong_secret_min_32_chars
JWT_SECRET_PREVIOUS=old_secret_1,old_secret_2 # Comma-separated previous secrets for rotation JWT_SECRET_PREVIOUS=old_secret_1,old_secret_2
JWT_REFRESH_SECRET=change_this_to_strong_refresh_secret_min_32_chars JWT_REFRESH_SECRET=change_this_to_strong_refresh_secret_min_32_chars
JWT_REFRESH_SECRET_PREVIOUS=old_refresh_secret_1,old_refresh_secret_2 # Comma-separated previous refresh secrets for rotation JWT_REFRESH_SECRET_PREVIOUS=old_refresh_secret_1,old_refresh_secret_2
JWT_EXPIRES_IN=1h JWT_EXPIRES_IN=1h
JWT_REFRESH_EXPIRES_IN=7d JWT_REFRESH_EXPIRES_IN=7d
TOKEN_ENCRYPTION_KEY=change_this_to_exactly_32_chars TOKEN_ENCRYPTION_KEY=change_this_to_exactly_32_chars
TOKEN_ENCRYPTION_KEY_PREVIOUS=old_encryption_key_1,old_encryption_key_2 # Comma-separated previous encryption keys for rotation TOKEN_ENCRYPTION_KEY_PREVIOUS=old_encryption_key_1,old_encryption_key_2
DEFAULT_PASSWORD=change_this_default_password_hash DEFAULT_PASSWORD=Vietpro@123
BCRYPT_ROUNDS=12 BCRYPT_ROUNDS=12
PASSWORD_MIN_LENGTH=8 PASSWORD_MIN_LENGTH=8
PASSWORD_MAX_LENGTH=128 PASSWORD_MAX_LENGTH=128
...@@ -71,13 +74,21 @@ INCLUDE_TOKENS_IN_RESPONSE=false ...@@ -71,13 +74,21 @@ INCLUDE_TOKENS_IN_RESPONSE=false
SKIP_SENSITIVE_CONFIRM=false SKIP_SENSITIVE_CONFIRM=false
ENABLE_REGISTER=true ENABLE_REGISTER=true
# ─────────────────────────────────────────────
# OIDC Provider (SECRETS - keep in .env)
# ─────────────────────────────────────────────
OIDC_ISSUER=http://localhost:3001
OIDC_ACCESS_TOKEN_TTL=900
OIDC_REFRESH_TOKEN_TTL=2592000
OIDC_COOKIE_KEYS=change_this_cookie_key_at_least_32_chars,another_cookie_key_for_rotation
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
# Cookie Settings # Cookie Settings
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
COOKIE_DOMAIN=.yourdomain.com COOKIE_DOMAIN=.localhost
COOKIE_CROSS_SITE=false COOKIE_CROSS_SITE=false
FORCE_SECURE_COOKIES=true FORCE_SECURE_COOKIES=false
DEV_SAMESITE_LAX=false DEV_SAMESITE_LAX=true
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
# Upload Settings # Upload Settings
...@@ -91,14 +102,14 @@ STORAGE_FILES_PATH=/storage/uploads/files ...@@ -91,14 +102,14 @@ STORAGE_FILES_PATH=/storage/uploads/files
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
# Storage (MinIO/S3) # Storage (MinIO/S3)
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
STORAGE_PROVIDER=minio STORAGE_PROVIDER=local
PUBLIC_BASE_URL=https://api.yourdomain.com PUBLIC_BASE_URL=http://localhost:3001
LOCAL_STORAGE_ROOT=./storage/uploads LOCAL_STORAGE_ROOT=./storage/uploads
MINIO_ENDPOINT=minio MINIO_ENDPOINT=localhost
MINIO_PORT=9000 MINIO_PORT=9000
MINIO_USE_SSL=true MINIO_USE_SSL=false
MINIO_ACCESS_KEY=your_minio_access_key MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=your_minio_secret_key MINIO_SECRET_KEY=minioadmin123
MINIO_BUCKET=uploads MINIO_BUCKET=uploads
MINIO_REGION=us-east-1 MINIO_REGION=us-east-1
...@@ -107,8 +118,8 @@ MINIO_REGION=us-east-1 ...@@ -107,8 +118,8 @@ MINIO_REGION=us-east-1
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
EMAIL_HOST=smtp.example.com EMAIL_HOST=smtp.example.com
EMAIL_PORT=587 EMAIL_PORT=587
EMAIL_USER=your_email@example.com EMAIL_USER=
EMAIL_PASS=your_email_password EMAIL_PASS=
EMAIL_FROM=noreply@yourdomain.com EMAIL_FROM=noreply@yourdomain.com
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
...@@ -177,7 +188,7 @@ CLAM_SCAN_TIMEOUT_MS=15000 ...@@ -177,7 +188,7 @@ CLAM_SCAN_TIMEOUT_MS=15000
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
LOG_LEVEL=info LOG_LEVEL=info
LOG_URL=./storage/logs LOG_URL=./storage/logs
LOG_MODE=file LOG_MODE=both
LOG_TIMEOUT=5000 LOG_TIMEOUT=5000
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
......
# BeKind Backend # SSO VietProDev Backend
Backend template built with TypeScript, Node.js, Express.js and PostgreSQL. Features auto-routing, Production-ready SSO Authorization Server built with TypeScript, Express.js, and Sequelize — supports **OIDC/OAuth2**, **bcryptjs**, **PostgreSQL**, **MongoDB audit logs**, and **Redis**.
Sequelize ORM, JWT authentication, Server-Sent Events (SSE), and OneSignal push notifications.
**Created by Nguyen Thi Nguyet Que**
## Quick Start ## Quick Start
```bash ```bash
pnpm install pnpm install
cp .env.example .env # fill in DB, Redis, JWT values — see docs/setup.md cp .env.example .env # fill in DB, Redis, JWT, OIDC, MongoDB values
psql -U postgres -c "CREATE DATABASE bekind;" docker compose up -d # start PostgreSQL, MongoDB, Redis, MinIO
pnpm migrate pnpm migrate # run SQL migrations (001-036)
pnpm dev pnpm dev # start development server
```
**Server**: `http://localhost:3001`
**Swagger UI**: `http://localhost:3001/swagger/index`
**OIDC Discovery**: `http://localhost:3001/.well-known/openid-configuration`
---
## Architecture
```
Browser/App
├─ REST API (JWT) ──── Express Backend
│ └─ /api/v1/* (auto-routed controllers)
└─ OIDC Flow ────────── oidc-provider
├─ /.well-known/openid-configuration
├─ /oauth/authorize
├─ /oauth/token
├─ /oauth/userinfo
├─ /oauth/logout
└─ /oidc/interaction/* (login, register, consent)
PostgreSQL ── users, roles, permissions, sessions, clients, oidc_grants
MongoDB ───── audit_logs
Redis ─────── rate limiting, caching, sessions
``` ```
Server: `http://localhost:3001` — Swagger UI: `http://localhost:3001/swagger/index` ## Stack
| Layer | Technology |
|---|---|
| Runtime | Node.js 20 + TypeScript |
| Framework | Express.js + express-automatic-routes |
| ORM | Sequelize (PostgreSQL) |
| Auth | OIDC/OAuth2 (oidc-provider) + JWT |
| Password | bcryptjs |
| Audit | MongoDB |
| Cache | Redis |
| Docs | OpenAPI 3.0 (Zod + zod-to-openapi) |
--- ---
## Important Notes ## Important Notes
⚠️ **Database Schema Changes**: NEVER modify database structure directly (SQL console, GUI tools, **Database Schema Changes**: NEVER modify database structure directly. Always use migration files in `sql/migrations/`.
etc.). Always use migration files in `sql/migrations/`. In production, restrict schema changes via
PostgreSQL permissions:
```sql ```sql
REVOKE CREATE ON SCHEMA public FROM app_user; REVOKE CREATE ON SCHEMA public FROM app_user;
...@@ -32,68 +65,75 @@ REVOKE ALTER ON ALL TABLES IN SCHEMA public FROM app_user; ...@@ -32,68 +65,75 @@ REVOKE ALTER ON ALL TABLES IN SCHEMA public FROM app_user;
--- ---
## Stack
| Layer | Technology |
| --------- | ------------------------------------- |
| Runtime | Node.js 20 + TypeScript |
| Framework | Express.js + express-automatic-routes |
| ORM | Sequelize (PostgreSQL) |
| Cache | Redis |
| Auth | JWT + HttpOnly Cookies / Bearer |
| Docs | OpenAPI 3.0 (Zod + zod-to-openapi) |
---
## Important Commands ## Important Commands
### Development ### Development
| Command | Description | | Command | Description |
| ----------------------------- | -------------------------------------- | |---|---|
| `pnpm dev` / `pnpm start:dev` | Run development server with hot reload | | `pnpm dev` / `pnpm start:dev` | Run development server with hot reload |
| `pnpm build` | Build for production | | `pnpm build` | Build for production |
| `pnpm start` | Run production server | | `pnpm start` | Run production server |
| `pnpm docker:dev` | Run with Docker Compose (full stack) |
### Database ### Database
| Command | Description | | Command | Description |
| -------------- | ----------------------------- | |---|---|
| `pnpm migrate` | Run SQL migrations | | `pnpm migrate` | Run SQL migrations |
| `pnpm gen-db` | Generate models from database | | `pnpm seed` | Seed default data |
| `pnpm db:setup` | Run migrations + seeds |
| `pnpm gen-db` | Generate models from database |
### Quality ### Quality
| Command | Description | | Command | Description |
| ----------------------- | -------------------------------------------------------- | |---|---|
| `pnpm lint` | ESLint check | | `pnpm lint` | ESLint check |
| `pnpm type-check` | TypeScript check | | `pnpm type-check` | TypeScript check |
| `pnpm quality:check` | Full check — lint + type-check + swagger + test coverage | | `pnpm quality:check` | lint + type-check + swagger + coverage |
| `pnpm swagger:validate` | Generate then validate Swagger spec | | `pnpm swagger:validate` | Generate then validate Swagger spec |
### Testing ### Testing
| Command | Description | | Command | Description |
| ----------------------- | --------------------------------------- | |---|---|
| `pnpm test:unit` | Unit tests only | | `pnpm test:unit` | Unit tests only |
| `pnpm test:integration` | Integration tests only | | `pnpm test:integration` | Integration tests only |
| `pnpm test:coverage` | Full suite + coverage report | | `pnpm test:coverage` | Full suite + coverage report |
| `pnpm test:critical` | Critical gate — auth + security (< 30s) | | `pnpm test:critical` | Critical gate — auth + security |
---
## OIDC Endpoints
| Endpoint | Description |
|---|---|
| `GET /.well-known/openid-configuration` | OIDC Discovery document |
| `GET /oauth/authorize` | Authorization endpoint |
| `POST /oauth/token` | Token endpoint |
| `GET /oauth/userinfo` | UserInfo endpoint |
| `GET /oauth/jwks` | JWKS endpoint |
| `POST /oauth/revoke` | Token revocation |
| `POST /oauth/introspect` | Token introspection |
| `GET /oidc/interaction/:uid` | Login page |
| `POST /oidc/interaction/:uid/login` | Submit login |
| `POST /oidc/interaction/:uid/register` | Register account |
| `POST /oidc/interaction/:uid/confirm` | Approve consent |
| `POST /oidc/interaction/:uid/cancel` | Cancel consent |
--- ---
## Docs ## Docs
| Topic | Link | | Topic | Link |
| ------------------- | -------------------------------------------------- | |---|---|
| Setup & Environment | [docs/setup.md](docs/setup.md) | | Setup & Environment | [docs/setup.md](docs/setup.md) |
| New API Development | [docs/api-development.md](docs/api-development.md) | | New API Development | [docs/api-development.md](docs/api-development.md) |
| Error Handling | [docs/error-handling.md](docs/error-handling.md) | | Error Handling | [docs/error-handling.md](docs/error-handling.md) |
| Configuration | [docs/configuration.md](docs/configuration.md) | | Configuration | [docs/configuration.md](docs/configuration.md) |
| Swagger / OpenAPI | [docs/swagger.md](docs/swagger.md) | | Swagger / OpenAPI | [docs/swagger.md](docs/swagger.md) |
| Architecture | [docs/architecture.md](docs/architecture.md) | | Architecture | [docs/architecture.md](docs/architecture.md) |
| Testing | [docs/testing.md](docs/testing.md) | | Testing | [docs/testing.md](docs/testing.md) |
| Conventions | [docs/conventions.md](docs/conventions.md) | | Conventions | [docs/conventions.md](docs/conventions.md) |
| Security | [docs/security.md](docs/security.md) | | Security | [docs/security.md](docs/security.md) |
<!-- Test -->
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
# Infrastructure services (always-on, no profile) # Infrastructure services (always-on, no profile)
# Run independently: docker compose up redis minio -d # Run independently: docker compose up -d
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
services: services:
postgres:
image: postgres:17-alpine
container_name: sso-postgres
restart: unless-stopped
ports:
- '${POSTGRES_PORT:-5432}:5432'
environment:
POSTGRES_USER: ${DB_USER:-postgres}
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
POSTGRES_DB: ${DB_NAME:-sso}
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- sso-network
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U ${DB_USER:-postgres}']
interval: 10s
timeout: 5s
retries: 5
mongo:
image: mongo:7
container_name: sso-mongo
restart: unless-stopped
ports:
- '${MONGO_PORT:-27017}:27017'
environment:
MONGO_INITDB_DATABASE: ${MONGODB_AUDIT_DATABASE:-sso_audit}
volumes:
- mongo_data:/data/db
networks:
- sso-network
healthcheck:
test: ['CMD', 'mongosh', '--eval', 'db.adminCommand({ping:1})']
interval: 10s
timeout: 5s
retries: 5
redis: redis:
image: redis:7-alpine image: redis:7-alpine
container_name: bekind-redis container_name: sso-redis
restart: unless-stopped restart: unless-stopped
ports: ports:
- '${REDIS_PORT:-6379}:6379' - '${REDIS_PORT:-6379}:6379'
...@@ -13,7 +51,7 @@ services: ...@@ -13,7 +51,7 @@ services:
- redis_data:/data - redis_data:/data
command: redis-server --appendonly yes command: redis-server --appendonly yes
networks: networks:
- bekind-network - sso-network
healthcheck: healthcheck:
test: ['CMD', 'redis-cli', 'ping'] test: ['CMD', 'redis-cli', 'ping']
interval: 10s interval: 10s
...@@ -22,7 +60,7 @@ services: ...@@ -22,7 +60,7 @@ services:
minio: minio:
image: minio/minio:RELEASE.2025-04-22T22-12-26Z image: minio/minio:RELEASE.2025-04-22T22-12-26Z
container_name: bekind-minio container_name: sso-minio
restart: unless-stopped restart: unless-stopped
ports: ports:
- '${MINIO_PORT:-9000}:9000' - '${MINIO_PORT:-9000}:9000'
...@@ -34,7 +72,7 @@ services: ...@@ -34,7 +72,7 @@ services:
- minio_data:/data - minio_data:/data
command: server /data --console-address ':9001' command: server /data --console-address ':9001'
networks: networks:
- bekind-network - sso-network
healthcheck: healthcheck:
test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live'] test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live']
interval: 30s interval: 30s
...@@ -53,22 +91,28 @@ services: ...@@ -53,22 +91,28 @@ services:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
target: development target: development
container_name: bekind-dev container_name: sso-dev
restart: unless-stopped restart: unless-stopped
ports: ports:
- '3001:3001' - '3001:3001'
env_file: env_file:
- .env - .env
environment: environment:
- DB_HOST=postgres
- REDIS_HOST=redis - REDIS_HOST=redis
- MINIO_ENDPOINT=minio - MINIO_ENDPOINT=minio
- MONGODB_AUDIT_URL=mongodb://mongo:27017
- CHOKIDAR_USEPOLLING=true - CHOKIDAR_USEPOLLING=true
volumes: volumes:
- .:/usr/src/app - .:/usr/src/app
- /usr/src/app/node_modules - /usr/src/app/node_modules
networks: networks:
- bekind-network - sso-network
depends_on: depends_on:
postgres:
condition: service_healthy
mongo:
condition: service_healthy
redis: redis:
condition: service_healthy condition: service_healthy
minio: minio:
...@@ -92,11 +136,17 @@ services: ...@@ -92,11 +136,17 @@ services:
env_file: env_file:
- .env.staging - .env.staging
environment: environment:
- DB_HOST=postgres
- REDIS_HOST=redis - REDIS_HOST=redis
- MINIO_ENDPOINT=minio - MINIO_ENDPOINT=minio
- MONGODB_AUDIT_URL=mongodb://mongo:27017
networks: networks:
- bekind-network - sso-network
depends_on: depends_on:
postgres:
condition: service_healthy
mongo:
condition: service_healthy
redis: redis:
condition: service_healthy condition: service_healthy
minio: minio:
...@@ -120,11 +170,17 @@ services: ...@@ -120,11 +170,17 @@ services:
env_file: env_file:
- .env.prod - .env.prod
environment: environment:
- DB_HOST=postgres
- REDIS_HOST=redis - REDIS_HOST=redis
- MINIO_ENDPOINT=minio - MINIO_ENDPOINT=minio
- MONGODB_AUDIT_URL=mongodb://mongo:27017
networks: networks:
- bekind-network - sso-network
depends_on: depends_on:
postgres:
condition: service_healthy
mongo:
condition: service_healthy
redis: redis:
condition: service_healthy condition: service_healthy
minio: minio:
...@@ -137,9 +193,11 @@ services: ...@@ -137,9 +193,11 @@ services:
start_period: 40s start_period: 40s
networks: networks:
bekind-network: sso-network:
driver: bridge driver: bridge
volumes: volumes:
postgres_data:
mongo_data:
redis_data: redis_data:
minio_data: minio_data:
{ {
"name": "bekind-backend", "name": "sso-vietprodev-backend",
"version": "1.0.0", "version": "1.0.0",
"description": "A Node.js backend template built with TypeScript, Express, and Sequelize, designed for rapid API development with a focus on best practices, security, and scalability.", "description": "SSO Backend built with TypeScript, Express, and Sequelize — supports OIDC/OAuth2, bcryptjs, PostgreSQL multi-pool, and MongoDB audit logging.",
"main": "dist/index.js", "main": "dist/index.js",
"engines": { "engines": {
"node": ">=20.1.0", "node": ">=20.1.0",
...@@ -80,9 +80,9 @@ ...@@ -80,9 +80,9 @@
"test:integration": "cross-env NODE_ENV=test jest tests/integration", "test:integration": "cross-env NODE_ENV=test jest tests/integration",
"test:critical": "cross-env NODE_ENV=test jest --testPathPattern=\"(auth|virusScan)\" --no-coverage", "test:critical": "cross-env NODE_ENV=test jest --testPathPattern=\"(auth|virusScan)\" --no-coverage",
"-----------------DOCKER------------------": "", "-----------------DOCKER------------------": "",
"docker:build": "docker build -t bekind-backend .", "docker:build": "docker build -t sso-vietprodev-backend .",
"docker:build:dev": "docker build --target development -t bekind-backend:dev .", "docker:build:dev": "docker build --target development -t sso-vietprodev-backend:dev .",
"docker:build:prod": "docker build --target production -t bekind-backend:prod .", "docker:build:prod": "docker build --target production -t sso-vietprodev-backend:prod .",
"docker:infra": "docker compose up redis minio -d", "docker:infra": "docker compose up redis minio -d",
"docker:infra:down": "docker compose down redis minio", "docker:infra:down": "docker compose down redis minio",
"docker:dev": "docker compose --profile dev up --build", "docker:dev": "docker compose --profile dev up --build",
...@@ -108,7 +108,7 @@ ...@@ -108,7 +108,7 @@
"backend", "backend",
"rest" "rest"
], ],
"author": "Nguyen Thi Nguyet Que", "author": "VietProDev Team",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@asteasolutions/zod-to-openapi": "^8.5.0", "@asteasolutions/zod-to-openapi": "^8.5.0",
...@@ -123,14 +123,17 @@ ...@@ -123,14 +123,17 @@
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"express": "^4.22.1", "express": "^4.22.1",
"express-automatic-routes": "^1.1.0", "express-automatic-routes": "^1.1.0",
"express-handlebars": "^8.0.1",
"express-validator": "^7.3.1", "express-validator": "^7.3.1",
"file-type": "^19.6.0", "file-type": "^19.6.0",
"handlebars": "^4.7.9", "handlebars": "^4.7.9",
"helmet": "^8.1.0", "helmet": "^8.1.0",
"ioredis": "^5.4.1", "ioredis": "^5.4.1",
"jsonwebtoken": "^9.0.3", "jsonwebtoken": "^9.0.3",
"mongodb": "^6.16.0",
"module-alias": "^2.2.3", "module-alias": "^2.2.3",
"multer": "^2.0.2", "multer": "^2.0.2",
"oidc-provider": "^9.8.4",
"mustache": "^4.2.0", "mustache": "^4.2.0",
"mv": "^2.1.1", "mv": "^2.1.1",
"node-schedule": "^2.1.1", "node-schedule": "^2.1.1",
...@@ -164,6 +167,7 @@ ...@@ -164,6 +167,7 @@
"@types/cookie-parser": "^1.4.10", "@types/cookie-parser": "^1.4.10",
"@types/cors": "^2.8.19", "@types/cors": "^2.8.19",
"@types/express": "^5.0.6", "@types/express": "^5.0.6",
"@types/express-handlebars": "^3.1.0",
"@types/ioredis": "^5.0.0", "@types/ioredis": "^5.0.0",
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
"@types/jsonwebtoken": "^9.0.10", "@types/jsonwebtoken": "^9.0.10",
......
...@@ -51,6 +51,9 @@ importers: ...@@ -51,6 +51,9 @@ importers:
express-automatic-routes: express-automatic-routes:
specifier: ^1.1.0 specifier: ^1.1.0
version: 1.1.0 version: 1.1.0
express-handlebars:
specifier: ^8.0.1
version: 8.0.7
express-validator: express-validator:
specifier: ^7.3.1 specifier: ^7.3.1
version: 7.3.1 version: 7.3.1
...@@ -72,6 +75,9 @@ importers: ...@@ -72,6 +75,9 @@ importers:
module-alias: module-alias:
specifier: ^2.2.3 specifier: ^2.2.3
version: 2.2.3 version: 2.2.3
mongodb:
specifier: ^6.16.0
version: 6.21.0(socks@2.8.8)
multer: multer:
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.0.2 version: 2.0.2
...@@ -87,6 +93,9 @@ importers: ...@@ -87,6 +93,9 @@ importers:
nodemailer: nodemailer:
specifier: ^8.0.7 specifier: ^8.0.7
version: 8.0.7 version: 8.0.7
oidc-provider:
specifier: ^9.8.4
version: 9.8.4
p-limit: p-limit:
specifier: ^7.3.0 specifier: ^7.3.0
version: 7.3.0 version: 7.3.0
...@@ -160,6 +169,9 @@ importers: ...@@ -160,6 +169,9 @@ importers:
'@types/express': '@types/express':
specifier: ^5.0.6 specifier: ^5.0.6
version: 5.0.6 version: 5.0.6
'@types/express-handlebars':
specifier: ^3.1.0
version: 3.1.0
'@types/ioredis': '@types/ioredis':
specifier: ^5.0.0 specifier: ^5.0.0
version: 5.0.0 version: 5.0.0
...@@ -1129,6 +1141,19 @@ packages: ...@@ -1129,6 +1141,19 @@ packages:
'@js-sdsl/ordered-map@4.4.2': '@js-sdsl/ordered-map@4.4.2':
resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==}
'@koa/cors@5.0.0':
resolution: {integrity: sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw==}
engines: {node: '>= 14.0.0'}
'@koa/router@15.6.0':
resolution: {integrity: sha512-iEOXlvGIBqSNkGXrg0XtMARAOm5zA24oedXxiTGEkrD4JgwVjfRDddCQvW1s4WEcwDYvyecRbf8BikXsuEEj8w==}
engines: {node: '>= 20'}
peerDependencies:
koa: ^2.0.0 || ^3.0.0
'@mongodb-js/saslprep@1.4.11':
resolution: {integrity: sha512-o9rAHc0IpIjuPSxRutWpE1F62x7n+4mVS4rCNHkzhIUMQcc18bb6xEq5wd2NdN0WjepIyXIppRshYI2kQDOZVA==}
'@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3':
resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==}
cpu: [arm64] cpu: [arm64]
...@@ -1481,6 +1506,9 @@ packages: ...@@ -1481,6 +1506,9 @@ packages:
'@types/debug@4.1.12': '@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
'@types/express-handlebars@3.1.0':
resolution: {integrity: sha512-Bn6j/tfhAnZEAbMtcNUFk6ESu1I6PE2pYLbUn1PR1MyNonUuQErlQ71n9DPppHK7uAuMCfgcF0oT28Lh0ej4SQ==}
'@types/express-serve-static-core@5.1.0': '@types/express-serve-static-core@5.1.0':
resolution: {integrity: sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==} resolution: {integrity: sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==}
...@@ -1579,6 +1607,12 @@ packages: ...@@ -1579,6 +1607,12 @@ packages:
'@types/web-push@3.6.4': '@types/web-push@3.6.4':
resolution: {integrity: sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==} resolution: {integrity: sha512-GnJmSr40H3RAnj0s34FNTcJi1hmWFV5KXugE0mYWnYhgTAHLJ/dJKAwDmvPJYMke0RplY2XE9LnM4hqSqKIjhQ==}
'@types/webidl-conversions@7.0.3':
resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==}
'@types/whatwg-url@11.0.5':
resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==}
'@types/yargs-parser@21.0.3': '@types/yargs-parser@21.0.3':
resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==}
...@@ -1868,6 +1902,10 @@ packages: ...@@ -1868,6 +1902,10 @@ packages:
bser@2.1.1: bser@2.1.1:
resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
bson@6.10.4:
resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==}
engines: {node: '>=16.20.1'}
buffer-equal-constant-time@1.0.1: buffer-equal-constant-time@1.0.1:
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
...@@ -2049,10 +2087,18 @@ packages: ...@@ -2049,10 +2087,18 @@ packages:
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
content-disposition@1.0.1:
resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==}
engines: {node: '>=18'}
content-type@1.0.5: content-type@1.0.5:
resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
content-type@2.0.0:
resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==}
engines: {node: '>=18'}
conventional-changelog-angular@7.0.0: conventional-changelog-angular@7.0.0:
resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==} resolution: {integrity: sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==}
engines: {node: '>=16'} engines: {node: '>=16'}
...@@ -2086,6 +2132,10 @@ packages: ...@@ -2086,6 +2132,10 @@ packages:
cookiejar@2.1.4: cookiejar@2.1.4:
resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==}
cookies@0.9.1:
resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==}
engines: {node: '>= 0.8'}
cors@2.8.5: cors@2.8.5:
resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
...@@ -2177,6 +2227,9 @@ packages: ...@@ -2177,6 +2227,9 @@ packages:
babel-plugin-macros: babel-plugin-macros:
optional: true optional: true
deep-equal@1.0.1:
resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==}
deep-extend@0.6.0: deep-extend@0.6.0:
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
engines: {node: '>=4.0.0'} engines: {node: '>=4.0.0'}
...@@ -2412,6 +2465,10 @@ packages: ...@@ -2412,6 +2465,10 @@ packages:
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
eta@4.6.0:
resolution: {integrity: sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA==}
engines: {node: '>=20'}
etag@1.8.1: etag@1.8.1:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
...@@ -2441,6 +2498,10 @@ packages: ...@@ -2441,6 +2498,10 @@ packages:
express-automatic-routes@1.1.0: express-automatic-routes@1.1.0:
resolution: {integrity: sha512-BWPf4Owmmxq804qRnoeiyJhwm7SKngDjR3l5tgVxupiESETgRoGCZZIZrj7+QmjIVgvSFO9+a+wZqfw8zddfSQ==} resolution: {integrity: sha512-BWPf4Owmmxq804qRnoeiyJhwm7SKngDjR3l5tgVxupiESETgRoGCZZIZrj7+QmjIVgvSFO9+a+wZqfw8zddfSQ==}
express-handlebars@8.0.7:
resolution: {integrity: sha512-b7aiFGIuTiGM99pXc9cr+CZKvm+kZgYwdsihgEGdD703xG5NZvMUL1U6UOlEv0kLNM2uA+rVecYGxkSWnYu7LQ==}
engines: {node: '>=22.22.2'}
express-validator@7.3.1: express-validator@7.3.1:
resolution: {integrity: sha512-IGenaSf+DnWc69lKuqlRE9/i/2t5/16VpH5bXoqdxWz1aCpRvEdrBuu1y95i/iL5QP8ZYVATiwLFhwk3EDl5vg==} resolution: {integrity: sha512-IGenaSf+DnWc69lKuqlRE9/i/2t5/16VpH5bXoqdxWz1aCpRvEdrBuu1y95i/iL5QP8ZYVATiwLFhwk3EDl5vg==}
engines: {node: '>= 8.0.0'} engines: {node: '>= 8.0.0'}
...@@ -2643,6 +2704,7 @@ packages: ...@@ -2643,6 +2704,7 @@ packages:
glob@10.5.0: glob@10.5.0:
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
hasBin: true hasBin: true
global-directory@4.0.1: global-directory@4.0.1:
...@@ -2718,9 +2780,17 @@ packages: ...@@ -2718,9 +2780,17 @@ packages:
html-escaper@2.0.2: html-escaper@2.0.2:
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
http-assert@1.5.0:
resolution: {integrity: sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==}
engines: {node: '>= 0.8'}
http-cache-semantics@4.2.0: http-cache-semantics@4.2.0:
resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==}
http-errors@1.8.1:
resolution: {integrity: sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==}
engines: {node: '>= 0.6'}
http-errors@2.0.0: http-errors@2.0.0:
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
...@@ -2769,6 +2839,10 @@ packages: ...@@ -2769,6 +2839,10 @@ packages:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
iconv-lite@0.7.2:
resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==}
engines: {node: '>=0.10.0'}
ieee754@1.2.1: ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
...@@ -3066,6 +3140,9 @@ packages: ...@@ -3066,6 +3140,9 @@ packages:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true hasBin: true
jose@6.2.3:
resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==}
js-tokens@4.0.0: js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
...@@ -3123,6 +3200,10 @@ packages: ...@@ -3123,6 +3200,10 @@ packages:
jws@4.0.1: jws@4.0.1:
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
keygrip@1.1.0:
resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==}
engines: {node: '>= 0.6'}
keyv@4.5.4: keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
...@@ -3130,6 +3211,13 @@ packages: ...@@ -3130,6 +3211,13 @@ packages:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'} engines: {node: '>=6'}
koa-compose@4.1.0:
resolution: {integrity: sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==}
koa@3.2.1:
resolution: {integrity: sha512-e7IpWJrnanNUroVK2taAgMxoEZvHLXdQiNjeExSu/DEIWm83jaKGBgb7tLmu2rMYpA027qFB3iLR/k3AVpFRnA==}
engines: {node: '>= 18'}
leven@3.1.0: leven@3.1.0:
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
engines: {node: '>=6'} engines: {node: '>=6'}
...@@ -3268,10 +3356,17 @@ packages: ...@@ -3268,10 +3356,17 @@ packages:
resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
media-typer@1.1.0:
resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==}
engines: {node: '>= 0.8'}
memoizee@0.4.17: memoizee@0.4.17:
resolution: {integrity: sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==} resolution: {integrity: sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==}
engines: {node: '>=0.12'} engines: {node: '>=0.12'}
memory-pager@1.5.0:
resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==}
meow@12.1.1: meow@12.1.1:
resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==}
engines: {node: '>=16.10'} engines: {node: '>=16.10'}
...@@ -3306,6 +3401,10 @@ packages: ...@@ -3306,6 +3401,10 @@ packages:
resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
mime-types@3.0.2:
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
engines: {node: '>=18'}
mime@1.6.0: mime@1.6.0:
resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==}
engines: {node: '>=4'} engines: {node: '>=4'}
...@@ -3398,6 +3497,36 @@ packages: ...@@ -3398,6 +3497,36 @@ packages:
moment@2.30.1: moment@2.30.1:
resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==}
mongodb-connection-string-url@3.0.2:
resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==}
mongodb@6.21.0:
resolution: {integrity: sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A==}
engines: {node: '>=16.20.1'}
peerDependencies:
'@aws-sdk/credential-providers': ^3.188.0
'@mongodb-js/zstd': ^1.1.0 || ^2.0.0
gcp-metadata: ^5.2.0
kerberos: ^2.0.1
mongodb-client-encryption: '>=6.0.0 <7'
snappy: ^7.3.2
socks: ^2.7.1
peerDependenciesMeta:
'@aws-sdk/credential-providers':
optional: true
'@mongodb-js/zstd':
optional: true
gcp-metadata:
optional: true
kerberos:
optional: true
mongodb-client-encryption:
optional: true
snappy:
optional: true
socks:
optional: true
ms@2.0.0: ms@2.0.0:
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
...@@ -3431,6 +3560,11 @@ packages: ...@@ -3431,6 +3560,11 @@ packages:
resolution: {integrity: sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==} resolution: {integrity: sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==}
engines: {node: '>=20.17'} engines: {node: '>=20.17'}
nanoid@5.1.11:
resolution: {integrity: sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==}
engines: {node: ^18 || >=20}
hasBin: true
napi-build-utils@2.0.0: napi-build-utils@2.0.0:
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
...@@ -3548,6 +3682,9 @@ packages: ...@@ -3548,6 +3682,9 @@ packages:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
oidc-provider@9.8.4:
resolution: {integrity: sha512-i8qe+wvhUQ7BSj6DxssIFAdpREouuqK91j2jGdAN78NIxTB5rxvU4ZniXELhUHIM4mzABeSGlp5wE6WTa8CY1Q==}
on-finished@2.4.1: on-finished@2.4.1:
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
...@@ -3653,6 +3790,9 @@ packages: ...@@ -3653,6 +3790,9 @@ packages:
path-to-regexp@0.1.12: path-to-regexp@0.1.12:
resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==}
path-to-regexp@8.4.2:
resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==}
path-type@4.0.0: path-type@4.0.0:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'} engines: {node: '>=8'}
...@@ -3819,6 +3959,10 @@ packages: ...@@ -3819,6 +3959,10 @@ packages:
queue-microtask@1.2.3: queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
quick-lru@7.3.0:
resolution: {integrity: sha512-k9lSsjl36EJdK7I06v7APZCbyGT2vMTsYSRX1Q2nbYmnkBqgUhRkAuzH08Ciotteu/PLJmIF2+tti7o3C/ts2g==}
engines: {node: '>=18'}
range-parser@1.2.1: range-parser@1.2.1:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
...@@ -3827,6 +3971,10 @@ packages: ...@@ -3827,6 +3971,10 @@ packages:
resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
raw-body@3.0.2:
resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
engines: {node: '>= 0.10'}
rc@1.2.8: rc@1.2.8:
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
hasBin: true hasBin: true
...@@ -4095,6 +4243,9 @@ packages: ...@@ -4095,6 +4243,9 @@ packages:
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
sparse-bitfield@3.0.3:
resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==}
split2@4.2.0: split2@4.2.0:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'} engines: {node: '>= 10.x'}
...@@ -4120,6 +4271,10 @@ packages: ...@@ -4120,6 +4271,10 @@ packages:
standard-as-callback@2.1.0: standard-as-callback@2.1.0:
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
statuses@1.5.0:
resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==}
engines: {node: '>= 0.6'}
statuses@2.0.1: statuses@2.0.1:
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
...@@ -4300,6 +4455,10 @@ packages: ...@@ -4300,6 +4455,10 @@ packages:
resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==}
hasBin: true hasBin: true
tr46@5.1.1:
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
engines: {node: '>=18'}
ts-api-utils@2.1.0: ts-api-utils@2.1.0:
resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
engines: {node: '>=18.12'} engines: {node: '>=18.12'}
...@@ -4372,6 +4531,10 @@ packages: ...@@ -4372,6 +4531,10 @@ packages:
tslib@2.8.1: tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tsscmp@1.0.6:
resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==}
engines: {node: '>=0.6.x'}
tsx@4.21.0: tsx@4.21.0:
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
...@@ -4404,6 +4567,10 @@ packages: ...@@ -4404,6 +4567,10 @@ packages:
resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
type-is@2.1.0:
resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==}
engines: {node: '>= 18'}
type@2.7.3: type@2.7.3:
resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==}
...@@ -4491,6 +4658,14 @@ packages: ...@@ -4491,6 +4658,14 @@ packages:
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
webidl-conversions@7.0.0:
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
engines: {node: '>=12'}
whatwg-url@14.2.0:
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
engines: {node: '>=18'}
which@2.0.2: which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
...@@ -5774,6 +5949,24 @@ snapshots: ...@@ -5774,6 +5949,24 @@ snapshots:
'@js-sdsl/ordered-map@4.4.2': {} '@js-sdsl/ordered-map@4.4.2': {}
'@koa/cors@5.0.0':
dependencies:
vary: 1.1.2
'@koa/router@15.6.0(koa@3.2.1)':
dependencies:
debug: 4.4.3(supports-color@5.5.0)
http-errors: 2.0.1
koa: 3.2.1
koa-compose: 4.1.0
path-to-regexp: 8.4.2
transitivePeerDependencies:
- supports-color
'@mongodb-js/saslprep@1.4.11':
dependencies:
sparse-bitfield: 3.0.3
'@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3':
optional: true optional: true
...@@ -6214,6 +6407,8 @@ snapshots: ...@@ -6214,6 +6407,8 @@ snapshots:
dependencies: dependencies:
'@types/ms': 2.1.0 '@types/ms': 2.1.0
'@types/express-handlebars@3.1.0': {}
'@types/express-serve-static-core@5.1.0': '@types/express-serve-static-core@5.1.0':
dependencies: dependencies:
'@types/node': 24.10.1 '@types/node': 24.10.1
...@@ -6338,6 +6533,12 @@ snapshots: ...@@ -6338,6 +6533,12 @@ snapshots:
dependencies: dependencies:
'@types/node': 24.10.1 '@types/node': 24.10.1
'@types/webidl-conversions@7.0.3': {}
'@types/whatwg-url@11.0.5':
dependencies:
'@types/webidl-conversions': 7.0.3
'@types/yargs-parser@21.0.3': {} '@types/yargs-parser@21.0.3': {}
'@types/yargs@17.0.35': '@types/yargs@17.0.35':
...@@ -6692,6 +6893,8 @@ snapshots: ...@@ -6692,6 +6893,8 @@ snapshots:
dependencies: dependencies:
node-int64: 0.4.0 node-int64: 0.4.0
bson@6.10.4: {}
buffer-equal-constant-time@1.0.1: {} buffer-equal-constant-time@1.0.1: {}
buffer-from@1.1.2: {} buffer-from@1.1.2: {}
...@@ -6896,8 +7099,12 @@ snapshots: ...@@ -6896,8 +7099,12 @@ snapshots:
dependencies: dependencies:
safe-buffer: 5.2.1 safe-buffer: 5.2.1
content-disposition@1.0.1: {}
content-type@1.0.5: {} content-type@1.0.5: {}
content-type@2.0.0: {}
conventional-changelog-angular@7.0.0: conventional-changelog-angular@7.0.0:
dependencies: dependencies:
compare-func: 2.0.0 compare-func: 2.0.0
...@@ -6928,6 +7135,11 @@ snapshots: ...@@ -6928,6 +7135,11 @@ snapshots:
cookiejar@2.1.4: {} cookiejar@2.1.4: {}
cookies@0.9.1:
dependencies:
depd: 2.0.0
keygrip: 1.1.0
cors@2.8.5: cors@2.8.5:
dependencies: dependencies:
object-assign: 4.1.1 object-assign: 4.1.1
...@@ -7010,6 +7222,8 @@ snapshots: ...@@ -7010,6 +7222,8 @@ snapshots:
dedent@1.7.2: {} dedent@1.7.2: {}
deep-equal@1.0.1: {}
deep-extend@0.6.0: {} deep-extend@0.6.0: {}
deep-is@0.1.4: {} deep-is@0.1.4: {}
...@@ -7018,8 +7232,7 @@ snapshots: ...@@ -7018,8 +7232,7 @@ snapshots:
delayed-stream@1.0.0: {} delayed-stream@1.0.0: {}
delegates@1.0.0: delegates@1.0.0: {}
optional: true
denque@2.1.0: {} denque@2.1.0: {}
...@@ -7273,6 +7486,8 @@ snapshots: ...@@ -7273,6 +7486,8 @@ snapshots:
esutils@2.0.3: {} esutils@2.0.3: {}
eta@4.6.0: {}
etag@1.8.1: {} etag@1.8.1: {}
event-emitter@0.3.5: event-emitter@0.3.5:
...@@ -7308,6 +7523,12 @@ snapshots: ...@@ -7308,6 +7523,12 @@ snapshots:
express-automatic-routes@1.1.0: {} express-automatic-routes@1.1.0: {}
express-handlebars@8.0.7:
dependencies:
glob: 10.5.0
graceful-fs: 4.2.11
handlebars: 4.7.9
express-validator@7.3.1: express-validator@7.3.1:
dependencies: dependencies:
lodash: 4.17.21 lodash: 4.17.21
...@@ -7669,9 +7890,22 @@ snapshots: ...@@ -7669,9 +7890,22 @@ snapshots:
html-escaper@2.0.2: {} html-escaper@2.0.2: {}
http-assert@1.5.0:
dependencies:
deep-equal: 1.0.1
http-errors: 1.8.1
http-cache-semantics@4.2.0: http-cache-semantics@4.2.0:
optional: true optional: true
http-errors@1.8.1:
dependencies:
depd: 1.1.2
inherits: 2.0.4
setprototypeof: 1.2.0
statuses: 1.5.0
toidentifier: 1.0.1
http-errors@2.0.0: http-errors@2.0.0:
dependencies: dependencies:
depd: 2.0.0 depd: 2.0.0
...@@ -7739,6 +7973,10 @@ snapshots: ...@@ -7739,6 +7973,10 @@ snapshots:
safer-buffer: 2.1.2 safer-buffer: 2.1.2
optional: true optional: true
iconv-lite@0.7.2:
dependencies:
safer-buffer: 2.1.2
ieee754@1.2.1: {} ieee754@1.2.1: {}
ignore-by-default@1.0.1: {} ignore-by-default@1.0.1: {}
...@@ -8205,6 +8443,8 @@ snapshots: ...@@ -8205,6 +8443,8 @@ snapshots:
jiti@2.6.1: {} jiti@2.6.1: {}
jose@6.2.3: {}
js-tokens@4.0.0: {} js-tokens@4.0.0: {}
js-yaml@3.14.2: js-yaml@3.14.2:
...@@ -8262,12 +8502,39 @@ snapshots: ...@@ -8262,12 +8502,39 @@ snapshots:
jwa: 2.0.1 jwa: 2.0.1
safe-buffer: 5.2.1 safe-buffer: 5.2.1
keygrip@1.1.0:
dependencies:
tsscmp: 1.0.6
keyv@4.5.4: keyv@4.5.4:
dependencies: dependencies:
json-buffer: 3.0.1 json-buffer: 3.0.1
kleur@3.0.3: {} kleur@3.0.3: {}
koa-compose@4.1.0: {}
koa@3.2.1:
dependencies:
accepts: 1.3.8
content-disposition: 1.0.1
content-type: 1.0.5
cookies: 0.9.1
delegates: 1.0.0
destroy: 1.2.0
encodeurl: 2.0.0
escape-html: 1.0.3
fresh: 0.5.2
http-assert: 1.5.0
http-errors: 2.0.1
koa-compose: 4.1.0
mime-types: 3.0.2
on-finished: 2.4.1
parseurl: 1.3.3
statuses: 2.0.2
type-is: 2.1.0
vary: 1.1.2
leven@3.1.0: {} leven@3.1.0: {}
levn@0.4.1: levn@0.4.1:
...@@ -8412,6 +8679,8 @@ snapshots: ...@@ -8412,6 +8679,8 @@ snapshots:
media-typer@0.3.0: {} media-typer@0.3.0: {}
media-typer@1.1.0: {}
memoizee@0.4.17: memoizee@0.4.17:
dependencies: dependencies:
d: 1.0.2 d: 1.0.2
...@@ -8423,6 +8692,8 @@ snapshots: ...@@ -8423,6 +8692,8 @@ snapshots:
next-tick: 1.1.0 next-tick: 1.1.0
timers-ext: 0.1.8 timers-ext: 0.1.8
memory-pager@1.5.0: {}
meow@12.1.1: {} meow@12.1.1: {}
merge-descriptors@1.0.3: {} merge-descriptors@1.0.3: {}
...@@ -8446,6 +8717,10 @@ snapshots: ...@@ -8446,6 +8717,10 @@ snapshots:
dependencies: dependencies:
mime-db: 1.52.0 mime-db: 1.52.0
mime-types@3.0.2:
dependencies:
mime-db: 1.54.0
mime@1.6.0: {} mime@1.6.0: {}
mime@2.6.0: {} mime@2.6.0: {}
...@@ -8526,6 +8801,19 @@ snapshots: ...@@ -8526,6 +8801,19 @@ snapshots:
moment@2.30.1: {} moment@2.30.1: {}
mongodb-connection-string-url@3.0.2:
dependencies:
'@types/whatwg-url': 11.0.5
whatwg-url: 14.2.0
mongodb@6.21.0(socks@2.8.8):
dependencies:
'@mongodb-js/saslprep': 1.4.11
bson: 6.10.4
mongodb-connection-string-url: 3.0.2
optionalDependencies:
socks: 2.8.8
ms@2.0.0: {} ms@2.0.0: {}
ms@2.1.3: {} ms@2.1.3: {}
...@@ -8568,6 +8856,8 @@ snapshots: ...@@ -8568,6 +8856,8 @@ snapshots:
nano-spawn@2.0.0: {} nano-spawn@2.0.0: {}
nanoid@5.1.11: {}
napi-build-utils@2.0.0: {} napi-build-utils@2.0.0: {}
natural-compare@1.4.0: {} natural-compare@1.4.0: {}
...@@ -8693,6 +8983,21 @@ snapshots: ...@@ -8693,6 +8983,21 @@ snapshots:
object-inspect@1.13.4: {} object-inspect@1.13.4: {}
oidc-provider@9.8.4:
dependencies:
'@koa/cors': 5.0.0
'@koa/router': 15.6.0(koa@3.2.1)
debug: 4.4.3(supports-color@5.5.0)
eta: 4.6.0
jose: 6.2.3
jsesc: 3.1.0
koa: 3.2.1
nanoid: 5.1.11
quick-lru: 7.3.0
raw-body: 3.0.2
transitivePeerDependencies:
- supports-color
on-finished@2.4.1: on-finished@2.4.1:
dependencies: dependencies:
ee-first: 1.1.1 ee-first: 1.1.1
...@@ -8793,6 +9098,8 @@ snapshots: ...@@ -8793,6 +9098,8 @@ snapshots:
path-to-regexp@0.1.12: {} path-to-regexp@0.1.12: {}
path-to-regexp@8.4.2: {}
path-type@4.0.0: {} path-type@4.0.0: {}
peek-readable@5.4.2: {} peek-readable@5.4.2: {}
...@@ -8949,6 +9256,8 @@ snapshots: ...@@ -8949,6 +9256,8 @@ snapshots:
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
quick-lru@7.3.0: {}
range-parser@1.2.1: {} range-parser@1.2.1: {}
raw-body@2.5.3: raw-body@2.5.3:
...@@ -8958,6 +9267,13 @@ snapshots: ...@@ -8958,6 +9267,13 @@ snapshots:
iconv-lite: 0.4.24 iconv-lite: 0.4.24
unpipe: 1.0.0 unpipe: 1.0.0
raw-body@3.0.2:
dependencies:
bytes: 3.1.2
http-errors: 2.0.1
iconv-lite: 0.7.2
unpipe: 1.0.0
rc@1.2.8: rc@1.2.8:
dependencies: dependencies:
deep-extend: 0.6.0 deep-extend: 0.6.0
...@@ -9267,6 +9583,10 @@ snapshots: ...@@ -9267,6 +9583,10 @@ snapshots:
source-map@0.6.1: {} source-map@0.6.1: {}
sparse-bitfield@3.0.3:
dependencies:
memory-pager: 1.5.0
split2@4.2.0: {} split2@4.2.0: {}
sprintf-js@1.0.3: {} sprintf-js@1.0.3: {}
...@@ -9298,6 +9618,8 @@ snapshots: ...@@ -9298,6 +9618,8 @@ snapshots:
standard-as-callback@2.1.0: {} standard-as-callback@2.1.0: {}
statuses@1.5.0: {}
statuses@2.0.1: {} statuses@2.0.1: {}
statuses@2.0.2: {} statuses@2.0.2: {}
...@@ -9493,6 +9815,10 @@ snapshots: ...@@ -9493,6 +9815,10 @@ snapshots:
touch@3.1.1: {} touch@3.1.1: {}
tr46@5.1.1:
dependencies:
punycode: 2.3.1
ts-api-utils@2.1.0(typescript@5.9.3): ts-api-utils@2.1.0(typescript@5.9.3):
dependencies: dependencies:
typescript: 5.9.3 typescript: 5.9.3
...@@ -9561,6 +9887,8 @@ snapshots: ...@@ -9561,6 +9887,8 @@ snapshots:
tslib@2.8.1: {} tslib@2.8.1: {}
tsscmp@1.0.6: {}
tsx@4.21.0: tsx@4.21.0:
dependencies: dependencies:
esbuild: 0.27.1 esbuild: 0.27.1
...@@ -9589,6 +9917,12 @@ snapshots: ...@@ -9589,6 +9917,12 @@ snapshots:
media-typer: 0.3.0 media-typer: 0.3.0
mime-types: 2.1.35 mime-types: 2.1.35
type-is@2.1.0:
dependencies:
content-type: 2.0.0
media-typer: 1.1.0
mime-types: 3.0.2
type@2.7.3: {} type@2.7.3: {}
typedarray@0.0.6: {} typedarray@0.0.6: {}
...@@ -9662,6 +9996,13 @@ snapshots: ...@@ -9662,6 +9996,13 @@ snapshots:
web-streams-polyfill@3.3.3: {} web-streams-polyfill@3.3.3: {}
webidl-conversions@7.0.0: {}
whatwg-url@14.2.0:
dependencies:
tr46: 5.1.1
webidl-conversions: 7.0.0
which@2.0.2: which@2.0.2:
dependencies: dependencies:
isexe: 2.0.0 isexe: 2.0.0
......
-- Migration: 035-add-oidc-schema.sql
-- Description: Add OIDC grants and clients tables for oidc-provider
-- Date: 2026-06-13
-- OIDC Grants table (stores AuthorizationCode, RefreshToken, AccessToken, Interaction references)
CREATE TABLE IF NOT EXISTS oidc_grants (
id VARCHAR(128) NOT NULL,
model VARCHAR(64) NOT NULL,
payload JSONB NOT NULL,
expires_at TIMESTAMPTZ,
consumed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (model, id)
);
CREATE INDEX IF NOT EXISTS idx_oidc_grants_expires_at ON oidc_grants (expires_at);
CREATE INDEX IF NOT EXISTS idx_oidc_grants_payload_uid ON oidc_grants ((payload->>'uid'));
CREATE INDEX IF NOT EXISTS idx_oidc_grants_payload_user_code ON oidc_grants ((payload->>'userCode'));
CREATE INDEX IF NOT EXISTS idx_oidc_grants_payload_grant_id ON oidc_grants ((payload->>'grantId'));
COMMENT ON TABLE oidc_grants IS 'Stores OIDC grants: AuthorizationCode, RefreshToken, AccessToken, Interaction';
-- Migration: 036-add-clients-schema.sql
-- Description: Add OIDC clients table for managing authorized applications
-- Date: 2026-06-13
CREATE TABLE IF NOT EXISTS clients (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
app_code TEXT UNIQUE NOT NULL,
client_id TEXT UNIQUE NOT NULL,
client_secret_hash TEXT,
name TEXT NOT NULL,
redirect_uris TEXT[] NOT NULL DEFAULT '{}',
post_logout_redirect_uris TEXT[] NOT NULL DEFAULT '{}',
grant_types TEXT[] NOT NULL DEFAULT '{authorization_code,refresh_token}',
response_types TEXT[] NOT NULL DEFAULT '{code}',
scopes TEXT[] NOT NULL DEFAULT '{openid,profile,email}',
token_endpoint_auth_method TEXT NOT NULL DEFAULT 'none',
require_pkce BOOLEAN NOT NULL DEFAULT TRUE,
status TEXT NOT NULL DEFAULT 'active',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE clients IS 'OIDC registered applications (clients)';
export const AUDIT_EVENTS = {
LOGIN_SUCCESS: 'LOGIN_SUCCESS',
LOGIN_FAILED: 'LOGIN_FAILED',
LOGOUT: 'LOGOUT',
TOKEN_REFRESH: 'TOKEN_REFRESH',
TOKEN_VERIFY_FAILED: 'TOKEN_VERIFY_FAILED',
PASSWORD_CHANGED: 'PASSWORD_CHANGED',
ROLE_CHANGED: 'ROLE_CHANGED',
DB_CONNECTION_FAILED: 'DB_CONNECTION_FAILED',
DB_FAILOVER_STARTED: 'DB_FAILOVER_STARTED',
DB_FAILOVER_SUCCESS: 'DB_FAILOVER_SUCCESS',
DB_FAILOVER_FAILED: 'DB_FAILOVER_FAILED',
DB_BACKUP_STARTED: 'DB_BACKUP_STARTED',
DB_BACKUP_SUCCESS: 'DB_BACKUP_SUCCESS',
DB_BACKUP_FAILED: 'DB_BACKUP_FAILED',
ADMIN_ACTION: 'ADMIN_ACTION',
SESSION_REUSED: 'SESSION_REUSED',
SESSION_EXPIRED: 'SESSION_EXPIRED',
ACCOUNT_LOCKED: 'ACCOUNT_LOCKED',
ACCOUNT_UNLOCKED: 'ACCOUNT_UNLOCKED',
REGISTER_SUCCESS: 'REGISTER_SUCCESS',
REGISTER_FAILED: 'REGISTER_FAILED',
EMAIL_VERIFICATION_SENT: 'EMAIL_VERIFICATION_SENT',
EMAIL_VERIFIED: 'EMAIL_VERIFIED',
} as const;
export type AuditEventType = keyof typeof AUDIT_EVENTS;
import { AuditRepository } from './auditRepository';
import { AuditLog } from './schemas';
import Logger from '../utils/logger';
interface QueuedAuditEvent {
event: Omit<AuditLog, 'createdAt'>;
retries: number;
firstAttempt: number;
}
const MAX_RETRIES = 3;
const RETRY_DELAY_MS = 2000;
export class AuditLoggerService {
private readonly repo = new AuditRepository();
private readonly queue: QueuedAuditEvent[] = [];
private processing = false;
private intervalHandle: NodeJS.Timeout | undefined;
async log(event: Omit<AuditLog, 'createdAt'>): Promise<void> {
try {
await this.repo.insert({ ...event, createdAt: new Date() });
} catch (e: unknown) {
Logger.warn(`[audit] write failed (will retry): ${(e as Error).message}`);
this.enqueue({ event, retries: 0, firstAttempt: Date.now() });
}
}
private enqueue(item: QueuedAuditEvent): void {
this.queue.push(item);
if (!this.processing) {
void this.processQueue();
}
}
startQueueProcessor(intervalMs = 30_000): void {
this.intervalHandle = setInterval(() => void this.processQueue(), intervalMs);
}
stopQueueProcessor(): void {
if (this.intervalHandle) {
clearInterval(this.intervalHandle);
this.intervalHandle = undefined;
}
}
private async processQueue(): Promise<void> {
if (this.processing || this.queue.length === 0) return;
this.processing = true;
while (this.queue.length > 0) {
const item = this.queue[0]!;
const delay = RETRY_DELAY_MS * Math.pow(2, item.retries);
if (Date.now() - item.firstAttempt > 60_000) {
Logger.error(`[audit] event dropped after 60s retries: ${item.event.eventType}`);
this.queue.shift();
continue;
}
if (item.retries > 0) {
await this.sleep(delay);
}
try {
await this.repo.insert({ ...item.event, createdAt: new Date() });
this.queue.shift();
} catch (e: unknown) {
item.retries++;
if (item.retries >= MAX_RETRIES) {
Logger.error(
`[audit] permanently failed after ${MAX_RETRIES} retries: ${item.event.eventType}`,
);
this.queue.shift();
} else {
Logger.warn(`[audit] retry ${item.retries}/${MAX_RETRIES} failed: ${(e as Error).message}`);
}
}
}
this.processing = false;
}
private sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
import { MongoClientService, MONGO_COLLECTIONS } from '../database/mongo';
import { AuditLog } from './schemas';
export class AuditRepository {
async insert(log: AuditLog): Promise<void> {
const db = await MongoClientService.getDb();
await db.collection(MONGO_COLLECTIONS.AUDIT_LOGS).insertOne(log);
}
async findByUserId(userId: string, limit = 100): Promise<AuditLog[]> {
const db = await MongoClientService.getDb();
const cursor = db
.collection(MONGO_COLLECTIONS.AUDIT_LOGS)
.find({ userId })
.sort({ createdAt: -1 })
.limit(limit);
const results = await cursor.toArray();
return results as unknown as AuditLog[];
}
async findByEventType(eventType: string, limit = 100): Promise<AuditLog[]> {
const db = await MongoClientService.getDb();
const cursor = db
.collection(MONGO_COLLECTIONS.AUDIT_LOGS)
.find({ eventType })
.sort({ createdAt: -1 })
.limit(limit);
const results = await cursor.toArray();
return results as unknown as AuditLog[];
}
}
export { AuditRepository } from './auditRepository';
export { AuditLoggerService } from './auditLoggerService';
export { AuditLog } from './schemas';
export { AUDIT_EVENTS } from './auditEvents';
export interface AuditLog {
eventType: string;
userId?: string;
clientApp?: string;
ip?: string;
userAgent?: string;
status: 'success' | 'failed' | 'info';
metadata?: Record<string, unknown>;
requestId?: string;
createdAt: Date;
}
export { AuditLog } from './auditLog';
export class OidcConfigService {
get issuer(): string {
return process.env.OIDC_ISSUER ?? 'http://localhost:3001';
}
get accessTokenTtl(): number {
return Number(process.env.OIDC_ACCESS_TOKEN_TTL ?? 900);
}
get refreshTokenTtl(): number {
return Number(process.env.OIDC_REFRESH_TOKEN_TTL ?? 2592000);
}
get cookieKeys(): string[] {
const keys = process.env.OIDC_COOKIE_KEYS ?? 'dev-cookie-key-minimum-32-chars';
return keys.split(',').filter(Boolean);
}
}
export const MONGO_COLLECTIONS = {
AUDIT_LOGS: 'audit_logs',
} as const;
export { MongoClientService } from './mongoClientService';
export { MONGO_COLLECTIONS } from './collections';
import { MongoClient, Db } from 'mongodb';
let client: MongoClient | undefined;
let db: Db | undefined;
export class MongoClientService {
private static _url: string | undefined;
private static _databaseName: string | undefined;
static configure(url: string, databaseName: string): void {
this._url = url;
this._databaseName = databaseName;
}
static getUrl(): string {
if (!this._url) {
const url = process.env.MONGODB_AUDIT_URL;
if (!url) {
throw new Error('Missing MONGODB_AUDIT_URL environment variable');
}
this._url = url;
}
return this._url;
}
static getDatabaseName(): string {
if (!this._databaseName) {
this._databaseName = process.env.MONGODB_AUDIT_DATABASE ?? 'sso_audit';
}
return this._databaseName;
}
static async getClient(): Promise<MongoClient> {
if (!client) {
client = new MongoClient(this.getUrl());
await client.connect();
}
return client;
}
static async getDb(): Promise<Db> {
if (!db) {
const c = await this.getClient();
db = c.db(this.getDatabaseName());
}
return db;
}
static async close(): Promise<void> {
if (client) {
await client.close();
client = undefined;
db = undefined;
}
}
}
import { Sequelize, QueryTypes } from 'sequelize';
export class OidcAdapterService {
constructor(private readonly sequelize: Sequelize) {}
createAdapter(name: string) {
const db = this.sequelize;
class PgOidcAdapter {
async upsert(id: string, payload: unknown, expiresIn: number): Promise<void> {
await db.query(
`INSERT INTO oidc_grants(model, id, payload, expires_at)
VALUES(:name, :id, :payload::jsonb, NOW() + (:expiresIn || ' seconds')::interval)
ON CONFLICT(model, id)
DO UPDATE SET payload = :payload::jsonb, expires_at = NOW() + (:expiresIn || ' seconds')::interval`,
{
replacements: { name, id, payload: JSON.stringify(payload), expiresIn },
type: QueryTypes.UPDATE,
},
);
}
async find(id: string): Promise<unknown> {
const rows = await db.query<{ payload: unknown }>(
`SELECT payload
FROM oidc_grants
WHERE model = :name
AND id = :id
AND (expires_at IS NULL OR expires_at > NOW())`,
{
replacements: { name, id },
type: QueryTypes.SELECT,
},
);
return rows[0]?.payload;
}
async findByUserCode(userCode: string): Promise<unknown> {
const rows = await db.query<{ payload: unknown }>(
`SELECT payload
FROM oidc_grants
WHERE model = :name
AND payload->>'userCode' = :userCode
AND (expires_at IS NULL OR expires_at > NOW())`,
{
replacements: { name, userCode },
type: QueryTypes.SELECT,
},
);
return rows[0]?.payload;
}
async findByUid(uid: string): Promise<unknown> {
const rows = await db.query<{ payload: unknown }>(
`SELECT payload
FROM oidc_grants
WHERE model = :name
AND payload->>'uid' = :uid
AND (expires_at IS NULL OR expires_at > NOW())`,
{
replacements: { name, uid },
type: QueryTypes.SELECT,
},
);
return rows[0]?.payload;
}
async destroy(id: string): Promise<void> {
await db.query('DELETE FROM oidc_grants WHERE model = :name AND id = :id', {
replacements: { name, id },
type: QueryTypes.DELETE,
});
}
async revokeByGrantId(grantId: string): Promise<void> {
await db.query("DELETE FROM oidc_grants WHERE payload->>'grantId' = :grantId", {
replacements: { grantId },
type: QueryTypes.DELETE,
});
}
async consume(id: string): Promise<void> {
await db.query(
`UPDATE oidc_grants
SET payload = jsonb_set(payload, '{consumed}', to_jsonb(EXTRACT(EPOCH FROM NOW())::bigint))
WHERE model = :name AND id = :id`,
{
replacements: { name, id },
type: QueryTypes.UPDATE,
},
);
}
}
return new PgOidcAdapter();
}
}
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Authorize - SSO VietProDev</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f5; display: flex; align-items: center; justify-content: center; min-height: 100vh; }
.consent-card { background: #fff; padding: 40px; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.1); width: 100%; max-width: 400px; text-align: center; }
.consent-card h2 { margin-bottom: 8px; color: #1a1a2e; font-size: 22px; }
.consent-card p { margin-bottom: 24px; color: #666; font-size: 14px; }
.client-name { font-weight: 600; color: #333; }
.permissions { background: #f9f9f9; border: 1px solid #eee; border-radius: 8px; padding: 16px; margin-bottom: 24px; text-align: left; }
.permissions h3 { font-size: 14px; color: #555; margin-bottom: 12px; }
.permissions ul { list-style: none; font-size: 13px; color: #555; }
.permissions ul li { padding: 4px 0; }
.permissions ul li::before { content: '✓'; color: #4f46e5; margin-right: 8px; }
button { width: 100%; padding: 12px; border: none; border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer; transition: background 0.2s; margin-bottom: 10px; }
.btn-approve { background: #4f46e5; color: #fff; }
.btn-approve:hover { background: #4338ca; }
.btn-deny { background: #fff; color: #dc2626; border: 1px solid #fecaca; }
.btn-deny:hover { background: #fef2f2; }
</style>
</head>
<body>
<div class="consent-card">
<h2>Authorize Application</h2>
<p><span class="client-name">{{ client }}</span> is requesting access to your account</p>
<div class="permissions">
<h3>This application will be able to:</h3>
<ul>
<li>View your profile information</li>
<li>Access your email address</li>
<li>Manage your authentication sessions</li>
</ul>
</div>
<form method="POST" action="/oidc/interaction/{{ uid }}/confirm">
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
<button type="submit" class="btn-approve">Authorize</button>
</form>
<form method="POST" action="/oidc/interaction/{{ uid }}/cancel">
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
<button type="submit" class="btn-deny">Cancel</button>
</form>
</div>
</body>
</html>
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - SSO VietProDev</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f5; display: flex; align-items: center; justify-content: center; min-height: 100vh; }
.login-card { background: #fff; padding: 40px; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.1); width: 100%; max-width: 400px; }
.login-card h2 { margin-bottom: 8px; color: #1a1a2e; font-size: 24px; }
.login-card p { margin-bottom: 24px; color: #666; font-size: 14px; }
.form-group { margin-bottom: 16px; }
.form-group label { display: block; margin-bottom: 6px; font-weight: 500; color: #333; font-size: 14px; }
.form-group input { width: 100%; padding: 10px 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; transition: border-color 0.2s; }
.form-group input:focus { outline: none; border-color: #4f46e5; }
.remember-row { display: flex; align-items: center; margin-bottom: 20px; font-size: 14px; }
.remember-row input { margin-right: 8px; }
button { width: 100%; padding: 12px; background: #4f46e5; color: #fff; border: none; border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer; transition: background 0.2s; }
button:hover { background: #4338ca; }
.error { background: #fef2f2; border: 1px solid #fecaca; color: #dc2626; padding: 10px 12px; border-radius: 8px; margin-bottom: 16px; font-size: 14px; }
.footer { margin-top: 20px; text-align: center; font-size: 13px; color: #888; }
.footer a { color: #4f46e5; text-decoration: none; }
</style>
</head>
<body>
<div class="login-card">
<h2>Sign In</h2>
<p>Sign in to access <strong>{{ client }}</strong></p>
{{#if error}}<div class="error">{{ error }}</div>{{/if}}
<form method="POST" action="/oidc/interaction/{{ uid }}/login">
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required autocomplete="email" placeholder="you@example.com">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required autocomplete="current-password" placeholder="Your password">
</div>
<div class="remember-row">
<input type="checkbox" id="remember" name="remember" value="1">
<label for="remember" style="margin-bottom:0; font-weight:normal;">Remember me</label>
</div>
<button type="submit">Sign In</button>
</form>
<div class="footer">
<a href="/oidc/interaction/{{ uid }}/register">Create an account</a>
</div>
</div>
</body>
</html>
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register - SSO VietProDev</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f5; display: flex; align-items: center; justify-content: center; min-height: 100vh; }
.register-card { background: #fff; padding: 40px; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.1); width: 100%; max-width: 400px; }
.register-card h2 { margin-bottom: 8px; color: #1a1a2e; font-size: 24px; }
.register-card p { margin-bottom: 24px; color: #666; font-size: 14px; }
.form-group { margin-bottom: 16px; }
.form-group label { display: block; margin-bottom: 6px; font-weight: 500; color: #333; font-size: 14px; }
.form-group input { width: 100%; padding: 10px 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; transition: border-color 0.2s; }
.form-group input:focus { outline: none; border-color: #4f46e5; }
.password-hint { font-size: 12px; color: #888; margin-top: 4px; }
button { width: 100%; padding: 12px; background: #4f46e5; color: #fff; border: none; border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer; transition: background 0.2s; }
button:hover { background: #4338ca; }
.error { background: #fef2f2; border: 1px solid #fecaca; color: #dc2626; padding: 10px 12px; border-radius: 8px; margin-bottom: 16px; font-size: 14px; }
.footer { margin-top: 20px; text-align: center; font-size: 13px; color: #888; }
.footer a { color: #4f46e5; text-decoration: none; }
</style>
</head>
<body>
<div class="register-card">
<h2>Create Account</h2>
<p>Register to access the application</p>
{{#if error}}<div class="error">{{ error }}</div>{{/if}}
<form method="POST" action="/oidc/interaction/{{ uid }}/register">
<input type="hidden" name="_csrf" value="{{ csrfToken }}">
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required autocomplete="email" placeholder="you@example.com">
</div>
<div class="form-group">
<label for="username">Username (optional)</label>
<input type="text" id="username" name="username" autocomplete="username" placeholder="your_username">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required autocomplete="new-password" placeholder="At least 12 characters">
<p class="password-hint">Minimum 12 characters</p>
</div>
<div class="form-group">
<label for="confirmPassword">Confirm Password</label>
<input type="password" id="confirmPassword" name="confirmPassword" required autocomplete="new-password" placeholder="Confirm your password">
</div>
<button type="submit">Create Account</button>
</form>
<div class="footer">
Already have an account? <a href="/oidc/interaction/{{ uid }}">Sign in</a>
</div>
</div>
</body>
</html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment