Skip to main content

allsource_core/infrastructure/web/
auth_api.rs

1use crate::infrastructure::security::{
2    auth::{Permission, Role, User},
3    middleware::{Admin, Authenticated, OptionalAuth},
4};
5use axum::{Json, extract::State, http::StatusCode};
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9// AppState is defined in api_v1.rs and re-exported
10use crate::infrastructure::web::api_v1::AppState;
11
12// ============================================================================
13// Request/Response Types
14// ============================================================================
15
16#[derive(Debug, Deserialize)]
17pub struct RegisterRequest {
18    pub username: String,
19    pub email: String,
20    pub password: String,
21    pub role: Option<Role>,
22    pub tenant_id: Option<String>,
23}
24
25#[derive(Debug, Serialize)]
26pub struct RegisterResponse {
27    pub user_id: Uuid,
28    pub username: String,
29    pub email: String,
30    pub role: Role,
31    pub tenant_id: String,
32}
33
34#[derive(Debug, Deserialize)]
35pub struct LoginRequest {
36    pub username: String,
37    pub password: String,
38}
39
40#[derive(Debug, Serialize)]
41pub struct LoginResponse {
42    pub token: String,
43    pub user: UserInfo,
44}
45
46#[derive(Debug, Serialize)]
47pub struct UserInfo {
48    pub id: Uuid,
49    pub username: String,
50    pub email: String,
51    pub role: Role,
52    pub tenant_id: String,
53}
54
55impl From<User> for UserInfo {
56    fn from(user: User) -> Self {
57        Self {
58            id: user.id,
59            username: user.username,
60            email: user.email,
61            role: user.role,
62            tenant_id: user.tenant_id,
63        }
64    }
65}
66
67#[derive(Debug, Deserialize)]
68pub struct CreateApiKeyRequest {
69    pub name: String,
70    pub role: Option<Role>,
71    pub expires_in_days: Option<i64>,
72    /// Admin override: create the key for a specific tenant instead of the caller's tenant.
73    /// Only honoured when the caller has Admin permission; ignored otherwise.
74    pub tenant_id: Option<String>,
75}
76
77#[derive(Debug, Serialize)]
78pub struct CreateApiKeyResponse {
79    pub id: Uuid,
80    pub name: String,
81    pub key: String,
82    pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
83}
84
85#[derive(Debug, Serialize)]
86pub struct ApiKeyInfo {
87    pub id: Uuid,
88    pub name: String,
89    pub tenant_id: String,
90    pub role: Role,
91    pub created_at: chrono::DateTime<chrono::Utc>,
92    pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
93    pub active: bool,
94    pub last_used: Option<chrono::DateTime<chrono::Utc>>,
95}
96
97// ============================================================================
98// Handlers
99// ============================================================================
100
101/// Register a new user
102/// POST /api/v1/auth/register
103pub async fn register_handler(
104    State(state): State<AppState>,
105    OptionalAuth(auth): OptionalAuth,
106    Json(req): Json<RegisterRequest>,
107) -> Result<(StatusCode, Json<RegisterResponse>), (StatusCode, String)> {
108    // Only admins can register other users (or allow self-registration in dev mode)
109    let is_admin_request = auth.is_some();
110    if let Some(auth_ctx) = auth {
111        auth_ctx
112            .require_permission(Permission::Admin)
113            .map_err(|_| {
114                (
115                    StatusCode::FORBIDDEN,
116                    "Admin permission required".to_string(),
117                )
118            })?;
119    }
120
121    let role = req.role.unwrap_or(Role::Developer);
122
123    // Tenant isolation: if no tenant_id is provided, create an isolated tenant
124    // for this user. This prevents new users from seeing other users' data.
125    // Admin-created users (with auth context) can specify a tenant_id explicitly.
126    let tenant_id = if let Some(tid) = req.tenant_id {
127        tid
128    } else if is_admin_request {
129        // Admin explicitly registering a user without tenant_id → use default
130        "default".to_string()
131    } else {
132        // Self-registration: create an isolated tenant named after the user
133        let tid = format!("tenant-{}", req.username.to_lowercase().replace(' ', "-"));
134        tracing::info!(
135            "Auto-creating isolated tenant '{tid}' for new user '{}'",
136            req.username
137        );
138        tid
139    };
140
141    let user = state
142        .auth_manager
143        .register_user(
144            req.username,
145            req.email,
146            &req.password,
147            role.clone(),
148            tenant_id.clone(),
149        )
150        .map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
151
152    Ok((
153        StatusCode::CREATED,
154        Json(RegisterResponse {
155            user_id: user.id,
156            username: user.username,
157            email: user.email,
158            role,
159            tenant_id,
160        }),
161    ))
162}
163
164/// Login with username and password
165/// POST /api/v1/auth/login
166pub async fn login_handler(
167    State(state): State<AppState>,
168    Json(req): Json<LoginRequest>,
169) -> Result<Json<LoginResponse>, (StatusCode, String)> {
170    let token = state
171        .auth_manager
172        .authenticate(&req.username, &req.password)
173        .map_err(|e| (StatusCode::UNAUTHORIZED, e.to_string()))?;
174
175    // Get user info
176    let user_id = state
177        .auth_manager
178        .validate_token(&token)
179        .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
180        .sub
181        .parse::<Uuid>()
182        .map_err(|_| {
183            (
184                StatusCode::INTERNAL_SERVER_ERROR,
185                "Invalid user ID".to_string(),
186            )
187        })?;
188
189    let user = state
190        .auth_manager
191        .get_user(&user_id)
192        .ok_or_else(|| (StatusCode::NOT_FOUND, "User not found".to_string()))?;
193
194    Ok(Json(LoginResponse {
195        token,
196        user: user.into(),
197    }))
198}
199
200/// Get current user info
201/// GET /api/v1/auth/me
202///
203/// Returns identity info for the authenticated caller. Works for:
204/// - JWT-authenticated users (looks up full user record)
205/// - API key authentication (returns identity from claims)
206/// - Dev mode (returns synthetic dev identity from claims)
207pub async fn me_handler(
208    State(state): State<AppState>,
209    Authenticated(auth_ctx): Authenticated,
210) -> Result<Json<UserInfo>, (StatusCode, String)> {
211    // Try to look up a full user record if the subject is a valid UUID
212    if let Ok(user_id) = auth_ctx.user_id().parse::<Uuid>()
213        && let Some(user) = state.auth_manager.get_user(&user_id)
214    {
215        return Ok(Json(user.into()));
216    }
217
218    // For API key auth and dev mode, synthesize identity from claims.
219    // This is the path Query Service uses to validate API keys — it only
220    // needs tenant_id and role, not a full user record.
221    Ok(Json(UserInfo {
222        id: auth_ctx.user_id().parse::<Uuid>().unwrap_or_default(),
223        username: auth_ctx.user_id().to_string(),
224        email: String::new(),
225        role: auth_ctx.claims.role.clone(),
226        tenant_id: auth_ctx.tenant_id().to_string(),
227    }))
228}
229
230/// Create API key
231/// POST /api/v1/auth/api-keys
232///
233/// Requires Admin permission. Service accounts and developers (including agent API keys)
234/// cannot create new keys — only the control plane (which holds an admin service JWT)
235/// may provision API keys on behalf of users.
236pub async fn create_api_key_handler(
237    State(state): State<AppState>,
238    Authenticated(auth_ctx): Authenticated,
239    Json(req): Json<CreateApiKeyRequest>,
240) -> Result<(StatusCode, Json<CreateApiKeyResponse>), (StatusCode, String)> {
241    let role = req.role.unwrap_or(Role::ServiceAccount);
242
243    // Only admins can create API keys.
244    // Service accounts (agents) and developers are explicitly blocked so that
245    // agent keys cannot self-replicate or escalate privileges.
246    auth_ctx
247        .require_permission(Permission::Admin)
248        .map_err(|_| {
249            (
250                StatusCode::FORBIDDEN,
251                "Admin permission required to create API keys".to_string(),
252            )
253        })?;
254
255    let expires_at = req
256        .expires_in_days
257        .map(|days| chrono::Utc::now() + chrono::Duration::days(days));
258
259    // Admins may specify a target tenant; everyone else is restricted to their own tenant.
260    let effective_tenant_id = if let Some(ref tid) = req.tenant_id {
261        if auth_ctx.require_permission(Permission::Admin).is_ok() {
262            tid.clone()
263        } else {
264            auth_ctx.tenant_id().to_string()
265        }
266    } else {
267        auth_ctx.tenant_id().to_string()
268    };
269
270    let (api_key, key) =
271        state
272            .auth_manager
273            .create_api_key(req.name.clone(), effective_tenant_id, role, expires_at);
274
275    Ok((
276        StatusCode::CREATED,
277        Json(CreateApiKeyResponse {
278            id: api_key.id,
279            name: req.name,
280            key,
281            expires_at,
282        }),
283    ))
284}
285
286/// List API keys
287/// GET /api/v1/auth/api-keys
288pub async fn list_api_keys_handler(
289    State(state): State<AppState>,
290    Authenticated(auth_ctx): Authenticated,
291) -> Result<Json<Vec<ApiKeyInfo>>, (StatusCode, String)> {
292    let keys = state.auth_manager.list_api_keys(auth_ctx.tenant_id());
293
294    let key_infos: Vec<ApiKeyInfo> = keys
295        .into_iter()
296        .map(|k| ApiKeyInfo {
297            id: k.id,
298            name: k.name,
299            tenant_id: k.tenant_id,
300            role: k.role,
301            created_at: k.created_at,
302            expires_at: k.expires_at,
303            active: k.active,
304            last_used: k.last_used,
305        })
306        .collect();
307
308    Ok(Json(key_infos))
309}
310
311/// Revoke API key
312/// DELETE /api/v1/auth/api-keys/:id
313pub async fn revoke_api_key_handler(
314    State(state): State<AppState>,
315    Authenticated(auth_ctx): Authenticated,
316    axum::extract::Path(key_id): axum::extract::Path<Uuid>,
317) -> Result<StatusCode, (StatusCode, String)> {
318    auth_ctx
319        .require_permission(Permission::Write)
320        .map_err(|_| {
321            (
322                StatusCode::FORBIDDEN,
323                "Write permission required".to_string(),
324            )
325        })?;
326
327    state
328        .auth_manager
329        .revoke_api_key(&key_id)
330        .map_err(|e| (StatusCode::NOT_FOUND, e.to_string()))?;
331
332    Ok(StatusCode::NO_CONTENT)
333}
334
335/// List all users (admin only)
336/// GET /api/v1/auth/users
337pub async fn list_users_handler(
338    State(state): State<AppState>,
339    Admin(_): Admin,
340) -> Json<Vec<UserInfo>> {
341    let users = state.auth_manager.list_users();
342    Json(users.into_iter().map(UserInfo::from).collect())
343}
344
345/// Delete user (admin only)
346/// DELETE /api/v1/auth/users/:id
347pub async fn delete_user_handler(
348    State(state): State<AppState>,
349    Admin(_): Admin,
350    axum::extract::Path(user_id): axum::extract::Path<Uuid>,
351) -> Result<StatusCode, (StatusCode, String)> {
352    state
353        .auth_manager
354        .delete_user(&user_id)
355        .map_err(|e| (StatusCode::NOT_FOUND, e.to_string()))?;
356
357    Ok(StatusCode::NO_CONTENT)
358}