···44 [username] TEXT NOT NULL UNIQUE,
55 -- Password will be null for users that have been invited but haven't set a password yet
66 [password] TEXT NULL,
77- [join_token] TEXT NULL UNIQUE,
77+ [reset_token] TEXT NULL UNIQUE,
88 PRIMARY KEY ([uuid])
99);
1010
+166-14
packages/node/src/api/auth.rs
···4455use eyre::{Context, ContextCompat, Result};
66use log::error;
77+use luminary_macros::wrap_err;
78use password_auth::verify_password;
89use rand_chacha::{
910 ChaCha12Rng,
1011 rand_core::{RngCore, SeedableRng},
1112};
1212-use salvo::{oapi::extract::JsonBody, prelude::*};
1313-use serde::Deserialize;
1313+use salvo::{
1414+ oapi::extract::{JsonBody, PathParam},
1515+ prelude::*,
1616+};
1717+use serde::{Deserialize, Serialize};
1418use sqlx::{SqlitePool, prelude::FromRow};
1519use uuid::Uuid;
1620···2024pub fn router() -> Router {
2125 return Router::with_path("/auth")
2226 .push(Router::with_path("login").post(login))
2323- .push(Router::with_path("logout").post(logout));
2727+ .push(Router::with_path("logout").post(logout))
2828+ .push(
2929+ Router::with_path("reset/{token}")
3030+ .get(verify_reset_token)
3131+ .post(reset_password),
3232+ )
3333+ .push(
3434+ Router::with_hoop(protected).push(
3535+ Router::with_path("users")
3636+ .get(get_users)
3737+ .post(create_user)
3838+ .push(Router::with_path("{user}").delete(delete_user)),
3939+ ),
4040+ );
2441}
25422643/// Reads username and password from the request body, and returns an authentication token if the credentials are valid.
···3956#[endpoint]
4057async fn logout(req: &mut Request, depot: &mut Depot) -> LuminaryResponse<()> {
4158 let auth = obtain!(depot, LuminaryAuthentication);
4242-4359 let token = extract_token(req).wrap_err("Missing or invalid authorization token")?;
44604561 auth.logout(token).await?;
4662 return Ok(().into());
4763}
48646565+/// Fetches a list of all users.
6666+#[endpoint]
6767+async fn get_users(depot: &mut Depot) -> LuminaryResponse<Vec<LuminaryUser>> {
6868+ let auth = obtain!(depot, LuminaryAuthentication);
6969+ let users = auth.get_users().await?;
7070+ return Ok(users.into());
7171+}
7272+7373+/// Creates a new user with the given username, returning a reset token that can be used to set the user's password.
7474+#[endpoint]
7575+async fn create_user(depot: &mut Depot, body: JsonBody<CreateUserRequest>) -> LuminaryResponse<String> {
7676+ let auth = obtain!(depot, LuminaryAuthentication);
7777+7878+ let reset_token = auth.create_user(&body.username).await?;
7979+ return Ok(reset_token.into());
8080+}
8181+8282+/// Request body for creating a new user.
8383+#[derive(Debug, Deserialize, ToSchema)]
8484+pub struct CreateUserRequest {
8585+ username: String,
8686+}
8787+8888+/// Deletes the user with the given UUID.
8989+#[endpoint]
9090+async fn delete_user(depot: &mut Depot, user: PathParam<String>) -> LuminaryResponse<()> {
9191+ let auth = obtain!(depot, LuminaryAuthentication);
9292+9393+ auth.delete_user(&user).await?;
9494+ return Ok(().into());
9595+}
9696+9797+/// Verifies that a reset token is valid.
9898+#[endpoint]
9999+async fn verify_reset_token(depot: &mut Depot, token: PathParam<String>) -> LuminaryResponse<String> {
100100+ let auth = obtain!(depot, LuminaryAuthentication);
101101+102102+ let user = auth
103103+ .get_user_from_reset_token(&token)
104104+ .await?
105105+ .wrap_err("Invalid reset token")?;
106106+107107+ return Ok(user.username.into());
108108+}
109109+110110+/// Resets a user's password using a reset token, which is invalidated after use.
111111+#[endpoint]
112112+async fn reset_password(
113113+ depot: &mut Depot,
114114+ token: PathParam<String>,
115115+ body: JsonBody<ResetPasswordRequest>,
116116+) -> LuminaryResponse<()> {
117117+ let auth = obtain!(depot, LuminaryAuthentication);
118118+119119+ let user = auth
120120+ .get_user_from_reset_token(&token)
121121+ .await?
122122+ .wrap_err("Invalid reset token")?;
123123+124124+ auth.set_password(&user.uuid, body.password.clone()).await?;
125125+ return Ok(().into());
126126+}
127127+128128+/// Request body for resetting a user's password.
129129+#[derive(Debug, Deserialize, ToSchema)]
130130+pub struct ResetPasswordRequest {
131131+ password: String,
132132+}
133133+49134/// Acts as the authentication backend for the Luminary Node, handling user authentication and bearer token management.
50135#[derive(Debug, Clone)]
51136pub struct LuminaryAuthentication {
···58143 Self { pool }
59144 }
601456161- /// Authenticates a user with the given credentials and returns a bearer token on success.
6262- pub async fn login(&self, credentials: LuminaryUserCredentials) -> Result<Option<String>> {
146146+ pub async fn verify_password(
147147+ &self,
148148+ credentials: LuminaryUserCredentials,
149149+ ) -> Result<Option<LuminaryUser>> {
63150 let user = sqlx::query_as!(
64151 LuminaryUser,
65152 "SELECT * FROM [user] WHERE [username] = ?",
···80167 .await
81168 .wrap_err("Password verification task failed")?;
82169170170+ return Ok(user);
171171+ }
172172+173173+ /// Authenticates a user with the given credentials and returns a bearer token on success.
174174+ pub async fn login(&self, credentials: LuminaryUserCredentials) -> Result<Option<String>> {
83175 // Terminate early if the user doesn't exist or the password is wrong
8484- let user = match user {
176176+ let user = match self.verify_password(credentials).await? {
85177 None => return Ok(None),
86178 Some(u) => u,
87179 };
···127219 return Ok(user);
128220 }
129221130130- async fn create_user(&self, username: &str) -> Result<String> {
222222+ /// Creates a new user with the given username and a random reset token, returning the reset token.
223223+ #[wrap_err("Failed to create user")]
224224+ pub async fn create_user(&self, username: &str) -> Result<String> {
131225 let uuid = Uuid::new_v4().to_string();
132226 let token = generate_token();
133227134228 sqlx::query!(
135135- "INSERT INTO [user] ([uuid], [username], [join_token]) VALUES (?, ?, ?)",
229229+ "INSERT INTO [user] ([uuid], [username], [reset_token]) VALUES (?, ?, ?)",
136230 uuid,
137231 username,
138232 token
···143237144238 return Ok(token);
145239 }
240240+241241+ /// Sets a user's password to the given value.
242242+ #[wrap_err("Failed to update password")]
243243+ pub async fn set_password(&self, uuid: &str, password: String) -> Result<()> {
244244+ let hashed_password = tokio::task::spawn_blocking(move || password_auth::generate_hash(password))
245245+ .await
246246+ .wrap_err("Failed to spawn hashing task")?;
247247+248248+ sqlx::query!(
249249+ "UPDATE [user] SET [password] = ?, [reset_token] = NULL WHERE [uuid] = ?",
250250+ hashed_password,
251251+ uuid
252252+ )
253253+ .execute(&self.pool)
254254+ .await
255255+ .wrap_err("Failed to set user password")?;
256256+257257+ return Ok(());
258258+ }
259259+260260+ /// Finds a user from their reset token, or [None] if the token is invalid.
261261+ #[wrap_err("Failed to get user from reset token")]
262262+ pub async fn get_user_from_reset_token(&self, token: &str) -> Result<Option<LuminaryUser>> {
263263+ let user = sqlx::query_as!(
264264+ LuminaryUser,
265265+ "SELECT * FROM [user] WHERE [reset_token] = ?",
266266+ token
267267+ )
268268+ .fetch_optional(&self.pool)
269269+ .await
270270+ .wrap_err("Failed to look up user from reset token")?;
271271+272272+ return Ok(user);
273273+ }
274274+275275+ /// Deletes a user from the database, along with all their sessions.
276276+ #[wrap_err("Failed to delete user")]
277277+ pub async fn delete_user(&self, uuid: &str) -> Result<()> {
278278+ sqlx::query!("DELETE FROM [user] WHERE [uuid] = ?", uuid)
279279+ .execute(&self.pool)
280280+ .await
281281+ .wrap_err("Failed to delete user")?;
282282+283283+ return Ok(());
284284+ }
285285+286286+ /// Fetches a list of all users.
287287+ #[wrap_err("Failed to get users")]
288288+ pub async fn get_users(&self) -> Result<Vec<LuminaryUser>> {
289289+ let users = sqlx::query_as!(LuminaryUser, "SELECT * FROM [user]")
290290+ .fetch_all(&self.pool)
291291+ .await
292292+ .wrap_err("Failed to query users")?;
293293+294294+ return Ok(users);
295295+ }
146296}
147297148298/// Salvo middleware for validating authentication.
···152302/// # Examples
153303/// ```
154304/// use salvo::{Router, oapi::endpoint};
155155-/// use crate::auth::protected;
305305+/// use crate::api::auth::protected;
156306///
157307/// let router = Router::new().hoop(protected).get(protected_handler);
158308///
···205355}
206356207357/// Represents a user in Luminary Node.
208208-#[derive(Clone, FromRow)]
358358+#[derive(Clone, Serialize, ToSchema, FromRow)]
209359pub struct LuminaryUser {
210360 uuid: String,
211361 username: String,
362362+ #[serde(skip_serializing)]
212363 password: Option<String>,
213213- join_token: Option<String>,
364364+ #[serde(skip_serializing)]
365365+ reset_token: Option<String>,
214366}
215367216368impl Debug for LuminaryUser {
···220372 .field("username", &self.username)
221373 .field("password", &"***")
222374 .field(
223223- "join_token",
224224- if self.join_token.is_none() {
375375+ "reset_token",
376376+ if self.reset_token.is_none() {
225377 &"None"
226378 } else {
227379 &"Some(***)"
+1-2
packages/node/src/api/mod.rs
···6767 .push(auth::router())
6868 .push(
6969 // New router for protected routes, to avoid repetition
7070- Router::new()
7171- .hoop(protected)
7070+ Router::with_hoop(protected)
7271 .push(Router::with_path("realtime").get(app_subscribe))
7372 .push(
7473 Router::with_path("/project/{project}")