chore: initial scaffold with running local stack and portal auth

Brings up Dezky's local development environment end-to-end:

Infrastructure (docker-compose):
- Traefik v3.7 reverse proxy with mkcert TLS (v3.2 couldn't speak Docker API 1.54)
- Postgres + Mongo + Redis with healthchecks and init script for per-service users
- Authentik 2025.10 (server + worker) as OIDC IdP
- Stalwart v0.16 mail server (image renamed from stalwartlabs/mail-server)
- OCIS 7.0 with PROXY_TLS=false and OCIS_CONFIG_DIR=/etc/ocis so init writes
  where the server reads
- Collabora office, plus the portal + provisioning service stubs
- Docker network aliases on Traefik so containers resolve auth.dezky.local etc.
  through the network (not host /etc/hosts)
- Docker socket mount parameterized for macOS Docker Desktop symlink path

Authentik provisioning (done via API after stack boot):
- ocis-provider (public client) + OCIS Files application
- dezky-portal provider (confidential) + Dezky Portal application
- Admin API token bound to akadmin manually since 2025.10's
  AUTHENTIK_BOOTSTRAP_TOKEN env var doesn't auto-materialize a token row

Portal (apps/portal):
- Nuxt 3 with nuxt-oidc-auth 1.0.0-beta.11 against generic 'oidc' preset
- Global auth middleware; login at /auth/oidc/login redirects to Authentik
- Visual implementation of Claude Design 'Auth' canvas: AuthShell, NodeMark,
  Auth* sub-components, design tokens as CSS custom properties
- Pages: auth/login, auth/expired, auth/disabled, index (post-login landing)
- mkcert root CA mounted into the portal so Node fetch trusts Authentik's
  self-signed cert (NODE_EXTRA_CA_CERTS) — dev only

Docs:
- AUTHENTIK-SETUP.md updated with manual token bind + portal provider scripted
  alternative
- NEXT-STEPS.md: Phase 1 and Phase 2 marked done with file locations and
  dev-mode caveats

Dev-mode shortcuts that need to be revisited before prod:
- skipAccessTokenParsing on the OIDC config
- NODE_EXTRA_CA_CERTS mkcert mount
- Bootstrap password still the generated value in .env
- Authentik admin token (dezky-bootstrap-token) is non-expiring
This commit is contained in:
Ronni Baslund
2026-05-23 21:25:11 +02:00
commit adfd9baafe
38 changed files with 14705 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
{
"name": "@dezky/provisioning",
"version": "0.0.1",
"private": true,
"description": "Dezky tenant provisioning worker — NestJS",
"scripts": {
"build": "nest build",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"test": "jest",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@nestjs/common": "^10.4.0",
"@nestjs/core": "^10.4.0",
"@nestjs/platform-fastify": "^10.4.0",
"@nestjs/config": "^3.3.0",
"@nestjs/mongoose": "^10.1.0",
"mongoose": "^8.7.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.0"
},
"devDependencies": {
"@nestjs/cli": "^10.4.0",
"@nestjs/testing": "^10.4.0",
"@types/node": "^20.0.0",
"typescript": "^5.5.0",
"ts-node": "^10.9.2"
},
"packageManager": "pnpm@9.12.0"
}
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common'
import { ConfigModule } from '@nestjs/config'
import { MongooseModule } from '@nestjs/mongoose'
import { HealthController } from './health.controller.js'
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
MongooseModule.forRoot(process.env.MONGODB_URI ?? 'mongodb://localhost:27017/dezky'),
],
controllers: [HealthController],
})
export class AppModule {}
@@ -0,0 +1,13 @@
import { Controller, Get } from '@nestjs/common'
@Controller('health')
export class HealthController {
@Get()
check() {
return {
status: 'ok',
service: 'dezky-provisioning',
timestamp: new Date().toISOString(),
}
}
}
+28
View File
@@ -0,0 +1,28 @@
// Dezky Provisioning Service — Entry point
// Handles tenant lifecycle: create, suspend, delete, billing webhooks.
import { NestFactory } from '@nestjs/core'
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify'
import { AppModule } from './app.module.js'
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter({ logger: true }),
)
app.enableCors({
origin: /^https:\/\/([a-z0-9-]+\.)?dezky\.local$/,
credentials: true,
})
const port = Number(process.env.PORT ?? 3001)
await app.listen(port, '0.0.0.0')
console.log(`Provisioning service listening on http://0.0.0.0:${port}`)
}
bootstrap().catch((err) => {
console.error('Failed to start provisioning service', err)
process.exit(1)
})
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext",
"target": "ES2022",
"lib": ["ES2022"],
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"declaration": true,
"removeComments": false,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": true,
"noImplicitAny": true,
"strictBindCallApply": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": false,
"esModuleInterop": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.spec.ts"]
}