Skip to content

Admin Panel

Audience: Tenant admins, course admins, and super-admins.

Scope: A tour of the admin surfaces that ship today. Detailed RBAC and the consumed-token impersonation contract live in development/auth.md and reference/user-management.md.

All /admin/* routes call requireRole(...) server-side. Tenant scoping flows from the JWT claim. super_admin accesses cross-tenant data via withTenantOverride().

Routes

Route Roles Purpose
/admin tenant_admin, course_admin, super_admin Landing dashboard with counts + recent audit.
/admin/users tenant_admin, super_admin Paginated user list with search / role / status filters.
/admin/users/\[id\] tenant_admin, super_admin User detail + actions (role change, deactivate, force sign-out, resend verification, password-reset link, impersonate).
/admin/users/invite tenant_admin, super_admin Send a tenant-scoped invitation.
/admin/users/invitations tenant_admin, super_admin Search invitations and resend or revoke invitations that have not been accepted.
/admin/courses tenant_admin, course_admin, super_admin Read-only catalogue + links to course-admin manager.
/admin/courses/\[slug\]/dashboard tenant_admin, course_admin (assigned), super_admin Per-course roster + analytics.
/admin/courses/\[slug\]/admins tenant_admin, super_admin Assign / revoke course_admin_assignments.
/admin/audit tenant_admin, super_admin Audit log viewer; tenant-scoped or global.
/admin/tenants super_admin Create + suspend tenants.

Mutating Actions

Every mutating action follows the same contract:

  • Validates input with Zod at the boundary.
  • Re-checks role / target ownership inside the action.
  • Rate-limits via RATE_LIMIT_POLICIES.adminMutation (see security-and-rate-limiting.md).
  • Writes an audit_log row.
  • Bumps target users.token_version on role change / deactivation / force-sign-out so live JWTs are invalidated on the next request.

Common Tasks

Invite and manage users

Tenant admins and super-admins can invite users from a tenant host. Invitations always belong to the current tenant; an invitation link cannot be accepted on a different tenant.

Send an invitation

  1. Open Invitations in the admin navigation.
  2. Select Invite a user.
  3. Enter the recipient's email address.
  4. Choose the role the recipient should receive:
    • Student — course selection is optional. Selecting a course sends the learner to that course's landing page after acceptance, but does not enrol them. Enrolment remains a separate administrator or learner action.
    • Course admin — a published course is required. Acceptance grants the course-admin role and access to that course, then opens its admin dashboard.
    • Tenant admin — no course is selected. Acceptance grants tenant-wide administration and opens the admin dashboard.
  5. Select Send invitation.

The course picker searches published courses by title or slug. If the CMS course list is temporarily unavailable, enter the exact course slug manually. The server still verifies that the course exists before sending.

Each invitation is valid for seven days. The system refuses a second active, unaccepted invitation for the same email and tenant; use Resend invitation on the existing invitation instead.

What the recipient sees

The email contains a tenant-specific, one-time invitation link:

  • A new user enters their name and creates a password. The account is created with the invited role.
  • An existing user signs in with the invited email address, returns to the invitation, and confirms acceptance.
  • If a different account is signed in, the recipient must sign out and continue with the invited account.

After acceptance, the invitation becomes Accepted and cannot be resent or revoked. The recipient is sent to the destination determined by the role and optional course selection described above.

Find and manage invitations

Open /admin/users/invitations from Invitations in the admin navigation. The list is scoped to the current tenant. Search matches email, role, or course slug.

Status Meaning
Pending The invitation is unaccepted and its link is still valid.
Accepted The recipient accepted the invitation; no row actions are available.
Expired The invitation was not accepted within its seven-day validity window.

Use the row actions menu for a pending or expired invitation:

  • Resend invitation creates a new one-time token, resets expiry to seven days, and emails a new link. The previous link stops working immediately.
  • Revoke invitation permanently invalidates and removes the invitation after confirmation. The audit history remains available.

Creating, resending, accepting, granting a role from, and revoking invitations write audit events. Use /admin/audit to investigate who performed an action and which invitation was affected.

Troubleshooting invitations

Problem Administrator action
Course cannot be found Confirm the course is published and select it from the picker, or verify the exact slug when using manual entry.
Course list is unavailable Enter the exact slug manually or retry after CMS connectivity is restored.
An active invitation already exists Find the email on Invitations and resend or revoke that invitation instead of creating another.
Too many invite attempts Wait briefly before retrying; invitation creation, resend, and revoke actions are rate-limited.
Link has expired Open the expired invitation's row actions and select Resend invitation.
Recipient did not receive the email Resend once, then check delivery configuration and provider logs in Email Delivery.
Recipient sees an account-mismatch message Ask them to sign out and accept the link while signed in with the invited email address.
Recipient needs course access as a student Complete enrolment separately; a course selected on a student invitation only controls the post-accept landing page.

Promote a user to course admin for one course

  1. Open /admin/courses/\[slug\]/admins.
  2. Search for the target user.
  3. Assign — this writes a course_admin_assignments row scoped to (user_id, tenant_id, course_slug).
  4. The user is signed out everywhere on next request (token-version bump) and signs back in with the new claim.

Force a user to re-authenticate

Open /admin/users/\[id\] and use Force sign out everywhere. The action bumps users.token_version and writes an audit row.

Impersonate a user for support

  1. Sign in as super_admin and browse the target tenant host.
  2. Open /admin/users/\[id\] for the target user.
  3. Use Impersonate to switch into their session, then complete the support check.
  4. Use the admin impersonation banner to Stop impersonation when done.

See Impersonation for prerequisites, limitations, and audit guidance.

Investigate suspicious activity

/admin/audit shows tenant-scoped audit events; super_admin can switch to a global view. Filter by actor, action, target, or tenant.