Skip to content

feat(server): admin-initiated password reset#74

Merged
dvcdsys merged 3 commits into
developfrom
feat/admin-password-reset
Jun 5, 2026
Merged

feat(server): admin-initiated password reset#74
dvcdsys merged 3 commits into
developfrom
feat/admin-password-reset

Conversation

@dvcdsys
Copy link
Copy Markdown
Owner

@dvcdsys dvcdsys commented Jun 5, 2026

What

Adds an admin-only password reset for existing users, plus an incidental
reconciliation of the OpenAPI codegen drift.

POST /api/v1/admin/users/{id}/reset-password — an admin sets a new temporary
password; the server flags the user must_change_password=1 and revokes the
target's existing sessions, so on next login the existing ChangePasswordPage
forces a new password (same as first login). Any admin may reset any user,
including another admin (a reset neither demotes nor disables anyone, so no
last-admin guard applies). Dashboard gets a "Reset password" action in the
users table.

Why

The only way to set a forced-change password was at user creation
(InviteUserDialog). There was no equivalent for an existing user who is
locked out or requests a reset. This mirrors the invite flow end-to-end and
reuses the existing first-login must_change_password machinery.

How

Two commits, each builds and tests green independently:

  1. feat(server): admin-initiated password reset — the feature.

    • users.AdminResetPassword: sibling of UpdatePassword that sets the
      flag instead of clearing it.
    • Handler gates on mustBeAdmin, validates new_password (≥8), revokes
      sessions via Sessions.DeleteAllForUser, returns the updated user.
    • Dashboard: ResetPasswordDialog (cloned from InviteUserDialog) + the
      useResetUserPassword hook, wired into the users table.
  2. chore(openapi): regenerate gen.go and route admin endpoints through the generated mux — the drift fix.

    • The committed openapi.gen.go had drifted from the pinned oapi-codegen
      (v2.7.0): it predated the embedding-provider endpoints, which were in the
      spec but hand-mounted in router.go as a stopgap. The feature commit
      added the reset route the same way.
    • This commit regenerates gen.go so HandlerFromMux owns all of them and
      removes the direct mounts (keeping both would double-register and panic
      chi). TestEmbeddingProvider/ResetUserPassword now take path params from
      the generated wrapper instead of chi.URLParam. The embedded
      /openapi.json blob now lists these endpoints (Swagger UI previously
      under-reported the API). make openapi-check is green again.
    • Incidental churn: regeneration also reorders some struct fields and adds
      .Valid() enum helpers the stale file lacked.

Backward-compatible / safe to ship: purely additive — no DB migration, no
changed/removed endpoints or behavior, no wire change in commit 2 (same routes,
methods, shapes). The dashboard ships inside the server binary, so there's no
version skew.

Pre-existing nit left untouched: router.go has a gofmt import-order quirk
(githubtokens/gitrepos) in a block this PR doesn't otherwise modify.

Type of change

  • New feature
  • Refactor (OpenAPI codegen reconciliation)

Checklist

  • go build ./... + go test ./... pass
  • make openapi-check green (gen.go in sync with doc/openapi.yaml)
  • Dashboard npm run build / typecheck pass
  • No secrets or API keys committed

🤖 Generated with Claude Code

dvcdsys and others added 3 commits June 5, 2026 14:58
Add POST /api/v1/admin/users/{id}/reset-password (admin only). Mirrors
the invite flow: an admin sets a new temporary password, the server flags
the user must_change_password=1 and revokes the target's existing
sessions, so on next login the existing ChangePasswordPage forces a new
password (same as first login). Any admin may reset any user, including
another admin — a reset neither demotes nor disables anyone, so no
last-admin guard applies.

- users.AdminResetPassword: sibling of UpdatePassword that SETS the flag
  instead of clearing it.
- Handler gates on mustBeAdmin, validates new_password (>=8), revokes
  sessions via Sessions.DeleteAllForUser, returns the updated user.
- Dashboard: ResetPasswordDialog (cloned from InviteUserDialog) wired into
  the users table, plus the useResetUserPassword hook.
- Documented in doc/openapi.yaml (source of truth).

Mounted directly in router.go, matching the existing embedding-provider
admin routes — the committed openapi.gen.go predates the pinned
oapi-codegen, so it is not regenerated here. The follow-up commit
reconciles that drift and moves these routes onto the generated mux.

Purely additive: no DB migration, no changed/removed endpoints or
behavior; the dashboard ships inside the server binary so there is no
version skew. Backward-compatible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…he generated mux

The committed internal/httpapi/openapi/openapi.gen.go had drifted from the
pinned oapi-codegen (v2.7.0): it predated the embedding-provider endpoints,
which were added to doc/openapi.yaml but mounted directly in router.go as a
stopgap. The previous commit added the password-reset endpoint the same way.

Regenerate gen.go from the spec so the generated mux owns all of them, and
drop the direct mounts (keeping both would double-register the routes and
panic chi at startup):

- TestEmbeddingProvider and ResetUserPassword now take their path params
  from the generated wrapper (kind/id) instead of chi.URLParam; chi import
  dropped from auth.go, swapped for the openapi pkg in admin_embeddings.go.
- router.go no longer hand-mounts the embedding-provider or reset-password
  routes — HandlerFromMux registers them from the spec.
- The embedded /openapi.json blob now lists these endpoints (previously it
  under-reported the API in Swagger UI / docs).

Incidental churn: regeneration also reorders some struct fields and adds
.Valid() enum helpers that the stale file lacked. `make openapi-check` is
green again. No wire/behavior change — same routes, methods, and shapes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…-depth)

The ≥8 length check previously lived only in the HTTP handler. Move the
policy behind a shared users.MinPasswordLength constant and also enforce it
in the AdminResetPassword service method, so a non-HTTP caller can't set a
too-short password. The handler now references the constant instead of a
literal. Adds service-level tests for the set-flag and reject-short paths.

Pre-existing Create/UpdatePassword keep their empty-only checks (untouched
to avoid affecting bootstrap and other flows); hardening those uniformly is
a separate concern.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@dvcdsys dvcdsys merged commit d6779ef into develop Jun 5, 2026
1 check passed
@dvcdsys dvcdsys deleted the feat/admin-password-reset branch June 5, 2026 14:19
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