Skip to content

Users & RBAC

Panel users, roles, permissions, 2FA setup.

All endpoints below are mounted under /api. Authentication: Authorization: Bearer <token> unless noted as public.

Endpoint count: 14

Endpoints

Method Path Handler Guards
DELETE /api/users/:uid deleteUser RequirePermission(db, "booting_users_management")
GET /api/users/:uid getUser RequirePermission(db, "booting_users_management")
PATCH /api/users/:uid updateUser RequirePermission(db, "booting_users_management")
GET /api/users/:uid/api-keys listAPIKeys RequirePermission(db, "booting_users_management")
POST /api/users/:uid/api-keys createAPIKey RequirePermission(db, "booting_users_management")
DELETE /api/users/:uid/api-keys/:kid deleteAPIKey RequirePermission(db, "booting_users_management")
GET /api/users/:uid/permissions getUserPermissions AuthWithDB(cfg.JWTSecret, db)
POST /api/users/:uid/permissions setUserPermissions RequirePermission(db, "booting_users_management")
POST /api/users/:uid/rewrite-configs rewriteUserConfigs RequirePermission(db, "booting_users_management")
GET /api/users/:uid/sudo getUserSudo RequirePermission(db, "booting_users_management")
POST /api/users/:uid/sudo setUserSudo RequirePermission(db, "booting_users_management")
GET /api/users/:uid/terminal userTerminal -
GET /api/users/:uid/webapps getUserWebapps RequirePermission(db, "booting_users_management")
POST /api/users/register-existing registerExistingUser RequirePermission(db, "booting_users_management")

Source

Handlers for this section live in api/routes/handlers_*.go in the StarkOne source. Each handler binds its request body with gin.ShouldBindJSON(&req) and responds with the standard envelope:

{ "data": { ... } }

On error:

{ "error": { "code": "string", "message": "string" } }

Reference

DELETE /api/users/:uid

Deletes a panel user by numeric ID; refuses to delete the calling user and logs a user_deleted audit action on success.

Guards: RequirePermission(db, "booting_users_management")

Path parameters

Name Type Description
uid integer Numeric ID of the panel user to delete.

Response - 200 OK

{ "data": { "success": true } }

Errors

Status Code Meaning
400 bad_request uid is not a valid integer.
400 bad_request Attempt to delete yourself ("cannot delete yourself").
500 delete_failed Underlying delete operation failed.

Side effects

  • Removes the user via users.Delete (DB write).
  • Writes an audit log entry (user_deleted) recording the caller and client IP.

Example

curl -X DELETE https://panel.example.com:2087/api/users/42 \
  -H "Authorization: Bearer $TOKEN"

GET /api/users/:uid

Returns a single panel user record by numeric ID.

Guards: RequirePermission(db, "booting_users_management")

Path parameters

Name Type Description
uid integer Numeric ID of the panel user.

Response - 200 OK

The user object is the users.User struct returned by users.GetByID. The exact field set is defined in the users package and is not enumerated in this handler.

{ "data": { "user": { "id": 42, "username": "user1" } } }

Errors

Status Code Meaning
400 bad_request uid is not a valid integer.
404 not_found No user with that ID.
500 server_error Lookup failed.

Example

curl https://panel.example.com:2087/api/users/42 \
  -H "Authorization: Bearer $TOKEN"

PATCH /api/users/:uid

Updates a panel user's email, full name, suspension state, and/or admin flag; empty string fields and omitted bool pointers are left unchanged.

Guards: RequirePermission(db, "booting_users_management")

Path parameters

Name Type Description
uid integer Numeric ID of the panel user to update.

Request body

{
  "email": "string (optional)",
  "fullname": "string (optional)",
  "is_suspended": "bool (optional)",
  "is_admin": "bool (optional)"
}
Field Type Required Description
email string no New email; ignored if empty string.
fullname string no New full name; ignored if empty string.
is_suspended bool no If present, sets suspension state; omit to leave unchanged.
is_admin bool no If present, sets admin flag; omit to leave unchanged.

Response - 200 OK

{ "data": { "success": true } }

Errors

Status Code Meaning
400 bad_request uid is not a valid integer, or body failed to bind.
500 server_error DB update failed.

Side effects

  • Updates the users row (sets updated_at) in the panel DB.

Example

curl -X PATCH https://panel.example.com:2087/api/users/42 \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"email": "[email protected]", "is_suspended": true}'

GET /api/users/:uid/api-keys

Lists API keys belonging to a user, returning metadata only (never the key value), newest first.

Guards: RequirePermission(db, "booting_users_management")

Path parameters

Name Type Description
uid integer Numeric ID of the owning user.

Response - 200 OK

{
  "data": {
    "keys": [
      {
        "id": 1,
        "name": "ci-token",
        "key_prefix": "bap_1a2b3c4d",
        "created_at": "2026-06-17 12:00:00",
        "last_used": null
      }
    ]
  }
}

Errors

Status Code Meaning
500 db_error Query for keys failed.

Example

curl https://panel.example.com:2087/api/users/42/api-keys \
  -H "Authorization: Bearer $TOKEN"

POST /api/users/:uid/api-keys

Generates a new API key for the user; returns the full token once (bap_ + 64 hex chars) and stores only its SHA-256 hash and 12-char prefix.

Guards: RequirePermission(db, "booting_users_management")

Path parameters

Name Type Description
uid integer Numeric ID of the owning user.

Request body

{ "name": "string (required)" }
Field Type Required Description
name string yes Human-readable label for the key.

Response - 200 OK

The key field is the plaintext token and is shown only in this response.

{
  "data": {
    "id": 1,
    "key": "bap_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
    "key_prefix": "bap_01234567",
    "name": "ci-token"
  }
}

Errors

Status Code Meaning
400 bad_request name missing ("name is required").
500 server_error Failed to generate random key bytes.
500 db_error Insert into api_keys failed.

Side effects

  • Inserts a row into the api_keys table (stores hash + prefix, not the token).

Example

curl -X POST https://panel.example.com:2087/api/users/42/api-keys \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "ci-token"}'

DELETE /api/users/:uid/api-keys/:kid

Deletes one of a user's API keys by key ID, scoped to that user.

Guards: RequirePermission(db, "booting_users_management")

Path parameters

Name Type Description
uid integer Numeric ID of the owning user.
kid integer Numeric ID of the API key to delete.

Response - 200 OK

Always returns success; the DELETE error is not surfaced (deleting a nonexistent key still returns success).

{ "data": { "success": true } }

Side effects

  • Deletes the matching row from api_keys where id = kid AND user_id = uid.

Example

curl -X DELETE https://panel.example.com:2087/api/users/42/api-keys/1 \
  -H "Authorization: Bearer $TOKEN"

GET /api/users/:uid/permissions

Returns the list of permission slugs granted to a user.

Guards: AuthWithDB(cfg.JWTSecret, db)

Path parameters

Name Type Description
uid integer Numeric ID of the panel user.

Response - 200 OK

{ "data": { "permissions": ["booting_users_management", "terminal_access"] } }

Errors

Status Code Meaning
400 bad_request uid is not a valid integer.
500 server_error Permission lookup failed.

Example

curl https://panel.example.com:2087/api/users/42/permissions \
  -H "Authorization: Bearer $TOKEN"

POST /api/users/:uid/permissions

Replaces a user's entire permission set with the supplied slugs in a single transaction (deletes existing relations, then re-inserts).

Guards: RequirePermission(db, "booting_users_management")

Path parameters

Name Type Description
uid integer Numeric ID of the panel user.

Request body

{ "permissions": ["string (required)"] }
Field Type Required Description
permissions array of string yes Full set of permission slugs to grant; replaces any existing grants. Unknown slugs simply match no row and are skipped.

Response - 200 OK

{ "data": { "success": true } }

Errors

Status Code Meaning
400 bad_request uid invalid, or body failed to bind.
500 server_error Transaction begin/delete/insert/commit failed.

Side effects

  • In a transaction: deletes all rows from permissionsrelations for the user, then inserts a row per matched permission slug.

Example

curl -X POST https://panel.example.com:2087/api/users/42/permissions \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"permissions": ["booting_users_management", "terminal_access"]}'

POST /api/users/:uid/rewrite-configs

Rebuilds all web server configs for a user via the StarkOne CLI rewrite_all, then reloads nginx and Apache.

Guards: RequirePermission(db, "booting_users_management")

Path parameters

Name Type Description
uid integer Numeric ID of the panel user whose configs are rewritten.

Response - 200 OK

Note: this handler returns a flat object, not the standard data envelope.

{ "success": true, "output": "<cli output>" }

Errors

Status Code Meaning
404 not_found No user with that ID.
500 rewrite_error The rewrite_all CLI command failed.

Side effects

  • Runs cli.Run("rewrite_all", <username>) to regenerate config files.
  • Runs systemctl reload nginx, systemctl reload apache2, and systemctl reload httpd (errors ignored).

Example

curl -X POST https://panel.example.com:2087/api/users/42/rewrite-configs \
  -H "Authorization: Bearer $TOKEN"

GET /api/users/:uid/sudo

Reports whether a system user has passwordless sudo, by checking for a /etc/sudoers.d/<username> file.

Guards: RequirePermission(db, "booting_users_management")

Path parameters

Name Type Description
uid integer or string Either a numeric panel user ID (resolved to a username via DB) or a system username. Must match ^[a-zA-Z0-9_-]+$.

Response - 200 OK

{ "data": { "sudo": false, "username": "user1" } }

Errors

Status Code Meaning
404 not_found Identifier invalid or user not found on the system.

Side effects

  • Read-only: stats /etc/sudoers.d/<username>; may run id -u <name> to resolve a username.

Example

curl https://panel.example.com:2087/api/users/42/sudo \
  -H "Authorization: Bearer $TOKEN"

POST /api/users/:uid/sudo

Grants or revokes passwordless sudo for a named system user by writing/removing a /etc/sudoers.d file and adjusting group membership.

Guards: RequirePermission(db, "booting_users_management")

Note: the target is taken from the request body username, not from the :uid path parameter.

Request body

{ "sudo": "bool (optional)", "username": "string (required)" }
Field Type Required Description
sudo bool no true to grant sudo, false (default) to revoke.
username string yes System username; must be non-empty and match ^[a-zA-Z0-9_-]+$ and exist on the host.

Response - 200 OK

{ "data": { "success": true, "username": "user1", "sudo": true } }

Errors

Status Code Meaning
400 bad_request Body failed to bind, username empty, or invalid characters.
404 not_found System user does not exist.
500 write_failed Writing the sudoers file failed.

Side effects

  • When granting: writes /etc/sudoers.d/<username> (mode 0440) with a NOPASSWD:ALL line; runs usermod -aG sudo <username> and usermod -aG wheel <username>.
  • When revoking: removes /etc/sudoers.d/<username>; runs gpasswd -d <username> sudo and gpasswd -d <username> wheel.

Example

curl -X POST https://panel.example.com:2087/api/users/42/sudo \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"username": "user1", "sudo": true}'

GET /api/users/:uid/terminal

Opens an interactive shell over a WebSocket (PTY) running as the target user; this endpoint upgrades the connection rather than returning JSON on success.

Guards: - (no router-level guard; authorization is enforced inside the handler via a terminal JWT)

Path parameters

Name Type Description
uid integer Numeric ID of the target panel user (ignored when username query param is supplied by a superuser).

Query parameters

Name Type Optional Description
token string no Terminal JWT signed with the panel JWT secret; carries UserID, Superuser.
username string yes Superuser-only: open a terminal for an arbitrary system username instead of :uid.

Response - 200 OK

On success the HTTP connection is upgraded to a WebSocket carrying the terminal session; no JSON body is returned. Errors below are returned as JSON before the upgrade.

Errors

Status Code Meaning
401 unauthorized Missing token query param, or token invalid/expired.
403 forbidden Caller lacks the terminal_access permission and is not a superuser; or a non-superuser supplied username; or a non-superuser targeted another user.
400 bad_request uid is not a valid integer.
404 not_found Target user not found.

Side effects

  • Upgrades to a WebSocket and spawns an interactive PTY shell as the target OS user (terminal.Handle).
  • Writes audit log entries terminal_session_start and terminal_session_end.

Example

# WebSocket upgrade; token passed as a query param.
curl -i -N \
  -H "Connection: Upgrade" -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  "https://panel.example.com:2087/api/users/42/terminal?token=$TERMINAL_JWT"

GET /api/users/:uid/webapps

Lists the websites a user has access to, with each website's ID, domain, and the user's permission type.

Guards: RequirePermission(db, "booting_users_management")

Path parameters

Name Type Description
uid integer Numeric ID of the panel user.

Response - 200 OK

{
  "data": {
    "websites": [
      { "website_id": 7, "domain": "example.com", "permission_type": "full" }
    ]
  }
}

Errors

Status Code Meaning
500 server_error Query joining user_websites_access and websites failed.

Example

curl https://panel.example.com:2087/api/users/42/webapps \
  -H "Authorization: Bearer $TOKEN"

POST /api/users/register-existing

Registers a DB-only panel user record for an OS user that already exists (e.g. webapp users); performs no useradd/passwd.

Guards: RequirePermission(db, "booting_users_management")

Request body

{ "username": "string (required)" }
Field Type Required Description
username string yes Existing OS username to register in the panel DB.

Response - 200 OK / 201 Created

If a user with that username already exists in the DB, returns 200 with existed: true. Otherwise inserts a new record and returns 201.

{ "data": { "user_id": 51, "username": "user1", "existed": true } }
{ "data": { "user_id": 52, "username": "user1" } }

Errors

Status Code Meaning
400 bad_request Body failed to bind / username missing.
500 db_error Insert into users failed.

Side effects

  • Inserts a users row (non-admin, non-superuser, random avatar) when the username is not already present. No OS-level user is created.

Example

curl -X POST https://panel.example.com:2087/api/users/register-existing \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"username": "user1"}'