refactor: split backend implementation into multiple files and switched to nightly channel

This commit is contained in:
antifallobst 2024-03-22 22:16:17 +01:00
parent b5f948a95e
commit ad354f73ed
Signed by: antifallobst
GPG Key ID: 2B4F402172791BAF
8 changed files with 249 additions and 214 deletions

2
rust-toolchain.toml Normal file
View File

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

1
rustfmt.toml Normal file
View File

@ -0,0 +1 @@
imports_granularity = "Crate"

View File

@ -11,7 +11,7 @@ struct NewResponse {
#[post("/account/invite/new")]
pub async fn new(backend: web::Data<Backend>, 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()

View File

@ -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;

View File

@ -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<Result<Uuid, Error>> {
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<Result<(), Error>> {
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<Result<User, 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(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<Result<Uuid, Error>> {
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<Result<String, Error>> {
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<Result<String, Error>> {
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<Result<(Uuid, String), Error>> {
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)))
}
}

40
src/backend/relay.rs Normal file
View File

@ -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<anyhow::Result<(Uuid, String), Error>> {
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)))
}
}

103
src/backend/tokens.rs Normal file
View File

@ -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<anyhow::Result<Uuid, Error>> {
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<anyhow::Result<(), Error>> {
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<anyhow::Result<String, Error>> {
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))
}
}

View File

@ -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<Result<User, 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(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<Result<Uuid, Error>> {
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<Result<String, Error>> {
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))
}
}