diff options
| author | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-12-09 13:07:14 +0100 |
|---|---|---|
| committer | Benedikt Peetz <benedikt.peetz@b-peetz.de> | 2025-12-09 13:07:14 +0100 |
| commit | c91dce4f77ae12453203f0a28b91efb6533cc095 (patch) | |
| tree | 4f50e755dff7f717d45309b08f9fe2c8c87f88bd /crates/rocie-server/src/storage | |
| parent | chore(rocie-client): Regenerate (diff) | |
| download | server-c91dce4f77ae12453203f0a28b91efb6533cc095.zip | |
feat(rocie-server): Implement basic user handling and authentication
Diffstat (limited to 'crates/rocie-server/src/storage')
| -rw-r--r-- | crates/rocie-server/src/storage/migrate/sql/0->1.sql | 8 | ||||
| -rw-r--r-- | crates/rocie-server/src/storage/sql/get/mod.rs | 7 | ||||
| -rw-r--r-- | crates/rocie-server/src/storage/sql/get/user/mod.rs | 77 | ||||
| -rw-r--r-- | crates/rocie-server/src/storage/sql/insert/mod.rs | 1 | ||||
| -rw-r--r-- | crates/rocie-server/src/storage/sql/insert/user/mod.rs | 117 | ||||
| -rw-r--r-- | crates/rocie-server/src/storage/sql/mod.rs | 1 | ||||
| -rw-r--r-- | crates/rocie-server/src/storage/sql/user.rs | 86 |
7 files changed, 294 insertions, 3 deletions
diff --git a/crates/rocie-server/src/storage/migrate/sql/0->1.sql b/crates/rocie-server/src/storage/migrate/sql/0->1.sql index 664f40f..e3dd879 100644 --- a/crates/rocie-server/src/storage/migrate/sql/0->1.sql +++ b/crates/rocie-server/src/storage/migrate/sql/0->1.sql @@ -27,6 +27,14 @@ CREATE TABLE parents ( FOREIGN KEY(parent) REFERENCES parents(id) ) STRICT; +-- Stores the registered users. +CREATE TABLE users ( + id TEXT UNIQUE NOT NULL PRIMARY KEY, + name TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + description TEXT +) STRICT; + -- Record with barcodes were bought, and how much of this buy is already used up. CREATE TABLE buys ( buy_id TEXT UNIQUE NOT NULL PRIMARY KEY, diff --git a/crates/rocie-server/src/storage/sql/get/mod.rs b/crates/rocie-server/src/storage/sql/get/mod.rs index 1fb54b0..92b34aa 100644 --- a/crates/rocie-server/src/storage/sql/get/mod.rs +++ b/crates/rocie-server/src/storage/sql/get/mod.rs @@ -1,7 +1,8 @@ +pub(crate) mod barcode; pub(crate) mod product; -pub(crate) mod product_parent; pub(crate) mod product_amount; +pub(crate) mod product_parent; +pub(crate) mod recipe; pub(crate) mod unit; pub(crate) mod unit_property; -pub(crate) mod barcode; -pub(crate) mod recipe; +pub(crate) mod user; diff --git a/crates/rocie-server/src/storage/sql/get/user/mod.rs b/crates/rocie-server/src/storage/sql/get/user/mod.rs new file mode 100644 index 0000000..e36c6cf --- /dev/null +++ b/crates/rocie-server/src/storage/sql/get/user/mod.rs @@ -0,0 +1,77 @@ +use crate::{ + app::App, + storage::sql::user::{PasswordHash, User, UserId}, +}; + +use sqlx::query; + +impl User { + pub(crate) async fn get_all(app: &App) -> Result<Vec<Self>, get_all::Error> { + let records = query!( + " + SELECT id, name, password_hash, description + FROM users +" + ) + .fetch_all(&app.db) + .await?; + + Ok(records + .into_iter() + .map(|record| Self { + id: UserId::from_db(&record.id), + name: record.name, + password_hash: PasswordHash::from_db(record.password_hash), + description: record.description, + }) + .collect()) + } + + pub(crate) async fn from_id(app: &App, id: UserId) -> Result<Option<Self>, from_id::Error> { + let record = query!( + " + SELECT name, password_hash, description + FROM users + WHERE id = ? +", + id + ) + .fetch_optional(&app.db) + .await?; + + if let Some(record) = record { + Ok(Some(Self { + name: record.name, + description: record.description, + id, + password_hash: PasswordHash::from_db(record.password_hash), + })) + } else { + Ok(None) + } + } +} + +pub(crate) mod get_all { + use actix_web::ResponseError; + + #[derive(thiserror::Error, Debug)] + pub(crate) enum Error { + #[error("Failed to execute the sql query")] + SqlError(#[from] sqlx::Error), + } + + impl ResponseError for Error {} +} + +pub(crate) mod from_id { + use actix_web::ResponseError; + + #[derive(thiserror::Error, Debug)] + pub(crate) enum Error { + #[error("Failed to execute the sql query")] + SqlError(#[from] sqlx::Error), + } + + impl ResponseError for Error {} +} diff --git a/crates/rocie-server/src/storage/sql/insert/mod.rs b/crates/rocie-server/src/storage/sql/insert/mod.rs index 8a15385..54717c3 100644 --- a/crates/rocie-server/src/storage/sql/insert/mod.rs +++ b/crates/rocie-server/src/storage/sql/insert/mod.rs @@ -13,6 +13,7 @@ pub(crate) mod product_parent; pub(crate) mod unit; pub(crate) mod unit_property; pub(crate) mod recipe; +pub(crate) mod user; pub(crate) trait Transactionable: Sized + std::fmt::Debug + Serialize + DeserializeOwned diff --git a/crates/rocie-server/src/storage/sql/insert/user/mod.rs b/crates/rocie-server/src/storage/sql/insert/user/mod.rs new file mode 100644 index 0000000..325253e --- /dev/null +++ b/crates/rocie-server/src/storage/sql/insert/user/mod.rs @@ -0,0 +1,117 @@ +use serde::{Deserialize, Serialize}; +use sqlx::query; +use uuid::Uuid; + +use crate::storage::sql::{ + insert::{Operations, Transactionable}, + user::{PasswordHash, User, UserId}, +}; + +#[derive(Debug, Deserialize, Serialize)] +pub(crate) enum Operation { + RegisterUser { + id: UserId, + name: String, + description: Option<String>, + password_hash: PasswordHash, + }, +} + +impl Transactionable for Operation { + type ApplyError = apply::Error; + type UndoError = undo::Error; + + async fn apply(self, txn: &mut sqlx::SqliteConnection) -> Result<(), apply::Error> { + match self { + Operation::RegisterUser { + id, + name, + description, + password_hash, + } => { + let password_hash = password_hash.to_string(); + + query!( + " + INSERT INTO users (id, name, password_hash, description) + VALUES (?,?,?,?) +", + id, + name, + password_hash, + description, + ) + .execute(txn) + .await?; + } + } + Ok(()) + } + + async fn undo(self, txn: &mut sqlx::SqliteConnection) -> Result<(), undo::Error> { + match self { + Operation::RegisterUser { + id, + name, + description, + password_hash, + } => { + let password_hash = password_hash.to_string(); + + query!( + " + DELETE FROM users + WHERE id = ? AND name = ? AND description = ? AND password_hash = ?; +", + id, + name, + description, + password_hash, + ) + .execute(txn) + .await?; + } + } + Ok(()) + } +} + +pub(crate) mod undo { + #[derive(thiserror::Error, Debug)] + pub(crate) enum Error { + #[error("Failed to execute undo sql statments: {0}")] + SqlError(#[from] sqlx::Error), + } +} +pub(crate) mod apply { + #[derive(thiserror::Error, Debug)] + pub(crate) enum Error { + #[error("Failed to execute apply sql statments: {0}")] + SqlError(#[from] sqlx::Error), + } +} + +impl User { + pub(crate) fn register( + name: String, + password_hash: PasswordHash, + description: Option<String>, + ops: &mut Operations<Operation>, + ) -> Self { + let id = UserId::from(Uuid::new_v4()); + + ops.push(Operation::RegisterUser { + id, + name: name.clone(), + description: description.clone(), + password_hash: password_hash.clone(), + }); + + Self { + id, + name, + description, + password_hash, + } + } +} diff --git a/crates/rocie-server/src/storage/sql/mod.rs b/crates/rocie-server/src/storage/sql/mod.rs index dd46eab..315c251 100644 --- a/crates/rocie-server/src/storage/sql/mod.rs +++ b/crates/rocie-server/src/storage/sql/mod.rs @@ -9,6 +9,7 @@ pub(crate) mod product_parent; pub(crate) mod recipe; pub(crate) mod unit; pub(crate) mod unit_property; +pub(crate) mod user; macro_rules! mk_id { ($name:ident and $stub_name:ident) => { diff --git a/crates/rocie-server/src/storage/sql/user.rs b/crates/rocie-server/src/storage/sql/user.rs new file mode 100644 index 0000000..2bac555 --- /dev/null +++ b/crates/rocie-server/src/storage/sql/user.rs @@ -0,0 +1,86 @@ +use std::fmt::Display; + +use argon2::{ + Argon2, PasswordHasher, PasswordVerifier, + password_hash::{SaltString, rand_core::OsRng}, +}; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +use crate::storage::sql::mk_id; + +/// The definition of an rocie user. +#[derive(ToSchema, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize)] +pub(crate) struct User { + /// The unique ID for this user. + pub(crate) id: UserId, + + /// The user-displayed name of this user. + pub(crate) name: String, + + /// The hash of the user's password. + pub(crate) password_hash: PasswordHash, + + /// An description of this user. + #[schema(nullable = false)] + pub(crate) description: Option<String>, +} + +/// This is stored as an PHC password string. +/// +/// This type corresponds to the string representation of a PHC string as +/// described in the [PHC string format specification][1]. +/// +/// PHC strings have the following format: +/// +/// ```text +/// $<id>[$v=<version>][$<param>=<value>(,<param>=<value>)*][$<salt>[$<hash>]] +/// ``` +#[derive(ToSchema, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Serialize, Deserialize)] +pub(crate) struct PasswordHash { + value: String, +} +impl PasswordHash { + pub(crate) fn from_db(password_hash: String) -> PasswordHash { + Self { + value: password_hash, + } + } + + pub(crate) fn from_password(password: &str) -> Self { + let salt = SaltString::generate(&mut OsRng); + + let argon2 = Argon2::default(); + + let password_hash = argon2 + .hash_password(password.as_bytes(), &salt) + .expect("to not fail") + .to_string(); + + Self { + value: password_hash, + } + } + + /// Check that self, and the other password have the same hash. + pub(crate) fn verify(&self, other: &str) -> bool { + let argon2 = Argon2::default(); + + argon2 + .verify_password(other.as_bytes(), &self.as_argon_hash()) + .is_ok() + } + + fn as_argon_hash(&self) -> argon2::PasswordHash<'_> { + argon2::PasswordHash::new(&self.value) + .expect("to be valid, as we are just deserializing a previously serialize value") + } +} + +impl Display for PasswordHash { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.value.fmt(f) + } +} + +mk_id!(UserId and UserIdStub); |
