revert(admin): restore user type toggles (#3306)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-08 20:04:01 +02:00
1 parent 0ec1cee99e
commit c486c2d3d0
87 files changed
+622 -13

No files matched your search

+152 -7
View File
@@ -6266,6 +6266,72 @@
}
}
},
"/admin/users/{user_id}/bot-status": {
"put": {
"operationId": "set_admin_user_bot_status",
"summary": "Set user bot status",
"tags": ["Admin"],
"responses": {
"200": {
"description": "Success",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserMutationResponse"}}}
},
"400": {
"description": "Bad Request - The request was malformed or contained invalid data",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
},
"401": {
"description": "Unauthorized - Authentication is required or the token is invalid",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
},
"403": {
"description": "Forbidden - You do not have permission to perform this action",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
},
"429": {
"description": "Too Many Requests - You are being rate limited",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ThrottledError"}}},
"headers": {
"Retry-After": {
"description": "Number of seconds to wait before retrying (only on 429)",
"schema": {"type": "integer"}
},
"X-RateLimit-Limit": {
"description": "The number of requests that can be made in the current window",
"schema": {"type": "integer"}
},
"X-RateLimit-Remaining": {
"description": "The number of remaining requests that can be made",
"schema": {"type": "integer"}
},
"X-RateLimit-Reset": {
"description": "Unix timestamp when the rate limit resets",
"schema": {"type": "integer"}
}
}
},
"500": {
"description": "Internal Server Error - An unexpected error occurred",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
}
},
"description": "Mark or unmark a user account as a bot. Controls bot badge visibility and API permissions. Creates audit log entry. Requires USER_UPDATE_BOT_STATUS permission.",
"security": [{"adminApiKey": []}],
"parameters": [
{
"name": "user_id",
"in": "path",
"required": true,
"schema": {"description": "The ID of the user", "allOf": [{"$ref": "#/components/schemas/SnowflakeType"}]},
"description": "The ID of the user"
}
],
"requestBody": {
"required": true,
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/AdminUserBotStatusRequest"}}}
}
}
},
"/admin/users/{user_id}/change-log": {
"get": {
"operationId": "list_admin_user_change_log",
@@ -7967,6 +8033,72 @@
]
}
},
"/admin/users/{user_id}/system-status": {
"put": {
"operationId": "set_admin_user_system_status",
"summary": "Set user system status",
"tags": ["Admin"],
"responses": {
"200": {
"description": "Success",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/UserMutationResponse"}}}
},
"400": {
"description": "Bad Request - The request was malformed or contained invalid data",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
},
"401": {
"description": "Unauthorized - Authentication is required or the token is invalid",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
},
"403": {
"description": "Forbidden - You do not have permission to perform this action",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
},
"429": {
"description": "Too Many Requests - You are being rate limited",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/ThrottledError"}}},
"headers": {
"Retry-After": {
"description": "Number of seconds to wait before retrying (only on 429)",
"schema": {"type": "integer"}
},
"X-RateLimit-Limit": {
"description": "The number of requests that can be made in the current window",
"schema": {"type": "integer"}
},
"X-RateLimit-Remaining": {
"description": "The number of remaining requests that can be made",
"schema": {"type": "integer"}
},
"X-RateLimit-Reset": {
"description": "Unix timestamp when the rate limit resets",
"schema": {"type": "integer"}
}
}
},
"500": {
"description": "Internal Server Error - An unexpected error occurred",
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}}}
}
},
"description": "Mark or unmark a user as a system account. System accounts have special permissions for automated operations. Creates audit log entry. Requires USER_UPDATE_BOT_STATUS permission.",
"security": [{"adminApiKey": []}],
"parameters": [
{
"name": "user_id",
"in": "path",
"required": true,
"schema": {"description": "The ID of the user", "allOf": [{"$ref": "#/components/schemas/SnowflakeType"}]},
"description": "The ID of the user"
}
],
"requestBody": {
"required": true,
"content": {"application/json": {"schema": {"$ref": "#/components/schemas/AdminUserSystemStatusRequest"}}}
}
}
},
"/admin/users/{user_id}/traits": {
"put": {
"operationId": "set_admin_user_traits",
@@ -9413,6 +9545,13 @@
},
"required": ["traits"]
},
"AdminUserSystemStatusRequest": {
"type": "object",
"properties": {
"system": {"type": "boolean", "description": "Whether the user should be marked as a system user"}
},
"required": ["system"]
},
"AdminStorePurchaseListResponse": {
"type": "object",
"properties": {
@@ -9877,6 +10016,11 @@
"required": ["entries", "next_page_token"],
"additionalProperties": false
},
"AdminUserBotStatusRequest": {
"type": "object",
"properties": {"bot": {"type": "boolean", "description": "Whether the user should be marked as a bot"}},
"required": ["bot"]
},
"AdminUserBanNoteRequest": {
"type": "object",
"properties": {
@@ -9991,7 +10135,7 @@
"type": "object",
"properties": {
"acls": {
"maxItems": 105,
"maxItems": 106,
"type": "array",
"items": {"$ref": "#/components/schemas/AdminAclType"},
"description": "List of access control permissions to assign"
@@ -12892,7 +13036,7 @@
},
"acls": {
"description": "Replacement list of access control permissions for the key",
"maxItems": 105,
"maxItems": 106,
"type": "array",
"items": {"$ref": "#/components/schemas/AdminAclType"}
}
@@ -12910,7 +13054,7 @@
"type": "string"
},
"acls": {
"maxItems": 105,
"maxItems": 106,
"type": "array",
"items": {"type": "string"},
"description": "List of access control permissions for the key"
@@ -12940,7 +13084,7 @@
"maximum": 365
},
"acls": {
"maxItems": 105,
"maxItems": 106,
"type": "array",
"items": {"$ref": "#/components/schemas/AdminAclType"},
"description": "List of access control permissions for the key"
@@ -12961,7 +13105,7 @@
"type": "string"
},
"acls": {
"maxItems": 105,
"maxItems": 106,
"type": "array",
"items": {"type": "string"},
"description": "List of access control permissions for the key"
@@ -12974,7 +13118,7 @@
"type": "object",
"properties": {
"acls": {
"maxItems": 105,
"maxItems": 106,
"type": "array",
"items": {"type": "string", "minLength": 1, "maxLength": 64},
"description": "Every admin access control permission the admin API recognises"
@@ -13076,6 +13220,7 @@
"user:view:email",
"user:view:ip",
"user:temp_ban",
"user:update:bot_status",
"user:update:dob",
"user:update:email",
"user:update:flags",
@@ -16123,7 +16268,7 @@
"description": "ISO 8601 timestamp when the pending deletion was scheduled",
"type": "string"
},
"acls": {"maxItems": 105, "type": "array", "items": {"type": "string"}},
"acls": {"maxItems": 106, "type": "array", "items": {"type": "string"}},
"traits": {"maxItems": 100, "type": "array", "items": {"type": "string"}},
"has_totp": {"type": "boolean"},
"authenticator_types": {"maxItems": 10, "type": "array", "items": {"$ref": "#/components/schemas/Int32Type"}},
+2
View File
@@ -90,6 +90,7 @@ pub const USER_VIEW_DOB: &str = "user:view:dob";
pub const USER_VIEW_EMAIL: &str = "user:view:email";
pub const USER_VIEW_IP: &str = "user:view:ip";
pub const USER_TEMP_BAN: &str = "user:temp_ban";
pub const USER_UPDATE_BOT_STATUS: &str = "user:update:bot_status";
pub const USER_UPDATE_DOB: &str = "user:update:dob";
pub const USER_UPDATE_EMAIL: &str = "user:update:email";
pub const USER_UPDATE_FLAGS: &str = "user:update:flags";
@@ -196,6 +197,7 @@ pub const ALL_ACLS: &[&str] = &[
USER_VIEW_EMAIL,
USER_VIEW_IP,
USER_TEMP_BAN,
USER_UPDATE_BOT_STATUS,
USER_UPDATE_DOB,
USER_UPDATE_EMAIL,
USER_UPDATE_FLAGS,
+22
View File
@@ -306,6 +306,28 @@ impl AdminApiClient {
Ok(resp.user)
}
pub async fn set_bot_status(&self, user_id: &str, is_bot: bool) -> ApiResult<AdminUser> {
let body = generated_types::AdminUserBotStatusRequest { bot: is_bot };
let response = self
.generated()
.set_admin_user_bot_status(&snowflake(user_id), &body)
.await
.map_err(|e| self.generated_error(e))?;
let resp: UserMutationResponse = self.generated_value(response.into_inner())?;
Ok(resp.user)
}
pub async fn set_system_status(&self, user_id: &str, is_system: bool) -> ApiResult<AdminUser> {
let body = generated_types::AdminUserSystemStatusRequest { system: is_system };
let response = self
.generated()
.set_admin_user_system_status(&snowflake(user_id), &body)
.await
.map_err(|e| self.generated_error(e))?;
let resp: UserMutationResponse = self.generated_value(response.into_inner())?;
Ok(resp.user)
}
pub async fn change_username(
&self,
user_id: &str,
+16
View File
@@ -178,6 +178,22 @@ pub async fn dispatch(
"Failed to clear user fields",
)
}
"set_bot_status" => {
let val = form.bool_value("bot");
DispatchOutcome::from_result(
client.set_bot_status(user_id, val).await,
"Bot status updated successfully",
"Failed to update bot status",
)
}
"set_system_status" => {
let val = form.bool_value("system");
DispatchOutcome::from_result(
client.set_system_status(user_id, val).await,
"System status updated successfully",
"Failed to update system status",
)
}
"change_username" => {
let Some(username) = get("username") else {
return DispatchOutcome::error("Username is required");
@@ -52,6 +52,7 @@ pub fn account_tab(
(sessions_card(config, sessions))
(quick_actions_card(base, user, csrf_token, username_sign_in, can_revoke_recovery_kit))
(clear_fields_card(base, user, csrf_token))
(user_status_card(base, user, csrf_token))
(security_actions_card(base, user, csrf_token))
(webauthn_credentials_card(base, user, webauthn_credentials, csrf_token))
}
@@ -310,6 +311,19 @@ fn clear_fields_card(base: &str, user: &AdminUser, csrf_token: &str) -> Markup {
}
}
fn user_status_card(base: &str, user: &AdminUser, csrf_token: &str) -> Markup {
let is_bot = user.bot;
let is_sys = user.system;
html! {
(card_with_header("User Status", html! {
div class="grid grid-cols-1 gap-4 md:grid-cols-2" {
(status_toggle(base, &user.id, "set_bot_status", is_bot, "bot", csrf_token))
(status_toggle(base, &user.id, "set_system_status", is_sys, "system", csrf_token))
}
}))
}
}
fn security_actions_card(base: &str, user: &AdminUser, csrf_token: &str) -> Markup {
html! {
(card_with_header("Security Actions", html! {
@@ -450,3 +464,40 @@ fn action_form(
}
}
}
fn status_toggle(
base: &str,
uid: &str,
action: &str,
active: bool,
kind: &str,
csrf: &str,
) -> Markup {
let status_val = if active { "false" } else { "true" };
let label = format!(
"{} {} Status",
if active { "Remove" } else { "Set" },
capitalize(kind)
);
let action_url = format!("{base}/users/{uid}?action={action}&status={status_val}&tab=account");
html! {
form method="post"
action=(&action_url)
hx-post=(&action_url)
hx-target="#flash-container"
hx-swap="none"
hx-push-url="false" {
(csrf_input(csrf))
input type="hidden" name=(kind) value=(status_val);
button type="submit" class=(BTN_CLS) { (label) }
}
}
}
fn capitalize(s: &str) -> String {
let mut c = s.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().chain(c).collect(),
}
}
@@ -437,6 +437,11 @@ fn acls_card(
(flag_checkbox("acls[]", item.to_string(), item, checked, true))
}
}
@for item in &user.acls {
@if !acl::ALL_ACLS.iter().any(|known| known == &item.as_str()) {
input type="hidden" name="acls[]" value=(item);
}
}
(form_actions(html! {
(submit_button("Save ACLs"))
}))