feat: revocable read-only API keys for machine access

Integrations such as the Wabenhain shop sync no longer need a service
account with a password (and without MFA). An admin creates a key under
Verwaltung > Automatisierung > API-Schlüssel.

- Key format ack_<8>_<43>; only its SHA-256 hash is stored, the key is
  shown exactly once. Optional expiry, "last used" timestamp, revocation
  takes effect immediately; create/revoke are audit-logged without the key.
- A key is not a user: JwtAuthGuard sets request.apiKey, PermissionsGuard
  admits it only to routes whose permission is among its scopes and never
  to user-only routes. Scopes come from a short allowlist (shop-sync:read).
- Managing keys requires the new permission api-keys:manage.
- Keys are excluded from backups on purpose; a restore means issuing a new
  key.

Additive migration (new table).

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-09-28 01:24:17 +02:00
1 parent ede86c062a
commit 445fb3b94b
21 files changed
+848 -5

No files matched your search

@@ -0,0 +1,42 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import {
ArrayMinSize,
ArrayUnique,
IsArray,
IsIn,
IsInt,
IsOptional,
IsString,
Max,
MaxLength,
Min,
MinLength,
} from 'class-validator';
import { API_KEY_SCOPES } from '../api-key-scopes';
export class CreateApiKeyDto {
@ApiProperty({
description: 'Wofür der Schlüssel ist, z. B. "Wabenhain-Shop"',
})
@IsString()
@MinLength(1)
@MaxLength(80)
name!: string;
@ApiProperty({ enum: API_KEY_SCOPES, isArray: true })
@IsArray()
@ArrayMinSize(1)
@ArrayUnique()
@IsIn(API_KEY_SCOPES, { each: true })
scopes!: string[];
@ApiPropertyOptional({
description:
'Gültigkeit in Tagen; ohne Angabe gilt der Schlüssel bis zum Widerruf',
})
@IsOptional()
@IsInt()
@Min(1)
@Max(730)
expiresInDays?: number;
}