feat(admin): add fork-specific user management for the DocSales instance - #14
Merged
Conversation
Vikunja's own /admin/users routes (list/create/status/delete) are gated behind license.FeatureAdminPanel, a paid Vikunja Pro feature - see pkg/license/license.go's note on why before touching that gate. Rather than unlock it, this reuses the exact same handlers and model functions under a new, separate /docsales-admin group that only requires a genuine instance admin (RequireInstanceAdmin), leaving the licensed group and its gate untouched. Also adds MigrateUserTasksAsAdmin, a new action with no Vikunja equivalent: reassigns every task created by or assigned to one user over to another, for merging a duplicate account (e.g. a new Google OIDC signup) into the one that holds the task history. Dedupes assignee rows the destination user already holds instead of violating the (task_id, user_id) unique pair. is_admin management (PATCH /users/:id/admin) is deliberately left out of the new group - granting/revoking instance-admin privileges stays on the licensed path.
Preview DeploymentPreview deployments for this PR are available at:
The preview environment will start automatically on first visit. Subsequent pushes to this PR will update the Run locally with Dockerdocker pull ghcr.io/docsales/vikunja:pr-14
docker run -p 3456:3456 ghcr.io/docsales/vikunja:pr-14Last updated for commit 5427ac3 |
This was referenced Jul 19, 2026
kigiela
added a commit
that referenced
this pull request
Jul 19, 2026
* feat(frontend): add /users page for the new unlicensed admin routes New /users route (client-side gated on plain isAdmin, no PRO_FEATURE license check) backed by pkg/routes/api/v1/docsalesadmin from #14. Mirrors views/admin/UsersView.vue's list/search/create/status/delete flow against /docsales-admin/users instead of /admin/users, minus the is_admin toggle and password management (those stay admin-panel-only). Adds a "migrate tasks" action per user: search the already-loaded user list, pick a destination, confirm - reassigns every task the source user created or is assigned to, via the new migrate-to endpoint. Depends on #14 merging first; the routes this hits don't exist on main yet. * fix(frontend): use max-block-size instead of max-height Matches the project's logical-properties stylelint rule (csstools/use-logical), same convention already used elsewhere (margin-block-end, inset-block-start, etc.). CI caught this on #15.
kigiela
added a commit
that referenced
this pull request
Jul 20, 2026
… (#20) * feat(frontend): add /users page for the new unlicensed admin routes New /users route (client-side gated on plain isAdmin, no PRO_FEATURE license check) backed by pkg/routes/api/v1/docsalesadmin from #14. Mirrors views/admin/UsersView.vue's list/search/create/status/delete flow against /docsales-admin/users instead of /admin/users, minus the is_admin toggle and password management (those stay admin-panel-only). Adds a "migrate tasks" action per user: search the already-loaded user list, pick a destination, confirm - reassigns every task the source user created or is assigned to, via the new migrate-to endpoint. Depends on #14 merging first; the routes this hits don't exist on main yet. * fix(frontend): use max-block-size instead of max-height Matches the project's logical-properties stylelint rule (csstools/use-logical), same convention already used elsewhere (margin-block-end, inset-block-start, etc.). CI caught this on #15.
kigiela
added a commit
that referenced
this pull request
Jul 20, 2026
#21) The docsales-admin create-user form (PR #14/#15) has no language field, so CreateUserBody.Language is always sent empty - and the shared "language" govalidator tag treated empty as invalid, rejecting every request from that form with a generic "invalid data" error. Treat an empty language as "not specified" (valid, left as-is) instead of a validation failure. Non-empty values are still checked against the registered translations exactly as before - this only affects callers that omit the field entirely, matching how user creation already behaves elsewhere (e.g. the CLI's `user create` command never sets a language either).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
We need to list/create/deactivate/delete users and, going forward,
migrate a user's tasks to another account (e.g. when someone switches
from local login to Google SSO and ends up with a duplicate account).
Vikunja's built-in admin panel covers most of this already, but this
instance isn't set up to use it, and we didn't want to depend on it for
something we need working today. So this adds a small, separate route
group scoped to this fork, reusing the same underlying handlers where
the shape already matches.
What
/api/v1/docsales-adminroute group, gated byRequireInstanceAdmin()(a plain
is_admincheck) - a fork-specific admin surface, keptseparate from Vikunja's own
/admingroup.already uses for list/create/status/delete - no duplicated logic.
MigrateUserTasksAsAdmin, new logic with no built-in equivalent:reassigns every task created by or assigned to one user over to
another. Skips assignee rows the destination already holds (would
otherwise violate the
(task_id, user_id)unique pair) by droppingthe source's duplicate instead of moving it.
AdminUserTasksMigratedEvent, audited the same way every otheradmin user action already is.
PATCH /users/:id/admin(granting/revokinginstance-admin) - that stays on the built-in path.
Verification
go build/go vet/gofmtclean. Addedpkg/models/docsales_admin_test.gofollowing the existing
admin_actions_test.gopattern/fixtures - coversthe happy path (27 fixture tasks + the one colliding assignee row) and
three error paths (self-migration, nonexistent source/destination).
go test ./pkg/models/... ./pkg/audit/...passes with no regressions.Frontend page for this follows in a separate PR.