quorum_public/db/queries/auth.rs
1//! Authentication related database queries
2//!
3//! This file contains functions for Creating an account (signup), login, token management and deleteing a user account.
4
5use crate::db::queries::tokens;
6use crate::models::user::UpdateUserProfileRequest;
7use crate::models::user::User;
8use crate::utility::auth_common::check_email_address;
9use axum::http::StatusCode;
10use quorum_core::db::DB;
11use std::error::Error;
12
13/// Creates a new user account in the database
14///
15/// # Arguments
16///* `db` - A reference to the database connection
17/// * `username` - The desired username for the new account
18/// * `email` - An optional email address for the new account
19/// * `password_hash` - The hashed password for the new account
20///
21/// # Returns
22/// * `Ok(User)` - The newly created user object if the operation was successful
23/// * `Err(Box<dyn Error>)` - An error if the operation failed, such as if the username is already taken or if there was a database error
24///
25/// # Errors
26/// * "Failed to create user" - If the database query did not return a user object, indicating that the user creation failed
27///
28/// # Example
29/// ```rust
30/// use crate::db::DB;
31/// use crate::db::queries::auth;
32/// async fn example_signup(db: &DB) {
33/// let username = "new_user";
34/// let email = Some("123@example.com");
35/// let password_hash = "hashed_password";
36/// match auth::signup_user(db, username, email, password_hash).await {
37/// Ok(user) => println!("User created: {:?}", user),
38/// Err(e) => eprintln!("Error creating user: {}", e),
39/// }
40/// }
41///```
42pub async fn signup_user(
43 db: &DB,
44 username: &str,
45 email: Option<&str>,
46 password: &str,
47 email_backup_codes: Option<Vec<String>>,
48) -> Result<User, Box<dyn Error + Send + Sync>> {
49 if password.len() < 12 || password.len() > 35 {
50 return Err("Invalid password length".into());
51 }
52
53 if let Some(email) = email
54 && !check_email_address(email)
55 {
56 return Err("Invalid email address".into());
57 }
58
59 let password_hash = crate::utility::auth_common::hash(password)
60 .map_err(|e| format!("Failed to hash password: {}", e))?;
61
62 let mut response = db
63 .query(
64 "CREATE users SET
65 username = $username,
66 email = $email,
67 password_hash = $password_hash,
68 email_backup_codes = $email_backup_codes",
69 )
70 .bind(("username", username.to_string()))
71 .bind(("email", email.map(|e| e.to_string())))
72 .bind(("password_hash", password_hash))
73 .bind(("email_backup_codes", email_backup_codes))
74 .await?;
75
76 let user: Vec<User> = response.take(0)?;
77 user.into_iter()
78 .next()
79 .ok_or("Failed to create user".into())
80}
81
82/// Deletes a user from the database by their ID
83///
84/// # Arguments
85/// * `db` - A reference to the database connection
86/// * `user_id` - The ID of the user to delete
87///
88/// # Returns
89/// * `Ok(())` - If the user was successfully deleted
90/// * `Err(Box<dyn Error>)` - An error if the operation failed, such as if there was a database error
91///
92/// # Errors
93/// * "Failed to delete user" - If the database query did not execute successfully, indicating that the user could not be deleted
94///
95/// # Example
96/// ```rust
97/// use crate::db::DB;
98/// use crate::db::queries::auth;
99/// async fn example_delete_user(db: &DB) {
100/// let user_id = "user_id_to_delete";
101/// match auth::delete_user_by_id(db, user_id).await {
102/// Ok(()) => println!("User deleted successfully"),
103/// Err(e) => eprintln!("Error deleting user: {}", e),
104/// }
105/// }
106///```
107pub async fn delete_user_by_id(db: &DB, user_id: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
108 tokens::delete_all_user_tokens(db, user_id).await?;
109
110 let query = format!("DELETE FROM users:{}", user_id);
111 db.query(&query).await?;
112 Ok(())
113}
114
115/// Stores a refresh token in the database for a specific user
116///
117/// # Arguments
118/// * `db` - A reference to the database connection
119/// * `user_id` - The ID of the user for whom the refresh token is being stored
120/// * `refresh_token` - The refresh token string to store
121/// * `expires_at` - The expiration time of the refresh token as a Unix timestamp
122///
123/// # Returns
124/// * `Ok(())` - If the refresh token was successfully stored
125/// * `Err(Box<dyn Error>)` - An error if the operation failed, such as if there was a database error
126///
127/// # Errors
128/// * "Failed to store refresh token" - If the database query did not execute successfully, indicating that the refresh token could not be stored
129///
130/// # Example
131/// ```rust
132/// use crate::db::DB;
133/// use crate::db::queries::auth;
134/// async fn example_store_refresh_token(db: &DB) {
135/// let user_id = "user_id";
136/// let refresh_token = "refresh_token_string";
137/// let expires_at = 1700000000; // Example expiration timestamp
138/// match auth::store_refresh_token(db, user_id, refresh_token, expires_at).await {
139/// Ok(()) => println!("Refresh token stored successfully"),
140/// Err(e) => eprintln!("Error storing refresh token: {}", e),
141/// }
142/// }
143///```
144pub async fn store_refresh_token(
145 db: &DB,
146 user_id: &str,
147 refresh_token: &str,
148 expires_at: i64,
149) -> Result<(), Box<dyn Error + Send + Sync>> {
150 tokens::store_refresh_token(db, user_id, refresh_token, expires_at).await
151}
152
153/// Revokes a refresh token, preventing it from being used for future authentication
154///
155/// # Arguments
156/// * `db` - A reference to the database connection
157/// * `refresh_token` - The refresh token string to revoke
158///
159/// # Returns
160/// * `Ok(())` - If the refresh token was successfully revoked
161/// * `Err(Box<dyn Error>)` - An error if the operation failed, such as if there was a database error
162///
163/// # Errors
164/// * "Failed to revoke refresh token" - If the database query did not execute successfully, indicating that the refresh token could not be revoked
165///
166/// # Example
167/// ```rust
168/// use crate::db::DB;
169/// use crate::db::queries::auth;
170/// async fn example_revoke_refresh_token(db: &DB) {
171/// let refresh_token = "refresh_token_string";
172/// match auth::revoke_refresh_token(db, refresh_token).await {
173/// Ok(()) => println!("Refresh token revoked successfully"),
174/// Err(e) => eprintln!("Error revoking refresh token: {}", e),
175/// }
176/// }
177///```
178pub async fn revoke_refresh_token(
179 db: &DB,
180 refresh_token: &str,
181) -> Result<(), Box<dyn Error + Send + Sync>> {
182 tokens::revoke_refresh_token(db, refresh_token).await
183}
184
185/// Get's user's data to allow them to be verified
186///
187/// # Arguments
188/// * `db` - A reference to the database connection
189/// * `username` - The username of the user to retrieve
190/// * `password` - The password of the user to retrieve
191///
192/// # Returns
193/// * `Ok(User)` - The user object if the operation was successful
194/// * `Err(Box<dyn Error>)` - An error if the operation failed, such
195///
196/// # Example
197/// ```rust
198/// use crate::db::DB;
199/// use crate::db::queries::auth;
200/// async fn example_get_user(db: &DB) {
201/// let username = "existing_user";
202/// let password = "user_password";
203/// match auth::get_user(db, username, password).await {
204/// Ok(user) => println!("User retrieved: {:?}", user),
205/// Err(e) => eprintln!("Error retrieving user: {}", e),
206/// }
207/// }
208///```
209pub async fn verify_user_credentials(
210 db: &DB,
211 username_or_email: &str,
212 password: &str,
213) -> Result<User, (StatusCode, String)> {
214 let mut response = db
215 .query("SELECT * FROM users WHERE (username = $value OR email = $value) AND is_banned = false AND is_deleted = false LIMIT 1")
216 .bind(("value", username_or_email.to_string()))
217 .await
218 .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Database error".to_string()))?;
219
220 let user: Option<User> = response.take::<Option<User>>(0).map_err(|_| {
221 (
222 StatusCode::INTERNAL_SERVER_ERROR,
223 "Database error".to_string(),
224 )
225 })?;
226
227 match user {
228 Some(u) => {
229 if let Some(ref hash) = u.password_hash {
230 if crate::utility::auth_common::verify(password, hash).unwrap_or(false) {
231 Ok(u)
232 } else {
233 Err((
234 StatusCode::UNAUTHORIZED,
235 "Invalid username or password".to_string(),
236 ))
237 }
238 } else {
239 Err((
240 StatusCode::UNAUTHORIZED,
241 "Invalid username or password".to_string(),
242 ))
243 }
244 }
245 None => Err((
246 StatusCode::UNAUTHORIZED,
247 "Invalid username or password".to_string(),
248 )),
249 }
250}
251
252/// Updates a user's profile information in the database
253///
254/// # Arguments
255/// * `db` - A reference to the database connection
256/// * `user_id` - The ID of the user whose profile is being updated
257/// * `email` - The new email address for the user
258/// * `username` - The new username for the user
259///
260/// # Returns
261/// * `Ok(User)` - The updated user object if the operation was successful
262/// * `Err((StatusCode, String))` - An error if the operation failed, containing an HTTP status code and an error message
263///
264/// # Example
265/// ```rust
266/// use crate::db::DB;
267/// use crate::db::queries::auth;
268/// async fn example_update_user_profile(db: &DB) {
269/// let user_id = "user_id";
270/// let new_email = "test@example.com";
271/// let new_username = "new_username";
272/// match auth::update_user_profile(db, user_id, new_email, new_username).await {
273/// Ok(user) => println!("User profile updated: {:?}", user),
274/// Err((status, message)) => eprintln!("Error updating user profile ({}): {}", status, message),
275/// }
276/// }
277///```
278pub async fn update_user_profile(
279 db: &DB,
280 payload: &UpdateUserProfileRequest,
281) -> Result<User, (StatusCode, String)> {
282 let mut response = db
283 .query(format!(
284 "UPDATE ONLY users:{} SET email = $email, username = $username RETURN AFTER",
285 payload.user_id
286 ))
287 .bind(("email", payload.email.clone()))
288 .bind(("username", payload.username.clone()))
289 .await
290 .map_err(|_| {
291 (
292 StatusCode::INTERNAL_SERVER_ERROR,
293 "Database error".to_string(),
294 )
295 })?;
296
297 let user: Option<User> = response.take(0).map_err(|_| {
298 (
299 StatusCode::INTERNAL_SERVER_ERROR,
300 "Failed to parse user".to_string(),
301 )
302 })?;
303
304 user.ok_or_else(|| (StatusCode::NOT_FOUND, "User not found".to_string()))
305}
306
307/// Promotes a user to admin status in the database
308///
309/// This function uses `#[allow(dead_code)]` due to rust providing a false positive warning
310/// that this is dead code and not used, despite obvious usage in `cli/server.rs` -> called from `cli/mod.rs`
311/// # Arguments
312/// * `db` - A reference to the database connection
313/// * `username` - The username of the user to promote to admin
314///
315/// # Returns
316/// * `Ok(())` - If the user was successfully promoted to admin
317/// * `Err(Box<dyn Error>)` - An error if the operation failed, such as if an admin already exists or if there was a database error
318///
319/// # Examples
320/// ```rust
321/// use crate::db::DB;
322/// use crate::db::queries::auth;
323/// async fn example_make_admin(db: &DB) {
324/// let username = "user_to_promote";
325/// match auth::make_admin(db, username).await {
326/// Ok(()) => println!("User promoted to admin successfully"),
327/// Err(e) => eprintln!("Error promoting user to admin: {}", e),
328/// }
329/// }
330///```
331#[allow(dead_code)]
332pub async fn make_admin(db: &DB, username: &str) -> Result<(), Box<dyn Error + Send + Sync>> {
333 // Check bootstrap flag first
334 let mut response = db
335 .query("SELECT first_admin_bootstrapped FROM server_config LIMIT 1")
336 .await?;
337
338 let bootstrapped: Option<bool> = response.take("first_admin_bootstrapped")?;
339 if bootstrapped.unwrap_or(false) {
340 return Err("An admin already exists. This command is one time only.".into());
341 }
342
343 // Promote the user
344 db.query("UPDATE users SET is_admin = true WHERE username = $username")
345 .bind(("username", username))
346 .await?;
347
348 // Lock the bootstrap
349 db.query("UPDATE server_config SET first_admin_bootstrapped = true")
350 .await?;
351
352 Ok(())
353}
354
355pub async fn validate_refresh_token(
356 db: &DB,
357 user_id: &str,
358 refresh_token: &str,
359) -> Result<(), Box<dyn Error + Send + Sync>> {
360 tokens::validate_refresh_token(db, user_id, refresh_token).await
361}