about summary refs log tree commit diff stats
path: root/crates/rocie-server/src/storage
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-12-09 13:07:14 +0100
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-12-09 13:07:14 +0100
commitc91dce4f77ae12453203f0a28b91efb6533cc095 (patch)
tree4f50e755dff7f717d45309b08f9fe2c8c87f88bd /crates/rocie-server/src/storage
parentchore(rocie-client): Regenerate (diff)
downloadserver-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.sql8
-rw-r--r--crates/rocie-server/src/storage/sql/get/mod.rs7
-rw-r--r--crates/rocie-server/src/storage/sql/get/user/mod.rs77
-rw-r--r--crates/rocie-server/src/storage/sql/insert/mod.rs1
-rw-r--r--crates/rocie-server/src/storage/sql/insert/user/mod.rs117
-rw-r--r--crates/rocie-server/src/storage/sql/mod.rs1
-rw-r--r--crates/rocie-server/src/storage/sql/user.rs86
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);