feat: backend foundation — auth, response envelopes, error handling #5

Merged
bjoern merged 2 commits from feat/backend-foundation into main 2026-06-28 12:30:27 +02:00
Member

Phase 1: Backend Foundation

Implements the security and response contract that all subsequent API endpoints will build on.

What's included

Static Bearer Token Auth (ADR-0003)

  • StaticBearerTokenAuthMiddleware validates Authorization: Bearer <token> on all /api/* routes
  • /health remains unauthenticated
  • Uses CryptographicOperations.FixedTimeEquals to prevent timing attacks
  • Token configured via DOUJIN_MANAGER_AUTH_TOKEN environment variable
  • Returns RFC 7807 application/problem+json on auth failure

Response Envelopes (ADR-0007)

  • ResourceResponse<T> — single resource: { data, links, actions }
  • CollectionResponse<T> — collection: { data, page, links, actions }
  • Link — navigational affordance (href, method, type)
  • HypermediaAction — command affordance (name, title, method, href, type, encType)
  • PageInfo — pagination metadata (page, pageSize, totalItems, totalPages)
  • ErrorResponse + ValidationErrorResponse — RFC 7807 problem+json with field-level errors

Global Exception Handling

  • GlobalExceptionMiddleware catches unhandled exceptions and returns 500 with consistent error envelope
  • Includes exception detail in Development environment, generic message in Production

Clean DI Registration

  • AddRestAdapter(token) — registers auth token holder
  • UseRestAdapterMiddleware() — wires exception + auth middleware in correct order

Changes

  • RestAdapter project now references Microsoft.AspNetCore.App framework
  • Added Microsoft.AspNetCore.Mvc.Testing to CPM for test-host support
  • Deleted _ProjectMarker.cs placeholder
  • Server Program.cs reads DOUJIN_MANAGER_AUTH_TOKEN (throws on startup if missing)

Tests (13 new, 70 total)

  • 6 auth tests: health open, missing header, malformed header, wrong token, correct token, case-insensitive Bearer prefix
  • 2 exception tests: unhandled exception → 500 problem+json, normal request unaffected
  • 5 envelope tests: resource/collection construction, error serialization, validation errors, null value omission

All 70 tests pass. 0 warnings, 0 errors.

## Phase 1: Backend Foundation Implements the security and response contract that all subsequent API endpoints will build on. ### What's included **Static Bearer Token Auth** (ADR-0003) - `StaticBearerTokenAuthMiddleware` validates `Authorization: Bearer <token>` on all `/api/*` routes - `/health` remains unauthenticated - Uses `CryptographicOperations.FixedTimeEquals` to prevent timing attacks - Token configured via `DOUJIN_MANAGER_AUTH_TOKEN` environment variable - Returns RFC 7807 `application/problem+json` on auth failure **Response Envelopes** (ADR-0007) - `ResourceResponse<T>` — single resource: `{ data, links, actions }` - `CollectionResponse<T>` — collection: `{ data, page, links, actions }` - `Link` — navigational affordance (href, method, type) - `HypermediaAction` — command affordance (name, title, method, href, type, encType) - `PageInfo` — pagination metadata (page, pageSize, totalItems, totalPages) - `ErrorResponse` + `ValidationErrorResponse` — RFC 7807 problem+json with field-level errors **Global Exception Handling** - `GlobalExceptionMiddleware` catches unhandled exceptions and returns 500 with consistent error envelope - Includes exception detail in Development environment, generic message in Production **Clean DI Registration** - `AddRestAdapter(token)` — registers auth token holder - `UseRestAdapterMiddleware()` — wires exception + auth middleware in correct order ### Changes - RestAdapter project now references `Microsoft.AspNetCore.App` framework - Added `Microsoft.AspNetCore.Mvc.Testing` to CPM for test-host support - Deleted `_ProjectMarker.cs` placeholder - Server `Program.cs` reads `DOUJIN_MANAGER_AUTH_TOKEN` (throws on startup if missing) ### Tests (13 new, 70 total) - **6 auth tests**: health open, missing header, malformed header, wrong token, correct token, case-insensitive Bearer prefix - **2 exception tests**: unhandled exception → 500 problem+json, normal request unaffected - **5 envelope tests**: resource/collection construction, error serialization, validation errors, null value omission All 70 tests pass. 0 warnings, 0 errors.
feat: backend foundation — auth, response envelopes, error handling
All checks were successful
CI / build (pull_request) Successful in 15s
CI / test (pull_request) Successful in 20s
e63bb97e2b
- StaticBearerTokenAuthMiddleware: validates Bearer token on all /api/* routes,
  /health stays open, uses FixedTimeEquals to prevent timing attacks
- Response envelope types (ADR-0007): ResourceResponse<T>, CollectionResponse<T>
  with Link and HypermediaAction records, PageInfo for pagination
- ErrorResponse + ValidationErrorResponse (RFC 7807 problem+json)
- GlobalExceptionMiddleware: catches unhandled exceptions, returns 500 with
  consistent error envelope, includes exception detail in Development
- RestAdapterExtensions: clean DI registration via AddRestAdapter() +
  UseRestAdapterMiddleware()
- RestAdapter now has FrameworkReference to Microsoft.AspNetCore.App
- Server Program.cs updated: token from DOUJIN_MANAGER_AUTH_TOKEN env var
- 13 new tests (6 auth, 2 exception middleware, 5 envelope serialization)
- Total: 70 tests passing

Summary

Summary
Generated on: 06/28/2026 - 10:22:35
Coverage date: 06/28/2026 - 10:22:31 - 06/28/2026 - 10:22:33
Parser: MultiReport (4x Cobertura)
Assemblies: 4
Classes: 61
Files: 55
Line coverage: 26.1% (607 of 2325)
Covered lines: 607
Uncovered lines: 1718
Coverable lines: 2325
Total lines: 3814
Branch coverage: 25.7% (87 of 338)
Covered branches: 87
Total branches: 338
Method coverage: Feature is only available for sponsors

Coverage

DoujinManager.ApplicationCore - 76.7%
Name Line Branch
DoujinManager.ApplicationCore 76.7% ****
DoujinManager.ApplicationCore.Entities.Chapter 87.5%
DoujinManager.ApplicationCore.Entities.Circle 0%
DoujinManager.ApplicationCore.Entities.Doujin 100%
DoujinManager.ApplicationCore.Entities.DoujinCircle 0%
DoujinManager.ApplicationCore.Entities.DoujinPerson 80%
DoujinManager.ApplicationCore.Entities.DoujinTag 75%
DoujinManager.ApplicationCore.Entities.ImageFile 90.9%
DoujinManager.ApplicationCore.Entities.Page 70%
DoujinManager.ApplicationCore.Entities.Person 85.7%
DoujinManager.ApplicationCore.Entities.Tag 100%
DoujinManager.ApplicationCore.Entities.Title 83.3%
DoujinManager.ApplicationCore.Entities.Variant 83.3%
DoujinManager.ApplicationCore.Ids.ChapterId 66.6%
DoujinManager.ApplicationCore.Ids.CircleId 66.6%
DoujinManager.ApplicationCore.Ids.DoujinId 100%
DoujinManager.ApplicationCore.Ids.ImageFileId 66.6%
DoujinManager.ApplicationCore.Ids.PageId 66.6%
DoujinManager.ApplicationCore.Ids.PersonId 66.6%
DoujinManager.ApplicationCore.Ids.TagId 66.6%
DoujinManager.ApplicationCore.Ids.TitleId 66.6%
DoujinManager.ApplicationCore.Ids.VariantId 66.6%
DoujinManager.ApplicationCore.Ports.ExtractedImage 100%
DoujinManager.ApplicationCore.Ports.ImageInspection 100%
DoujinManager.Infrastructure - 21.8%
Name Line Branch
DoujinManager.Infrastructure 21.8% 73%
DoujinManager.Infrastructure.Archives.ZipExtractor 100% 87.5%
DoujinManager.Infrastructure.Data.Configurations.ChapterConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.CircleConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinCircleConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinPersonConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.DoujinTagConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.ImageFileConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.PageConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.PersonConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.TagConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.TitleConfiguration 100%
DoujinManager.Infrastructure.Data.Configurations.VariantConfiguration 100%
DoujinManager.Infrastructure.Data.DoujinManagerDbContext 62.5%
DoujinManager.Infrastructure.Data.GuidIdGenerator 0%
DoujinManager.Infrastructure.Data.Migrations.DoujinManagerDbContextModelSna
pshot
0%
DoujinManager.Infrastructure.Data.Migrations.InitialCreate 0%
DoujinManager.Infrastructure.Data.ModelBuilderExtensions 50%
DoujinManager.Infrastructure.Data.StronglyTypedIdConverterFactory 69.2%
DoujinManager.Infrastructure.Images.SkiaSharpImageInspector 88.2% 69.3%
DoujinManager.Infrastructure.Images.SkiaSharpThumbnailGenerator 94.5% 66.6%
DoujinManager.Infrastructure.Storage.FilesystemImageStorage 100% 100%
DoujinManager.Infrastructure.Storage.FilesystemThumbnailStorage 95% 75%
DoujinManager.RestAdapter - 94.6%
Name Line Branch
DoujinManager.RestAdapter 94.6% 87.5%
DoujinManager.RestAdapter.Auth.StaticBearerTokenAuthMiddleware 100% 91.6%
DoujinManager.RestAdapter.Envelopes.CollectionResponse`1 80%
DoujinManager.RestAdapter.Envelopes.EnvelopeJsonOptions 100%
DoujinManager.RestAdapter.Envelopes.ErrorResponse 100%
DoujinManager.RestAdapter.Envelopes.HypermediaAction 42.8%
DoujinManager.RestAdapter.Envelopes.Link 75%
DoujinManager.RestAdapter.Envelopes.PageInfo 100%
DoujinManager.RestAdapter.Envelopes.ResourceResponse`1 100%
DoujinManager.RestAdapter.Envelopes.ValidationError 100%
DoujinManager.RestAdapter.Envelopes.ValidationErrorResponse 100%
DoujinManager.RestAdapter.Middleware.GlobalExceptionMiddleware 100% 50%
DoujinManager.RestAdapter.RestAdapterExtensions 100% 100%
DoujinManager.Server - 0%
Name Line Branch
DoujinManager.Server 0% 0%
Microsoft.AspNetCore.OpenApi.Generated 0% 0%
Program 0% 0%
System.Runtime.CompilerServices 0%
<!-- coverage-comment --> # Summary <details open><summary>Summary</summary> ||| |:---|:---| | Generated on: | 06/28/2026 - 10:22:35 | | Coverage date: | 06/28/2026 - 10:22:31 - 06/28/2026 - 10:22:33 | | Parser: | MultiReport (4x Cobertura) | | Assemblies: | 4 | | Classes: | 61 | | Files: | 55 | | **Line coverage:** | 26.1% (607 of 2325) | | Covered lines: | 607 | | Uncovered lines: | 1718 | | Coverable lines: | 2325 | | Total lines: | 3814 | | **Branch coverage:** | 25.7% (87 of 338) | | Covered branches: | 87 | | Total branches: | 338 | | **Method coverage:** | [Feature is only available for sponsors](https://reportgenerator.io/pro) | </details> ## Coverage <details><summary>DoujinManager.ApplicationCore - 76.7%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.ApplicationCore**|**76.7%**|****| |DoujinManager.ApplicationCore.Entities.Chapter|87.5%|| |DoujinManager.ApplicationCore.Entities.Circle|0%|| |DoujinManager.ApplicationCore.Entities.Doujin|100%|| |DoujinManager.ApplicationCore.Entities.DoujinCircle|0%|| |DoujinManager.ApplicationCore.Entities.DoujinPerson|80%|| |DoujinManager.ApplicationCore.Entities.DoujinTag|75%|| |DoujinManager.ApplicationCore.Entities.ImageFile|90.9%|| |DoujinManager.ApplicationCore.Entities.Page|70%|| |DoujinManager.ApplicationCore.Entities.Person|85.7%|| |DoujinManager.ApplicationCore.Entities.Tag|100%|| |DoujinManager.ApplicationCore.Entities.Title|83.3%|| |DoujinManager.ApplicationCore.Entities.Variant|83.3%|| |DoujinManager.ApplicationCore.Ids.ChapterId|66.6%|| |DoujinManager.ApplicationCore.Ids.CircleId|66.6%|| |DoujinManager.ApplicationCore.Ids.DoujinId|100%|| |DoujinManager.ApplicationCore.Ids.ImageFileId|66.6%|| |DoujinManager.ApplicationCore.Ids.PageId|66.6%|| |DoujinManager.ApplicationCore.Ids.PersonId|66.6%|| |DoujinManager.ApplicationCore.Ids.TagId|66.6%|| |DoujinManager.ApplicationCore.Ids.TitleId|66.6%|| |DoujinManager.ApplicationCore.Ids.VariantId|66.6%|| |DoujinManager.ApplicationCore.Ports.ExtractedImage|100%|| |DoujinManager.ApplicationCore.Ports.ImageInspection|100%|| </details> <details><summary>DoujinManager.Infrastructure - 21.8%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.Infrastructure**|**21.8%**|**73%**| |DoujinManager.Infrastructure.Archives.ZipExtractor|100%|87.5%| |DoujinManager.Infrastructure.Data.Configurations.ChapterConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.CircleConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinCircleConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinPersonConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.DoujinTagConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.ImageFileConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.PageConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.PersonConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.TagConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.TitleConfiguration|100%|| |DoujinManager.Infrastructure.Data.Configurations.VariantConfiguration|100%|| |DoujinManager.Infrastructure.Data.DoujinManagerDbContext|62.5%|| |DoujinManager.Infrastructure.Data.GuidIdGenerator|0%|| |DoujinManager.Infrastructure.Data.Migrations.DoujinManagerDbContextModelSna<br/>pshot|0%|| |DoujinManager.Infrastructure.Data.Migrations.InitialCreate|0%|| |DoujinManager.Infrastructure.Data.ModelBuilderExtensions|50%|| |DoujinManager.Infrastructure.Data.StronglyTypedIdConverterFactory|69.2%|| |DoujinManager.Infrastructure.Images.SkiaSharpImageInspector|88.2%|69.3%| |DoujinManager.Infrastructure.Images.SkiaSharpThumbnailGenerator|94.5%|66.6%| |DoujinManager.Infrastructure.Storage.FilesystemImageStorage|100%|100%| |DoujinManager.Infrastructure.Storage.FilesystemThumbnailStorage|95%|75%| </details> <details><summary>DoujinManager.RestAdapter - 94.6%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.RestAdapter**|**94.6%**|**87.5%**| |DoujinManager.RestAdapter.Auth.StaticBearerTokenAuthMiddleware|100%|91.6%| |DoujinManager.RestAdapter.Envelopes.CollectionResponse`1|80%|| |DoujinManager.RestAdapter.Envelopes.EnvelopeJsonOptions|100%|| |DoujinManager.RestAdapter.Envelopes.ErrorResponse|100%|| |DoujinManager.RestAdapter.Envelopes.HypermediaAction|42.8%|| |DoujinManager.RestAdapter.Envelopes.Link|75%|| |DoujinManager.RestAdapter.Envelopes.PageInfo|100%|| |DoujinManager.RestAdapter.Envelopes.ResourceResponse`1|100%|| |DoujinManager.RestAdapter.Envelopes.ValidationError|100%|| |DoujinManager.RestAdapter.Envelopes.ValidationErrorResponse|100%|| |DoujinManager.RestAdapter.Middleware.GlobalExceptionMiddleware|100%|50%| |DoujinManager.RestAdapter.RestAdapterExtensions|100%|100%| </details> <details><summary>DoujinManager.Server - 0%</summary> |**Name**|**Line**|**Branch**| |:---|---:|---:| |**DoujinManager.Server**|**0%**|**0%**| |Microsoft.AspNetCore.OpenApi.Generated|0%|0%| |Program|0%|0%| |System.Runtime.CompilerServices|0%|| </details>
Author
Member

🤖 Hermes automated review: changes requested

Thanks for the backend foundation work. I found one blocking security/configuration issue and one non-blocking regression while reviewing PR #5.

Blocking findings

  • Major — empty/whitespace auth tokens are accepted at startup (backend/src/DoujinManager.Server/Program.cs:12, backend/src/DoujinManager.RestAdapter/RestAdapterExtensions.cs:17)
    • The server only checks whether DOUJIN_MANAGER_AUTH_TOKEN is null. If the variable is set to "" or whitespace, the app starts with a weak/empty shared secret. Because this is the only protection for /api/*, startup/DI should fail closed for empty or whitespace tokens.
    • Suggested fix: validate with string.IsNullOrWhiteSpace(authToken) before AddRestAdapter, and ideally also guard inside AddRestAdapter(string authToken) so tests/other hosts cannot register an empty token.

Minor findings

  • OpenAPI registration was accidentally removed (backend/src/DoujinManager.Server/Program.cs:45-47)
    • app.MapOpenApi() still runs in Development, but builder.Services.AddOpenApi() was removed from service registration. In a local runtime smoke test with Development enabled, /openapi/v1.json returned 404 No OpenAPI document with the name 'v1' was found.
    • Suggested fix: restore builder.Services.AddOpenApi(); unless intentionally dropping the development OpenAPI endpoint.

Checks performed

  • Static scan: no shell/eval/pickle/SQL-injection patterns found. The only token-like hit was the test constant ValidToken = "test-token-12345", which I treated as a test fixture rather than a secret.
  • dotnet build --no-restore: initially failed because restored assets did not yet include the newly added TestHost package.
  • Bounded dotnet restore, then dotnet build --no-restore: passed with 0 warnings/errors.
  • dotnet test --no-restore --no-build: passed all 70 tests.
  • Runtime smoke test: /health returned 200; unauthenticated /api/... returned 401; Development /openapi/v1.json returned the 404 noted above.

Automated daily review. I never merge PRs.

## 🤖 Hermes automated review: changes requested Thanks for the backend foundation work. I found one blocking security/configuration issue and one non-blocking regression while reviewing PR #5. ### Blocking findings - **Major — empty/whitespace auth tokens are accepted at startup** (`backend/src/DoujinManager.Server/Program.cs:12`, `backend/src/DoujinManager.RestAdapter/RestAdapterExtensions.cs:17`) - The server only checks whether `DOUJIN_MANAGER_AUTH_TOKEN` is `null`. If the variable is set to `""` or whitespace, the app starts with a weak/empty shared secret. Because this is the only protection for `/api/*`, startup/DI should fail closed for empty or whitespace tokens. - **Suggested fix:** validate with `string.IsNullOrWhiteSpace(authToken)` before `AddRestAdapter`, and ideally also guard inside `AddRestAdapter(string authToken)` so tests/other hosts cannot register an empty token. ### Minor findings - **OpenAPI registration was accidentally removed** (`backend/src/DoujinManager.Server/Program.cs:45-47`) - `app.MapOpenApi()` still runs in Development, but `builder.Services.AddOpenApi()` was removed from service registration. In a local runtime smoke test with Development enabled, `/openapi/v1.json` returned `404 No OpenAPI document with the name 'v1' was found.` - **Suggested fix:** restore `builder.Services.AddOpenApi();` unless intentionally dropping the development OpenAPI endpoint. ### Checks performed - Static scan: no shell/eval/pickle/SQL-injection patterns found. The only token-like hit was the test constant `ValidToken = "test-token-12345"`, which I treated as a test fixture rather than a secret. - `dotnet build --no-restore`: initially failed because restored assets did not yet include the newly added TestHost package. - Bounded `dotnet restore`, then `dotnet build --no-restore`: passed with 0 warnings/errors. - `dotnet test --no-restore --no-build`: passed all 70 tests. - Runtime smoke test: `/health` returned 200; unauthenticated `/api/...` returned 401; Development `/openapi/v1.json` returned the 404 noted above. Automated daily review. I never merge PRs.
fix: reject empty/whitespace auth tokens, restore AddOpenApi()
All checks were successful
CI / build (pull_request) Successful in 14s
CI / test (pull_request) Successful in 21s
81cef089a2
Address PR #5 review feedback:

1. Empty/whitespace token guard (Major):
   - Program.cs: check IsNullOrWhiteSpace after reading env var
   - AddRestAdapter(): ArgumentException if token is null/empty/whitespace
   - 5 new tests (null, empty, whitespace, tab, valid token accepted)

2. Restore AddOpenApi() (Minor):
   - Accidentally removed during AddRestAdapter refactor
   - Restored in Program.cs after AddRestAdapter() call

75 tests pass, 0 warnings, 0 errors.
Author
Member

Both findings addressed in commit 81cef08.

1. Empty/whitespace auth tokens (Major) — fixed

  • Program.cs now checks string.IsNullOrWhiteSpace(authToken) after reading the env var, before AddRestAdapter.
  • AddRestAdapter(string authToken) itself also validates via ArgumentException — any caller (tests, other hosts) is protected.
  • 5 new tests covering null, "", " ", "\t", and a valid-token acceptance test.

2. AddOpenApi() accidentally removed (Minor) — fixed

  • Restored builder.Services.AddOpenApi(); in Program.cs after the AddRestAdapter() call. /openapi/v1.json will work again in Development.

75 tests pass (5 new), 0 warnings, 0 errors.

Both findings addressed in commit `81cef08`. ### 1. Empty/whitespace auth tokens (Major) — fixed - `Program.cs` now checks `string.IsNullOrWhiteSpace(authToken)` after reading the env var, before `AddRestAdapter`. - `AddRestAdapter(string authToken)` itself also validates via `ArgumentException` — any caller (tests, other hosts) is protected. - 5 new tests covering `null`, `""`, `" "`, `"\t"`, and a valid-token acceptance test. ### 2. `AddOpenApi()` accidentally removed (Minor) — fixed - Restored `builder.Services.AddOpenApi();` in `Program.cs` after the `AddRestAdapter()` call. `/openapi/v1.json` will work again in Development. **75 tests pass** (5 new), 0 warnings, 0 errors.
bjoern merged commit 410cc3a078 into main 2026-06-28 12:30:27 +02:00
bjoern deleted branch feat/backend-foundation 2026-06-28 12:30:28 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
TeamAI/doujin-manager!5
No description provided.