diff --git a/Cargo.lock b/Cargo.lock index 1142995..b6e10d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,6 +44,18 @@ version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" +[[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 = "askama" version = "0.14.0" @@ -233,6 +245,15 @@ dependencies = [ "serde", ] +[[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" @@ -626,6 +647,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -1121,7 +1151,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.5", "smallvec", "zeroize", ] @@ -1206,6 +1236,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1226,11 +1267,16 @@ name = "persmgr-gui" version = "0.1.0" dependencies = [ "anyhow", + "argon2", "askama", "axum", + "base64", + "pulldown-cmark", + "rand 0.9.2", "serde", "serde_json", "sqlx", + "time", "tokio", "toml", "tower", @@ -1314,6 +1360,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pulldown-cmark" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0" +dependencies = [ + "bitflags", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + [[package]] name = "quote" version = "1.0.40" @@ -1336,8 +1401,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", ] [[package]] @@ -1347,7 +1422,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", ] [[package]] @@ -1359,6 +1444,15 @@ dependencies = [ "getrandom 0.2.16", ] +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + [[package]] name = "redox_syscall" version = "0.5.17" @@ -1381,7 +1475,7 @@ dependencies = [ "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", @@ -1534,7 +1628,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ "digest", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1695,7 +1789,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand", + "rand 0.8.5", "rsa", "serde", "sha1", @@ -1733,7 +1827,7 @@ dependencies = [ "md-5", "memchr", "once_cell", - "rand", + "rand 0.8.5", "serde", "serde_json", "sha2", @@ -2121,7 +2215,7 @@ dependencies = [ "futures", "http", "parking_lot", - "rand", + "rand 0.8.5", "serde", "serde_json", "thiserror", @@ -2239,6 +2333,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" +[[package]] +name = "unicode-width" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" + [[package]] name = "url" version = "2.5.7" diff --git a/Cargo.toml b/Cargo.toml index e3266b7..6e55d14 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,11 +5,16 @@ edition = "2024" [dependencies] anyhow = "1.0.99" +argon2 = { version = "0.5.3", features = ["simple", "std"] } askama = "0.14.0" axum = "0.8.4" +base64 = "0.22.1" +pulldown-cmark = "0.13.0" +rand = "0.9.2" serde = { version = "1.0.219", features = ["derive"] } serde_json = "1.0.143" sqlx = { version = "0.8.6", features = ["macros", "postgres", "runtime-tokio"] } +time = "0.3.43" tokio = { version = "1.47.1", features = ["full"] } toml = "0.9.5" tower = { version = "0.5.2", features = ["full"] } diff --git a/migrations/20250906202709_sessions.down.sql b/migrations/20250906202709_sessions.down.sql new file mode 100644 index 0000000..7dad582 --- /dev/null +++ b/migrations/20250906202709_sessions.down.sql @@ -0,0 +1,3 @@ +-- Add down migration script here + +DROP TABLE IF EXISTS sessions; diff --git a/migrations/20250906202709_sessions.up.sql b/migrations/20250906202709_sessions.up.sql new file mode 100644 index 0000000..3a5e9ac --- /dev/null +++ b/migrations/20250906202709_sessions.up.sql @@ -0,0 +1,7 @@ +-- Add up migration script here + +CREATE TABLE IF NOT EXISTS sessions ( + user_id BIGINT NOT NULL, + session_key TEXT NOT NULL UNIQUE, + expires BIGINT NOT NULL +) diff --git a/src/api/user/register.rs b/src/api/user/register.rs index 0efb2da..40a0a37 100644 --- a/src/api/user/register.rs +++ b/src/api/user/register.rs @@ -1,17 +1,82 @@ -use axum::{ - body::Body, - extract::State, - http::{HeaderMap, HeaderValue, StatusCode}, - response::{IntoResponse, Response}, +use argon2::{ + Argon2, Params, + password_hash::{PasswordHasher, SaltString, rand_core::OsRng}, }; +use axum::{ + extract::{Json, State}, + http::{Response, StatusCode}, +}; +use base64::{Engine as _, engine::general_purpose}; +use serde::Deserialize; +use time::{Duration, OffsetDateTime}; use crate::db::Database; -pub async fn route(State(db): State) -> Response { +#[derive(Debug, Clone, Deserialize)] +pub struct ReqBody { + email: String, + username: String, + password: String, +} + +pub async fn route(State(db): State, Json(body): Json) -> Response { + let salt = SaltString::generate(&mut OsRng); + + let argon2 = Argon2::new( + argon2::Algorithm::Argon2id, + argon2::Version::V0x13, + Params::DEFAULT, + ); + let hash = argon2 + .hash_password(body.password.as_bytes(), salt.as_salt()) + .unwrap() + .to_string(); + + let mut user = crate::db::tables::user::User::default(); + user.username = body.username; + user.email = body.email; + user.pw_salt = salt.to_string(); + user.pw_hash = hash; + + if let Err(e) = user.insert_new(&db.pool()).await { + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(format!("ERROR: Failed to create user: {e}")) + .unwrap(); + } + + let Ok(user) = crate::db::tables::user::User::get_by_username(&db.pool(), user.username).await + else { + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(String::from("ERROR: Failed to get created user")) + .unwrap(); + }; + + let session_key = { + let mut buf = [0u8; 32]; + rand::fill(&mut buf); + general_purpose::STANDARD.encode(&buf) + }; + + let mut session = crate::db::tables::sessions::Session::default(); + session.user_id = user.id; + session.session_key = session_key; + session.expires = OffsetDateTime::now_utc() + .saturating_add(Duration::days(30)) + .unix_timestamp(); + + if let Err(e) = session.insert_new(db.pool()).await { + return Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(format!("ERROR: Failed to create session for user: {e}",)) + .unwrap(); + }; + Response::builder() .header("Location", "/") - .header("Set-Cookie", &format!("session=meowmeowmeow")) + .header("Set-Cookie", &format!("session={}", session.session_key)) .status(StatusCode::SEE_OTHER) - .body(Body::empty()) + .body(String::new()) .unwrap() } diff --git a/src/db/tables/mod.rs b/src/db/tables/mod.rs index 22d12a3..e2f7bd7 100644 --- a/src/db/tables/mod.rs +++ b/src/db/tables/mod.rs @@ -1 +1,2 @@ +pub mod sessions; pub mod user; diff --git a/src/db/tables/sessions.rs b/src/db/tables/sessions.rs new file mode 100644 index 0000000..f0417cf --- /dev/null +++ b/src/db/tables/sessions.rs @@ -0,0 +1,58 @@ +use anyhow::Result; + +use crate::db::CurrPool; + +#[derive(Debug, Default, Clone)] +pub struct Session { + pub user_id: i64, + pub session_key: String, + pub expires: i64, +} + +impl Session { + pub async fn insert_new(&self, pool: &CurrPool) -> Result { + let session = sqlx::query_as!( + Session, + r#" + INSERT INTO sessions (user_id, session_key, expires) + VALUES ($1, $2, $3) + RETURNING * + "#, + self.user_id, + self.session_key, + self.expires + ) + .fetch_one(pool) + .await?; + + Ok(session) + } + pub async fn get_by_username(pool: &CurrPool, user_id: i64) -> anyhow::Result { + let session = sqlx::query_as!( + Session, + "SELECT * FROM sessions WHERE user_id = $1", + user_id + ) + .fetch_one(pool) + .await?; + Ok(session) + } + pub async fn get_by_session_key(pool: &CurrPool, session_key: String) -> anyhow::Result { + let session = sqlx::query_as!( + Session, + "SELECT * FROM sessions WHERE session_key = $1", + session_key + ) + .fetch_one(pool) + .await?; + Ok(session) + } + + pub async fn remove_old_sessions(pool: &CurrPool) -> anyhow::Result<()> { + let curr_time = time::OffsetDateTime::now_utc().unix_timestamp(); + sqlx::query!("DELETE FROM sessions WHERE expires < $1", curr_time) + .execute(pool) + .await?; + Ok(()) + } +}