Files
teOS/apps/api/src/modules/setup/setup.controller.ts
Flexomatic81 0e8d5aef85 feat: add Docker deployment, web installer, and local test environment
- Multi-stage Dockerfiles for API (NestJS) and Web (Next.js standalone)
- docker-compose.prod.yml: full production stack (postgres, redis, keycloak,
  api, web) with optional Caddy/Let's Encrypt via --profile ssl
- docker-compose.local.yml: identical local test stack, all ports exposed
- docker/postgres/init.sql: auto-creates tos_app DB on first start
- Caddyfile: reverse proxy for app domain + auth subdomain
- install.sh: interactive installer (domain, SSL mode, secret generation)
- NestJS SetupModule: @Public() endpoints for /setup/status, /setup/admin,
  /setup/branding, /setup/complete with setup-token guard
- Web installer: 4-step flow (system check, admin creation, branding, complete)
  at /[locale]/setup/* with public middleware bypass
- i18n: installer namespace added to de.json and en.json
- CORS: x-setup-token header allowed in main.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 21:17:34 +01:00

68 lines
2.5 KiB
TypeScript

import { Controller, Get, Post, Put, Body, Headers } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiHeader, ApiResponse } from '@nestjs/swagger';
import { Public } from '../../auth/decorators/public.decorator';
import { SetupService } from './setup.service';
import { CreateAdminDto } from './dto/create-admin.dto';
import { SaveBrandingDto } from './dto/save-branding.dto';
@ApiTags('setup')
@Public()
@Controller('setup')
export class SetupController {
constructor(private readonly setupService: SetupService) {}
@Get('status')
@ApiOperation({ summary: 'Get current setup status' })
@ApiResponse({
status: 200,
description: 'Returns whether setup is completed and token is configured',
schema: {
type: 'object',
properties: {
completed: { type: 'boolean', example: false },
tokenConfigured: { type: 'boolean', example: true },
},
},
})
getStatus() {
return this.setupService.getStatus();
}
@Post('admin')
@ApiOperation({ summary: 'Create initial admin user in Keycloak and local DB' })
@ApiHeader({ name: 'x-setup-token', required: true })
@ApiResponse({ status: 201, description: 'Admin user created' })
@ApiResponse({ status: 401, description: 'Invalid setup token' })
@ApiResponse({ status: 403, description: 'Setup already completed' })
@ApiResponse({ status: 409, description: 'User already exists in Keycloak' })
createAdmin(
@Body() dto: CreateAdminDto,
@Headers('x-setup-token') token: string,
) {
return this.setupService.createAdmin(dto, token ?? '');
}
@Put('branding')
@ApiOperation({ summary: 'Save branding settings (app name, company, logo)' })
@ApiHeader({ name: 'x-setup-token', required: true })
@ApiResponse({ status: 200, description: 'Branding settings saved' })
@ApiResponse({ status: 401, description: 'Invalid setup token' })
@ApiResponse({ status: 403, description: 'Setup already completed' })
saveBranding(
@Body() dto: SaveBrandingDto,
@Headers('x-setup-token') token: string,
) {
return this.setupService.saveBranding(dto, token ?? '');
}
@Post('complete')
@ApiOperation({ summary: 'Mark setup as completed' })
@ApiHeader({ name: 'x-setup-token', required: true })
@ApiResponse({ status: 201, description: 'Setup marked as completed' })
@ApiResponse({ status: 401, description: 'Invalid setup token' })
@ApiResponse({ status: 403, description: 'Setup already completed' })
completeSetup(@Headers('x-setup-token') token: string) {
return this.setupService.completeSetup(token ?? '');
}
}