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};
8use serde::Deserialize;
9
10use crate::api::common::{display_name, hash_password, validate_account_name, validate_password};
11use crate::auth::{self, clear_session_cookie, parse_session_cookie, session_cookie};
12use crate::db::User;
13use crate::error::{ApiError, AppState};
14
15/// GET /api/auth/me
16///
17/// - No users at all → `200 {"first_boot": true}`
18/// - No/invalid session → `401`
19/// - Valid session → user info + visible roots
20pub async fn me(
21 State(state): State<Arc<AppState>>,
22 headers: HeaderMap,
23) -> Result<Json<Me>, ApiError> {
24 if state.db.user_count().await? == 0 {
25 return Ok(Json(Me {
26 first_boot: true,
27 user: None,
28 roots: Vec::new(),
29 allow_writable_shares: false,
30 }));
31 }
32
33 let user = require_user(&state, &headers).await?;
34 Ok(Json(me_for(&state, &user).await?))
35}
36
37/// The signed-in user behind the session cookie, or 401.
38async fn require_user(state: &AppState, headers: &HeaderMap) -> Result<User, ApiError> {
39 let Some(token) = parse_session_cookie(headers) else {
40 return Err(ApiError::new(StatusCode::UNAUTHORIZED, "not signed in"));
41 };
42 state.db.session_user(&token).await?.ok_or_else(|| {
43 ApiError::new(
44 StatusCode::UNAUTHORIZED,
45 "session expired, please sign in again",
46 )
47 })
48}
49
50/// Build the `/api/auth/me` payload for an authenticated user.
51async fn me_for(state: &AppState, user: &User) -> Result<Me, ApiError> {
52 let roots: Vec<RootInfo> = state
53 .db
54 .user_roots(user.id)
55 .await?
56 .into_iter()
57 .map(|r| RootInfo {
58 id: r.id,
59 name: display_name(&state.root, &r.path),
60 path: r.path,
61 mode: r.mode,
62 })
63 .collect();
64
65 Ok(Me {
66 first_boot: false,
67 user: Some(UserInfo {
68 id: user.id,
69 name: user.name.clone(),
70 is_admin: user.is_admin,
71 single_click_open: user.single_click,
72 }),
73 roots,
74 allow_writable_shares: state.db.allow_writable_shares().await?,
75 })
76}
77
78/// PUT /api/auth/me
79///
80/// Update the signed-in user's profile settings. Returns the fresh `/me`
81/// payload so clients can apply the change immediately.
82#[derive(Deserialize)]
83pub(crate) struct ProfilePatch {
84 pub single_click_open: bool,
85}
86
87pub async fn update_profile(
88 State(state): State<Arc<AppState>>,
89 headers: HeaderMap,
90 Json(body): Json<ProfilePatch>,
91) -> Result<Json<Me>, ApiError> {
92 let mut user = require_user(&state, &headers).await?;
93 state
94 .db
95 .set_user_single_click(user.id, body.single_click_open)
96 .await?;
97 user.single_click = body.single_click_open;
98 Ok(Json(me_for(&state, &user).await?))
99}
100
101/// POST /api/auth/setup — create the first admin account.
102/// Only available while no users exist.
103pub async fn setup(
104 State(state): State<Arc<AppState>>,
105 Json(body): Json<Credentials>,
106) -> Result<Response, ApiError> {
107 let name = body.name.trim();
108 validate_account_name(name)?;
109 validate_password(&body.password)?;
110 if state.db.user_count().await? > 0 {
111 return Err(ApiError::new(
112 StatusCode::CONFLICT,
113 "server is already set up",
114 ));
115 }
116
117 let pass_hash = hash_password(&body.password).await?;
118 let user = state.db.create_admin(name, &pass_hash).await?;
119
120 let token = auth::random_token();
121 state.db.create_session(user.id, &token).await?;
122
123 let mut res = Json(OkResp { ok: true }).into_response();
124 res.headers_mut().insert(
125 header::SET_COOKIE,
126 session_cookie(&token, state.https).parse().unwrap(),
127 );
128 Ok(res)
129}
130
131/// POST /api/auth/login
132pub async fn login(
133 State(state): State<Arc<AppState>>,
134 Json(body): Json<Credentials>,
135) -> Result<Response, ApiError> {
136 let Some(user) = state.db.verify_password(&body.name, &body.password).await? else {
137 return Err(ApiError::new(
138 StatusCode::UNAUTHORIZED,
139 "invalid name or password",
140 ));
141 };
142
143 let token = auth::random_token();
144 state.db.create_session(user.id, &token).await?;
145
146 let mut res = Json(OkResp { ok: true }).into_response();
147 res.headers_mut().insert(
148 header::SET_COOKIE,
149 session_cookie(&token, state.https).parse().unwrap(),
150 );
151 Ok(res)
152}
153
154/// POST /api/auth/logout
155pub async fn logout(State(state): State<Arc<AppState>>, headers: HeaderMap) -> Response {
156 if let Some(token) = parse_session_cookie(&headers) {
157 let _ = state.db.delete_session(&token).await;
158 }
159 let mut res = Json(OkResp { ok: true }).into_response();
160 res.headers_mut().insert(
161 header::SET_COOKIE,
162 clear_session_cookie(state.https).parse().unwrap(),
163 );
164 res
165}
166