From 67ad8c2b6400748341ad8b737f780c3342855d21 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 23 Feb 2025 11:20:25 +0100 Subject: [PATCH] feat: add user and session models --- Cargo.lock | 185 +++++++++++++++++++++ Cargo.toml | 5 +- migrations/2025-02-23-095541_auth/down.sql | 2 + migrations/2025-02-23-095541_auth/up.sql | 13 ++ src/db/models/mod.rs | 2 + src/db/models/session.rs | 34 ++++ src/db/models/user.rs | 67 ++++++++ src/db/schema.rs | 21 +++ 8 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 migrations/2025-02-23-095541_auth/down.sql create mode 100644 migrations/2025-02-23-095541_auth/up.sql create mode 100644 src/db/models/session.rs create mode 100644 src/db/models/user.rs diff --git a/Cargo.lock b/Cargo.lock index a25af84..e78c074 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,18 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "autocfg" version = "1.4.0" @@ -92,12 +104,42 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bitflags" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.10.0" @@ -119,6 +161,25 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "darling" version = "0.20.10" @@ -208,6 +269,17 @@ dependencies = [ "syn", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "dsl_auto_type" version = "0.1.3" @@ -282,6 +354,27 @@ dependencies = [ "pin-utils", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gimli" version = "0.31.1" @@ -536,10 +629,13 @@ checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" name = "otter" version = "0.1.0" dependencies = [ + "argon2", "axum", "diesel", "diesel_migrations", "libsqlite3-sys", + "rand", + "serde", "tokio", "tower-http", "tracing", @@ -575,6 +671,17 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -605,6 +712,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + [[package]] name = "proc-macro2" version = "1.0.93" @@ -634,6 +750,36 @@ dependencies = [ "scheduled-thread-pool", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redox_syscall" version = "0.5.9" @@ -785,6 +931,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.98" @@ -1008,6 +1160,12 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + [[package]] name = "unicode-ident" version = "1.0.17" @@ -1026,6 +1184,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1135,3 +1299,24 @@ checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1" dependencies = [ "memchr", ] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index cf64589..d9889d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,10 +4,13 @@ version = "0.1.0" edition = "2021" [dependencies] +argon2 = "0.5.3" axum = "0.8.1" -diesel = { version = "2.2.7", features = ["r2d2", "sqlite"] } +diesel = { version = "2.2.7", features = ["r2d2", "sqlite", "returning_clauses_for_sqlite_3_35"] } diesel_migrations = { version = "2.2.0", features = ["sqlite"] } libsqlite3-sys = { version = "0.31.0", features = ["bundled"] } +rand = "0.8.5" +serde = { version = "1.0.218", features = ["derive"] } tokio = { version = "1.43.0", features = ["full"] } tower-http = { version = "0.6.2", features = ["set-header", "trace"] } tracing = "0.1.41" diff --git a/migrations/2025-02-23-095541_auth/down.sql b/migrations/2025-02-23-095541_auth/down.sql new file mode 100644 index 0000000..6341b5c --- /dev/null +++ b/migrations/2025-02-23-095541_auth/down.sql @@ -0,0 +1,2 @@ +drop table sessions; +drop table users; diff --git a/migrations/2025-02-23-095541_auth/up.sql b/migrations/2025-02-23-095541_auth/up.sql new file mode 100644 index 0000000..a081f00 --- /dev/null +++ b/migrations/2025-02-23-095541_auth/up.sql @@ -0,0 +1,13 @@ +create table users ( + id bigint primary key not null, + username text unique not null, + password_hash text not null +); + +create table sessions ( + id bigint primary key not null, + user_id bigint not null + references users (id) + on delete cascade, + unique (id, user_id) +); diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index e69de29..f950c0f 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -0,0 +1,2 @@ +pub mod session; +pub mod user; diff --git a/src/db/models/session.rs b/src/db/models/session.rs new file mode 100644 index 0000000..ac8fc3c --- /dev/null +++ b/src/db/models/session.rs @@ -0,0 +1,34 @@ +use diesel::prelude::*; +use rand::Rng; + +use super::user::User; +use crate::db::{schema::*, DbPool, DbResult}; + +#[derive(Clone, Queryable, Selectable, Insertable, Associations)] +#[diesel(belongs_to(super::user::User))] +#[diesel(table_name = sessions)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct Session { + pub id: i64, + pub user_id: i64, +} + +impl Session { + pub fn new_for_user(pool: &DbPool, user_id: i64) -> DbResult { + let id: i64 = rand::thread_rng().gen(); + + Ok(Self { id, user_id } + .insert_into(sessions::table) + .returning(Self::as_returning()) + .get_result(&mut pool.get()?)?) + } + + pub fn user_from_id(pool: &DbPool, id: i64) -> DbResult> { + Ok(sessions::dsl::sessions + .inner_join(users::table) + .filter(sessions::id.eq(id)) + .select(User::as_select()) + .get_result(&mut pool.get()?) + .optional()?) + } +} diff --git a/src/db/models/user.rs b/src/db/models/user.rs new file mode 100644 index 0000000..f9c9bb9 --- /dev/null +++ b/src/db/models/user.rs @@ -0,0 +1,67 @@ +use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier}; +use diesel::prelude::*; +use rand::rngs::OsRng; +use serde::{Deserialize, Serialize}; + +use crate::db::{schema::*, DbPool, DbResult}; + +#[derive(Serialize, Deserialize, Clone, Queryable, Selectable)] +#[diesel(table_name = users)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct User { + pub id: i64, + pub username: String, + pub password_hash: String, +} + +#[derive(Deserialize, Insertable)] +#[diesel(table_name = users)] +#[diesel(check_for_backend(diesel::sqlite::Sqlite))] +pub struct NewUser { + pub username: String, + pub password_hash: String, +} + +fn hash_password(password: impl AsRef) -> String { + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + + argon2 + .hash_password(password.as_ref().as_bytes(), &salt) + .unwrap() + .to_string() +} + +impl NewUser { + pub fn new(username: String, password: String) -> Self { + Self { + username, + password_hash: hash_password(&password), + } + } + + pub fn insert(self, pool: &DbPool) -> DbResult { + Ok(diesel::insert_into(users::table) + .values(self) + .returning(User::as_returning()) + .get_result(&mut pool.get()?)?) + } +} + +impl User { + pub fn by_username(pool: &DbPool, username: impl AsRef) -> DbResult> { + Ok(users::dsl::users + .select(User::as_select()) + .filter(users::username.eq(username.as_ref())) + .first(&mut pool.get()?) + .optional()?) + } + + pub fn verify_password(&self, password: impl AsRef) -> bool { + let password_hash = PasswordHash::new(&self.password_hash).unwrap(); + + Argon2::default() + .verify_password(password.as_ref().as_bytes(), &password_hash) + .is_ok() + } +} diff --git a/src/db/schema.rs b/src/db/schema.rs index 4dc25e2..d9b70af 100644 --- a/src/db/schema.rs +++ b/src/db/schema.rs @@ -1,2 +1,23 @@ // @generated automatically by Diesel CLI. +diesel::table! { + sessions (id) { + id -> BigInt, + user_id -> BigInt, + } +} + +diesel::table! { + users (id) { + id -> BigInt, + username -> Text, + password_hash -> Text, + } +} + +diesel::joinable!(sessions -> users (user_id)); + +diesel::allow_tables_to_appear_in_same_query!( + sessions, + users, +);