diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..271800c --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" \ No newline at end of file diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..d9ba5fd --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +imports_granularity = "Crate" \ No newline at end of file diff --git a/src/api/endpoints/account/invite.rs b/src/api/endpoints/account/invite.rs index c1f35e3..56fda04 100644 --- a/src/api/endpoints/account/invite.rs +++ b/src/api/endpoints/account/invite.rs @@ -11,7 +11,7 @@ struct NewResponse { #[post("/account/invite/new")] pub async fn new(backend: web::Data, auth: BearerAuth) -> impl Responder { - match backend.create_invite(auth.token()).await { + match backend.create_invite_token(auth.token()).await { Err(e) => { error!("{e}"); HttpResponse::InternalServerError().finish() diff --git a/src/api/mod.rs b/src/api/mod.rs index 18786c0..5b2ef75 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,5 +1,4 @@ -use crate::backend::Backend; -use crate::config::Config; +use crate::{backend::Backend, config::Config}; use actix_web::{web, App, HttpServer}; use anyhow::Result; use log::info; diff --git a/src/backend/mod.rs b/src/backend/mod.rs index f715217..ed1ecbd 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -1,27 +1,18 @@ mod db_structures; pub mod error; mod permissions; +mod relay; +mod tokens; mod user; -use crate::backend::{error::Error, permissions::Permission, user::IntoUser}; use crate::config::Config; -use anyhow::{bail, Result}; -use argon2::{ - password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, - Argon2, -}; -use chrono::Days; -use db_structures::{AuthTokensRow, InviteTokensRow, UsersRow}; +use anyhow::Result; use log::info; -use rand::distributions::DistString; -use sqlx::{types::chrono::Utc, MySqlPool}; -use std::str::FromStr; -use user::User; -use uuid::Uuid; +use sqlx::MySqlPool; #[derive(Debug, Clone)] pub struct Backend { - pool: MySqlPool, + pub pool: MySqlPool, } impl Backend { @@ -70,196 +61,4 @@ impl Backend { Ok(Self { pool }) } - - /// Returns the UUID of the user who owns the auth token. - pub async fn resolve_auth_token(&self, token: &str) -> Result> { - match sqlx::query_as!( - AuthTokensRow, - r#"SELECT * FROM AuthTokens WHERE token = ?;"#, - token - ) - .fetch_one(&self.pool) - .await - { - Err(e) => match e { - sqlx::Error::RowNotFound => Ok(Err(Error::InvalidToken)), - _ => Err(e.into()), - }, - Ok(row) => { - if row.expire > Utc::now().naive_utc() { - Ok(Ok(Uuid::from_str(&row.userid)?)) - } else { - sqlx::query!(r#"DELETE FROM AuthTokens WHERE token = ?;"#, token) - .execute(&self.pool) - .await?; - Ok(Err(Error::TokenExpired)) - } - } - } - } - - /// Check whether an invite-token is valid or not. - async fn check_invite_token(&self, token: &str) -> Result> { - match sqlx::query_as!( - InviteTokensRow, - r#"SELECT * FROM InviteTokens WHERE token = ?;"#, - token - ) - .fetch_one(&self.pool) - .await - { - Err(e) => match e { - sqlx::Error::RowNotFound => Ok(Err(Error::InvalidToken)), - _ => Err(e.into()), - }, - Ok(row) => { - sqlx::query!(r#"DELETE FROM InviteTokens WHERE token = ?;"#, token) - .execute(&self.pool) - .await?; - if row.expire > Utc::now().naive_utc() { - Ok(Ok(())) - } else { - Ok(Err(Error::TokenExpired)) - } - } - } - } - - /// Returns detailed information about the user identified by the UUID. - async fn get_user(&self, userid: Uuid) -> Result> { - 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(Err(Error::UserNotFound)), - _ => Err(e.into()), - }, - Ok(row) => Ok(Ok(row.try_into()?)), - } - } - - /// Creates a new account and returns its UUID. - pub async fn account_register( - &self, - token: String, - password: String, - ) -> Result> { - if let Err(e) = self.check_invite_token(&token).await? { - return Ok(Err(e)); - } - - 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 (?, ?, 0);"#, - userid.as_bytes().as_slice(), - hash - ) - .execute(&self.pool) - .await?; - - Ok(Ok(userid)) - } - - /// Generates an auth token for a user, given their UUID and password. - pub async fn authenticate( - &self, - userid: Uuid, - password: String, - ) -> Result> { - let user = match userid.into_user(&self).await? { - Ok(user) => user, - Err(e) => return Ok(Err(e)), - }; - - if !user.verify_password(&password)? { - return Ok(Err(Error::AuthenticationFailure)); - } - - let mut token = rand::distributions::Alphanumeric.sample_string(&mut OsRng, 48); - // just for the case, that there's some duplication - loop { - match self.resolve_auth_token(&token).await? { - Ok(_) => token = rand::distributions::Alphanumeric.sample_string(&mut OsRng, 48), - Err(Error::InvalidToken) | Err(Error::TokenExpired) => break, - Err(e) => bail!("!THIS ERROR SHOULDN'T BE HERE! -> {e}"), - } - } - - sqlx::query!( - r#"INSERT INTO AuthTokens VALUES (?, ?, ?);"#, - token, - userid.as_bytes().as_slice(), - Utc::now().naive_utc().checked_add_days(Days::new(14)) - ) - .execute(&self.pool) - .await?; - - Ok(Ok(token)) - } - - /// Generates a new invite token, if the user identified by the UUID has the permission to do so. - pub async fn create_invite(&self, user: impl IntoUser) -> Result> { - let user = match user.into_user(&self).await? { - Ok(user) => user, - Err(e) => return Ok(Err(e)), - }; - - if !user.has_permission(Permission::GenerateInviteTokens) { - return Ok(Err(Error::PermissionDenied( - "This user is not authorized to generate invite codes", - ))); - } - - let token = rand::distributions::Alphanumeric.sample_string(&mut OsRng, 48); - - sqlx::query!( - r#"INSERT INTO InviteTokens VALUES (?, ?);"#, - token, - Utc::now().naive_utc().checked_add_days(Days::new(7)) - ) - .execute(&self.pool) - .await?; - - Ok(Ok(token)) - } - - pub async fn create_relay(&self, user: impl IntoUser) -> Result> { - let _user = match user.into_user(&self).await? { - Ok(user) => user, - Err(e) => return Ok(Err(e)), - }; - - let relay_id = Uuid::new_v4(); - let secret = rand::distributions::Alphanumeric.sample_string(&mut OsRng, 48); - - let salt = SaltString::generate(&mut OsRng); - - let secret_hash = Argon2::default() - .hash_password(secret.as_bytes(), &salt) - .map_err(|_| anyhow::Error::msg("Failed to hash the relay secret"))? - .to_string(); - - sqlx::query!( - r#"INSERT INTO Relays VALUES (?, ?);"#, - relay_id.as_bytes().as_slice(), - secret_hash - ) - .execute(&self.pool) - .await?; - - Ok(Ok((relay_id, secret))) - } } diff --git a/src/backend/relay.rs b/src/backend/relay.rs new file mode 100644 index 0000000..4762405 --- /dev/null +++ b/src/backend/relay.rs @@ -0,0 +1,40 @@ +use crate::backend::{error::Error, user::IntoUser, Backend}; +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, + Argon2, +}; +use rand::distributions::DistString; +use uuid::Uuid; + +impl Backend { + /// Creates the structures for a new relay + pub async fn create_relay( + &self, + user: impl IntoUser, + ) -> anyhow::Result> { + let _user = match user.into_user(&self).await? { + Ok(user) => user, + Err(e) => return Ok(Err(e)), + }; + + let relay_id = Uuid::new_v4(); + let secret = rand::distributions::Alphanumeric.sample_string(&mut OsRng, 48); + + let salt = SaltString::generate(&mut OsRng); + + let secret_hash = Argon2::default() + .hash_password(secret.as_bytes(), &salt) + .map_err(|_| anyhow::Error::msg("Failed to hash the relay secret"))? + .to_string(); + + sqlx::query!( + r#"INSERT INTO Relays VALUES (?, ?);"#, + relay_id.as_bytes().as_slice(), + secret_hash + ) + .execute(&self.pool) + .await?; + + Ok(Ok((relay_id, secret))) + } +} diff --git a/src/backend/tokens.rs b/src/backend/tokens.rs new file mode 100644 index 0000000..f660164 --- /dev/null +++ b/src/backend/tokens.rs @@ -0,0 +1,103 @@ +use crate::backend::{ + db_structures::{AuthTokensRow, InviteTokensRow}, + error::Error, + permissions::Permission, + user::IntoUser, + Backend, +}; +use argon2::password_hash::rand_core::OsRng; +use chrono::{Days, Utc}; +use rand::distributions::DistString; +use std::str::FromStr; +use uuid::Uuid; + +impl Backend { + /// Returns the UUID of the user who owns the auth token. + pub async fn resolve_auth_token( + &self, + token: &str, + ) -> anyhow::Result> { + match sqlx::query_as!( + AuthTokensRow, + r#"SELECT * FROM AuthTokens WHERE token = ?;"#, + token + ) + .fetch_one(&self.pool) + .await + { + Err(e) => match e { + sqlx::Error::RowNotFound => Ok(Err(Error::InvalidToken)), + _ => Err(e.into()), + }, + Ok(row) => { + if row.expire > Utc::now().naive_utc() { + Ok(Ok(Uuid::from_str(&row.userid)?)) + } else { + sqlx::query!(r#"DELETE FROM AuthTokens WHERE token = ?;"#, token) + .execute(&self.pool) + .await?; + Ok(Err(Error::TokenExpired)) + } + } + } + } + + /// Check whether an invite-token is valid or not. + pub async fn check_invite_token( + &self, + token: &str, + ) -> anyhow::Result> { + match sqlx::query_as!( + InviteTokensRow, + r#"SELECT * FROM InviteTokens WHERE token = ?;"#, + token + ) + .fetch_one(&self.pool) + .await + { + Err(e) => match e { + sqlx::Error::RowNotFound => Ok(Err(Error::InvalidToken)), + _ => Err(e.into()), + }, + Ok(row) => { + sqlx::query!(r#"DELETE FROM InviteTokens WHERE token = ?;"#, token) + .execute(&self.pool) + .await?; + if row.expire > Utc::now().naive_utc() { + Ok(Ok(())) + } else { + Ok(Err(Error::TokenExpired)) + } + } + } + } + + /// Generates a new invite token, if the user identified by the UUID has the permission to do so. + pub async fn create_invite_token( + &self, + user: impl IntoUser, + ) -> anyhow::Result> { + let user = match user.into_user(&self).await? { + Ok(user) => user, + Err(e) => return Ok(Err(e)), + }; + + if !user.has_permission(Permission::GenerateInviteTokens) { + return Ok(Err(Error::PermissionDenied( + "This user is not authorized to generate invite codes", + ))); + } + + let token = rand::distributions::Alphanumeric.sample_string(&mut OsRng, 48); + + sqlx::query!( + r#"INSERT INTO InviteTokens VALUES (?, ?);"#, + token, + Utc::now().naive_utc().checked_add_days(Days::new(7)) + ) + .execute(&self.pool) + .await?; + + Ok(Ok(token)) + } +} diff --git a/src/backend/user.rs b/src/backend/user.rs index e821f7a..48e4b7c 100644 --- a/src/backend/user.rs +++ b/src/backend/user.rs @@ -1,8 +1,12 @@ -use crate::backend::db_structures::UsersRow; -use crate::backend::error::Error; -use crate::backend::{permissions::Permission, Backend}; -use anyhow::Result; -use argon2::PasswordVerifier; +use crate::backend::{db_structures::UsersRow, error::Error, permissions::Permission, Backend}; +use anyhow::{bail, Result}; +use argon2::{ + password_hash::{rand_core::OsRng, SaltString}, + Argon2, PasswordHasher, PasswordVerifier, +}; +use chrono::Days; +use rand::distributions::DistString; +use sqlx::types::chrono::Utc; use std::str::FromStr; use uuid::Uuid; @@ -80,3 +84,90 @@ impl IntoUser for User { Ok(Ok(self)) } } + +impl Backend { + /// Returns detailed information about the user identified by the UUID. + async fn get_user(&self, userid: Uuid) -> Result> { + 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(Err(Error::UserNotFound)), + _ => Err(e.into()), + }, + Ok(row) => Ok(Ok(row.try_into()?)), + } + } + + /// Creates a new account and returns its UUID. + pub async fn account_register( + &self, + token: String, + password: String, + ) -> Result> { + if let Err(e) = self.check_invite_token(&token).await? { + return Ok(Err(e)); + } + + 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 (?, ?, 0);"#, + userid.as_bytes().as_slice(), + hash + ) + .execute(&self.pool) + .await?; + + Ok(Ok(userid)) + } + + /// Generates an auth token for a user, given their UUID and password. + pub async fn authenticate( + &self, + userid: Uuid, + password: String, + ) -> Result> { + let user = match userid.into_user(&self).await? { + Ok(user) => user, + Err(e) => return Ok(Err(e)), + }; + + if !user.verify_password(&password)? { + return Ok(Err(Error::AuthenticationFailure)); + } + + let mut token = rand::distributions::Alphanumeric.sample_string(&mut OsRng, 48); + // just for the case, that there's some duplication + loop { + match self.resolve_auth_token(&token).await? { + Ok(_) => token = rand::distributions::Alphanumeric.sample_string(&mut OsRng, 48), + Err(Error::InvalidToken) | Err(Error::TokenExpired) => break, + Err(e) => bail!("!THIS ERROR SHOULDN'T BE HERE! -> {e}"), + } + } + + sqlx::query!( + r#"INSERT INTO AuthTokens VALUES (?, ?, ?);"#, + token, + userid.as_bytes().as_slice(), + Utc::now().naive_utc().checked_add_days(Days::new(14)) + ) + .execute(&self.pool) + .await?; + + Ok(Ok(token)) + } +}