auth.rs
⎇
Raw
1use std::sync::Arc;
2
3use api_types::{Credentials, Me, OkResp, RootInfo, UserInfo};
4use axum::Json;
5use axum::extract::State;
6use axum::http::{HeaderMap, StatusCode, header};
7use axum::response::{IntoResponse, Response};
8
9use crate::api::common::{display_name, validate_name, validate_password};
10use crate::auth::{self, clear_session_cookie, parse_session_cookie, session_cookie};
11use crate::error::{ApiError, AppState};
12
13/// GET /api/auth/me
14///
15/// - No users at all → `200 {"first_boot": true}`
16/// - No/invalid session → `401`
17/// - Valid session → user info + visible roots
18pub async fn me(
19 State(state): State<Arc<AppState>>,
20 headers: HeaderMap,
21) -> Result<Json<Me>, ApiError> {
22 if state.db.user_count().await == 0 {
23 return Ok(Json(Me {
24 first_boot: true,
25 user: None,
26 roots: Vec::new(),
27 allow_writable_shares: false,
28 }));
29 }
30
31 let Some(token) = parse_session_cookie(&headers) else {
32 return Err(ApiError::new(StatusCode::UNAUTHORIZED, "not signed in"));
33 };
34 let Some(user) = state.db.session_user(&token).await else {
35 return Err(ApiError::new(
36 StatusCode::UNAUTHORIZED,
37 "session expired, please sign in again",
38 ));
39 };
40
41 let roots: Vec<RootInfo> = state
42 .db
43 .user_roots(user.id)
44 .await
45 .into_iter()
46 .map(|r| RootInfo {
47 id: r.id,
48 name: display_name(&state.root, &r.path),
49 path: r.path,
50 mode: r.mode,
51 })
52 .collect();
53
54 Ok(Json(Me {
55 first_boot: false,
56 user: Some(UserInfo {
57 id: user.id,
58 name: user.name,
59 is_admin: user.is_admin,
60 }),
61 roots,
62 allow_writable_shares: state.db.allow_writable_shares().await,
63 }))
64}
65
66/// POST /api/auth/setup — create the first admin account.
67/// Only available while no users exist.
68pub async fn setup(
69 State(state): State<Arc<AppState>>,
70 Json(body): Json<Credentials>,
71) -> Result<Response, ApiError> {
72 let name = body.name.trim();
73 validate_name(name)?;
74 validate_password(&body.password)?;
75 if state.db.user_count().await > 0 {
76 return Err(ApiError::new(
77 StatusCode::CONFLICT,
78 "server is already set up",
79 ));
80 }
81
82 let pass_hash = auth::hash_password(&body.password).map_err(|e| {
83 ApiError::new(
84 StatusCode::INTERNAL_SERVER_ERROR,
85 format!("hashing failed: {e}"),
86 )
87 })?;
88 let user = state.db.create_admin(name, &pass_hash).await?;
89
90 let token = auth::random_token();
91 state.db.create_session(user.id, &token).await?;
92
93 let mut res = Json(OkResp { ok: true }).into_response();
94 res.headers_mut().insert(
95 header::SET_COOKIE,
96 session_cookie(&token, state.https).parse().unwrap(),
97 );
98 Ok(res)
99}
100
101/// POST /api/auth/login
102pub async fn login(
103 State(state): State<Arc<AppState>>,
104 Json(body): Json<Credentials>,
105) -> Result<Response, ApiError> {
106 let Some(user) = state.db.verify_password(&body.name, &body.password).await else {
107 return Err(ApiError::new(
108 StatusCode::UNAUTHORIZED,
109 "invalid name or password",
110 ));
111 };
112
113 let token = auth::random_token();
114 state.db.create_session(user.id, &token).await?;
115
116 let mut res = Json(OkResp { ok: true }).into_response();
117 res.headers_mut().insert(
118 header::SET_COOKIE,
119 session_cookie(&token, state.https).parse().unwrap(),
120 );
121 Ok(res)
122}
123
124/// POST /api/auth/logout
125pub async fn logout(State(state): State<Arc<AppState>>, headers: HeaderMap) -> Response {
126 if let Some(token) = parse_session_cookie(&headers) {
127 let _ = state.db.delete_session(&token).await;
128 }
129 let mut res = Json(OkResp { ok: true }).into_response();
130 res.headers_mut().insert(
131 header::SET_COOKIE,
132 clear_session_cookie(state.https).parse().unwrap(),
133 );
134 res
135}
136