hermod_api/db/
user.rs

1use std::fmt::Debug;
2use std::str::FromStr;
3
4use anyhow::Context;
5use argon2::{password_hash::SaltString, Algorithm, Argon2, Params, PasswordHasher, Version};
6use serde::{Deserialize, Serialize};
7use sqlx::PgPool;
8use uuid::Uuid;
9
10/// Represents a user record in the database.
11#[derive(sqlx::FromRow, Serialize, Deserialize, Clone)]
12pub struct User {
13    pub id: Uuid,
14    pub username: String,
15    pub password: String,
16}
17
18impl Debug for User {
19    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
20        f.debug_struct("User")
21            .field("user_id", &self.id)
22            .field("username", &self.username)
23            .finish()
24    }
25}
26
27/// Struct used to create a new user in the database, password is hashed in `store()`
28pub struct NewUser {
29    pub id: Uuid,
30    pub username: String,
31    pub password: String,
32}
33
34impl NewUser {
35    /// Struct with unqiue defaults for each field
36    pub fn default() -> Self {
37        Self {
38            id: Uuid::new_v4(),
39            username: Uuid::new_v4().to_string(),
40            password: Uuid::new_v4().to_string(),
41        }
42    }
43
44    pub fn new(username: String, password: String) -> Self {
45        Self {
46            username: username.to_ascii_lowercase(),
47            password,
48            ..Self::default()
49        }
50    }
51
52    /// Store this struct in the users table
53    pub async fn store(&self, pool: &PgPool) -> Result<(), anyhow::Error> {
54        let salt = SaltString::generate(&mut rand::thread_rng());
55        // Match production parameters
56        let password_hash = Argon2::new(
57            Algorithm::Argon2id,
58            Version::V0x13,
59            Params::new(15000, 2, 1, None)?,
60        )
61        .hash_password(self.password.as_bytes(), &salt)?
62        .to_string();
63        sqlx::query!(
64            "INSERT INTO account (id, username, password)
65            VALUES ($1, $2, $3)",
66            self.id,
67            self.username.to_lowercase(),
68            password_hash,
69        )
70        .execute(pool)
71        .await?;
72        Ok(())
73    }
74}
75
76/// Returns a user from the database with the given `user_id`.
77pub async fn get_user_by_id(user_id: String, db_pool: &PgPool) -> Result<User, anyhow::Error> {
78    let user_id = Uuid::from_str(&user_id)?;
79    let user = sqlx::query_as!(User, "SELECT * FROM account WHERE id=$1", user_id)
80        .fetch_one(db_pool)
81        .await
82        .context(format!(
83            "Failed to fetch user with user_id {}",
84            user_id.to_string()
85        ))?;
86    Ok(user)
87}