use axum::{
Json,
extract::{Path, State},
response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
use crate::routes::{graphql::app_state::AppState, studio::not_implemented};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AdminUser {
pub sub: String,
pub email: String,
pub provider: String,
pub created_at: String,
pub last_sign_in: Option<String>,
pub mfa_enrolled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserListResponse {
pub users: Vec<AdminUser>,
pub total: u64,
pub page: u32,
pub page_size: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserInviteRequest {
pub email: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserInviteResponse {
pub success: bool,
pub message: String,
}
pub async fn list_users_handler(State(_state): State<AppState>) -> Response {
not_implemented(
"studio.users.list",
"FraiseQL does not maintain a user directory; identities are owned by the \
configured identity provider. Enumerate users there.",
)
}
pub async fn invite_user_handler(
State(_state): State<AppState>,
Json(_req): Json<UserInviteRequest>,
) -> Response {
not_implemented(
"studio.users.invite",
"No invitation subsystem is wired: FraiseQL does not send magic links. Invite \
users through the configured identity provider.",
)
}
pub async fn revoke_user_handler(
Path(user_id): Path<String>,
State(state): State<AppState>,
) -> Response {
let Some(manager) = state.revocation_manager.as_ref() else {
return not_implemented(
"studio.users.revoke",
"Token revocation is not configured, so no session can be revoked. Enable \
[security.token_revocation] in fraiseql.toml.",
);
};
match manager.revoke_all_for_user(&user_id).await {
Ok(()) => Json(serde_json::json!({
"success": true,
"user_id": user_id,
"message": "All sessions revoked",
}))
.into_response(),
Err(e) => {
tracing::error!(error = %e, user = %user_id, "revoke-all failed");
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"error": "revocation_failed",
"message": "The revocation store rejected the write; sessions are NOT revoked.",
})),
)
.into_response()
},
}
}
pub async fn mfa_status_handler(
Path(_user_id): Path<String>,
State(_state): State<AppState>,
) -> Response {
not_implemented(
"studio.users.mfa",
"MFA enrollment state is not exposed through the admin API.",
)
}