1use axum::extract::{Path, State};
8use axum::http::{header, HeaderMap, StatusCode};
9use axum::response::{AppendHeaders, IntoResponse};
10use axum::routing::{get, post};
11use axum::{Json, Router};
12use chrono::Utc;
13use serde_json::{json, Value};
14use uuid::Uuid;
15
16use crate::auth::{self, Principal, Role};
17use crate::error::{AppError, AppResult};
18use crate::models::{
19 ApiKey, ApiKeyCreate, ApiKeyView, LoginRequest, User, UserCreate, UserUpdate, UserView,
20};
21use crate::state::AppState;
22
23pub fn router() -> Router<AppState> {
24 Router::new()
25 .route("/api/v1/auth/login", post(login))
26 .route("/api/v1/auth/logout", post(logout))
27 .route("/api/v1/auth/me", get(me))
28 .route("/api/v1/users", get(list_users).post(create_user))
29 .route(
30 "/api/v1/users/{id}",
31 axum::routing::patch(update_user).delete(delete_user),
32 )
33 .route("/api/v1/api-keys", get(list_api_keys).post(create_api_key))
34 .route(
35 "/api/v1/api-keys/{id}",
36 axum::routing::delete(delete_api_key),
37 )
38}
39
40const MIN_PASSWORD_LEN: usize = 8;
41
42async fn login(
43 State(st): State<AppState>,
44 Json(body): Json<LoginRequest>,
45) -> AppResult<impl IntoResponse> {
46 let candidate = sqlx::query_as::<_, User>("SELECT * FROM users WHERE username = ?")
47 .bind(body.username.trim())
48 .fetch_optional(&st.pool)
49 .await?;
50 let phc = candidate
53 .as_ref()
54 .map(|u| u.password_hash.as_str())
55 .unwrap_or_else(|| auth::dummy_password_hash());
56 let password_ok = auth::verify_password(&body.password, phc);
57 let user = match candidate {
58 Some(u) if u.active && password_ok => u,
59 _ => return Err(AppError::Unauthorized("invalid credentials".into())),
60 };
61 let (token, expires_at) = auth::issue_session(&st.pool, &st.cfg, &user.id).await?;
62 let principal = Principal {
63 id: user.id.clone(),
64 name: user
65 .display_name
66 .clone()
67 .unwrap_or_else(|| user.username.clone()),
68 role: Role::parse(&user.role).unwrap_or(Role::Viewer),
69 kind: crate::auth::PrincipalKind::User,
70 };
71 auth::audit(&st.pool, &principal, "login", "user", &user.id, json!({})).await;
72 let cookie = auth::session_cookie(&token, &st.cfg);
76 let body = Json(json!({
77 "token": token,
78 "expires_at": expires_at,
79 "user": UserView::from(user),
80 }));
81 Ok((AppendHeaders([(header::SET_COOKIE, cookie)]), body))
82}
83
84async fn logout(State(st): State<AppState>, headers: HeaderMap) -> AppResult<impl IntoResponse> {
85 if let Some(tok) = auth::token_from_headers(&headers) {
86 auth::revoke_session(&st.pool, &tok).await?;
87 }
88 let cookie = auth::clear_session_cookie(&st.cfg);
90 Ok((
91 StatusCode::NO_CONTENT,
92 AppendHeaders([(header::SET_COOKIE, cookie)]),
93 ))
94}
95
96async fn me(principal: Principal) -> AppResult<Json<Value>> {
97 Ok(Json(json!({
98 "id": principal.id,
99 "name": principal.name,
100 "role": principal.role.as_str(),
101 "kind": match principal.kind {
102 crate::auth::PrincipalKind::User => "user",
103 crate::auth::PrincipalKind::ApiKey => "api_key",
104 crate::auth::PrincipalKind::System => "system",
105 },
106 })))
107}
108
109async fn list_users(
110 State(st): State<AppState>,
111 principal: Principal,
112) -> AppResult<Json<Vec<UserView>>> {
113 principal.require(principal.can_admin(), "manage users")?;
114 let users = sqlx::query_as::<_, User>("SELECT * FROM users ORDER BY username ASC")
115 .fetch_all(&st.pool)
116 .await?;
117 Ok(Json(users.into_iter().map(UserView::from).collect()))
118}
119
120async fn create_user(
121 State(st): State<AppState>,
122 principal: Principal,
123 Json(body): Json<UserCreate>,
124) -> AppResult<(StatusCode, Json<UserView>)> {
125 principal.require(principal.can_admin(), "create users")?;
126 let username = body.username.trim();
127 if username.is_empty() {
128 return Err(AppError::BadRequest("`username` is required".into()));
129 }
130 if body.password.len() < MIN_PASSWORD_LEN {
131 return Err(AppError::BadRequest(format!(
132 "`password` must be at least {MIN_PASSWORD_LEN} characters"
133 )));
134 }
135 let role = body.role.as_deref().unwrap_or("viewer");
136 if !Role::is_valid(role) {
137 return Err(AppError::BadRequest(
138 "`role` must be admin|manager|guard|viewer|integration".into(),
139 ));
140 }
141 let hash = auth::hash_password(&body.password)?;
142 let id = format!("usr_{}", Uuid::new_v4().simple());
143 let now = Utc::now();
144 sqlx::query(
145 "INSERT INTO users (id, username, password_hash, role, display_name, active, created_at, updated_at)
146 VALUES (?,?,?,?,?,?,?,?)",
147 )
148 .bind(&id)
149 .bind(username)
150 .bind(hash)
151 .bind(role)
152 .bind(&body.display_name)
153 .bind(body.active.unwrap_or(true))
154 .bind(now)
155 .bind(now)
156 .execute(&st.pool)
157 .await?;
158 auth::audit(
159 &st.pool,
160 &principal,
161 "create_user",
162 "user",
163 &id,
164 json!({ "role": role }),
165 )
166 .await;
167 let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = ?")
168 .bind(&id)
169 .fetch_one(&st.pool)
170 .await?;
171 Ok((StatusCode::CREATED, Json(UserView::from(user))))
172}
173
174async fn update_user(
175 State(st): State<AppState>,
176 principal: Principal,
177 Path(id): Path<String>,
178 Json(body): Json<UserUpdate>,
179) -> AppResult<Json<UserView>> {
180 principal.require(principal.can_admin(), "modify users")?;
181 let cur = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = ?")
182 .bind(&id)
183 .fetch_optional(&st.pool)
184 .await?
185 .ok_or_else(|| AppError::NotFound(format!("user {id} not found")))?;
186
187 let role = body.role.unwrap_or_else(|| cur.role.clone());
188 if !Role::is_valid(&role) {
189 return Err(AppError::BadRequest(
190 "`role` must be admin|manager|guard|viewer|integration".into(),
191 ));
192 }
193 let active = body.active.unwrap_or(cur.active);
194 if cur.role == "admin" && (role != "admin" || !active) {
196 let other_admins: i64 = sqlx::query_scalar(
197 "SELECT COUNT(*) FROM users WHERE role = 'admin' AND active = 1 AND id != ?",
198 )
199 .bind(&id)
200 .fetch_one(&st.pool)
201 .await?;
202 if other_admins == 0 {
203 return Err(AppError::BadRequest(
204 "cannot demote or disable the last active admin".into(),
205 ));
206 }
207 }
208 let display_name = body.display_name.or(cur.display_name);
209 let password_hash = match body.password {
210 Some(p) if p.len() >= MIN_PASSWORD_LEN => auth::hash_password(&p)?,
211 Some(_) => {
212 return Err(AppError::BadRequest(format!(
213 "`password` must be at least {MIN_PASSWORD_LEN} characters"
214 )))
215 }
216 None => cur.password_hash,
217 };
218 sqlx::query(
219 "UPDATE users SET password_hash=?, role=?, display_name=?, active=?, updated_at=? WHERE id=?",
220 )
221 .bind(&password_hash)
222 .bind(&role)
223 .bind(&display_name)
224 .bind(active)
225 .bind(Utc::now())
226 .bind(&id)
227 .execute(&st.pool)
228 .await?;
229 if !active {
231 let _ = sqlx::query("DELETE FROM sessions WHERE user_id = ?")
232 .bind(&id)
233 .execute(&st.pool)
234 .await;
235 }
236 auth::audit(
237 &st.pool,
238 &principal,
239 "update_user",
240 "user",
241 &id,
242 json!({ "role": role, "active": active }),
243 )
244 .await;
245 let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = ?")
246 .bind(&id)
247 .fetch_one(&st.pool)
248 .await?;
249 Ok(Json(UserView::from(user)))
250}
251
252async fn delete_user(
253 State(st): State<AppState>,
254 principal: Principal,
255 Path(id): Path<String>,
256) -> AppResult<StatusCode> {
257 principal.require(principal.can_admin(), "delete users")?;
258 if principal.id == id {
259 return Err(AppError::BadRequest(
260 "cannot delete your own account".into(),
261 ));
262 }
263 let cur = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = ?")
264 .bind(&id)
265 .fetch_optional(&st.pool)
266 .await?
267 .ok_or_else(|| AppError::NotFound(format!("user {id} not found")))?;
268 if cur.role == "admin" {
269 let other_admins: i64 = sqlx::query_scalar(
270 "SELECT COUNT(*) FROM users WHERE role = 'admin' AND active = 1 AND id != ?",
271 )
272 .bind(&id)
273 .fetch_one(&st.pool)
274 .await?;
275 if other_admins == 0 {
276 return Err(AppError::BadRequest(
277 "cannot delete the last active admin".into(),
278 ));
279 }
280 }
281 sqlx::query("DELETE FROM users WHERE id = ?")
282 .bind(&id)
283 .execute(&st.pool)
284 .await?;
285 auth::audit(&st.pool, &principal, "delete_user", "user", &id, json!({})).await;
286 Ok(StatusCode::NO_CONTENT)
287}
288
289async fn list_api_keys(
290 State(st): State<AppState>,
291 principal: Principal,
292) -> AppResult<Json<Vec<ApiKeyView>>> {
293 principal.require(principal.can_admin(), "manage API keys")?;
294 let keys = sqlx::query_as::<_, ApiKey>("SELECT * FROM api_keys ORDER BY created_at DESC")
295 .fetch_all(&st.pool)
296 .await?;
297 Ok(Json(keys.into_iter().map(ApiKeyView::from).collect()))
298}
299
300async fn create_api_key(
301 State(st): State<AppState>,
302 principal: Principal,
303 Json(body): Json<ApiKeyCreate>,
304) -> AppResult<(StatusCode, Json<Value>)> {
305 principal.require(principal.can_admin(), "create API keys")?;
306 if body.name.trim().is_empty() {
307 return Err(AppError::BadRequest("`name` is required".into()));
308 }
309 let role = body.role.as_deref().unwrap_or("integration");
310 if !Role::is_valid(role) {
311 return Err(AppError::BadRequest(
312 "`role` must be admin|manager|guard|viewer|integration".into(),
313 ));
314 }
315 let key = auth::random_token(auth::APIKEY_PREFIX);
316 let prefix: String = key.chars().take(12).collect();
317 let id = format!("key_{}", Uuid::new_v4().simple());
318 sqlx::query(
319 "INSERT INTO api_keys (id, name, key_hash, key_prefix, role, active, created_at)
320 VALUES (?,?,?,?,?,1,?)",
321 )
322 .bind(&id)
323 .bind(body.name.trim())
324 .bind(auth::token_hash(&key))
325 .bind(&prefix)
326 .bind(role)
327 .bind(Utc::now())
328 .execute(&st.pool)
329 .await?;
330 auth::audit(
331 &st.pool,
332 &principal,
333 "create_api_key",
334 "api_key",
335 &id,
336 json!({ "role": role }),
337 )
338 .await;
339 Ok((
341 StatusCode::CREATED,
342 Json(json!({ "id": id, "name": body.name.trim(), "role": role, "key": key })),
343 ))
344}
345
346async fn delete_api_key(
347 State(st): State<AppState>,
348 principal: Principal,
349 Path(id): Path<String>,
350) -> AppResult<StatusCode> {
351 principal.require(principal.can_admin(), "delete API keys")?;
352 let res = sqlx::query("DELETE FROM api_keys WHERE id = ?")
353 .bind(&id)
354 .execute(&st.pool)
355 .await?;
356 if res.rows_affected() == 0 {
357 return Err(AppError::NotFound(format!("api key {id} not found")));
358 }
359 auth::audit(
360 &st.pool,
361 &principal,
362 "delete_api_key",
363 "api_key",
364 &id,
365 json!({}),
366 )
367 .await;
368 Ok(StatusCode::NO_CONTENT)
369}