diff --git a/Cargo.lock b/Cargo.lock index 84142e9..a353aab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -181,6 +181,21 @@ dependencies = [ "syn 2.0.53", ] +[[package]] +name = "actix-web-httpauth" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d613edf08a42ccc6864c941d30fe14e1b676a77d16f1dbadc1174d065a0a775" +dependencies = [ + "actix-utils", + "actix-web", + "base64", + "futures-core", + "futures-util", + "log", + "pin-project-lite", +] + [[package]] name = "addr2line" version = "0.21.0" @@ -239,6 +254,21 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.13" @@ -293,6 +323,18 @@ version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +[[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 = "atoi" version = "2.0.0" @@ -350,6 +392,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" @@ -380,6 +431,12 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bumpalo" +version = "3.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" + [[package]] name = "byteorder" version = "1.5.0" @@ -417,6 +474,18 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "windows-targets 0.52.4", +] + [[package]] name = "colorchoice" version = "1.0.0" @@ -927,17 +996,45 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icrc-server" version = "0.1.0" dependencies = [ "actix-web", + "actix-web-httpauth", "anyhow", + "argon2", "compile-time-run", "dotenvy", "env_logger", "log", + "serde", "sqlx", + "thiserror", + "uuid", ] [[package]] @@ -984,6 +1081,15 @@ dependencies = [ "libc", ] +[[package]] +name = "js-sys" +version = "0.3.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1273,6 +1379,17 @@ dependencies = [ "windows-targets 0.48.5", ] +[[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.14" @@ -1697,6 +1814,7 @@ dependencies = [ "atoi", "byteorder", "bytes", + "chrono", "crc", "crossbeam-queue", "either", @@ -1758,6 +1876,7 @@ dependencies = [ "sha2", "sqlx-core", "sqlx-mysql", + "sqlx-postgres", "sqlx-sqlite", "syn 1.0.109", "tempfile", @@ -1776,6 +1895,7 @@ dependencies = [ "bitflags 2.4.2", "byteorder", "bytes", + "chrono", "crc", "digest", "dotenvy", @@ -1817,6 +1937,7 @@ dependencies = [ "base64", "bitflags 2.4.2", "byteorder", + "chrono", "crc", "dotenvy", "etcetera", @@ -1852,6 +1973,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" dependencies = [ "atoi", + "chrono", "flume", "futures-channel", "futures-core", @@ -2121,6 +2243,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "uuid" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +dependencies = [ + "getrandom", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -2145,6 +2276,60 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" +[[package]] +name = "wasm-bindgen" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.53", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.53", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" + [[package]] name = "whoami" version = "1.5.1" @@ -2155,6 +2340,15 @@ dependencies = [ "wasite", ] +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.4", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index ad67944..72c06d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,11 @@ log = "0.4.21" env_logger = "0.11.3" dotenvy = "0.15.7" compile-time-run = "0.2.12" +uuid = { version = "1.7.0", features = ["v4"] } +thiserror = "1.0.58" +argon2 = "0.5.3" +serde = { version = "1.0.197", features = ["default"] } actix-web = "4.5.1" -sqlx = { version = "0.7.4", features = ["runtime-tokio", "tls-native-tls", "mysql"] } +actix-web-httpauth = "0.8.1" +sqlx = { version = "0.7.4", features = ["runtime-tokio", "tls-native-tls", "mysql", "chrono"] } diff --git a/src/api/endpoints.rs b/src/api/endpoints.rs index 08bcdd3..9dfea2c 100644 --- a/src/api/endpoints.rs +++ b/src/api/endpoints.rs @@ -1,6 +1,41 @@ -use actix_web::{post, HttpResponse, Responder}; +use crate::backend::{error::AccountRegisterError, Backend}; +use actix_web::{post, web, HttpResponse, Responder}; +use log::error; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize)] +struct AccountRegisterRequest { + token: String, + password: String, +} + +#[derive(Debug, Serialize)] +struct AccountRegisterResponse { + uuid: String, +} #[post("/account/register")] -pub async fn account_register() -> impl Responder { - HttpResponse::Ok() +pub async fn account_register( + backend: web::Data, + body: web::Json, +) -> impl Responder { + let body = body.into_inner(); + match backend.account_register(body.token, body.password).await { + Err(e) => { + error!("{e}"); + HttpResponse::InternalServerError().finish() + } + Ok(res) => match res { + Err(e) => match e { + AccountRegisterError::InvalidToken => HttpResponse::Unauthorized().finish(), + AccountRegisterError::SqlError(e) => { + error!("{e}"); + HttpResponse::InternalServerError().finish() + } + }, + Ok(uuid) => HttpResponse::Ok().json(AccountRegisterResponse { + uuid: uuid.to_string(), + }), + }, + } } diff --git a/src/api/mod.rs b/src/api/mod.rs index 2ef5a46..02869b6 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -14,12 +14,12 @@ pub async fn start(config: &Config, backend: Backend) -> Result<()> { }) .bind((config.addr.as_str(), config.port))?; + info!("API starting"); if let Some(threads) = config.threads { server.workers(threads as usize).run().await?; } else { server.run().await?; } - info!("API online"); Ok(()) } diff --git a/src/backend/db_structures.rs b/src/backend/db_structures.rs new file mode 100644 index 0000000..c3fa7a1 --- /dev/null +++ b/src/backend/db_structures.rs @@ -0,0 +1,11 @@ +use sqlx::types::chrono::NaiveDateTime; + +pub struct ActivationTokensRow { + pub token: String, + pub expire: NaiveDateTime, +} + +pub struct UsersRow { + pub userid: String, + pub password: String, +} diff --git a/src/backend/error.rs b/src/backend/error.rs new file mode 100644 index 0000000..d9fb72b --- /dev/null +++ b/src/backend/error.rs @@ -0,0 +1,10 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum AccountRegisterError { + #[error("The given token is invalid")] + InvalidToken, + + #[error("SQL Error: {0}")] + SqlError(sqlx::Error), +} diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 2331049..597fc8a 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -1,35 +1,138 @@ +mod db_structures; +pub mod error; + +use crate::backend::error::AccountRegisterError; use crate::config::Config; use anyhow::Result; +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, + Argon2, +}; +use db_structures::{ActivationTokensRow, UsersRow}; use log::info; -use sqlx::MySqlPool; +use sqlx::{types::chrono::Utc, MySqlPool}; +use uuid::Uuid; #[derive(Debug, Clone)] pub struct Backend { pool: MySqlPool, } -pub async fn start(config: &Config) -> Result { - let pool = MySqlPool::connect(config.database.as_str()).await?.into(); +impl Backend { + pub async fn start(config: &Config) -> Result { + let pool = MySqlPool::connect(config.database.as_str()).await?.into(); - sqlx::query!( - r#"CREATE TABLE IF NOT EXISTS Users ( - username VARCHAR(32) NOT NULL PRIMARY KEY, - password VARCHAR(98) NOT NULL + sqlx::query!( + r#"CREATE TABLE IF NOT EXISTS Users ( + userid UUID NOT NULL PRIMARY KEY, + password VARCHAR(128) NOT NULL );"# - ) - .execute(&pool) - .await?; + ) + .execute(&pool) + .await?; - sqlx::query!( - r#"CREATE TABLE IF NOT EXISTS Relays ( - id BINARY(16) NOT NULL PRIMARY KEY, - secret VARCHAR(98) NOT NULL + sqlx::query!( + r#"CREATE TABLE IF NOT EXISTS Relays ( + id UUID NOT NULL PRIMARY KEY, + secret VARCHAR(128) NOT NULL );"# - ) - .execute(&pool) - .await?; + ) + .execute(&pool) + .await?; - info!("Backend initialized"); + sqlx::query!( + r#"CREATE TABLE IF NOT EXISTS ActivationTokens ( + token CHAR(48) NOT NULL PRIMARY KEY, + expire DATETIME NOT NULL + );"# + ) + .execute(&pool) + .await?; - Ok(Backend { pool }) + sqlx::query!( + r#"CREATE TABLE IF NOT EXISTS AuthTokens ( + token CHAR(48) NOT NULL PRIMARY KEY, + userid UUID NOT NULL, + exipre DATETIME NOT NULL + );"# + ) + .execute(&pool) + .await?; + + info!("Backend initialized"); + + Ok(Self { pool }) + } + + async fn check_activation_token(&self, token: &str) -> Result { + match sqlx::query_as!( + ActivationTokensRow, + r#"SELECT * FROM ActivationTokens WHERE token = ?;"#, + token + ) + .fetch_one(&self.pool) + .await + { + Err(e) => match e { + sqlx::Error::RowNotFound => Ok(false), + _ => Err(e), + }, + Ok(row) => { + sqlx::query!(r#"DELETE FROM ActivationTokens WHERE token = ?;"#, token) + .execute(&self.pool) + .await?; + if row.expire > Utc::now().naive_utc() { + Ok(true) + } else { + Ok(false) + } + } + } + } + + async fn get_user(&self, userid: Uuid) -> Result, sqlx::Error> { + match sqlx::query_as!( + UsersRow, + r#"SELECT * FROM Users WHERE userid = ?;"#, + userid.as_bytes().as_slice() + ) + .fetch_one(&self.pool) + .await + { + Err(e) => match e { + sqlx::Error::RowNotFound => Ok(None), + _ => Err(e), + }, + Ok(_row) => Ok(Some(())), + } + } + + pub async fn account_register( + &self, + token: String, + password: String, + ) -> Result> { + if !self.check_activation_token(&token).await? { + return Ok(Err(AccountRegisterError::InvalidToken)); + } + + let salt = SaltString::generate(&mut OsRng); + + let hash = Argon2::default() + .hash_password(password.as_bytes(), &salt) + .map_err(|_| anyhow::Error::msg("Failed to hash the password"))? + .to_string(); + + let userid = Uuid::new_v4(); + + sqlx::query!( + r#"INSERT INTO Users VALUES (?, ?);"#, + userid.as_bytes().as_slice(), + hash + ) + .execute(&self.pool) + .await?; + + Ok(Ok(userid)) + } } diff --git a/src/main.rs b/src/main.rs index ec737b6..238a3ce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,6 +2,7 @@ mod api; mod backend; mod config; +use crate::backend::Backend; use anyhow::Result; use compile_time_run::run_command_str; use config::Config; @@ -23,7 +24,7 @@ async fn main() -> Result<()> { println!("{config:#?}"); - let backend = backend::start(&config).await?; + let backend = Backend::start(&config).await?; api::start(&config, backend).await?; Ok(())