Skip to content

Merge develop into main (Phase 2: Identity + CRM + Groups) - #139

Closed
derpixler wants to merge 5 commits into
mainfrom
merge/develop-to-main
Closed

Merge develop into main (Phase 2: Identity + CRM + Groups)#139
derpixler wants to merge 5 commits into
mainfrom
merge/develop-to-main

Conversation

@derpixler

Copy link
Copy Markdown
Owner

Merge-branch from main, bringing develop's Phase 2 (Sprint 02 — #132) into main. Conflicts (5 files: godoc #126 vs Phase-2 rewrites) auto-resolved toward develop (Phase 2 superset, develop already contains the godoc via #125). Automatically merged with -X theirs.

derpixler and others added 5 commits June 24, 2026 14:06
- Package-level doc comments for all 12 packages
- Exported types, functions, methods documented
- Inline comments for non-obvious code paths
- RequirePermission: explicit TODO for Phase 2 RBAC
- ActorMiddleware: explained as future request-scoped init
- ScheduledJobs: documented as placeholder for future phases

19 files, no functional changes.
Docs: godoc comments on all exported symbols
* Phase 2: auth foundations — password hashing, JWT, mail service

- internal/auth: bcrypt password hash/verify; HS256 JWT issue/verify
  (access + 2FA-pending tokens, alg-confusion/expiry/issuer checks)
- internal/core/mail: Mailer interface, NoopMailer, stdlib SMTP mailer
- deps: add golang-jwt/jwt/v5; promote x/crypto to direct

* Phase 2: add secrets (AES-256-GCM) and require ENCRYPTION_KEY

- internal/core/secrets: Cipher with SHA-256 key derivation, base64
  nonce||ciphertext; round-trip, tamper, wrong-key tests
- config: ENCRYPTION_KEY now required (.env.example + config tests)

* Phase 2: add dbexec actor transaction wrapper

- internal/core/dbexec: WithActor/WithActorOptions run work in a tx that
  sets app.actor_user_id (audit) and optionally app.allow_delete
- testcontainers integration test: audit actor recorded, rollback on
  error, soft-delete guard enforced and bypassed

* Phase 2: bootstrap sqlc query layer (internal/db)

- fix sqlc.yaml paths (were not resolved relative to the config dir, so
  generation never worked): queries, schema, out
- add sqlc/queries/users.sql (GetUserByID, CountActiveUsers)
- generate internal/db: db.go, models.go (all tables), querier.go, users.sql.go

* Phase 2: add HTML email support to core mail (#22)

- mail.Message gains HTML field; SMTPMailer builds multipart/alternative
  (text+html), or text/html when only HTML is set
- add Renderer (html/template) for rendering HTML email bodies
- tests: multipart parsing, html-only, renderer render/escape/errors

* Phase 2: add generic EAV metadata handler (#23)

- internal/core/metadata: Store over the 12 *_meta tables (allow-listed),
  Get/Set(upsert)/Delete/All; DBTX works on pool or actor tx
- .golangci.yml: exclude gosec for metadata (dynamic table name from a
  fixed allow-list), matching the database/ precedent
- tests: allow-list unit test + testcontainers CRUD integration

* Phase 2: real JWT auth middleware + permission checks (#127)

- middleware: Actor gains Permissions + HasRole/HasPermission; replace
  AuthSkeleton with Authenticate(Verifier) (injected, no import cycle);
  RequirePermission checks real permissions (admin = wildcard);
  ActorMiddleware propagates the actor into the request context
- app/router + cmd/api: NewRouter takes a middleware.Verifier; main builds
  it from auth.TokenManager (access tokens only)
- tests: authenticate none/valid/invalid, permission grant/deny, context
  propagation; router/main updated to the new signature

* Phase 2: add German full-text search helper (#24)

- internal/core/search: Searcher over the 7 search_vector tables
  (allow-listed); websearch_to_tsquery('german', ...) + ts_rank ranking,
  deleted_at filter, limit clamping, empty-query short-circuit
- .golangci.yml: exclude gosec for search (dynamic table from allow-list)
- tests: allow-list + empty-query unit; testcontainers integration
  (match, case-insensitive, no-match, soft-delete exclusion)

* Phase 2: auth user repository + queries; fix audit trigger (#12)

- internal/auth/repository.go: Repository with user methods; reads on pool,
  writes via dbexec.WithActor (audit + soft-delete enforcement)
- sqlc/queries/users.sql: CreateUser, GetUserByID/Email, ListUsers,
  CountActiveUsers, UpdateUser, SoftDeleteUser, UserExistsByEmail (+ regen db)
- schema.sql: audit_trigger_func() reads OLD/NEW fields via to_jsonb(...)->>'col'
  (NULL-safe), fixing 42703 on UPDATE for audited tables without
  status/deleted_at/anonymized_at (e.g. users)
- testcontainers integration: user CRUD + audit actor attribution

Partial #12 (users); roles/permissions to follow.

* Phase 2: auth roles, permissions & resolution repository (#12)

- sqlc/queries/roles.sql: ListRoles, GetRole, AssignRole (idempotent),
  RemoveRole, ListUserRoles, UserHasRole
- sqlc/queries/permissions.sql: ListPermissions, GetPermissionsForUser
  (user_roles -> role_permissions, active roles), GetPermissionsForRole,
  AddRolePermission, RemoveRolePermission (+ regen db)
- internal/auth/repository.go: role/user-role + permission methods
  (join tables are not audited/soft-deletable -> direct pool writes)
- testcontainers integration: role assign/remove/list, permission
  resolution (mitglied=7, admin=47, DISTINCT), role_permissions CRUD

Completes #12 (users + roles + permissions + user_roles + role_permissions).

* Phase 2: role-assignment endpoints + OpenAPI contract (#17)

- internal/auth: Service, Handler, DTOs, RegisterRoutes; establishes the
  handler/service/repository HTTP pattern. Endpoints GET/POST
  /api/users/:id/roles and DELETE /api/users/:id/roles/:slug
  (RequirePermission users.read / users.write)
- internal/app/router.go: mount auth routes; serve GET /api/openapi.yaml
- api/openapi.yaml + api/embed.go: spec-first OpenAPI 3.1 contract
  (health + role endpoints), embedded via go:embed
- internal/app/openapi_test.go: kin-openapi spec validation + route-parity
  (every mounted route documented, every operation mounted)
- testcontainers HTTP integration: assign/list/remove + 401/403/404/422
- deps: add getkin/kin-openapi

Completes #17.

* Phase 2: permission check with admin bypass (#18)

- sqlc/queries/permissions.sql: UserHasPermission (role_permissions join)
- internal/auth: Repository.UserHasPermission; Service.CheckPermission
  (admin role grants all permissions implicitly, else resolve via roles)
- testcontainers integration: kassierer grant/deny, admin bypass (incl. a
  non-catalog permission), role-less user denied, seed 47 perms / 5 roles

Completes #18.

* Phase 2: groups CRUD module + endpoints + OpenAPI (#20)

- sqlc/queries/groups.sql: Create/Get/List/ListByType/Update/SoftDelete
- internal/groups: Repository (writes via dbexec.WithActor), Service
  (group_type validation, NotFound mapping), Handler + RegisterRoutes;
  endpoints GET/POST /api/groups, GET/PATCH/DELETE /api/groups/:id
  (RequirePermission groups.read/groups.write), optional ?group_type filter
- internal/app/router.go: mount group routes
- api/openapi.yaml: /groups + /groups/{id} paths and schemas
- testcontainers integration: repo CRUD/FindByType/soft-delete/audit; HTTP
  create/list/filter/get/update/delete + 401/403/404/422

Completes #20 (group CRUD; members tracked in #21).

* Phase 2: group members module + endpoints + OpenAPI (#21)

- sqlc/queries/group_members.sql: AddMember (upsert role), RemoveMember,
  ListMembers, ListUserGroups
- internal/groups: member repository (direct pool; not audited),
  Service.AddMember/ListMembers/RemoveMember/ListUserGroups (role_in_group
  validation, group existence -> 404), handler + member routes
  (GET/POST /api/groups/:id/members, DELETE .../:user_id, GET /api/users/:id/groups)
- api/openapi.yaml: member + user-groups paths and schemas
- testcontainers integration: member repo (add/upsert-role/remove/list);
  HTTP add/list/user-groups/remove + 401/403/404/422

Completes #21.

* Phase 2: CRM module (address, contacts, preferences) + OpenAPI (#19)

- sqlc/queries/{crm,contacts}.sql: address & preferences upsert/get;
  contacts list/get/create/update/soft-delete + clear-primary-per-type
- internal/crm: Repository (address/preferences direct pool; contacts via
  dbexec.WithActor with clear-primary tx), Service (validation, NotFound,
  is_primary orchestration, belongs-to-user check), Handler + RegisterRoutes;
  endpoints GET/PUT /api/users/:id/address, GET/PUT .../preferences,
  GET/POST .../contacts, PATCH/DELETE .../contacts/:cid
- internal/app/router.go: mount CRM routes
- api/openapi.yaml: address/preferences/contacts paths and schemas
- testcontainers integration: address 1:1, preferences, contacts CRUD +
  is_primary/SetPrimary/audit; HTTP CRUD + errors

Completes #19.

* Phase 2: auth & users HTTP - login, register, user CRUD, search (#128)

- internal/auth: NewVerifier (token->Actor, moved from main); Service gains
  TokenManager + Login (verify password -> token with roles+permissions),
  Register (admin-gated; 409 on duplicate email), ListUsers/GetUser/UpdateUser/
  DeleteUser, SearchUsers (core/search #24 + GetUsersByIDs, rank-ordered)
- routes: POST /api/auth/login (public), POST /api/auth/register (users.write),
  GET /api/users, GET/PATCH/DELETE /api/users/:id, GET /api/search/users
- wiring: NewRouter takes *auth.TokenManager; auth.RegisterRoutes(rg, pool, tm)
- sqlc: GetUsersByIDs (regen db)
- api/openapi.yaml: login/register/users/search paths and schemas
- testcontainers integration: login (valid/invalid/claims), register
  (create/dup/validation), user CRUD, search + permission/401/403/404

Completes #128.

* Phase 2: TOTP 2FA service core + secrets cipher wiring (#15, #16)

- deps: add pquerna/otp (TOTP codes + provisioning URIs)
- sqlc/queries/totp.sql: Get/Upsert/DeleteTOTPSecret; regen db
- internal/auth: TOTP repo methods; Service extended with Setup2FA
  (secret + recovery codes), Confirm2FA, Verify2FA (lockout after 5
  failures), ConsumeRecoveryCode (single-use), Disable2FA,
  IssueAccessForUser; cipher is passed through NewRouter/RegisterRoutes/
  NewService for TOTP secret encryption
- wiring: main builds secrets.Cipher from ENCRYPTION_KEY, passes through
  NewRouter -> auth.RegisterRoutes -> Service

Partial #15, #16 (TOTP foundation; endpoints + login integration to follow).

* Phase 2: 2FA endpoints + login integration (#15, #16)

- Login service: checks TOTP state; returns a pending token and
  requires_2fa flag when 2FA is active instead of a full access token
- Handler: Login responds {requires_2fa, temp_token} when 2FA active
- 2FA endpoints: setup (provisioning URI + recovery codes), confirm
  (activates TOTP), verify (temp_token + code -> access token),
  recovery (redeem single-use code), disable (code required)
- loginResponse gained Requires2FA / TempToken fields
- api/openapi.yaml: 2FA paths and schemas
- testcontainers integration: full 2FA flow (setup/confirm/login-2fa/
  verify/recovery/disable) with real TOTP, #15/#16

Completes #15 and #16.

* Phase 2: E2E registration flow test (#25)

- internal/auth/e2e_test.go: full HTTP integration exercising
  register -> role assign -> login -> profile -> 2FA setup+confirm ->
  login-2fa -> verify -> profile, end-to-end via the public API

Completes #25 (and Sprint 02).

* Phase 2: hybrid test suite + API documentation (#129)

- scripts/lib/common.sh: extracted harness (colors, helpers, global steps:
  prereqs, build, lint, coverage, compose, docker-build, makefile, full-check)
- scripts/tests/tp2.sh: curated TP2 unit (§§ 1.1–1.12) + integration steps
  (§§ 2.1–2.20) across all modules (auth/crm/groups)
- scripts/test.sh: combined TP1+TP2 dispatcher (menu, --ci mode, backward-
  compatible step numbers 01–10; 04/05 aggregate both phases)
- docs/testing.md: test suite usage, CI, per-package test commands
- docs/api/{auth,crm,groups}.md: endpoint overviews with examples
- docs/README.md: documentation index

Completes #129.

* Phase 2: add timestamped log files to test suite (#129)

- scripts/test.sh: each run writes a timestamped log to logs/test-<YYYYmmdd-HHMMSS>.log
  via exec > >(tee ...) 2>&1
- .gitignore: exclude logs/
- fix CI_MODE label display

* Phase 2: promote pquerna/otp to direct dependency (#15, #16)

* Phase 2: improve test coverage to ~80% (#130)

- auth (74.8->80.5%): error paths for RemoveRole (404), DeleteUser (404),
  UpdateUser (422/404), pagination query params
- 2FA: Confirm2FA wrong-code (422), Disable2FA wrong-code (401)
- groups (74.4->79.0%): Delete nonexistent (404), RemoveMember nonexistent
  group (404), pagination happy/invalid paths
- crm (76.1->79.6%): DeleteContact nonexistent (404), GetPreferences
  nonexistent user (404)
- search: limit>maxLimit clamp branch

Completes #130.

* fix: change default port from 8080 to 8088 (port conflict)

8080 was occupied by another service (WordPress) on the dev machine.
8088 is free in the 80xx range.

- .env.example, config.go: default APP_PORT -> 8088
- config_test.go: update default port assertion
- docker-compose.prod.yml: port mapping, env, healthcheck -> 8088
- scripts/lib/common.sh: health check curl -> localhost:8088
- README.md: quick-start example -> localhost:8088

* fix: support 'help' and '--help' in test.sh (#131)

* fix: show detailed failure list in test suite summary

- FAILED_STEPS array collects failed check descriptions
- run_cmd, run_go_test and direct FAILED increments populate it
- summary() lists each failure below the count line

* fix: coverage step no longer marks FAIL when threshold is met

go test -cover exits non-zero if any package has 0% coverage (cmd/api),
which caused a cosmetic FAIL. The threshold check now handles PASS/FAIL
independently.

* Phase 2: password reset (forgot + reset via email) (#133)

- sqlc/queries/users.sql: UpdatePassword query (+ regen db)
- internal/auth: Repository.UpdatePassword, Service.ForgotPassword
  (bcrypt token + users_meta store + email via mailer, no enumeration),
  Service.ResetPassword (token verify, expiry, single-use, password update)
- routes: POST /auth/password/forgot + POST /auth/password/reset (public)
- wiring: mail.Mailer passed through NewRouter/RegisterRoutes/Service
  (also unblocks Email-OTP for #15/#16 follow-up)
- api/openapi.yaml: password reset paths and schemas
- test: forgot flow, wrong token, invalid id, missing fields (#133)

Completes #133.

* feat: add self-hosted Redoc API docs at GET /api/docs

- api/redoc.js: vendored redoc.standalone.js (~1MB, MIT license)
- api/redoc.html: minimal HTML loading Redoc against /api/openapi.yaml
- api/embed.go: embed both redoc files
- router: serve GET /api/docs + GET /api/docs/redoc.js
- openapi_test: exempt /docs + /docs/redoc.js from route parity

* test: cover password reset expiry + used-flag edge cases (#133)

* docs: add RBAC and password reset architecture docs

* feat: email-2FA as a standalone second factor (#134)

Email-OTP second factor parallel to TOTP: setup/confirm/verify/resend/disable. 6-digit codes (bcrypt-hashed, single-use, 10m expiry, 5-failure lockout) stored in users_meta. Login requires 2FA if TOTP or email is enabled. Adds OpenAPI paths + TempTokenRequest schema and auth API docs.

* test: detailed gated logging + email-2FA tests

doReq logs request/response; tstep/tlog/assertStatus helpers (HTTP in auth_test, white-box in package auth via testlog_test.go). Password redacted, JWT collapsed to eyJ…, gated behind -v. Adds email-2FA flow + lockout tests.

* test: add Email-2FA step 2.21 to TP2 integration suite

* fix: provide ENCRYPTION_KEY in docker-compose.prod.yml

App config requires ENCRYPTION_KEY; add it (dev default, overridable) so the prod-compose health check no longer panics on startup.

* feat(core): add module SDK skeleton (Module interface + Registry)

* feat(core): add EventBus, Cache, Searcher seams + wire into module Deps

* refactor(app): module-driven router via module.Registry + module adapters

* refactor(auth): introduce identity Provider seam (local default, OIDC-ready)

* refactor(app): composition-root module assembly + registry lifecycle
@derpixler
derpixler force-pushed the merge/develop-to-main branch from f616b79 to f4318c8 Compare June 29, 2026 13:32
@derpixler derpixler closed this Jun 30, 2026
@derpixler
derpixler deleted the merge/develop-to-main branch June 30, 2026 06:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant