diff --git a/Cargo.lock b/Cargo.lock index f741f3d..1ad0c89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -51,6 +51,17 @@ version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +[[package]] +name = "argon2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95c2fcf79ad1932ac6269a738109997a83c227c09b75842ae564dc8ede6a861c" +dependencies = [ + "base64ct", + "blake2", + "password-hash", +] + [[package]] name = "async-trait" version = "0.1.68" @@ -156,12 +167,27 @@ version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[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" @@ -666,6 +692,7 @@ name = "mabel" version = "0.1.0" dependencies = [ "anyhow", + "argon2", "axum", "axum-macros", "chrono", @@ -822,6 +849,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 = "paste" version = "1.0.12" diff --git a/Cargo.toml b/Cargo.toml index 5283594..93f55e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,4 +15,8 @@ serde = { version = "1" } serde_json = { version = "1", features = ["raw_value"] } figment = { version = "0.10", features = ["toml", "env"] } chrono = { version = "0.4", features = ["serde"] } -anyhow = "1.0" \ No newline at end of file +anyhow = "1.0" +argon2 = "0.5" + +[profile.dev.package.sqlx-macros] +opt-level = 3 \ No newline at end of file diff --git a/migrations/20230628223219_create-base.down.sql b/migrations/20230628223219_create-base.down.sql index 81dbd06..a20f899 100644 --- a/migrations/20230628223219_create-base.down.sql +++ b/migrations/20230628223219_create-base.down.sql @@ -1,5 +1,4 @@ DROP TABLE pages; DROP TABLE sites; DROP TABLE audit; -DROP TABLE users; -DROP TABLE roles; \ No newline at end of file +DROP TABLE users; \ No newline at end of file diff --git a/migrations/20230628223219_create-base.up.sql b/migrations/20230628223219_create-base.up.sql index 60ae5e7..b84df1c 100644 --- a/migrations/20230628223219_create-base.up.sql +++ b/migrations/20230628223219_create-base.up.sql @@ -4,7 +4,7 @@ CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4 (), name VARCHAR UNIQUE NOT NULL, email VARCHAR, - password BYTEA, + password VARCHAR, display_name VARCHAR, bio TEXT, roles UUID[] NOT NULL, @@ -13,11 +13,6 @@ CREATE TABLE users ( deleted_at TIMESTAMP ); -CREATE TABLE roles ( - id UUID PRIMARY KEY DEFAULT uuid_generate_v4 (), - scopes VARCHAR[] NOT NULL -); - CREATE TABLE sites ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4 (), owner UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..0a04704 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,25 @@ +use anyhow::{anyhow, Result}; +use argon2::{ + password_hash::{rand_core::OsRng, SaltString}, + Argon2, PasswordHash, PasswordHasher, PasswordVerifier, +}; + +pub fn random() -> String { + SaltString::generate(&mut OsRng).to_string() +} + +pub fn hash(plaintext: &String) -> Result { + let salt = SaltString::generate(&mut OsRng); + let hashed = Argon2::default() + .hash_password(plaintext.as_bytes(), &salt) + .map_err(|err| anyhow!(err))? + .to_string(); + Ok(hashed) +} + +pub fn verify(plaintext: &String, hash: &String) -> Result { + let parsed_hash = PasswordHash::new(&hash).map_err(|err| anyhow!(err))?; + Ok(Argon2::default() + .verify_password(plaintext.as_bytes(), &parsed_hash) + .is_ok()) +} diff --git a/src/main.rs b/src/main.rs index b6db33b..75bfbad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,7 @@ +mod auth; mod content; mod error; +mod roles; mod routes; mod state; diff --git a/src/roles.rs b/src/roles.rs new file mode 100644 index 0000000..6b3bcde --- /dev/null +++ b/src/roles.rs @@ -0,0 +1,3 @@ +use uuid::{uuid, Uuid}; + +pub const ROLE_SUPERADMIN: Uuid = uuid!("8adbaff9-d3c7-44b9-ac67-93833a67380e"); diff --git a/src/routes/admin.rs b/src/routes/admin.rs index e139989..a4b2fdc 100644 --- a/src/routes/admin.rs +++ b/src/routes/admin.rs @@ -2,12 +2,16 @@ use std::sync::Arc; use axum::extract::State; use axum::http::StatusCode; +use axum::Json; +use serde_json::{json, Value}; +use crate::auth::{hash, random}; use crate::error::AppError; +use crate::roles::ROLE_SUPERADMIN; use crate::state::AppState; -pub async fn bootstrap(State(state): State>) -> Result<(), AppError> { - // Only allow this request if the user db is completely empty! +pub async fn bootstrap(State(state): State>) -> Result, AppError> { + // Only allow this request if the user table is completely empty! let empty = sqlx::query!( "SELECT CASE WHEN EXISTS(SELECT 1 FROM users) THEN false ELSE true END AS empty;" ) @@ -23,7 +27,24 @@ pub async fn bootstrap(State(state): State>) -> Result<(), AppErro }), true => { //todo add user - Ok(()) + let username = "admin"; + let password = random(); + + sqlx::query!( + r#" + INSERT INTO users ( name, display_name, password, roles ) + VALUES ( $1, $2, $3, $4 ) + RETURNING id + "#, + username, + "Administrator", + hash(&password)?, + &[ROLE_SUPERADMIN], + ) + .fetch_one(&state.database) + .await?; + + Ok(Json(json!({"username": username, "password": password}))) } } }