Skip to content

feat(admin): add user create, disable/enable, and OAuth/authorization management#162

Merged
appleboy merged 11 commits intomainfrom
worktree-admin
Apr 12, 2026
Merged

feat(admin): add user create, disable/enable, and OAuth/authorization management#162
appleboy merged 11 commits intomainfrom
worktree-admin

Conversation

@appleboy
Copy link
Copy Markdown
Member

Summary

  • Admin Create User — new /admin/users/new page with form, auto-generated password, and one-time password display
  • Disable/Enable UserIsActive field on User model, middleware enforcement (disabled users auto-logout), token revocation on disable, admin UI toggle
  • OAuth Connections View — per-user /admin/users/:id/connections page showing linked providers (GitHub/Gitea/Microsoft) with unlink action
  • User Authorizations View — per-user /admin/users/:id/authorizations page showing granted apps with revoke action
  • Clickable Stat Cards — user detail stat cards (Active Tokens, OAuth Connections, Authorized Apps) now link to their respective management pages
  • Dashboard Update — shows disabled user count when present
  • Code Quality — extracted getFlashMessage/flashAndRedirect helpers, unified disable/enable handler, consolidated error variables
  • Security Hardening — tokens revoked before disable (not after), CountUsersByRole only counts active admins, username sanitized on create, efficient ownership-verified OAuth connection queries

New Routes (8)

Method Path Description
GET /admin/users/new Create user form
POST /admin/users Submit create user
POST /admin/users/:id/disable Disable user
POST /admin/users/:id/enable Enable user
GET /admin/users/:id/connections View OAuth connections
POST /admin/users/:id/connections/:conn_id/delete Unlink OAuth provider
GET /admin/users/:id/authorizations View authorized apps
POST /admin/users/:id/authorizations/:uuid/revoke Revoke authorization

Model Change

User.IsActive (bool, default:true) — GORM AutoMigrate handles the migration automatically.

Test plan

  • make generate && make build succeeds
  • go test ./internal/... all packages pass
  • Create user via /admin/users/new — verify password shown once
  • Disable user → verify they are logged out and cannot log in
  • Enable user → verify they can log in again
  • Cannot disable self or last admin
  • View/unlink OAuth connections from user detail page
  • View/revoke authorized apps from user detail page
  • Stat cards on user detail page link to correct pages
  • Dashboard shows disabled user count

🤖 Generated with Claude Code

…authorizations management

- Add admin create user page with auto-generated password support
- Add disable/enable user with IsActive field, middleware enforcement, and token revocation
- Add per-user OAuth connections view with provider badges and unlink action
- Add per-user authorized apps view with revoke action
- Make user detail stat cards clickable, linking to tokens, connections, and authorizations
- Add Status column and Create User button to users list page
- Show disabled user count on admin dashboard
- Extract getFlashMessage and flashAndRedirect helpers to reduce handler duplication
- Sanitize username input on admin user creation using existing sanitizeUsername
- Count only active admins in last-admin guard to prevent disabled admins inflating count
- Revoke tokens before disabling user to close security window
- Add GetOAuthConnectionByUserAndID for efficient ownership-verified queries

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 10, 2026 14:54
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds comprehensive admin-side user management capabilities (create local users, enable/disable accounts, and manage per-user OAuth connections/authorizations), plus related UI/metrics updates.

Changes:

  • Introduces User.IsActive with admin enable/disable flows and session enforcement for disabled users.
  • Adds new admin pages for creating users, viewing/unlinking OAuth connections, and viewing/revoking authorized apps.
  • Enhances admin UI (clickable stat cards, status badges, dashboard disabled-user count) and refactors flash-message helpers.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
internal/templates/static/css/pages/admin-forms.css Adds link styling for clickable user stat cards.
internal/templates/props.go Adds props structs for new admin user management pages.
internal/templates/admin_users.templ Adds “Create User” CTA and user status column/badge.
internal/templates/admin_user_detail.templ Displays user status, adds clickable stat cards, adds enable/disable actions.
internal/templates/admin_user_created.templ New “created” page showing one-time password with copy action.
internal/templates/admin_user_create.templ New admin “create user” form.
internal/templates/admin_user_connections.templ New OAuth connections list + unlink action UI.
internal/templates/admin_user_authorizations.templ New authorized-apps list + revoke action UI.
internal/templates/admin_dashboard.templ Shows disabled user count when present.
internal/store/user.go Ensures external users are active; changes CountUsersByRole to count only active users.
internal/store/user_test.go Updates test user factory for new IsActive field.
internal/store/types/dashboard.go Adds DisabledUsers to dashboard counts DTO.
internal/store/sqlite.go Seeds default admin with IsActive: true.
internal/store/oauth_connection.go Adds ownership-verified connection lookup by (user_id, id).
internal/store/dashboard.go Adds disabled-users scalar count to dashboard query.
internal/services/user.go Adds admin create user, enable/disable, and OAuth connection deletion service methods.
internal/models/user.go Adds IsActive field to User model.
internal/models/audit_log.go Adds new audit event types for user created/disabled/enabled and OAuth connection deletion.
internal/mocks/mock_store.go Updates mocks for new store interface method.
internal/middleware/auth.go Clears session when a disabled user is encountered.
internal/handlers/utils.go Adds getFlashMessage / flashAndRedirect helpers.
internal/handlers/user_admin.go Adds routes/handlers for create user, enable/disable, connections, authorizations; refactors flash usage.
internal/core/store.go Extends OAuthConnectionStore interface with GetOAuthConnectionByUserAndID.
internal/bootstrap/router.go Registers new admin routes.
internal/bootstrap/handlers.go Wires AuthorizationService into UserAdminHandler.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/templates/admin_user_detail.templ Outdated
Comment thread internal/templates/admin_user_authorizations.templ Outdated
Comment thread internal/services/user.go
Comment thread internal/services/user.go
Comment thread internal/services/user.go Outdated
Comment thread internal/handlers/user_admin.go Outdated
Comment thread internal/handlers/user_admin.go
Comment thread internal/services/user.go
Comment thread internal/services/user.go Outdated
Comment thread internal/services/user.go
- URL-encode query params in user detail and authorization templates
- Handle non-NotFound DB errors in username/email uniqueness checks
- Handle non-NotFound DB errors in OAuth connection lookup
- Use operation-neutral error message for self-active-status guard
- Block disabled users from authenticating in Authenticate method
- Differentiate 400 vs 500 status in CreateUser error handling
- Validate disable/enable before revoking tokens to prevent side effects
- Add ValidateSetUserActiveStatus for pre-flight guard checks
- Add 14 unit tests for CreateUserAdmin, SetUserActiveStatus, DeleteUserOAuthConnection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/templates/admin_user_connections.templ
Comment thread internal/handlers/user_admin.go Outdated
Comment thread internal/handlers/user_admin.go
Comment thread internal/services/user.go
Comment thread internal/services/user.go Outdated
Comment thread internal/store/user.go
- Replace deprecated strings.Title with manual capitalization
- Use sentinel ErrOAuthConnectionNotFound for proper 404 vs 500 handling
- Handle gorm.ErrDuplicatedKey race in CreateUserAdmin
- Update CountUsersByRole docstring to reflect active-only behavior
- Add test for CountUsersByRole excluding disabled users

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@appleboy appleboy requested a review from Copilot April 10, 2026 15:51
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/services/user.go
Comment thread internal/services/user.go
Comment thread internal/handlers/user_admin.go
Comment thread internal/handlers/user_admin.go Outdated
Comment thread internal/handlers/user_admin.go Outdated
- Block disabled users from OAuth login paths (AuthenticateWithOAuth)
- Differentiate email vs username conflict in CreateUserAdmin DuplicatedKey
- Map toggleUserActive errors to proper HTTP status codes (400/404/500)
- Return 404 for authorization not found in RevokeUserAuthorization
- Add tests for OAuth disabled user blocking

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/services/user.go
Replace duplicated validation checks in SetUserActiveStatus with a call
to ValidateSetUserActiveStatus for defense-in-depth, reducing drift risk.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/handlers/user_admin.go
Comment thread internal/services/user.go
Comment thread internal/templates/static/css/pages/admin-forms.css
…tyle

- Map ErrAccountDisabled to 403 in OAuth callback handler instead of
  falling through to generic 500 error
- Add :focus-visible style for clickable stat card links (accessibility)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/services/user.go
Comment thread internal/services/user_test.go
Comment thread internal/services/user_test.go
- Map ErrAccountDisabled to specific message in login handler instead of
  generic "Invalid username or password"
- Use require.ErrorIs for sentinel error assertions in OAuth connection
  delete tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/handlers/user_admin.go
Comment thread internal/services/user.go Outdated
Comment thread internal/handlers/user_admin.go
… inputs

- Replace brittle error text inspection with DB re-query to determine
  which unique constraint was violated on CreateUser race
- Trim whitespace on form inputs in handler so repopulated form shows
  normalized values matching what the service validates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@appleboy appleboy requested a review from Copilot April 11, 2026 02:57
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- Extract toAuthorizationDisplaySlice helper to deduplicate conversion loop
- Move input sanitization to service layer as single source of truth
- Rename ErrCannotDisableSelf to ErrCannotChangeOwnStatus for clarity
- Remove redundant re-validation in SetUserActiveStatus to match DeleteUserAdmin pattern
- Apply getFlashMessage/flashAndRedirect helpers to client handler

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/store/user.go
Comment thread internal/services/user.go
…in SetUserActiveStatus

CountUsersByRole only counts active users, so guards in UpdateUserAdmin,
ValidateDeleteUser, and DeleteUserAdmin must skip when the target itself
is disabled. Otherwise deleting or demoting a disabled admin is rejected
because the active count drops to 1.

Move validation into SetUserActiveStatus so direct callers cannot bypass
self or last-admin invariants. Handlers still call ValidateSetUserActiveStatus
to gate token revocation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/services/user.go
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/services/user.go
Comment thread internal/services/user.go
Comment thread internal/services/user.go Outdated
Comment thread internal/handlers/utils.go
… session errors

Authenticate previously only checked IsActive when GetUserByUsername hit. If
the username changed upstream, the HTTP-API path could still resolve a
disabled account by external_id and let it log in. Re-check IsActive after
syncExternalUser to close that gap.

updateOAuthConnectionAndGetUser used to persist refreshed OAuth tokens
before checking the owning user, so a disabled account could rotate stored
credentials simply by completing the provider redirect. Fetch the user and
verify IsActive before any update.

The session_save_error context key set by getFlashMessage / flashAndRedirect
was never read, so session save failures were effectively silent. Log them
instead, since flash messages are advisory and a save failure does not
warrant aborting the request.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@appleboy appleboy merged commit ab19c60 into main Apr 12, 2026
20 of 21 checks passed
@appleboy appleboy deleted the worktree-admin branch April 12, 2026 03:21
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.

2 participants