Extract internal/projects.Service.Create (H-9) #104

Merged
qwc merged 1 commit from refactor/projects-service-create into main 2026-05-18 19:48:30 +02:00
Owner

Summary

Workstream item #9. Audit finding H-9 (High) — three divergent project-create paths.

handleAdminCreateProject (admin form), handleAPICreateProject (POST /api/projects), and autoCreateProject (upload to nonexistent slug) each re-implemented the same sequence: slug validation, visibility defaulting, public-visibility-admin gate, projects.Create, EnsureProjectDir, auto-grant editor access. They had already drifted — the admin form was missing slug validation pre-#96, the API path didn't parse retention_days, auto-create skipped both visibility validation and retention. Several earlier audit findings (C-5, M-5) had to be fixed in parallel across all three.

Changes

  • New internal/projects package with Service.Create(ctx, CreateOptions). Validation + visibility gate + persist + storage-dir + creator auto-grant all live here. Storage-dir failure is non-fatal (matches prior behavior).
  • Sentinel errors for typed mapping at the HTTP layer:
    • ErrInvalidSlug
    • ErrInvalidVisibility
    • ErrPublicRequiresAdmin
    • ErrSlugConflict
  • The three handlers each reduce to: parse request → service.Create → switch on error kind. Net 63-line reduction across them.
  • Auto-create uses errors.Is(err, projects.ErrSlugConflict) to fall back to GetBySlug for the race-recovery path that was previously matched by substring on the error message.
  • Service unit tests (7 cases): invalid slug / invalid visibility / default name / public-by-editor rejected / public-by-admin allowed / creator auto-grant on custom / no auto-grant for admin / slug conflict.
  • Wiring: the service is constructed inside handler.New from the existing Deps. main.go is unchanged.

Test plan

  • go test ./... passes — including all the existing handler tests around create (TestEditorCanCreateProject, TestEditorCreateCustomProjectGetsAccess, the public-visibility-admin set, auto_create_test)
  • New service tests cover the validation, visibility, and grant matrix
  • Manual: editor creates private project via admin form → succeeds, gets auto-grant
  • Manual: editor tries visibility=public via API → 403
  • Manual: editor uploads to nonexistent slug with auto_create: true → project appears with private visibility and editor sees it on their list

🤖 Generated with Claude Code

## Summary Workstream item #9. Audit finding **H-9 (High)** — three divergent project-create paths. `handleAdminCreateProject` (admin form), `handleAPICreateProject` (POST /api/projects), and `autoCreateProject` (upload to nonexistent slug) each re-implemented the same sequence: slug validation, visibility defaulting, public-visibility-admin gate, `projects.Create`, `EnsureProjectDir`, auto-grant editor access. They had already drifted — the admin form was missing slug validation pre-#96, the API path didn't parse `retention_days`, auto-create skipped both visibility validation and retention. Several earlier audit findings (C-5, M-5) had to be fixed in parallel across all three. ## Changes - **New `internal/projects` package** with `Service.Create(ctx, CreateOptions)`. Validation + visibility gate + persist + storage-dir + creator auto-grant all live here. Storage-dir failure is non-fatal (matches prior behavior). - **Sentinel errors** for typed mapping at the HTTP layer: - `ErrInvalidSlug` - `ErrInvalidVisibility` - `ErrPublicRequiresAdmin` - `ErrSlugConflict` - **The three handlers** each reduce to: parse request → `service.Create` → switch on error kind. Net 63-line reduction across them. - **Auto-create** uses `errors.Is(err, projects.ErrSlugConflict)` to fall back to `GetBySlug` for the race-recovery path that was previously matched by substring on the error message. - **Service unit tests** (7 cases): invalid slug / invalid visibility / default name / public-by-editor rejected / public-by-admin allowed / creator auto-grant on custom / no auto-grant for admin / slug conflict. - **Wiring**: the service is constructed inside `handler.New` from the existing `Deps`. `main.go` is unchanged. ## Test plan - [x] `go test ./...` passes — including all the existing handler tests around create (`TestEditorCanCreateProject`, `TestEditorCreateCustomProjectGetsAccess`, the public-visibility-admin set, `auto_create_test`) - [x] New service tests cover the validation, visibility, and grant matrix - [ ] Manual: editor creates private project via admin form → succeeds, gets auto-grant - [ ] Manual: editor tries `visibility=public` via API → 403 - [ ] Manual: editor uploads to nonexistent slug with `auto_create: true` → project appears with private visibility and editor sees it on their list 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Extract internal/projects.Service.Create (H-9)
All checks were successful
CI / build (pull_request) Successful in 58s
CI / docker (pull_request) Has been skipped
CI / test (pull_request) Successful in 1m22s
e587989304
Audit finding H-9. Three project-create paths each duplicated the same
sequence: slug validation, visibility defaulting and validation,
public-visibility-admin-only gate, projects.Create, EnsureProjectDir,
auto-grant editor access to the creator. They had already drifted in
subtle ways: the admin form skipped slug validation pre-PR #96, the API
path didn't parse retention_days, auto-create skipped both visibility
validation and retention. Several earlier audit findings (C-5, M-5)
existed in parallel across the three paths.

New internal/projects package with Service.Create(ctx, CreateOptions):
  - Validation (slug, visibility) returns typed sentinel errors.
  - Public-visibility-admin-only check (C-5) lives once.
  - Auto-grant editor to non-admin creator on non-public projects.
  - Storage-directory creation is non-fatal (logged, project still
    persisted) — matches prior behavior, no first-upload regressions.
  - Slug-conflict returns ErrSlugConflict; auto-create handler uses
    errors.Is to fall back to GetBySlug (preserving the race-recovery
    path).

Sentinel errors:
  - ErrInvalidSlug
  - ErrInvalidVisibility
  - ErrPublicRequiresAdmin
  - ErrSlugConflict

handleAdminCreateProject, handleAPICreateProject, autoCreateProject each
reduce to: parse request → service.Create → map errors to HTTP status.
Net 63-line reduction across the three handlers; one rule per concern,
not three.

Service constructed in handler.New from existing Deps (no main.go change
required) and exposed as h.projectService.

7 service unit tests cover the matrix: invalid slug, invalid visibility,
default name, public-by-editor rejected, public-by-admin allowed,
creator grant for non-admin on custom, no auto-grant for admin, slug
conflict. Full test suite still passes (validates handler paths still
behave the same as before the refactor).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
qwc merged commit edd8b59ebe into main 2026-05-18 19:48:30 +02:00
qwc deleted branch refactor/projects-service-create 2026-05-18 19:48:30 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
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
qwc-open/asiakirjat!104
No description provided.