[ee] feat: show disabled toggle in superadmin user list, add disabled field to API

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hugocasa
2026-03-25 11:22:12 +01:00
parent d65a0b2647
commit ed4761ac23
8 changed files with 41323 additions and 10 deletions
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email, login_type::text, verified, super_admin, devops, name, company, username, NULL::bool as operator_only, first_time_user, role_source FROM password ORDER BY super_admin DESC, devops DESC, email LIMIT $1 OFFSET $2",
"query": "SELECT email, login_type::text, verified, super_admin, devops, name, company, username, NULL::bool as operator_only, first_time_user, role_source, disabled FROM password ORDER BY super_admin DESC, devops DESC, email LIMIT $1 OFFSET $2",
"describe": {
"columns": [
{
@@ -57,6 +57,11 @@
"ordinal": 10,
"name": "role_source",
"type_info": "Varchar"
},
{
"ordinal": 11,
"name": "disabled",
"type_info": "Bool"
}
],
"parameters": {
@@ -76,8 +81,9 @@
true,
null,
false,
false,
false
]
},
"hash": "05027983ffdb11824190543754d0be922e1463d2046753cf80377369a90013ab"
"hash": "115a9cb44d0a41952c08dc36e0331410d32a8d672cfa4929e9e3763c51daa1bc"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "WITH active_users AS (SELECT distinct username as email FROM (SELECT username, timestamp, operation FROM audit_partitioned UNION ALL SELECT username, timestamp, operation FROM audit) AS a WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login' OR operation = 'users.token.refresh')),\n authors as (SELECT distinct email FROM usr WHERE usr.operator IS false)\n SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, devops, name, company, username, first_time_user, role_source\n FROM password\n WHERE email IN (SELECT email FROM active_users)\n ORDER BY super_admin DESC, devops DESC\n LIMIT $1 OFFSET $2",
"query": "WITH active_users AS (SELECT distinct username as email FROM (SELECT username, timestamp, operation FROM audit_partitioned UNION ALL SELECT username, timestamp, operation FROM audit) AS a WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login' OR operation = 'users.token.refresh')),\n authors as (SELECT distinct email FROM usr WHERE usr.operator IS false)\n SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, devops, name, company, username, first_time_user, role_source, disabled\n FROM password\n WHERE email IN (SELECT email FROM active_users)\n ORDER BY super_admin DESC, devops DESC\n LIMIT $1 OFFSET $2",
"describe": {
"columns": [
{
@@ -57,6 +57,11 @@
"ordinal": 10,
"name": "role_source",
"type_info": "Varchar"
},
{
"ordinal": 11,
"name": "disabled",
"type_info": "Bool"
}
],
"parameters": {
@@ -76,8 +81,9 @@
true,
true,
false,
false,
false
]
},
"hash": "60118de85463098220b1c74f667b6fedb0f3f0040844c3774145e8f1f4c023ce"
"hash": "a5fd115e7be5129d623543bbfa7b5b31f0efc6d8ef73f691009c73f833dcee10"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT email, login_type::TEXT, super_admin, devops, verified, name, company, username, NULL::bool as operator_only, first_time_user, role_source FROM password WHERE email = $1",
"query": "SELECT email, login_type::TEXT, super_admin, devops, verified, name, company, username, NULL::bool as operator_only, first_time_user, role_source, disabled FROM password WHERE email = $1",
"describe": {
"columns": [
{
@@ -57,6 +57,11 @@
"ordinal": 10,
"name": "role_source",
"type_info": "Varchar"
},
{
"ordinal": 11,
"name": "disabled",
"type_info": "Bool"
}
],
"parameters": {
@@ -75,8 +80,9 @@
true,
null,
false,
false,
false
]
},
"hash": "65c59e224e460351c2f88261f8b1b1e7ce2bb160270b59c0f359b7952453b2b9"
"hash": "f0c9c54740cc1c0c2a6fa4e79d4d504b7b5cb7a39538ab9abeb44f781c711493"
}
+22 -3
View File
@@ -157,6 +157,7 @@ pub struct GlobalUserInfo {
operator_only: Option<bool>,
first_time_user: bool,
role_source: String,
disabled: bool,
}
#[derive(Serialize, Debug)]
@@ -213,6 +214,7 @@ pub struct EditUser {
pub is_super_admin: Option<bool>,
pub is_devops: Option<bool>,
pub name: Option<String>,
pub disabled: Option<bool>,
}
#[derive(Deserialize)]
@@ -396,7 +398,7 @@ async fn list_users_as_super_admin(
GlobalUserInfo,
"WITH active_users AS (SELECT distinct username as email FROM (SELECT username, timestamp, operation FROM audit_partitioned UNION ALL SELECT username, timestamp, operation FROM audit) AS a WHERE timestamp > NOW() - INTERVAL '1 month' AND (operation = 'users.login' OR operation = 'oauth.login' OR operation = 'users.token.refresh')),
authors as (SELECT distinct email FROM usr WHERE usr.operator IS false)
SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, devops, name, company, username, first_time_user, role_source
SELECT email, email NOT IN (SELECT email FROM authors) as operator_only, login_type::text, verified, super_admin, devops, name, company, username, first_time_user, role_source, disabled
FROM password
WHERE email IN (SELECT email FROM active_users)
ORDER BY super_admin DESC, devops DESC
@@ -409,7 +411,7 @@ async fn list_users_as_super_admin(
} else {
sqlx::query_as!(
GlobalUserInfo,
"SELECT email, login_type::text, verified, super_admin, devops, name, company, username, NULL::bool as operator_only, first_time_user, role_source FROM password ORDER BY super_admin DESC, devops DESC, email LIMIT \
"SELECT email, login_type::text, verified, super_admin, devops, name, company, username, NULL::bool as operator_only, first_time_user, role_source, disabled FROM password ORDER BY super_admin DESC, devops DESC, email LIMIT \
$1 OFFSET $2",
per_page as i32,
offset as i32
@@ -625,7 +627,7 @@ async fn global_whoami(
) -> JsonResult<GlobalUserInfo> {
let user = sqlx::query_as!(
GlobalUserInfo,
"SELECT email, login_type::TEXT, super_admin, devops, verified, name, company, username, NULL::bool as operator_only, first_time_user, role_source FROM password WHERE \
"SELECT email, login_type::TEXT, super_admin, devops, verified, name, company, username, NULL::bool as operator_only, first_time_user, role_source, disabled FROM password WHERE \
email = $1",
email
)
@@ -648,6 +650,7 @@ async fn global_whoami(
operator_only: None,
first_time_user: false,
role_source: "manual".to_string(),
disabled: false,
}))
} else {
Err(user.unwrap_err())
@@ -1407,6 +1410,22 @@ async fn update_user(
.await?;
}
if let Some(d) = eu.disabled {
sqlx::query_scalar!(
"UPDATE password SET disabled = $1 WHERE email = $2",
d,
&email_to_update
)
.execute(&mut *tx)
.await?;
if d {
// Delete all tokens for immediate session revocation
sqlx::query!("DELETE FROM token WHERE email = $1", &email_to_update)
.execute(&mut *tx)
.await?;
}
}
audit_log(
&mut *tx,
&authed,
+5
View File
@@ -588,6 +588,8 @@ paths:
type: boolean
name:
type: string
disabled:
type: boolean
responses:
"200":
description: user updated
@@ -23168,6 +23170,8 @@ components:
role_source:
type: string
enum: ["manual", "instance_group"]
disabled:
type: boolean
required:
- email
@@ -23176,6 +23180,7 @@ components:
- verified
- first_time_user
- role_source
- disabled
Flow:
allOf:
@@ -332,6 +332,7 @@
{/if}
<Cell head>Name</Cell>
<Cell head>Auth</Cell>
<Cell head>Enabled</Cell>
{#if activeOnly}
<Cell head>Kind</Cell>
{/if}
@@ -343,7 +344,7 @@
</Head>
<tbody>
{#if filteredUsers && users}
{#each filteredUsers.slice(0, nbDisplayed) as { email, super_admin, devops, login_type, name, username, operator_only, role_source }, i (email)}
{#each filteredUsers.slice(0, nbDisplayed) as { email, super_admin, devops, login_type, name, username, operator_only, role_source, disabled }, i (email)}
<tr class={i % 2 === 0 ? 'bg-surface-tertiary' : 'bg-surface'}>
<Cell first class="max-w-[200px]"
><a href="mailto:{email}" title={email} class="truncate block"
@@ -377,6 +378,26 @@
><span title={login_type} class="truncate block">{login_type}</span
></Cell
>
<Cell>
<Toggle
size="xs"
checked={!disabled}
on:change={async () => {
try {
await UserService.globalUserUpdate({
email,
requestBody: {
disabled: !disabled
}
})
sendUserToast(disabled ? 'User enabled' : 'User disabled')
listUsers(activeOnly)
} catch (e) {
sendUserToast('Failed to update user', true)
}
}}
/>
</Cell>
{#if activeOnly}
<Cell>
{#if operator_only}
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff