Skip to content

feat(admin): add fork-specific user management for the DocSales instance - #14

Merged
kigiela merged 1 commit into
mainfrom
feat/docsales-admin-users
Jul 19, 2026
Merged

feat(admin): add fork-specific user management for the DocSales instance#14
kigiela merged 1 commit into
mainfrom
feat/docsales-admin-users

Conversation

@kigiela

@kigiela kigiela commented Jul 19, 2026

Copy link
Copy Markdown

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

  • New /api/v1/docsales-admin route group, gated by RequireInstanceAdmin()
    (a plain is_admin check) - a fork-specific admin surface, kept
    separate from Vikunja's own /admin group.
  • Reuses the same handlers/model functions the built-in admin group
    already uses for list/create/status/delete - no duplicated logic.
  • Adds 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 dropping
    the source's duplicate instead of moving it.
  • New AdminUserTasksMigratedEvent, audited the same way every other
    admin user action already is.
  • Deliberately excludes PATCH /users/:id/admin (granting/revoking
    instance-admin) - that stays on the built-in path.

Verification

go build/go vet/gofmt clean. Added pkg/models/docsales_admin_test.go
following the existing admin_actions_test.go pattern/fixtures - covers
the 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.

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.
@github-actions

Copy link
Copy Markdown

Preview Deployment

Preview deployments for this PR are available at:

URL Tag Commit
https://pr-14.preview.vikunja.dev ghcr.io/docsales/vikunja:pr-14 latest
https://sha-5427ac376323bb2ffbfe1a39eda8666352b23841.preview.vikunja.dev ghcr.io/docsales/vikunja:sha-5427ac376323bb2ffbfe1a39eda8666352b23841 5427ac3

The preview environment will start automatically on first visit. Subsequent pushes to this PR will update the pr-14 image — the preview picks up the new version on restart. The per-commit URLs point to a specific version and will not change.

Run locally with Docker
docker pull ghcr.io/docsales/vikunja:pr-14
docker run -p 3456:3456 ghcr.io/docsales/vikunja:pr-14

Last updated for commit 5427ac3

@kigiela kigiela changed the title feat(admin): add unlicensed user-management routes for this fork feat(admin): add fork-specific user management for the DocSales instance Jul 19, 2026
@kigiela
kigiela merged commit 1d41202 into main Jul 19, 2026
44 checks passed
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).
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