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:
On error:
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
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
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.
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
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
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
usersrow (setsupdated_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
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
| 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_keystable (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).
Side effects
- Deletes the matching row from
api_keyswhereid = 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
Errors
| Status | Code | Meaning |
|---|---|---|
400 |
bad_request |
uid is not a valid integer. |
500 |
server_error |
Permission lookup failed. |
Example
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
| 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
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
permissionsrelationsfor 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.
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, andsystemctl 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
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 runid -u <name>to resolve a username.
Example
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
| 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
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 aNOPASSWD:ALLline; runsusermod -aG sudo <username>andusermod -aG wheel <username>. - When revoking: removes
/etc/sudoers.d/<username>; runsgpasswd -d <username> sudoandgpasswd -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_startandterminal_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
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
| 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.
Errors
| Status | Code | Meaning |
|---|---|---|
400 |
bad_request |
Body failed to bind / username missing. |
500 |
db_error |
Insert into users failed. |
Side effects
- Inserts a
usersrow (non-admin, non-superuser, random avatar) when the username is not already present. No OS-level user is created.
Example