feat(services): Phase B — sqlx pool, services, Akten/Frist endpoints
Implements docs/design-kanzlai-integration.md §8 Phase B.
Pool & infrastructure:
- internal/db/pool.go — sqlx connection pool via DATABASE_URL
(lazy, sync.Once, returns nil if unset)
- cmd/server/main.go wires pool + services on startup; skips gracefully
if DATABASE_URL unset (existing endpoints still work)
Services (internal/services/):
- holidays.go — ported from KanzlAI. Audit §1.6 fix: replaces unguarded
map with sync.Map of *yearEntry (sync.Once per year), race-safe under
concurrent readers.
- deadline_calculator.go — ported. days/weeks/months + before/after
timing + holiday/weekend adjustment via HolidayService.
- deadline_rule_service.go — ported, DB-backed. List, GetRuleTree,
GetFullTimeline (recursive CTE for cross-type spawns), GetByIDs,
ListProceedingTypes.
- user_service.go — reads paliad.users; GetByID returns (nil, nil) for
users who haven't onboarded yet (safe default = no visibility).
- akte_service.go — new. Office-scoped visibility enforced at the app
layer (defense-in-depth alongside RLS). ListVisibleForUser uses the
visibility predicate directly in SQL so indexes can drive the query.
Create/Update/Delete enforce role gates:
* associates can only create in their own office
* only admins can move an Akte between offices
* only partners/admins can toggle firm_wide_visible
* only partners/admins can delete (soft, status='archived')
Writes an akten_events row on create, status change, firm-wide toggle,
collaborator change.
- parteien_service.go — ported. Visibility inherited from the parent
Akte via AkteService.GetByID gate.
Sentinel errors:
- services.ErrNotVisible → handlers return 404 (never leak existence)
- services.ErrForbidden → 403
- services.ErrInvalidInput → 400
Auth context:
- internal/auth/user.go — WithUserID middleware extracts the `sub` claim
from the Supabase JWT session cookie and injects uuid.UUID into the
request context. Runs after Client.Middleware (which already validated
the cookie expiry). Handlers use auth.UserIDFromContext().
Handlers (internal/handlers/):
- akten.go — full CRUD for /api/akten + /api/akten/{id}/parteien.
All require DB configured (503 otherwise) and authenticated user
(401 otherwise). Returns 404 for non-visible IDs.
- deadline_rules_db.go — GET /api/deadline-rules, GET
/api/proceeding-types-db, POST /api/deadlines/calculate.
The /api/deadlines/calculate endpoint lives alongside the existing
in-memory /api/tools/fristenrechner; Phase C swaps the UI over and
deletes the in-memory rule tree.
- handlers.Register now takes an optional *Services bundle; when
DATABASE_URL unset the DB-backed endpoints return 503 with a clear
error message.
Tests (internal/services/):
- holidays_test.go — Easter algorithm (5 years spot-checked), German
federal holidays, weekend + Neujahr adjustment, concurrent cache
reads under -race.
- deadline_calculator_test.go — days/weeks/months calc, before timing,
Karfreitag→Ostermontag skip (lands on Tue 2026-04-07), batch with
zero-duration rule.
- akte_service_test.go — live DB test behind `TEST_DATABASE_URL` (skip
otherwise). Verifies 4-Akte × 3-user visibility model AND role
enforcement (associate can't delete, can't cross-office-create,
invalid office rejected).
Manual verification:
- `go build ./...` + `go vet ./...` clean
- `go test ./internal/services/ -race` passes (DB tests skip without URL)
- With TEST_DATABASE_URL set, all visibility + role tests pass
- Live HTTP smoke test with forged JWT cookie:
* /api/deadline-rules returns 40 rules
* /api/proceeding-types-db returns 7 types
* /api/deadlines/calculate INF + 2026-04-15 returns calculated deadlines
* /api/akten returns [] (user has no paliad.users row yet — safe default)
* /login, / still work (no regressions)
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"mgit.msbls.de/m/patholo/internal/auth"
|
||||
"mgit.msbls.de/m/patholo/internal/db"
|
||||
"mgit.msbls.de/m/patholo/internal/handlers"
|
||||
"mgit.msbls.de/m/patholo/internal/services"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -29,22 +30,39 @@ func main() {
|
||||
log.Println("GITEA_TOKEN not set — file proxy will not be able to access private repos")
|
||||
}
|
||||
|
||||
// Apply database migrations before binding the listener.
|
||||
// DATABASE_URL is optional during the Phase A → Phase B transition: the
|
||||
// DATABASE_URL is optional during the Phase A → Phase D transition. The
|
||||
// existing knowledge-platform features (Kostenrechner, Glossar, etc.) work
|
||||
// without a DB. Once Phase B services land, this becomes required.
|
||||
if dbURL := os.Getenv("DATABASE_URL"); dbURL != "" {
|
||||
// without a DB. Akten/Frist endpoints return 503 until DATABASE_URL is set.
|
||||
dbURL := os.Getenv("DATABASE_URL")
|
||||
var svcBundle *handlers.Services
|
||||
if dbURL != "" {
|
||||
log.Println("applying database migrations…")
|
||||
if err := db.ApplyMigrations(dbURL); err != nil {
|
||||
log.Fatalf("migration failed: %v", err)
|
||||
}
|
||||
log.Println("database migrations applied")
|
||||
|
||||
pool, err := db.OpenPool(dbURL)
|
||||
if err != nil {
|
||||
log.Fatalf("open db pool: %v", err)
|
||||
}
|
||||
holidays := services.NewHolidayService(pool)
|
||||
users := services.NewUserService(pool)
|
||||
akteSvc := services.NewAkteService(pool, users)
|
||||
svcBundle = &handlers.Services{
|
||||
Akte: akteSvc,
|
||||
Parteien: services.NewParteienService(pool, akteSvc),
|
||||
Rules: services.NewDeadlineRuleService(pool),
|
||||
Calculator: services.NewDeadlineCalculator(holidays),
|
||||
Users: users,
|
||||
}
|
||||
log.Println("Phase B services initialised")
|
||||
} else {
|
||||
log.Println("DATABASE_URL not set — skipping migrations (Phase A features unavailable)")
|
||||
log.Println("DATABASE_URL not set — Akten/Frist endpoints will return 503")
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
handlers.Register(mux, client, giteaToken)
|
||||
handlers.Register(mux, client, giteaToken, svcBundle)
|
||||
|
||||
log.Printf("paliad server starting on :%s", port)
|
||||
if err := http.ListenAndServe(":"+port, mux); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user