mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-26 23:50:17 +00:00
feat: add role-based access control (admin/member)
Adds a `role` field (admin|member) to user management: Schema: - `role TEXT NOT NULL DEFAULT 'member'` added to users table in both PostgreSQL V14 migration and libSQL schema/incremental migration - UserRecord gains `role: String` field - UserIdentity gains `role: String` field, populated from DB in DbAuthenticator and defaulting to "admin" for single-user mode Access control: - AdminUser extractor: returns 403 Forbidden if role != "admin" - /api/admin/users/* handlers: require AdminUser (create, list, detail, update, suspend, activate) - POST /api/invitations: requires AdminUser (only admins can invite) - User creation accepts optional "role" param (defaults to "member") - Invitation acceptance creates users with "member" role Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
@@ -24,6 +24,8 @@ use crate::db::Database;
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UserIdentity {
|
||||
pub user_id: String,
|
||||
/// `admin` or `member`.
|
||||
pub role: String,
|
||||
/// Additional user scopes this identity can read from.
|
||||
pub workspace_read_scopes: Vec<String>,
|
||||
}
|
||||
@@ -61,6 +63,7 @@ impl MultiAuthState {
|
||||
hash,
|
||||
UserIdentity {
|
||||
user_id,
|
||||
role: "admin".to_string(),
|
||||
workspace_read_scopes: Vec::new(),
|
||||
},
|
||||
)],
|
||||
@@ -163,7 +166,8 @@ impl DbAuthenticator {
|
||||
|
||||
let identity = UserIdentity {
|
||||
user_id: user_record.id.clone(),
|
||||
workspace_read_scopes: Vec::new(), // DB-backed users don't have static scopes yet
|
||||
role: user_record.role.clone(),
|
||||
workspace_read_scopes: Vec::new(),
|
||||
};
|
||||
|
||||
// Record token usage (best-effort, don't block auth)
|
||||
@@ -230,6 +234,31 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// Axum extractor that requires the authenticated user to have the `admin` role.
|
||||
///
|
||||
/// Use instead of `AuthenticatedUser` on endpoints that modify system-wide
|
||||
/// state (user management, model selection, extension/skill installation).
|
||||
pub struct AdminUser(pub UserIdentity);
|
||||
|
||||
impl<S> FromRequestParts<S> for AdminUser
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = (StatusCode, &'static str);
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
|
||||
let identity = parts
|
||||
.extensions
|
||||
.get::<UserIdentity>()
|
||||
.cloned()
|
||||
.ok_or((StatusCode::UNAUTHORIZED, "Not authenticated"))?;
|
||||
if identity.role != "admin" {
|
||||
return Err((StatusCode::FORBIDDEN, "Admin role required"));
|
||||
}
|
||||
Ok(AdminUser(identity))
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether query-string token auth is allowed for this request.
|
||||
///
|
||||
/// Only GET requests to streaming endpoints may use `?token=xxx`. This
|
||||
@@ -346,6 +375,7 @@ mod tests {
|
||||
"tok-alice".to_string(),
|
||||
UserIdentity {
|
||||
user_id: "alice".to_string(),
|
||||
role: "admin".to_string(),
|
||||
workspace_read_scopes: Vec::new(),
|
||||
},
|
||||
);
|
||||
@@ -353,6 +383,7 @@ mod tests {
|
||||
"tok-bob".to_string(),
|
||||
UserIdentity {
|
||||
user_id: "bob".to_string(),
|
||||
role: "admin".to_string(),
|
||||
workspace_read_scopes: Vec::new(),
|
||||
},
|
||||
);
|
||||
@@ -622,6 +653,7 @@ mod tests {
|
||||
"tok-alice".to_string(),
|
||||
UserIdentity {
|
||||
user_id: "alice".to_string(),
|
||||
role: "admin".to_string(),
|
||||
workspace_read_scopes: vec!["shared".to_string()],
|
||||
},
|
||||
);
|
||||
@@ -629,6 +661,7 @@ mod tests {
|
||||
"tok-bob".to_string(),
|
||||
UserIdentity {
|
||||
user_id: "bob".to_string(),
|
||||
role: "admin".to_string(),
|
||||
workspace_read_scopes: vec!["shared".to_string(), "alice".to_string()],
|
||||
},
|
||||
);
|
||||
|
||||
@@ -8,14 +8,14 @@ use rand::rngs::OsRng;
|
||||
use sha2::{Digest, Sha256};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::channels::web::auth::AuthenticatedUser;
|
||||
use crate::channels::web::auth::{AdminUser, AuthenticatedUser};
|
||||
use crate::channels::web::server::GatewayState;
|
||||
use crate::db::{InvitationRecord, UserRecord};
|
||||
|
||||
/// POST /api/invitations — create an invitation.
|
||||
/// POST /api/invitations — create an invitation (admin only).
|
||||
pub async fn invitations_create_handler(
|
||||
State(state): State<Arc<GatewayState>>,
|
||||
AuthenticatedUser(user): AuthenticatedUser,
|
||||
AdminUser(user): AdminUser,
|
||||
Json(body): Json<serde_json::Value>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
let store = state.store.as_ref().ok_or((
|
||||
@@ -170,6 +170,7 @@ pub async fn invitations_accept_handler(
|
||||
email: invitation.email.clone(),
|
||||
display_name: display_name.clone(),
|
||||
status: "active".to_string(),
|
||||
role: "member".to_string(),
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
last_login_at: None,
|
||||
|
||||
@@ -9,14 +9,14 @@ use axum::{
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::channels::web::auth::AuthenticatedUser;
|
||||
use crate::channels::web::auth::{AdminUser, AuthenticatedUser};
|
||||
use crate::channels::web::server::GatewayState;
|
||||
use crate::db::UserRecord;
|
||||
|
||||
/// POST /api/admin/users — create a new user.
|
||||
pub async fn users_create_handler(
|
||||
State(state): State<Arc<GatewayState>>,
|
||||
AuthenticatedUser(user): AuthenticatedUser,
|
||||
AdminUser(user): AdminUser,
|
||||
Json(body): Json<serde_json::Value>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
let store = state.store.as_ref().ok_or((
|
||||
@@ -34,6 +34,17 @@ pub async fn users_create_handler(
|
||||
.to_string();
|
||||
|
||||
let email = body.get("email").and_then(|v| v.as_str()).map(String::from);
|
||||
let role = body
|
||||
.get("role")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("member")
|
||||
.to_string();
|
||||
if role != "admin" && role != "member" {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"role must be 'admin' or 'member'".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let user_id = Uuid::new_v4().to_string();
|
||||
|
||||
@@ -43,6 +54,7 @@ pub async fn users_create_handler(
|
||||
email,
|
||||
display_name: display_name.clone(),
|
||||
status: "active".to_string(),
|
||||
role,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
last_login_at: None,
|
||||
@@ -60,6 +72,7 @@ pub async fn users_create_handler(
|
||||
"email": user_record.email,
|
||||
"display_name": user_record.display_name,
|
||||
"status": user_record.status,
|
||||
"role": user_record.role,
|
||||
"created_at": user_record.created_at.to_rfc3339(),
|
||||
"created_by": user_record.created_by,
|
||||
})))
|
||||
@@ -88,6 +101,7 @@ pub async fn users_list_handler(
|
||||
"email": u.email,
|
||||
"display_name": u.display_name,
|
||||
"status": u.status,
|
||||
"role": u.role,
|
||||
"created_at": u.created_at.to_rfc3339(),
|
||||
"updated_at": u.updated_at.to_rfc3339(),
|
||||
"last_login_at": u.last_login_at.map(|dt| dt.to_rfc3339()),
|
||||
@@ -121,6 +135,7 @@ pub async fn users_detail_handler(
|
||||
"email": user_record.email,
|
||||
"display_name": user_record.display_name,
|
||||
"status": user_record.status,
|
||||
"role": user_record.role,
|
||||
"created_at": user_record.created_at.to_rfc3339(),
|
||||
"updated_at": user_record.updated_at.to_rfc3339(),
|
||||
"last_login_at": user_record.last_login_at.map(|dt| dt.to_rfc3339()),
|
||||
@@ -172,6 +187,7 @@ pub async fn users_update_handler(
|
||||
"email": updated.email,
|
||||
"display_name": updated.display_name,
|
||||
"status": updated.status,
|
||||
"role": updated.role,
|
||||
"created_at": updated.created_at.to_rfc3339(),
|
||||
"updated_at": updated.updated_at.to_rfc3339(),
|
||||
"metadata": updated.metadata,
|
||||
|
||||
@@ -3102,6 +3102,7 @@ mod tests {
|
||||
// without needing the full auth middleware layer.
|
||||
req.extensions_mut().insert(UserIdentity {
|
||||
user_id: "test".to_string(),
|
||||
role: "admin".to_string(),
|
||||
workspace_read_scopes: Vec::new(),
|
||||
});
|
||||
|
||||
@@ -3186,6 +3187,7 @@ mod tests {
|
||||
// without needing the full auth middleware layer.
|
||||
req.extensions_mut().insert(UserIdentity {
|
||||
user_id: "test".to_string(),
|
||||
role: "admin".to_string(),
|
||||
workspace_read_scopes: Vec::new(),
|
||||
});
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ fn two_user_auth() -> MultiAuthState {
|
||||
"tok-alice".to_string(),
|
||||
UserIdentity {
|
||||
user_id: "alice".to_string(),
|
||||
role: "admin".to_string(),
|
||||
workspace_read_scopes: vec!["shared".to_string()],
|
||||
},
|
||||
);
|
||||
@@ -40,6 +41,7 @@ fn two_user_auth() -> MultiAuthState {
|
||||
"tok-bob".to_string(),
|
||||
UserIdentity {
|
||||
user_id: "bob".to_string(),
|
||||
role: "admin".to_string(),
|
||||
workspace_read_scopes: vec!["shared".to_string(), "alice".to_string()],
|
||||
},
|
||||
);
|
||||
@@ -188,6 +190,7 @@ mod workspace_pool {
|
||||
);
|
||||
let identity = UserIdentity {
|
||||
user_id: "alice".to_string(),
|
||||
role: "admin".to_string(),
|
||||
workspace_read_scopes: vec![],
|
||||
};
|
||||
let ws = pool.get_or_create(&identity).await;
|
||||
@@ -216,6 +219,7 @@ mod workspace_pool {
|
||||
);
|
||||
let identity = UserIdentity {
|
||||
user_id: "alice".to_string(),
|
||||
role: "admin".to_string(),
|
||||
workspace_read_scopes: vec![],
|
||||
};
|
||||
let ws = pool.get_or_create(&identity).await;
|
||||
@@ -239,6 +243,7 @@ mod workspace_pool {
|
||||
);
|
||||
let identity = UserIdentity {
|
||||
user_id: "bob".to_string(),
|
||||
role: "admin".to_string(),
|
||||
workspace_read_scopes: vec!["alice".to_string(), "shared".to_string()],
|
||||
};
|
||||
let ws = pool.get_or_create(&identity).await;
|
||||
@@ -265,10 +270,12 @@ mod workspace_pool {
|
||||
);
|
||||
let alice_id = UserIdentity {
|
||||
user_id: "alice".to_string(),
|
||||
role: "admin".to_string(),
|
||||
workspace_read_scopes: vec![],
|
||||
};
|
||||
let bob_id = UserIdentity {
|
||||
user_id: "bob".to_string(),
|
||||
role: "admin".to_string(),
|
||||
workspace_read_scopes: vec![],
|
||||
};
|
||||
|
||||
@@ -300,6 +307,7 @@ mod workspace_pool {
|
||||
);
|
||||
let identity = UserIdentity {
|
||||
user_id: "alice".to_string(),
|
||||
role: "admin".to_string(),
|
||||
workspace_read_scopes: vec!["token-scope".to_string()],
|
||||
};
|
||||
let ws = pool.get_or_create(&identity).await;
|
||||
|
||||
+22
-17
@@ -10,7 +10,7 @@ use crate::db::libsql::LibSqlBackend;
|
||||
use crate::db::{ApiTokenRecord, DatabaseError, InvitationRecord, UserRecord, UserStore};
|
||||
|
||||
fn row_to_user(row: &libsql::Row) -> Result<UserRecord, DatabaseError> {
|
||||
let metadata_str = get_text(row, 8);
|
||||
let metadata_str = get_text(row, 9);
|
||||
let metadata: serde_json::Value = serde_json::from_str(&metadata_str)
|
||||
.map_err(|e| DatabaseError::Serialization(e.to_string()))?;
|
||||
Ok(UserRecord {
|
||||
@@ -18,10 +18,11 @@ fn row_to_user(row: &libsql::Row) -> Result<UserRecord, DatabaseError> {
|
||||
email: get_opt_text(row, 1),
|
||||
display_name: get_text(row, 2),
|
||||
status: get_text(row, 3),
|
||||
created_at: get_ts(row, 4),
|
||||
updated_at: get_ts(row, 5),
|
||||
last_login_at: get_opt_ts(row, 6),
|
||||
created_by: get_opt_text(row, 7),
|
||||
role: get_text(row, 4),
|
||||
created_at: get_ts(row, 5),
|
||||
updated_at: get_ts(row, 6),
|
||||
last_login_at: get_opt_ts(row, 7),
|
||||
created_by: get_opt_text(row, 8),
|
||||
metadata,
|
||||
})
|
||||
}
|
||||
@@ -69,14 +70,15 @@ impl UserStore for LibSqlBackend {
|
||||
|
||||
conn.execute(
|
||||
r#"
|
||||
INSERT INTO users (id, email, display_name, status, created_at, updated_at, last_login_at, created_by, metadata)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
|
||||
INSERT INTO users (id, email, display_name, status, role, created_at, updated_at, last_login_at, created_by, metadata)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10)
|
||||
"#,
|
||||
params![
|
||||
user.id.as_str(),
|
||||
opt_text(user.email.as_deref()),
|
||||
user.display_name.as_str(),
|
||||
user.status.as_str(),
|
||||
user.role.as_str(),
|
||||
fmt_ts(&user.created_at),
|
||||
fmt_ts(&user.updated_at),
|
||||
fmt_opt_ts(&user.last_login_at),
|
||||
@@ -94,7 +96,7 @@ impl UserStore for LibSqlBackend {
|
||||
let mut rows = conn
|
||||
.query(
|
||||
r#"
|
||||
SELECT id, email, display_name, status, created_at, updated_at,
|
||||
SELECT id, email, display_name, status, role, created_at, updated_at,
|
||||
last_login_at, created_by, metadata
|
||||
FROM users WHERE id = ?1
|
||||
"#,
|
||||
@@ -118,7 +120,7 @@ impl UserStore for LibSqlBackend {
|
||||
let mut rows = conn
|
||||
.query(
|
||||
r#"
|
||||
SELECT id, email, display_name, status, created_at, updated_at,
|
||||
SELECT id, email, display_name, status, role, created_at, updated_at,
|
||||
last_login_at, created_by, metadata
|
||||
FROM users WHERE email = ?1
|
||||
"#,
|
||||
@@ -144,7 +146,7 @@ impl UserStore for LibSqlBackend {
|
||||
let mut rows = if let Some(status) = status {
|
||||
conn.query(
|
||||
r#"
|
||||
SELECT id, email, display_name, status, created_at, updated_at,
|
||||
SELECT id, email, display_name, status, role, created_at, updated_at,
|
||||
last_login_at, created_by, metadata
|
||||
FROM users WHERE status = ?1
|
||||
ORDER BY created_at DESC
|
||||
@@ -156,7 +158,7 @@ impl UserStore for LibSqlBackend {
|
||||
} else {
|
||||
conn.query(
|
||||
r#"
|
||||
SELECT id, email, display_name, status, created_at, updated_at,
|
||||
SELECT id, email, display_name, status, role, created_at, updated_at,
|
||||
last_login_at, created_by, metadata
|
||||
FROM users
|
||||
ORDER BY created_at DESC
|
||||
@@ -316,7 +318,7 @@ impl UserStore for LibSqlBackend {
|
||||
SELECT
|
||||
t.id, t.user_id, t.name, t.token_prefix, t.expires_at,
|
||||
t.last_used_at, t.created_at, t.revoked_at,
|
||||
u.id, u.email, u.display_name, u.status, u.created_at,
|
||||
u.id, u.email, u.display_name, u.status, u.role, u.created_at,
|
||||
u.updated_at, u.last_login_at, u.created_by, u.metadata
|
||||
FROM api_tokens t
|
||||
JOIN users u ON u.id = t.user_id
|
||||
@@ -351,7 +353,7 @@ impl UserStore for LibSqlBackend {
|
||||
revoked_at: get_opt_ts(&row, 7),
|
||||
};
|
||||
|
||||
let metadata_str = get_text(&row, 16);
|
||||
let metadata_str = get_text(&row, 17);
|
||||
let metadata: serde_json::Value = serde_json::from_str(&metadata_str)
|
||||
.map_err(|e| DatabaseError::Serialization(e.to_string()))?;
|
||||
|
||||
@@ -360,10 +362,11 @@ impl UserStore for LibSqlBackend {
|
||||
email: get_opt_text(&row, 9),
|
||||
display_name: get_text(&row, 10),
|
||||
status: get_text(&row, 11),
|
||||
created_at: get_ts(&row, 12),
|
||||
updated_at: get_ts(&row, 13),
|
||||
last_login_at: get_opt_ts(&row, 14),
|
||||
created_by: get_opt_text(&row, 15),
|
||||
role: get_text(&row, 12),
|
||||
created_at: get_ts(&row, 13),
|
||||
updated_at: get_ts(&row, 14),
|
||||
last_login_at: get_opt_ts(&row, 15),
|
||||
created_by: get_opt_text(&row, 16),
|
||||
metadata,
|
||||
};
|
||||
|
||||
@@ -541,6 +544,7 @@ mod tests {
|
||||
email: Some(format!("{}@test.com", id)),
|
||||
display_name: id.to_string(),
|
||||
status: "active".to_string(),
|
||||
role: "member".to_string(),
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
last_login_at: None,
|
||||
@@ -730,6 +734,7 @@ mod tests {
|
||||
email: Some("[email protected]".to_string()),
|
||||
display_name: "New User".to_string(),
|
||||
status: "active".to_string(),
|
||||
role: "member".to_string(),
|
||||
created_at: Utc::now(),
|
||||
updated_at: Utc::now(),
|
||||
last_login_at: None,
|
||||
|
||||
@@ -587,6 +587,7 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
email TEXT UNIQUE,
|
||||
display_name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
role TEXT NOT NULL DEFAULT 'member',
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
last_login_at TEXT,
|
||||
@@ -775,6 +776,7 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
email TEXT UNIQUE,
|
||||
display_name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
role TEXT NOT NULL DEFAULT 'member',
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
||||
last_login_at TEXT,
|
||||
|
||||
@@ -320,6 +320,8 @@ pub struct UserRecord {
|
||||
pub display_name: String,
|
||||
/// `active`, `suspended`, or `deactivated`.
|
||||
pub status: String,
|
||||
/// `admin` or `member`.
|
||||
pub role: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub last_login_at: Option<DateTime<Utc>>,
|
||||
|
||||
+10
-7
@@ -2291,14 +2291,15 @@ impl Store {
|
||||
let conn = self.conn().await?;
|
||||
conn.execute(
|
||||
r#"
|
||||
INSERT INTO users (id, email, display_name, status, created_at, updated_at, last_login_at, created_by, metadata)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
INSERT INTO users (id, email, display_name, status, role, created_at, updated_at, last_login_at, created_by, metadata)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
"#,
|
||||
&[
|
||||
&user.id,
|
||||
&user.email,
|
||||
&user.display_name,
|
||||
&user.status,
|
||||
&user.role,
|
||||
&user.created_at,
|
||||
&user.updated_at,
|
||||
&user.last_login_at,
|
||||
@@ -2314,7 +2315,7 @@ impl Store {
|
||||
pub async fn get_user(&self, id: &str) -> Result<Option<UserRecord>, DatabaseError> {
|
||||
let conn = self.conn().await?;
|
||||
let row = conn
|
||||
.query_opt("SELECT id, email, display_name, status, created_at, updated_at, last_login_at, created_by, metadata FROM users WHERE id = $1", &[&id])
|
||||
.query_opt("SELECT id, email, display_name, status, role, created_at, updated_at, last_login_at, created_by, metadata FROM users WHERE id = $1", &[&id])
|
||||
.await?;
|
||||
Ok(row.map(|r| row_to_user(&r)))
|
||||
}
|
||||
@@ -2326,7 +2327,7 @@ impl Store {
|
||||
) -> Result<Option<UserRecord>, DatabaseError> {
|
||||
let conn = self.conn().await?;
|
||||
let row = conn
|
||||
.query_opt("SELECT id, email, display_name, status, created_at, updated_at, last_login_at, created_by, metadata FROM users WHERE email = $1", &[&email])
|
||||
.query_opt("SELECT id, email, display_name, status, role, created_at, updated_at, last_login_at, created_by, metadata FROM users WHERE email = $1", &[&email])
|
||||
.await?;
|
||||
Ok(row.map(|r| row_to_user(&r)))
|
||||
}
|
||||
@@ -2337,13 +2338,13 @@ impl Store {
|
||||
let rows = match status {
|
||||
Some(s) => {
|
||||
conn.query(
|
||||
"SELECT id, email, display_name, status, created_at, updated_at, last_login_at, created_by, metadata FROM users WHERE status = $1 ORDER BY created_at DESC",
|
||||
"SELECT id, email, display_name, status, role, created_at, updated_at, last_login_at, created_by, metadata FROM users WHERE status = $1 ORDER BY created_at DESC",
|
||||
&[&s],
|
||||
)
|
||||
.await?
|
||||
}
|
||||
None => {
|
||||
conn.query("SELECT id, email, display_name, status, created_at, updated_at, last_login_at, created_by, metadata FROM users ORDER BY created_at DESC", &[])
|
||||
conn.query("SELECT id, email, display_name, status, role, created_at, updated_at, last_login_at, created_by, metadata FROM users ORDER BY created_at DESC", &[])
|
||||
.await?
|
||||
}
|
||||
};
|
||||
@@ -2475,7 +2476,7 @@ impl Store {
|
||||
.query_opt(
|
||||
r#"
|
||||
SELECT t.id, t.user_id, t.name, t.token_prefix, t.expires_at, t.last_used_at, t.created_at, t.revoked_at,
|
||||
u.id as u_id, u.email, u.display_name, u.status, u.created_at as u_created_at, u.updated_at, u.last_login_at, u.created_by, u.metadata
|
||||
u.id as u_id, u.email, u.display_name, u.status, u.role, u.created_at as u_created_at, u.updated_at, u.last_login_at, u.created_by, u.metadata
|
||||
FROM api_tokens t
|
||||
JOIN users u ON t.user_id = u.id
|
||||
WHERE t.token_hash = $1
|
||||
@@ -2502,6 +2503,7 @@ impl Store {
|
||||
email: r.get("email"),
|
||||
display_name: r.get("display_name"),
|
||||
status: r.get("status"),
|
||||
role: r.get("role"),
|
||||
created_at: r.get("u_created_at"),
|
||||
updated_at: r.get("updated_at"),
|
||||
last_login_at: r.get("last_login_at"),
|
||||
@@ -2639,6 +2641,7 @@ fn row_to_user(row: &tokio_postgres::Row) -> UserRecord {
|
||||
email: row.get("email"),
|
||||
display_name: row.get("display_name"),
|
||||
status: row.get("status"),
|
||||
role: row.get("role"),
|
||||
created_at: row.get("created_at"),
|
||||
updated_at: row.get("updated_at"),
|
||||
last_login_at: row.get("last_login_at"),
|
||||
|
||||
Reference in New Issue
Block a user