diff --git a/Cargo.lock b/Cargo.lock index 34e67ab6..a610f322 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -259,7 +259,7 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "authbeam" -version = "0.7.0" +version = "1.0.0" dependencies = [ "axum", "axum-extra", @@ -267,6 +267,7 @@ dependencies = [ "databeam", "dotenv", "hcaptcha", + "mime_guess", "regex", "reqwest", "serde", @@ -3634,7 +3635,7 @@ dependencies = [ [[package]] name = "rainbeam" -version = "1.14.2" +version = "1.15.0" dependencies = [ "ammonia", "askama", diff --git a/crates/authbeam/Cargo.toml b/crates/authbeam/Cargo.toml index dc1fb593..aeff70e7 100644 --- a/crates/authbeam/Cargo.toml +++ b/crates/authbeam/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "authbeam" -version = "0.7.0" +version = "1.0.0" edition = "2021" description = "Authentication manager" authors = ["trisuaso", "swmff"] @@ -27,6 +27,7 @@ axum-extra = { version = "0.9.3", features = ["cookie"] } regex = "1.10.5" reqwest = "0.12.5" hcaptcha = "2.4.6" +mime_guess = "2.0.5" [lib] doctest = false diff --git a/crates/authbeam/src/api/general.rs b/crates/authbeam/src/api/general.rs new file mode 100644 index 00000000..4e0cf616 --- /dev/null +++ b/crates/authbeam/src/api/general.rs @@ -0,0 +1,291 @@ +use crate::database::Database; +use crate::model::{DatabaseError, ProfileCreate, ProfileLogin, TokenContext}; +use axum::http::{HeaderMap, HeaderValue}; +use hcaptcha::Hcaptcha; +use databeam::DefaultReturn; + +use axum::response::IntoResponse; +use axum::{ + extract::{Query, State}, + Json, +}; +use axum_extra::extract::cookie::CookieJar; + +/// [`Database::create_profile`] +pub async fn create_request( + jar: CookieJar, + headers: HeaderMap, + State(database): State, + Json(props): Json, +) -> impl IntoResponse { + if let Some(_) = jar.get("__Secure-Token") { + return ( + HeaderMap::new(), + serde_json::to_string(&DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: (), + }) + .unwrap(), + ); + } + + // get real ip + let real_ip = if let Some(ref real_ip_header) = database.config.real_ip_header { + headers + .get(real_ip_header.to_owned()) + .unwrap_or(&HeaderValue::from_static("")) + .to_str() + .unwrap_or("") + .to_string() + } else { + String::new() + }; + + // check ip + if database.get_ipban_by_ip(real_ip.clone()).await.is_ok() { + return ( + HeaderMap::new(), + serde_json::to_string(&DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: (), + }) + .unwrap(), + ); + } + + // create profile + let res = match database.create_profile(props, real_ip).await { + Ok(r) => r, + Err(e) => { + return ( + HeaderMap::new(), + serde_json::to_string(&DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }) + .unwrap(), + ); + } + }; + + // return + let mut headers = HeaderMap::new(); + + headers.insert( + "Set-Cookie", + format!( + "__Secure-Token={}; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age={}", + res, + 60* 60 * 24 * 365 + ) + .parse() + .unwrap(), + ); + + ( + headers, + serde_json::to_string(&DefaultReturn { + success: true, + message: res.clone(), + payload: (), + }) + .unwrap(), + ) +} + +/// [`Database::get_profile_by_username_password`] +pub async fn login_request( + headers: HeaderMap, + State(database): State, + Json(props): Json, +) -> impl IntoResponse { + // check hcaptcha + if let Err(e) = props + .valid_response(&database.config.captcha.secret, None) + .await + { + return ( + HeaderMap::new(), + serde_json::to_string(&DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }) + .unwrap(), + ); + } + + // ... + let mut ua = match database + .get_profile_by_username(props.username.clone()) + .await + { + Ok(ua) => ua, + Err(e) => { + return ( + HeaderMap::new(), + serde_json::to_string(&DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }) + .unwrap(), + ) + } + }; + + // check password + let input_password = shared::hash::hash_salted(props.password.clone(), ua.salt); + + if input_password != ua.password { + return ( + HeaderMap::new(), + serde_json::to_string(&DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: (), + }) + .unwrap(), + ); + } + + // get real ip + let real_ip = if let Some(ref real_ip_header) = database.config.real_ip_header { + headers + .get(real_ip_header.to_owned()) + .unwrap_or(&HeaderValue::from_static("")) + .to_str() + .unwrap_or("") + .to_string() + } else { + String::new() + }; + + // check ip + if database.get_ipban_by_ip(real_ip.clone()).await.is_ok() { + return ( + HeaderMap::new(), + serde_json::to_string(&DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: (), + }) + .unwrap(), + ); + } + + // ... + let token = databeam::utility::uuid(); + let token_hashed = databeam::utility::hash(token.clone()); + + ua.tokens.push(token_hashed); + ua.ips.push(real_ip); + ua.token_context.push(TokenContext::default()); + + database + .edit_profile_tokens_by_id(props.username.clone(), ua.tokens, ua.ips, ua.token_context) + .await + .unwrap(); + + // return + let mut headers = HeaderMap::new(); + + headers.insert( + "Set-Cookie", + format!( + "__Secure-Token={}; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age={}", + token, + 60* 60 * 24 * 365 + ) + .parse() + .unwrap(), + ); + + ( + headers, + serde_json::to_string(&DefaultReturn { + success: true, + message: token, + payload: (), + }) + .unwrap(), + ) +} + +#[derive(serde::Deserialize)] +pub struct CallbackQueryProps { + pub uid: String, // this uid will need to be sent to the client as a token +} + +pub async fn callback_request(Query(params): Query) -> impl IntoResponse { + // return + ( + [ + ("Content-Type".to_string(), "text/html".to_string()), + ( + "Set-Cookie".to_string(), + format!( + "__Secure-Token={}; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age={}", + params.uid, + 60 * 60 * 24 * 365 + ), + ), + ], + " + + " + ) +} + +pub async fn logout_request(jar: CookieJar) -> impl IntoResponse { + // check for cookie + if let Some(_) = jar.get("__Secure-Token") { + return ( + [ + ("Content-Type".to_string(), "text/plain".to_string()), + ( + "Set-Cookie".to_string(), + "__Secure-Token=refresh; SameSite=Strict; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=0".to_string(), + ) ], + "You have been signed out. You can now close this tab.", + ); + } + + // return + ( + [ + ("Content-Type".to_string(), "text/plain".to_string()), + ("Set-Cookie".to_string(), String::new()), + ], + "Failed to sign out of account.", + ) +} + +pub async fn remove_tag(jar: CookieJar) -> impl IntoResponse { + // check for cookie + // anonymous users cannot remove their own tag + if let Some(_) = jar.get("__Secure-Token") { + return ( + [ + ("Content-Type".to_string(), "text/plain".to_string()), + ( + "Set-Cookie2".to_string(), + "__Secure-Question-Tag=refresh; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=0".to_string() + ) + ], + "You have been signed out. You can now close this tab.", + ); + } + + // return + ( + [ + ("Content-Type".to_string(), "text/plain".to_string()), + ("Set-Cookie".to_string(), String::new()), + ], + "Failed to remove tag.", + ) +} diff --git a/crates/authbeam/src/api/ipbans.rs b/crates/authbeam/src/api/ipbans.rs new file mode 100644 index 00000000..8d83d575 --- /dev/null +++ b/crates/authbeam/src/api/ipbans.rs @@ -0,0 +1,100 @@ +use crate::database::Database; +use crate::model::{DatabaseError, IpBanCreate}; +use databeam::DefaultReturn; + +use axum::response::IntoResponse; +use axum::{ + extract::{Path, State}, + Json, +}; +use axum_extra::extract::cookie::CookieJar; + +/// Create a ipban +pub async fn create_request( + jar: CookieJar, + State(database): State, + Json(props): Json, +) -> impl IntoResponse { + // get user from token + let auth_user = match jar.get("__Secure-Token") { + Some(c) => match database + .get_profile_by_unhashed(c.value_trimmed().to_string()) + .await + { + Ok(ua) => ua, + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }); + } + }, + None => { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: (), + }); + } + }; + + // return + match database.create_ipban(props, auth_user).await { + Ok(_) => Json(DefaultReturn { + success: true, + message: "Acceptable".to_string(), + payload: (), + }), + Err(e) => Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }), + } +} + +/// Delete an ipban +pub async fn delete_request( + jar: CookieJar, + Path(id): Path, + State(database): State, +) -> impl IntoResponse { + // get user from token + let auth_user = match jar.get("__Secure-Token") { + Some(c) => match database + .get_profile_by_unhashed(c.value_trimmed().to_string()) + .await + { + Ok(ua) => ua, + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }); + } + }, + None => { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: (), + }); + } + }; + + // return + match database.delete_ipban(id, auth_user).await { + Ok(_) => Json(DefaultReturn { + success: true, + message: "Acceptable".to_string(), + payload: (), + }), + Err(e) => Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }), + } +} diff --git a/crates/authbeam/src/api/ipblocks.rs b/crates/authbeam/src/api/ipblocks.rs new file mode 100644 index 00000000..8712af12 --- /dev/null +++ b/crates/authbeam/src/api/ipblocks.rs @@ -0,0 +1,100 @@ +use crate::database::Database; +use crate::model::{DatabaseError, IpBlockCreate}; +use databeam::DefaultReturn; + +use axum::response::IntoResponse; +use axum::{ + extract::{Path, State}, + Json, +}; +use axum_extra::extract::cookie::CookieJar; + +/// Create a ipblock +pub async fn create_request( + jar: CookieJar, + State(database): State, + Json(props): Json, +) -> impl IntoResponse { + // get user from token + let auth_user = match jar.get("__Secure-Token") { + Some(c) => match database + .get_profile_by_unhashed(c.value_trimmed().to_string()) + .await + { + Ok(ua) => ua, + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }); + } + }, + None => { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: (), + }); + } + }; + + // return + match database.create_ipblock(props, auth_user).await { + Ok(_) => Json(DefaultReturn { + success: true, + message: "Acceptable".to_string(), + payload: (), + }), + Err(e) => Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }), + } +} + +/// Delete an ipblock +pub async fn delete_request( + jar: CookieJar, + Path(id): Path, + State(database): State, +) -> impl IntoResponse { + // get user from token + let auth_user = match jar.get("__Secure-Token") { + Some(c) => match database + .get_profile_by_unhashed(c.value_trimmed().to_string()) + .await + { + Ok(ua) => ua, + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }); + } + }, + None => { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: (), + }); + } + }; + + // return + match database.delete_ipblock(id, auth_user).await { + Ok(_) => Json(DefaultReturn { + success: true, + message: "Acceptable".to_string(), + payload: (), + }), + Err(e) => Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }), + } +} diff --git a/crates/authbeam/src/api/me.rs b/crates/authbeam/src/api/me.rs new file mode 100644 index 00000000..88f23e93 --- /dev/null +++ b/crates/authbeam/src/api/me.rs @@ -0,0 +1,337 @@ +use crate::database::Database; +use crate::model::{DatabaseError, TokenContext, TokenPermission}; +use serde::{Deserialize, Serialize}; +use databeam::DefaultReturn; + +use axum::http::{HeaderMap, HeaderValue}; +use axum::response::IntoResponse; +use axum::{extract::State, Json}; +use axum_extra::extract::cookie::CookieJar; + +/// Returns the current user's username +pub async fn get_request(jar: CookieJar, State(database): State) -> impl IntoResponse { + // get user from token + let auth_user = match jar.get("__Secure-Token") { + Some(c) => match database + .get_profile_by_unhashed(c.value_trimmed().to_string()) + .await + { + Ok(ua) => ua, + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }); + } + }, + None => { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: (), + }); + } + }; + + // return + Json(DefaultReturn { + success: true, + message: auth_user.username, + payload: (), + }) +} + +#[derive(Serialize, Deserialize)] +pub struct DeleteProfile { + password: String, +} + +/// Delete the current user's profile +pub async fn delete_request( + jar: CookieJar, + State(database): State, + Json(req): Json, +) -> impl IntoResponse { + // get user from token + let auth_user = match jar.get("__Secure-Token") { + Some(c) => match database + .get_profile_by_unhashed(c.value_trimmed().to_string()) + .await + { + Ok(ua) => ua, + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }); + } + }, + None => { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: (), + }); + } + }; + + // get profile + let hashed = shared::hash::hash_salted(req.password, auth_user.salt); + + if hashed != auth_user.password { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: (), + }); + } + + // return + if let Err(e) = database.delete_profile_by_id(auth_user.id).await { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }); + } + + Json(DefaultReturn { + success: true, + message: "Profile deleted, goodbye!".to_string(), + payload: (), + }) +} + +/// Generate a new token and session (like logging in while already logged in) +pub async fn generate_token_request( + jar: CookieJar, + headers: HeaderMap, + State(database): State, + Json(props): Json, +) -> impl IntoResponse { + // get user from token + let mut existing_permissions: Option> = None; + let mut auth_user = match jar.get("__Secure-Token") { + Some(c) => { + let token = c.value_trimmed().to_string(); + + match database.get_profile_by_unhashed(token.clone()).await { + Ok(ua) => { + // check token permission + let token = ua.token_context_from_token(&token); + + if let Some(ref permissions) = token.permissions { + existing_permissions = Some(permissions.to_owned()) + } + + if !token.can_do(TokenPermission::GenerateTokens) { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: None, + }); + } + + // return + ua + } + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: None, + }); + } + } + } + None => { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: None, + }); + } + }; + + // for every token that doesn't have a context, insert the default context + for (i, _) in auth_user.tokens.clone().iter().enumerate() { + if let None = auth_user.token_context.get(i) { + auth_user.token_context.insert(i, TokenContext::default()) + } + } + + // get real ip + let real_ip = if let Some(ref real_ip_header) = database.config.real_ip_header { + headers + .get(real_ip_header.to_owned()) + .unwrap_or(&HeaderValue::from_static("")) + .to_str() + .unwrap_or("") + .to_string() + } else { + String::new() + }; + + // check ip + if database.get_ipban_by_ip(real_ip.clone()).await.is_ok() { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: None, + }); + } + + // check given context + if let Some(ref permissions) = props.permissions { + // make sure we don't want anything we don't have + // if our permissions are "None", allow any permission to be granted + for permission in permissions { + if let Some(ref existing) = existing_permissions { + if !existing.contains(permission) { + return Json(DefaultReturn { + success: false, + message: DatabaseError::OutOfScope.to_string(), + payload: None, + }); + } + } else { + break; + } + } + } + + // ... + let token = databeam::utility::uuid(); + let token_hashed = databeam::utility::hash(token.clone()); + + auth_user.tokens.push(token_hashed); + auth_user.ips.push(String::new()); // don't actually store ip, this endpoint is used by external apps + auth_user.token_context.push(props); + + database + .edit_profile_tokens_by_id( + auth_user.id, + auth_user.tokens, + auth_user.ips, + auth_user.token_context, + ) + .await + .unwrap(); + + // return + return Json(DefaultReturn { + success: true, + message: "Generated token!".to_string(), + payload: Some(token), + }); +} + +#[derive(Serialize, Deserialize)] +pub struct UpdateTokens { + pub tokens: Vec, +} + +/// Update the current user's session tokens +pub async fn update_tokens_request( + jar: CookieJar, + State(database): State, + Json(req): Json, +) -> impl IntoResponse { + // get user from token + let mut auth_user = match jar.get("__Secure-Token") { + Some(c) => { + let token = c.value_trimmed().to_string(); + + match database.get_profile_by_unhashed(token.clone()).await { + Ok(ua) => { + // check token permission + if !ua + .token_context_from_token(&token) + .can_do(TokenPermission::ManageAccount) + { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: (), + }); + } + + // return + ua + } + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }); + } + } + } + None => { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: (), + }); + } + }; + + // for every token that doesn't have a context, insert the default context + for (i, _) in auth_user.tokens.clone().iter().enumerate() { + if let None = auth_user.token_context.get(i) { + auth_user.token_context.insert(i, TokenContext::default()) + } + } + + // get diff + let mut removed_indexes = Vec::new(); + + for (i, token) in auth_user.tokens.iter().enumerate() { + if !req.tokens.contains(token) { + removed_indexes.push(i); + } + } + + // edit dependent vecs + for i in removed_indexes.clone() { + if (auth_user.ips.len() < i) | (auth_user.ips.len() == 0) { + break; + } + + auth_user.ips.remove(i); + } + + for i in removed_indexes.clone() { + if (auth_user.token_context.len() < i) | (auth_user.token_context.len() == 0) { + break; + } + + auth_user.token_context.remove(i); + } + + // return + if let Err(e) = database + .edit_profile_tokens_by_id( + auth_user.id, + req.tokens, + auth_user.ips, + auth_user.token_context, + ) + .await + { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }); + } + + Json(DefaultReturn { + success: true, + message: "Tokens updated!".to_string(), + payload: (), + }) +} diff --git a/crates/authbeam/src/api/mod.rs b/crates/authbeam/src/api/mod.rs new file mode 100644 index 00000000..ff6626f0 --- /dev/null +++ b/crates/authbeam/src/api/mod.rs @@ -0,0 +1,101 @@ +//! Responds to API requests +use crate::database::Database; +use crate::model::DatabaseError; +use databeam::DefaultReturn; + +use axum::response::IntoResponse; +use axum::{ + routing::{delete, get, post}, + Json, Router, +}; + +pub mod general; +pub mod ipbans; +pub mod ipblocks; +pub mod me; +pub mod notifications; +pub mod profile; +pub mod relationships; +pub mod warnings; + +pub async fn not_found() -> impl IntoResponse { + Json(DefaultReturn:: { + success: false, + message: DatabaseError::NotFound.to_string(), + payload: 404, + }) +} + +pub fn routes(database: Database) -> Router { + Router::new() + // relationships + .route( + "/relationships/follow/:id", + post(relationships::follow_request), + ) + .route( + "/relationships/friend/:id", + post(relationships::friend_request), + ) + .route( + "/relationships/block/:id", + post(relationships::block_request), + ) + .route( + "/relationships/current/:id", + delete(relationships::delete_request), + ) + // profiles + .route( + "/profile/:id/tokens/generate", + post(profile::generate_token_request), + ) + .route("/profile/:id/tokens", post(profile::update_tokens_request)) + .route("/profile/:id/tier", post(profile::update_tier_request)) + .route("/profile/:id/group", post(profile::update_group_request)) + .route( + "/profile/:id/password", + post(profile::update_password_request), + ) + .route( + "/profile/:id/username", + post(profile::update_username_request), + ) + .route( + "/profile/:id/metadata", + post(profile::update_metdata_request), + ) + .route("/profile/:id/badges", post(profile::update_badges_request)) + .route("/profile/:id/banner", get(profile::banner_request)) + .route("/profile/:id/avatar", get(profile::avatar_request)) + .route("/profile/:id", delete(profile::delete_request)) + .route("/profile/:id", get(profile::get_request)) + // notifications + .route("/notifications/:id", delete(notifications::delete_request)) + .route( + "/notifications/clear", + delete(notifications::delete_all_request), + ) + // warnings + .route("/warnings", post(warnings::create_request)) + .route("/warnings/:id", delete(warnings::delete_request)) + // ipbans + .route("/ipbans", post(ipbans::create_request)) + .route("/ipbans/:id", delete(ipbans::delete_request)) + // ipblocks + .route("/ipblocks", post(ipblocks::create_request)) + .route("/ipblocks/:id", delete(ipblocks::delete_request)) + // me + .route("/me/tokens/generate", post(me::generate_token_request)) + .route("/me/tokens", post(me::update_tokens_request)) + .route("/me/delete", post(me::delete_request)) + .route("/me", get(me::get_request)) + // account + .route("/register", post(general::create_request)) + .route("/login", post(general::login_request)) + .route("/callback", get(general::callback_request)) + .route("/logout", post(general::logout_request)) + .route("/untag", post(general::remove_tag)) + // ... + .with_state(database) +} diff --git a/crates/authbeam/src/api/notifications.rs b/crates/authbeam/src/api/notifications.rs new file mode 100644 index 00000000..eb0b0d3b --- /dev/null +++ b/crates/authbeam/src/api/notifications.rs @@ -0,0 +1,104 @@ +use crate::database::Database; +use crate::model::DatabaseError; +use databeam::DefaultReturn; + +use axum::response::IntoResponse; +use axum::{ + extract::{Path, State}, + Json, +}; +use axum_extra::extract::cookie::CookieJar; + +/// Delete a notification +pub async fn delete_request( + jar: CookieJar, + Path(id): Path, + State(database): State, +) -> impl IntoResponse { + // get user from token + let auth_user = match jar.get("__Secure-Token") { + Some(c) => match database + .get_profile_by_unhashed(c.value_trimmed().to_string()) + .await + { + Ok(ua) => ua, + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }); + } + }, + None => { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: (), + }); + } + }; + + // return + if let Err(e) = database.delete_notification(id, auth_user).await { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }); + } + + Json(DefaultReturn { + success: true, + message: "Notification deleted".to_string(), + payload: (), + }) +} + +/// Delete the current user's notifications +pub async fn delete_all_request( + jar: CookieJar, + State(database): State, +) -> impl IntoResponse { + // get user from token + let auth_user = match jar.get("__Secure-Token") { + Some(c) => match database + .get_profile_by_unhashed(c.value_trimmed().to_string()) + .await + { + Ok(ua) => ua, + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }); + } + }, + None => { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: (), + }); + } + }; + + // return + if let Err(e) = database + .delete_notifications_by_recipient(auth_user.id.clone(), auth_user) + .await + { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }); + } + + Json(DefaultReturn { + success: true, + message: "Notifications cleared!".to_string(), + payload: (), + }) +} diff --git a/crates/authbeam/src/api.rs b/crates/authbeam/src/api/profile.rs similarity index 50% rename from crates/authbeam/src/api.rs rename to crates/authbeam/src/api/profile.rs index a5f86bb3..eaac9784 100644 --- a/crates/authbeam/src/api.rs +++ b/crates/authbeam/src/api/profile.rs @@ -1,736 +1,244 @@ -//! Responds to API requests use crate::database::Database; use crate::model::{ - AuthError, IpBanCreate, IpBlockCreate, NotificationCreate, Permission, ProfileCreate, - ProfileLogin, SetProfileBadges, SetProfileGroup, SetProfileMetadata, SetProfilePassword, - SetProfileTier, SetProfileUsername, TokenContext, TokenPermission, WarningCreate, + DatabaseError, NotificationCreate, Permission, SetProfileBadges, SetProfileGroup, + SetProfileMetadata, SetProfilePassword, SetProfileTier, SetProfileUsername, TokenContext, + TokenPermission, }; -use axum::body::Bytes; -use axum::http::{HeaderMap, HeaderValue}; -use hcaptcha::Hcaptcha; -use serde::{Deserialize, Serialize}; use databeam::DefaultReturn; +use axum::body::Body; +use axum::http::{HeaderMap, HeaderValue}; use axum::response::IntoResponse; use axum::{ - extract::{Path, Query, State}, - routing::{delete, get, post}, - Json, Router, + extract::{Path, State}, + Json, }; use axum_extra::extract::cookie::CookieJar; -pub fn routes(database: Database) -> Router { - Router::new() - // profiles - // .route("/profile/:username/group", post(set_group_request)) - .route( - "/profile/:username/tokens/generate", - post(other_generate_token_request), - ) - .route( - "/profile/:username/tokens", - post(other_update_tokens_request), - ) - .route("/profile/:username/tier", post(set_tier_request)) - .route("/profile/:username/password", post(set_password_request)) - .route("/profile/:username/username", post(set_username_request)) - .route("/profile/:username/metadata", post(update_metdata_request)) - .route("/profile/:username/badges", post(update_badges_request)) - .route("/profile/:username/avatar", get(profile_avatar_request)) - .route("/profile/:username", delete(delete_other_request)) - .route("/profile/:username", get(profile_inspect_request)) - // notifications - .route("/notifications/:id", delete(delete_notification_request)) - .route( - "/notifications/clear", - delete(delete_all_notifications_request), - ) - // warnings - .route("/warnings", post(create_warning_request)) - .route("/warnings/:id", delete(delete_warning_request)) - // ipbans - .route("/ipbans", post(create_ipban_request)) - .route("/ipbans/:id", delete(delete_ipban_request)) - // ipblocks - .route("/ipblocks", post(create_ipblock_request)) - .route("/ipblocks/:id", delete(delete_ipblock_request)) - // me - .route("/me/tokens/generate", post(generate_token_request)) - .route("/me/tokens", post(update_my_tokens_request)) - .route("/me/delete", post(delete_me_request)) - .route("/me", get(me_request)) - // account - .route("/register", post(create_profile_request)) - .route("/login", post(login_request)) - .route("/callback", get(callback_request)) - .route("/logout", post(logout_request)) - .route("/untag", post(remove_tag)) - // ... - .with_state(database) -} - -/// [`Database::create_profile`] -pub async fn create_profile_request( - jar: CookieJar, - headers: HeaderMap, - State(database): State, - Json(props): Json, -) -> impl IntoResponse { - if let Some(_) = jar.get("__Secure-Token") { - return ( - HeaderMap::new(), - serde_json::to_string(&DefaultReturn { - success: false, - message: AuthError::NotAllowed.to_string(), - payload: (), - }) - .unwrap(), - ); - } +use std::{fs::File, io::Read}; - // get real ip - let real_ip = if let Some(ref real_ip_header) = database.config.real_ip_header { - headers - .get(real_ip_header.to_owned()) - .unwrap_or(&HeaderValue::from_static("")) - .to_str() - .unwrap_or("") - .to_string() - } else { - String::new() - }; +pub fn read_image(static_dir: String, image: String) -> Vec { + let mut bytes = Vec::new(); - // check ip - if database.get_ipban_by_ip(real_ip.clone()).await.is_ok() { - return ( - HeaderMap::new(), - serde_json::to_string(&DefaultReturn { - success: false, - message: AuthError::NotAllowed.to_string(), - payload: (), - }) - .unwrap(), - ); + for byte in File::open(format!("{static_dir}/images/{image}",)) + .unwrap() + .bytes() + { + bytes.push(byte.unwrap()) } - // create profile - let res = match database.create_profile(props, real_ip).await { - Ok(r) => r, - Err(e) => { - return ( - HeaderMap::new(), - serde_json::to_string(&DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }) - .unwrap(), - ); - } - }; - - // return - let mut headers = HeaderMap::new(); - - headers.insert( - "Set-Cookie", - format!( - "__Secure-Token={}; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age={}", - res, - 60* 60 * 24 * 365 - ) - .parse() - .unwrap(), - ); - - ( - headers, - serde_json::to_string(&DefaultReturn { - success: true, - message: res.clone(), - payload: (), - }) - .unwrap(), - ) + bytes } -/// [`Database::get_profile_by_username_password`] -pub async fn login_request( - headers: HeaderMap, +/// Get a profile's avatar image +pub async fn avatar_request( + Path(id): Path, State(database): State, - Json(props): Json, ) -> impl IntoResponse { - // check hcaptcha - if let Err(e) = props - .valid_response(&database.config.captcha.secret, None) - .await - { - return ( - HeaderMap::new(), - serde_json::to_string(&DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }) - .unwrap(), - ); - } - - // ... - let mut ua = match database - .get_profile_by_username(props.username.clone()) - .await - { + // get user + let auth_user = match database.get_profile(id).await { Ok(ua) => ua, - Err(e) => { + Err(_) => { return ( - HeaderMap::new(), - serde_json::to_string(&DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }) - .unwrap(), - ) + [("Content-Type", "image/svg+xml")], + Body::from(read_image( + database.config.static_dir, + "default-avatar.svg".to_string(), + )), + ); } }; - // check password - let input_password = shared::hash::hash_salted(props.password.clone(), ua.salt); - - if input_password != ua.password { - return ( - HeaderMap::new(), - serde_json::to_string(&DefaultReturn { - success: false, - message: AuthError::NotAllowed.to_string(), - payload: (), - }) - .unwrap(), - ); - } - - // get real ip - let real_ip = if let Some(ref real_ip_header) = database.config.real_ip_header { - headers - .get(real_ip_header.to_owned()) - .unwrap_or(&HeaderValue::from_static("")) - .to_str() - .unwrap_or("") - .to_string() - } else { - String::new() + // ... + let avatar_url = match auth_user.metadata.kv.get("sparkler:avatar_url") { + Some(r) => r, + None => "", }; - // check ip - if database.get_ipban_by_ip(real_ip.clone()).await.is_ok() { + if avatar_url.starts_with(&database.config.host) { return ( - HeaderMap::new(), - serde_json::to_string(&DefaultReturn { - success: false, - message: AuthError::NotAllowed.to_string(), - payload: (), - }) - .unwrap(), + [("Content-Type", "image/svg+xml")], + Body::from(read_image( + database.config.static_dir, + "default-avatar.svg".to_string(), + )), ); } - // ... - let token = databeam::utility::uuid(); - let token_hashed = databeam::utility::hash(token.clone()); - - ua.tokens.push(token_hashed); - ua.ips.push(real_ip); - ua.token_context.push(TokenContext::default()); - - database - .edit_profile_tokens_by_id(props.username.clone(), ua.tokens, ua.ips, ua.token_context) - .await - .unwrap(); - - // return - let mut headers = HeaderMap::new(); - - headers.insert( - "Set-Cookie", - format!( - "__Secure-Token={}; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age={}", - token, - 60* 60 * 24 * 365 - ) - .parse() - .unwrap(), - ); - - ( - headers, - serde_json::to_string(&DefaultReturn { - success: true, - message: token, - payload: (), - }) - .unwrap(), - ) -} - -/// Delete a notification -pub async fn delete_notification_request( - jar: CookieJar, - Path(id): Path, - State(database): State, -) -> impl IntoResponse { - // get user from token - let auth_user = match jar.get("__Secure-Token") { - Some(c) => match database - .get_profile_by_unhashed(c.value_trimmed().to_string()) - .await - { - Ok(ua) => ua, - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }); - } - }, - None => { - return Json(DefaultReturn { - success: false, - message: AuthError::NotAllowed.to_string(), - payload: (), - }); + for host in database.config.blocked_hosts { + if avatar_url.starts_with(&host) { + return ( + [("Content-Type", "image/svg+xml")], + Body::from(read_image( + database.config.static_dir, + "default-avatar.svg".to_string(), + )), + ); } - }; - - // return - if let Err(e) = database.delete_notification(id, auth_user).await { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }); } - Json(DefaultReturn { - success: true, - message: "Notification deleted".to_string(), - payload: (), - }) -} - -/// Delete the current user's notifications -pub async fn delete_all_notifications_request( - jar: CookieJar, - State(database): State, -) -> impl IntoResponse { - // get user from token - let auth_user = match jar.get("__Secure-Token") { - Some(c) => match database - .get_profile_by_unhashed(c.value_trimmed().to_string()) - .await - { - Ok(ua) => ua, - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }); - } - }, - None => { - return Json(DefaultReturn { - success: false, - message: AuthError::NotAllowed.to_string(), - payload: (), - }); - } - }; - - // return - if let Err(e) = database - .delete_notifications_by_recipient(auth_user.id.clone(), auth_user) - .await - { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }); + // get profile image + if avatar_url.is_empty() { + return ( + [("Content-Type", "image/svg+xml")], + Body::from(read_image( + database.config.static_dir, + "default-avatar.svg".to_string(), + )), + ); } - Json(DefaultReturn { - success: true, - message: "Notifications cleared!".to_string(), - payload: (), - }) -} - -/// Returns the current user's username -pub async fn me_request(jar: CookieJar, State(database): State) -> impl IntoResponse { - // get user from token - let auth_user = match jar.get("__Secure-Token") { - Some(c) => match database - .get_profile_by_unhashed(c.value_trimmed().to_string()) - .await - { - Ok(ua) => ua, - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }); + let guessed_mime = mime_guess::from_path(avatar_url) + .first_raw() + .unwrap_or("application/octet-stream"); + + match database.http.get(avatar_url).send().await { + Ok(stream) => { + if let Some(ct) = stream.headers().get("Content-Type") { + if !ct.to_str().unwrap().starts_with("image/") { + // if we failed to load the image, we might get back text/html or something + // we're going to return the default image if we got something that isn't + // an image (or has an incorrect mime) + return ( + [("Content-Type", "image/svg+xml")], + Body::from(read_image( + database.config.static_dir, + "default-avatar.svg".to_string(), + )), + ); + } } - }, - None => { - return Json(DefaultReturn { - success: false, - message: AuthError::NotAllowed.to_string(), - payload: (), - }); - } - }; - - // return - Json(DefaultReturn { - success: true, - message: auth_user.username, - payload: (), - }) -} - -#[derive(Serialize, Deserialize)] -pub struct DeleteProfile { - password: String, -} -/// Delete the current user's profile -pub async fn delete_me_request( - jar: CookieJar, - State(database): State, - Json(req): Json, -) -> impl IntoResponse { - // get user from token - let auth_user = match jar.get("__Secure-Token") { - Some(c) => match database - .get_profile_by_unhashed(c.value_trimmed().to_string()) - .await - { - Ok(ua) => ua, - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }); - } - }, - None => { - return Json(DefaultReturn { - success: false, - message: AuthError::NotAllowed.to_string(), - payload: (), - }); + ( + [( + "Content-Type", + if guessed_mime == "text/html" { + "text/plain" + } else { + guessed_mime + }, + )], + Body::from_stream(stream.bytes_stream()), + ) } - }; - - // get profile - let hashed = shared::hash::hash_salted(req.password, auth_user.salt); - - if hashed != auth_user.password { - return Json(DefaultReturn { - success: false, - message: AuthError::NotAllowed.to_string(), - payload: (), - }); + Err(_) => ( + [("Content-Type", "image/svg+xml")], + Body::from(read_image( + database.config.static_dir, + "default-avatar.svg".to_string(), + )), + ), } - - // return - if let Err(e) = database.delete_profile_by_id(auth_user.id).await { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }); - } - - Json(DefaultReturn { - success: true, - message: "Profile deleted, goodbye!".to_string(), - payload: (), - }) } -/// Generate a new token and session (like logging in while already logged in) -pub async fn generate_token_request( - jar: CookieJar, - headers: HeaderMap, +/// Get a profile's banner image +pub async fn banner_request( + Path(id): Path, State(database): State, - Json(props): Json, ) -> impl IntoResponse { - // get user from token - let mut existing_permissions: Option> = None; - let mut auth_user = match jar.get("__Secure-Token") { - Some(c) => { - let token = c.value_trimmed().to_string(); - - match database.get_profile_by_unhashed(token.clone()).await { - Ok(ua) => { - // check token permission - let token = ua.token_context_from_token(&token); - - if let Some(ref permissions) = token.permissions { - existing_permissions = Some(permissions.to_owned()) - } - - if !token.can_do(TokenPermission::GenerateTokens) { - return Json(DefaultReturn { - success: false, - message: AuthError::NotAllowed.to_string(), - payload: None, - }); - } - - // return - ua - } - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: None, - }); - } - } - } - None => { - return Json(DefaultReturn { - success: false, - message: AuthError::NotAllowed.to_string(), - payload: None, - }); - } - }; - - // for every token that doesn't have a context, insert the default context - for (i, _) in auth_user.tokens.clone().iter().enumerate() { - if let None = auth_user.token_context.get(i) { - auth_user.token_context.insert(i, TokenContext::default()) + // get user + let auth_user = match database.get_profile(id).await { + Ok(ua) => ua, + Err(_) => { + return ( + [("Content-Type", "image/svg+xml")], + Body::from(read_image( + database.config.static_dir, + "default-banner.svg".to_string(), + )), + ); } - } - - // get real ip - let real_ip = if let Some(ref real_ip_header) = database.config.real_ip_header { - headers - .get(real_ip_header.to_owned()) - .unwrap_or(&HeaderValue::from_static("")) - .to_str() - .unwrap_or("") - .to_string() - } else { - String::new() }; - // check ip - if database.get_ipban_by_ip(real_ip.clone()).await.is_ok() { - return Json(DefaultReturn { - success: false, - message: AuthError::NotAllowed.to_string(), - payload: None, - }); - } - - // check given context - if let Some(ref permissions) = props.permissions { - // make sure we don't want anything we don't have - // if our permissions are "None", allow any permission to be granted - for permission in permissions { - if let Some(ref existing) = existing_permissions { - if !existing.contains(permission) { - return Json(DefaultReturn { - success: false, - message: AuthError::OutOfScope.to_string(), - payload: None, - }); - } - } else { - break; - } - } - } - // ... - let token = databeam::utility::uuid(); - let token_hashed = databeam::utility::hash(token.clone()); - - auth_user.tokens.push(token_hashed); - auth_user.ips.push(String::new()); // don't actually store ip, this endpoint is used by external apps - auth_user.token_context.push(props); - - database - .edit_profile_tokens_by_id( - auth_user.id, - auth_user.tokens, - auth_user.ips, - auth_user.token_context, - ) - .await - .unwrap(); - - // return - return Json(DefaultReturn { - success: true, - message: "Generated token!".to_string(), - payload: Some(token), - }); -} - -#[derive(Serialize, Deserialize)] -pub struct UpdateTokens { - tokens: Vec, -} - -/// Update the current user's session tokens -pub async fn update_my_tokens_request( - jar: CookieJar, - State(database): State, - Json(req): Json, -) -> impl IntoResponse { - // get user from token - let mut auth_user = match jar.get("__Secure-Token") { - Some(c) => { - let token = c.value_trimmed().to_string(); - - match database.get_profile_by_unhashed(token.clone()).await { - Ok(ua) => { - // check token permission - if !ua - .token_context_from_token(&token) - .can_do(TokenPermission::ManageAccount) - { - return Json(DefaultReturn { - success: false, - message: AuthError::NotAllowed.to_string(), - payload: (), - }); - } - - // return - ua - } - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }); - } - } - } - None => { - return Json(DefaultReturn { - success: false, - message: AuthError::NotAllowed.to_string(), - payload: (), - }); - } + let banner_url = match auth_user.metadata.kv.get("sparkler:banner_url") { + Some(r) => r, + None => "", }; - // for every token that doesn't have a context, insert the default context - for (i, _) in auth_user.tokens.clone().iter().enumerate() { - if let None = auth_user.token_context.get(i) { - auth_user.token_context.insert(i, TokenContext::default()) - } - } - - // get diff - let mut removed_indexes = Vec::new(); - - for (i, token) in auth_user.tokens.iter().enumerate() { - if !req.tokens.contains(token) { - removed_indexes.push(i); - } - } - - // edit dependent vecs - for i in removed_indexes.clone() { - if (auth_user.ips.len() < i) | (auth_user.ips.len() == 0) { - break; - } - - auth_user.ips.remove(i); + if banner_url.starts_with(&database.config.host) { + return ( + [("Content-Type", "image/svg+xml")], + Body::from(read_image( + database.config.static_dir, + "default-banner.svg".to_string(), + )), + ); } - for i in removed_indexes.clone() { - if (auth_user.token_context.len() < i) | (auth_user.token_context.len() == 0) { - break; + for host in database.config.blocked_hosts { + if banner_url.starts_with(&host) { + return ( + [("Content-Type", "image/svg+xml")], + Body::from(read_image( + database.config.static_dir, + "default-banner.svg".to_string(), + )), + ); } - - auth_user.token_context.remove(i); - } - - // return - if let Err(e) = database - .edit_profile_tokens_by_id( - auth_user.id, - req.tokens, - auth_user.ips, - auth_user.token_context, - ) - .await - { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }); } - Json(DefaultReturn { - success: true, - message: "Tokens updated!".to_string(), - payload: (), - }) -} - -/// Get a profile's avatar image -pub async fn profile_avatar_request( - Path(username): Path, - State(database): State, -) -> impl IntoResponse { - // get user - let auth_user = match database.get_profile_by_username(username).await { - Ok(ua) => ua, - Err(_) => { - return Bytes::from_static(&[0x0u8]); - } - }; - // get profile image - if auth_user.metadata.avatar_url.is_empty() { - return Bytes::from_static(&[0]); + if banner_url.is_empty() { + return ( + [("Content-Type", "image/svg+xml")], + Body::from(read_image( + database.config.static_dir, + "default-banner.svg".to_string(), + )), + ); } - match database - .http - .get(auth_user.metadata.avatar_url) - .send() - .await - { - Ok(r) => r.bytes().await.unwrap(), - Err(_) => Bytes::from_static(&[0x0u8]), + let guessed_mime = mime_guess::from_path(banner_url) + .first_raw() + .unwrap_or("application/octet-stream"); + + match database.http.get(banner_url).send().await { + Ok(stream) => { + if let Some(ct) = stream.headers().get("Content-Type") { + if !ct.to_str().unwrap().starts_with("image/") { + // if we failed to load the image, we might get back text/html or something + // we're going to return the default image if we got something that isn't + // an image (or has an incorrect mime) + return ( + [("Content-Type", "image/svg+xml")], + Body::from(read_image( + database.config.static_dir, + "default-banner.svg".to_string(), + )), + ); + } + } + + ( + [( + "Content-Type", + if guessed_mime == "text/html" { + "text/plain" + } else { + guessed_mime + }, + )], + Body::from_stream(stream.bytes_stream()), + ) + } + Err(_) => ( + [("Content-Type", "image/svg+xml")], + Body::from(read_image( + database.config.static_dir, + "default-banner.svg".to_string(), + )), + ), } } /// View a profile's information -pub async fn profile_inspect_request( - Path(username): Path, +pub async fn get_request( + Path(id): Path, State(database): State, ) -> impl IntoResponse { // get user - let mut auth_user = match database.get_profile_by_username(username).await { + let mut auth_user = match database.get_profile(id).await { Ok(ua) => ua, Err(e) => { return Json(DefaultReturn { @@ -755,32 +263,48 @@ pub async fn profile_inspect_request( }) } -/// Change a profile's group -pub async fn set_group_request( +/// Change a profile's tier +pub async fn update_tier_request( jar: CookieJar, - Path(username): Path, + Path(id): Path, State(database): State, - Json(props): Json, + Json(props): Json, ) -> impl IntoResponse { // get user from token let auth_user = match jar.get("__Secure-Token") { - Some(c) => match database - .get_profile_by_unhashed(c.value_trimmed().to_string()) - .await - { - Ok(ua) => ua, - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: None, - }); + Some(c) => { + let token = c.value_trimmed().to_string(); + + match database.get_profile_by_unhashed(token.clone()).await { + Ok(ua) => { + // check token permission + if !ua + .token_context_from_token(&token) + .can_do(TokenPermission::Moderator) + { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: None, + }); + } + + // return + ua + } + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: None, + }); + } } - }, + } None => { return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: None, }); } @@ -802,13 +326,13 @@ pub async fn set_group_request( // we must have the "Manager" permission to edit other users return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: None, }); } // get other user - let other_user = match database.get_profile_by_username(username.clone()).await { + let other_user = match database.get_profile(id.clone()).await { Ok(ua) => ua, Err(e) => { return Json(DefaultReturn { @@ -835,14 +359,14 @@ pub async fn set_group_request( // we cannot manager other managers return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: None, }); } // push update // TODO: try not to clone - if let Err(e) = database.edit_profile_group(username, props.group).await { + if let Err(e) = database.edit_profile_tier(id, props.tier).await { return Json(DefaultReturn { success: false, message: e.to_string(), @@ -854,52 +378,36 @@ pub async fn set_group_request( Json(DefaultReturn { success: true, message: "Acceptable".to_string(), - payload: Some(props.group), + payload: Some(props.tier), }) } -/// Change a profile's tier -pub async fn set_tier_request( +/// Change a profile's group +pub async fn update_group_request( jar: CookieJar, Path(username): Path, State(database): State, - Json(props): Json, + Json(props): Json, ) -> impl IntoResponse { // get user from token let auth_user = match jar.get("__Secure-Token") { - Some(c) => { - let token = c.value_trimmed().to_string(); - - match database.get_profile_by_unhashed(token.clone()).await { - Ok(ua) => { - // check token permission - if !ua - .token_context_from_token(&token) - .can_do(TokenPermission::Moderator) - { - return Json(DefaultReturn { - success: false, - message: AuthError::NotAllowed.to_string(), - payload: None, - }); - } - - // return - ua - } - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: None, - }); - } + Some(c) => match database + .get_profile_by_unhashed(c.value_trimmed().to_string()) + .await + { + Ok(ua) => ua, + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: None, + }); } - } + }, None => { return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: None, }); } @@ -921,13 +429,13 @@ pub async fn set_tier_request( // we must have the "Manager" permission to edit other users return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: None, }); } // get other user - let other_user = match database.get_profile_by_username(username.clone()).await { + let other_user = match database.get_profile(username.clone()).await { Ok(ua) => ua, Err(e) => { return Json(DefaultReturn { @@ -954,14 +462,14 @@ pub async fn set_tier_request( // we cannot manager other managers return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: None, }); } // push update // TODO: try not to clone - if let Err(e) = database.edit_profile_tier(username, props.tier).await { + if let Err(e) = database.edit_profile_group(username, props.group).await { return Json(DefaultReturn { success: false, message: e.to_string(), @@ -970,19 +478,36 @@ pub async fn set_tier_request( } // return + if let Err(e) = database + .audit( + auth_user.id, + format!( + "Changed user group: [{}](/+u/{})", + other_user.id, other_user.id + ), + ) + .await + { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: None, + }); + }; + Json(DefaultReturn { success: true, message: "Acceptable".to_string(), - payload: Some(props.tier), + payload: Some(props.group), }) } /// Update the given user's session tokens -pub async fn other_update_tokens_request( +pub async fn update_tokens_request( jar: CookieJar, Path(id): Path, State(database): State, - Json(req): Json, + Json(req): Json, ) -> impl IntoResponse { // get user from token let auth_user = match jar.get("__Secure-Token") { @@ -998,7 +523,7 @@ pub async fn other_update_tokens_request( { return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: (), }); } @@ -1018,7 +543,7 @@ pub async fn other_update_tokens_request( None => { return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: (), }); } @@ -1038,7 +563,7 @@ pub async fn other_update_tokens_request( if auth_user.id == other.id { return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: (), }); } @@ -1058,7 +583,7 @@ pub async fn other_update_tokens_request( // we must have the "Manager" permission to edit other users return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: (), }); } @@ -1079,7 +604,7 @@ pub async fn other_update_tokens_request( // we cannot manager other managers return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: (), }); } @@ -1137,7 +662,7 @@ pub async fn other_update_tokens_request( } /// Generate a new token and session (like logging in while already logged in) -pub async fn other_generate_token_request( +pub async fn generate_token_request( jar: CookieJar, headers: HeaderMap, Path(id): Path, @@ -1158,7 +683,7 @@ pub async fn other_generate_token_request( { return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: None, }); } @@ -1178,7 +703,7 @@ pub async fn other_generate_token_request( None => { return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: None, }); } @@ -1198,7 +723,7 @@ pub async fn other_generate_token_request( if auth_user.id == other.id { return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: None, }); } @@ -1218,7 +743,7 @@ pub async fn other_generate_token_request( // we must have the "Manager" permission to edit other users return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: None, }); } @@ -1239,7 +764,7 @@ pub async fn other_generate_token_request( // we cannot manager other managers return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: None, }); } @@ -1267,7 +792,7 @@ pub async fn other_generate_token_request( if database.get_ipban_by_ip(real_ip.clone()).await.is_ok() { return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: None, }); } @@ -1286,163 +811,19 @@ pub async fn other_generate_token_request( .unwrap(); // return - return Json(DefaultReturn { - success: true, - message: "Generated token!".to_string(), - payload: Some(token), - }); -} - -/// Change a profile's password -pub async fn set_password_request( - jar: CookieJar, - Path(username): Path, - State(database): State, - Json(props): Json, -) -> impl IntoResponse { - // get user from token - let auth_user = match jar.get("__Secure-Token") { - Some(c) => { - let token = c.value_trimmed().to_string(); - - match database.get_profile_by_unhashed(token.clone()).await { - Ok(ua) => { - // check token permission - if !ua - .token_context_from_token(&token) - .can_do(TokenPermission::ManageAccount) - { - return Json(DefaultReturn { - success: false, - message: AuthError::NotAllowed.to_string(), - payload: None, - }); - } - - // return - ua - } - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: None, - }); - } - } - } - None => { - return Json(DefaultReturn { - success: false, - message: AuthError::NotAllowed.to_string(), - payload: None, - }); - } - }; - - // check permission - let mut is_manager = false; - if auth_user.username != username { - let group = match database.get_group_by_id(auth_user.group).await { - Ok(g) => g, - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: None, - }) - } - }; - - if !group.permissions.contains(&Permission::Manager) { - // we must have the "Manager" permission to edit other users - return Json(DefaultReturn { - success: false, - message: AuthError::NotAllowed.to_string(), - payload: None, - }); - } else { - is_manager = true; - } - - // get other user - let other_user = match database.get_profile_by_username(username.clone()).await { - Ok(ua) => ua, - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: None, - }); - } - }; - - // check permission - let group = match database.get_group_by_id(other_user.group).await { - Ok(g) => g, - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: None, - }) - } - }; - - if group.permissions.contains(&Permission::Manager) { - // we cannot manager other managers - return Json(DefaultReturn { - success: false, - message: AuthError::NotAllowed.to_string(), - payload: None, - }); - } - } - - // check user permissions - // returning NotAllowed here will block them from editing their profile - // we don't want to waste resources on rule breakers - if auth_user.group == -1 { - // group -1 (even if it exists) is for marking users as banned - return Json(DefaultReturn { - success: false, - message: AuthError::NotAllowed.to_string(), - payload: None, - }); - } - - // push update - // TODO: try not to clone - if let Err(e) = database - .edit_profile_password_by_name( - username, - props.password, - props.new_password.clone(), - is_manager == false, - ) - .await - { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: None, - }); - } - - // return - Json(DefaultReturn { + return Json(DefaultReturn { success: true, - message: "Acceptable".to_string(), - payload: Some(props.new_password), - }) + message: "Generated token!".to_string(), + payload: Some(token), + }); } -/// Change a profile's username -pub async fn set_username_request( +/// Change a profile's password +pub async fn update_password_request( jar: CookieJar, - Path(username): Path, + Path(id): Path, State(database): State, - Json(props): Json, + Json(props): Json, ) -> impl IntoResponse { // get user from token let auth_user = match jar.get("__Secure-Token") { @@ -1458,7 +839,7 @@ pub async fn set_username_request( { return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: None, }); } @@ -1478,14 +859,15 @@ pub async fn set_username_request( None => { return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: None, }); } }; // check permission - if auth_user.username != username { + let mut is_manager = false; + if auth_user.id != id && auth_user.username != id { let group = match database.get_group_by_id(auth_user.group).await { Ok(g) => g, Err(e) => { @@ -1501,13 +883,15 @@ pub async fn set_username_request( // we must have the "Manager" permission to edit other users return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: None, }); + } else { + is_manager = true; } // get other user - let other_user = match database.get_profile_by_username(username.clone()).await { + let other_user = match database.get_profile(id.clone()).await { Ok(ua) => ua, Err(e) => { return Json(DefaultReturn { @@ -1534,7 +918,7 @@ pub async fn set_username_request( // we cannot manager other managers return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: None, }); } @@ -1547,7 +931,7 @@ pub async fn set_username_request( // group -1 (even if it exists) is for marking users as banned return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: None, }); } @@ -1555,7 +939,12 @@ pub async fn set_username_request( // push update // TODO: try not to clone if let Err(e) = database - .edit_profile_username_by_name(username, props.password, props.new_name.clone()) + .edit_profile_password_by_name( + id, + props.password, + props.new_password.clone(), + is_manager == false, + ) .await { return Json(DefaultReturn { @@ -1569,16 +958,16 @@ pub async fn set_username_request( Json(DefaultReturn { success: true, message: "Acceptable".to_string(), - payload: Some(props.new_name), + payload: Some(props.new_password), }) } -/// Update a user's metadata -pub async fn update_metdata_request( +/// Change a profile's username +pub async fn update_username_request( jar: CookieJar, - Path(username): Path, + Path(id): Path, State(database): State, - Json(props): Json, + Json(props): Json, ) -> impl IntoResponse { // get user from token let auth_user = match jar.get("__Secure-Token") { @@ -1590,12 +979,12 @@ pub async fn update_metdata_request( // check token permission if !ua .token_context_from_token(&token) - .can_do(TokenPermission::ManageProfile) + .can_do(TokenPermission::ManageAccount) { return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), - payload: (), + message: DatabaseError::NotAllowed.to_string(), + payload: None, }); } @@ -1606,7 +995,7 @@ pub async fn update_metdata_request( return Json(DefaultReturn { success: false, message: e.to_string(), - payload: (), + payload: None, }); } } @@ -1614,21 +1003,21 @@ pub async fn update_metdata_request( None => { return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), - payload: (), + message: DatabaseError::NotAllowed.to_string(), + payload: None, }); } }; // check permission - if auth_user.username != username { + if auth_user.id != id && auth_user.username != id { let group = match database.get_group_by_id(auth_user.group).await { Ok(g) => g, Err(e) => { return Json(DefaultReturn { success: false, message: e.to_string(), - payload: (), + payload: None, }) } }; @@ -1637,19 +1026,19 @@ pub async fn update_metdata_request( // we must have the "Manager" permission to edit other users return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), - payload: (), + message: DatabaseError::NotAllowed.to_string(), + payload: None, }); } // get other user - let other_user = match database.get_profile_by_username(username.clone()).await { + let other_user = match database.get_profile(id.clone()).await { Ok(ua) => ua, Err(e) => { return Json(DefaultReturn { success: false, message: e.to_string(), - payload: (), + payload: None, }); } }; @@ -1661,7 +1050,7 @@ pub async fn update_metdata_request( return Json(DefaultReturn { success: false, message: e.to_string(), - payload: (), + payload: None, }) } }; @@ -1670,8 +1059,8 @@ pub async fn update_metdata_request( // we cannot manager other managers return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), - payload: (), + message: DatabaseError::NotAllowed.to_string(), + payload: None, }); } } @@ -1683,35 +1072,38 @@ pub async fn update_metdata_request( // group -1 (even if it exists) is for marking users as banned return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), - payload: (), + message: DatabaseError::NotAllowed.to_string(), + payload: None, }); } - // return - match database - .edit_profile_metadata_by_name(username, props.metadata) + // push update + // TODO: try not to clone + if let Err(e) = database + .edit_profile_username_by_id(id, props.password, props.new_name.clone()) .await { - Ok(_) => Json(DefaultReturn { - success: true, - message: "Acceptable".to_string(), - payload: (), - }), - Err(e) => Json(DefaultReturn { + return Json(DefaultReturn { success: false, message: e.to_string(), - payload: (), - }), + payload: None, + }); } + + // return + Json(DefaultReturn { + success: true, + message: "Acceptable".to_string(), + payload: Some(props.new_name), + }) } /// Update a user's metadata -pub async fn update_badges_request( +pub async fn update_metdata_request( jar: CookieJar, - Path(username): Path, + Path(id): Path, State(database): State, - Json(props): Json, + Json(props): Json, ) -> impl IntoResponse { // get user from token let auth_user = match jar.get("__Secure-Token") { @@ -1723,11 +1115,11 @@ pub async fn update_badges_request( // check token permission if !ua .token_context_from_token(&token) - .can_do(TokenPermission::Moderator) + .can_do(TokenPermission::ManageProfile) { return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: (), }); } @@ -1747,118 +1139,14 @@ pub async fn update_badges_request( None => { return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), - payload: (), - }); - } - }; - - // check permission - let group = match database.get_group_by_id(auth_user.group).await { - Ok(g) => g, - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }) - } - }; - - if !group.permissions.contains(&Permission::Helper) { - // we must have the "Helper" permission to edit other users' badges - return Json(DefaultReturn { - success: false, - message: AuthError::NotAllowed.to_string(), - payload: (), - }); - } - - // get other user - let other_user = match database.get_profile_by_username(username.clone()).await { - Ok(ua) => ua, - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }); - } - }; - - // check permission - let other_group = match database.get_group_by_id(other_user.group).await { - Ok(g) => g, - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }) - } - }; - - if other_group.permissions.contains(&Permission::Helper) - && !group.permissions.contains(&Permission::Manager) - { - // we cannot manage other helpers without manager - return Json(DefaultReturn { - success: false, - message: AuthError::NotAllowed.to_string(), - payload: (), - }); - } - - // return - match database - .edit_profile_badges_by_name(username, props.badges) - .await - { - Ok(_) => Json(DefaultReturn { - success: true, - message: "Acceptable".to_string(), - payload: (), - }), - Err(e) => Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }), - } -} - -/// Delete another user -pub async fn delete_other_request( - jar: CookieJar, - Path(id): Path, - State(database): State, -) -> impl IntoResponse { - // get user from token - let auth_user = match jar.get("__Secure-Token") { - Some(c) => match database - .get_profile_by_unhashed(c.value_trimmed().to_string()) - .await - { - Ok(ua) => ua, - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }); - } - }, - None => { - return Json(DefaultReturn { - success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: (), }); } }; // check permission - if auth_user.username != id { + if auth_user.id != id && auth_user.username != id { let group = match database.get_group_by_id(auth_user.group).await { Ok(g) => g, Err(e) => { @@ -1870,46 +1158,26 @@ pub async fn delete_other_request( } }; - // get other user - let other_user = match database.get_profile_by_id(id.clone()).await { - Ok(ua) => ua, - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }); - } - }; - if !group.permissions.contains(&Permission::Manager) { // we must have the "Manager" permission to edit other users return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: (), }); - } else { - let actor_id = auth_user.id; - if let Err(e) = database - .create_notification( - NotificationCreate { - title: format!("[{actor_id}](/+u/{actor_id})"), - content: format!("Deleted a profile: @{}", other_user.username), - address: format!("/+u/{actor_id}"), - recipient: "*(audit)".to_string(), // all staff, audit - }, - None, - ) - .await - { + } + + // get other user + let other_user = match database.get_profile(id.clone()).await { + Ok(ua) => ua, + Err(e) => { return Json(DefaultReturn { success: false, message: e.to_string(), payload: (), }); } - } + }; // check permission let group = match database.get_group_by_id(other_user.group).await { @@ -1927,7 +1195,7 @@ pub async fn delete_other_request( // we cannot manager other managers return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: (), }); } @@ -1940,58 +1208,16 @@ pub async fn delete_other_request( // group -1 (even if it exists) is for marking users as banned return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: (), }); - } - - // return - match database.delete_profile_by_id(id).await { - Ok(_) => Json(DefaultReturn { - success: true, - message: "Acceptable".to_string(), - payload: (), - }), - Err(e) => Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }), - } -} - -/// Create a warning -pub async fn create_warning_request( - jar: CookieJar, - State(database): State, - Json(props): Json, -) -> impl IntoResponse { - // get user from token - let auth_user = match jar.get("__Secure-Token") { - Some(c) => match database - .get_profile_by_unhashed(c.value_trimmed().to_string()) - .await - { - Ok(ua) => ua, - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }); - } - }, - None => { - return Json(DefaultReturn { - success: false, - message: AuthError::NotAllowed.to_string(), - payload: (), - }); - } - }; + } // return - match database.create_warning(props, auth_user).await { + match database + .edit_profile_metadata_by_id(id, props.metadata) + .await + { Ok(_) => Json(DefaultReturn { success: true, message: "Acceptable".to_string(), @@ -2005,83 +1231,111 @@ pub async fn create_warning_request( } } -/// Delete a warning -pub async fn delete_warning_request( +/// Update a user's metadata +pub async fn update_badges_request( jar: CookieJar, Path(id): Path, State(database): State, + Json(props): Json, ) -> impl IntoResponse { // get user from token let auth_user = match jar.get("__Secure-Token") { - Some(c) => match database - .get_profile_by_unhashed(c.value_trimmed().to_string()) - .await - { - Ok(ua) => ua, - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }); + Some(c) => { + let token = c.value_trimmed().to_string(); + + match database.get_profile_by_unhashed(token.clone()).await { + Ok(ua) => { + // check token permission + if !ua + .token_context_from_token(&token) + .can_do(TokenPermission::Moderator) + { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: (), + }); + } + + // return + ua + } + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }); + } } - }, + } None => { return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: (), }); } }; - // return - match database.delete_warning(id, auth_user).await { - Ok(_) => Json(DefaultReturn { - success: true, - message: "Acceptable".to_string(), - payload: (), - }), - Err(e) => Json(DefaultReturn { + // check permission + let group = match database.get_group_by_id(auth_user.group).await { + Ok(g) => g, + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }) + } + }; + + if !group.permissions.contains(&Permission::Helper) { + // we must have the "Helper" permission to edit other users' badges + return Json(DefaultReturn { success: false, - message: e.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: (), - }), + }); } -} -/// Create a ipban -pub async fn create_ipban_request( - jar: CookieJar, - State(database): State, - Json(props): Json, -) -> impl IntoResponse { - // get user from token - let auth_user = match jar.get("__Secure-Token") { - Some(c) => match database - .get_profile_by_unhashed(c.value_trimmed().to_string()) - .await - { - Ok(ua) => ua, - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }); - } - }, - None => { + // get other user + let other_user = match database.get_profile(id.clone()).await { + Ok(ua) => ua, + Err(e) => { return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: e.to_string(), payload: (), }); } }; + // check permission + let other_group = match database.get_group_by_id(other_user.group).await { + Ok(g) => g, + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }) + } + }; + + if other_group.permissions.contains(&Permission::Helper) + && !group.permissions.contains(&Permission::Manager) + { + // we cannot manage other helpers without manager + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: (), + }); + } + // return - match database.create_ipban(props, auth_user).await { + match database.edit_profile_badges_by_id(id, props.badges).await { Ok(_) => Json(DefaultReturn { success: true, message: "Acceptable".to_string(), @@ -2095,8 +1349,8 @@ pub async fn create_ipban_request( } } -/// Delete an ipban -pub async fn delete_ipban_request( +/// Delete another user +pub async fn delete_request( jar: CookieJar, Path(id): Path, State(database): State, @@ -2119,39 +1373,27 @@ pub async fn delete_ipban_request( None => { return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: (), }); } }; - // return - match database.delete_ipban(id, auth_user).await { - Ok(_) => Json(DefaultReturn { - success: true, - message: "Acceptable".to_string(), - payload: (), - }), - Err(e) => Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }), - } -} + // check permission + if auth_user.username != id { + let group = match database.get_group_by_id(auth_user.group).await { + Ok(g) => g, + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }) + } + }; -/// Create a ipblock -pub async fn create_ipblock_request( - jar: CookieJar, - State(database): State, - Json(props): Json, -) -> impl IntoResponse { - // get user from token - let auth_user = match jar.get("__Secure-Token") { - Some(c) => match database - .get_profile_by_unhashed(c.value_trimmed().to_string()) - .await - { + // get other user + let other_user = match database.get_profile_by_id(id.clone()).await { Ok(ua) => ua, Err(e) => { return Json(DefaultReturn { @@ -2160,63 +1402,73 @@ pub async fn create_ipblock_request( payload: (), }); } - }, - None => { + }; + + if !group.permissions.contains(&Permission::Manager) { + // we must have the "Manager" permission to edit other users return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: (), }); + } else { + let actor_id = auth_user.id; + if let Err(e) = database + .create_notification( + NotificationCreate { + title: format!("[{actor_id}](/+u/{actor_id})"), + content: format!("Deleted a profile: @{}", other_user.username), + address: format!("/+u/{actor_id}"), + recipient: "*(audit)".to_string(), // all staff, audit + }, + None, + ) + .await + { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }); + } } - }; - - // return - match database.create_ipblock(props, auth_user).await { - Ok(_) => Json(DefaultReturn { - success: true, - message: "Acceptable".to_string(), - payload: (), - }), - Err(e) => Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }), - } -} -/// Delete an ipblock -pub async fn delete_ipblock_request( - jar: CookieJar, - Path(id): Path, - State(database): State, -) -> impl IntoResponse { - // get user from token - let auth_user = match jar.get("__Secure-Token") { - Some(c) => match database - .get_profile_by_unhashed(c.value_trimmed().to_string()) - .await - { - Ok(ua) => ua, + // check permission + let group = match database.get_group_by_id(other_user.group).await { + Ok(g) => g, Err(e) => { return Json(DefaultReturn { success: false, message: e.to_string(), payload: (), - }); + }) } - }, - None => { + }; + + if group.permissions.contains(&Permission::Manager) { + // we cannot manager other managers return Json(DefaultReturn { success: false, - message: AuthError::NotAllowed.to_string(), + message: DatabaseError::NotAllowed.to_string(), payload: (), }); } - }; + } + + // check user permissions + // returning NotAllowed here will block them from editing their profile + // we don't want to waste resources on rule breakers + if auth_user.group == -1 { + // group -1 (even if it exists) is for marking users as banned + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: (), + }); + } // return - match database.delete_ipblock(id, auth_user).await { + match database.delete_profile_by_id(id).await { Ok(_) => Json(DefaultReturn { success: true, message: "Acceptable".to_string(), @@ -2229,88 +1481,3 @@ pub async fn delete_ipblock_request( }), } } - -// general -pub async fn not_found() -> impl IntoResponse { - Json(DefaultReturn:: { - success: false, - message: String::from("Path does not exist"), - payload: 404, - }) -} - -// auth -#[derive(serde::Deserialize)] -pub struct CallbackQueryProps { - pub uid: String, // this uid will need to be sent to the client as a token -} - -pub async fn callback_request(Query(params): Query) -> impl IntoResponse { - // return - ( - [ - ("Content-Type".to_string(), "text/html".to_string()), - ( - "Set-Cookie".to_string(), - format!( - "__Secure-Token={}; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age={}", - params.uid, - 60 * 60 * 24 * 365 - ), - ), - ], - " - - " - ) -} - -pub async fn logout_request(jar: CookieJar) -> impl IntoResponse { - // check for cookie - if let Some(_) = jar.get("__Secure-Token") { - return ( - [ - ("Content-Type".to_string(), "text/plain".to_string()), - ( - "Set-Cookie".to_string(), - "__Secure-Token=refresh; SameSite=Strict; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=0".to_string(), - ) ], - "You have been signed out. You can now close this tab.", - ); - } - - // return - ( - [ - ("Content-Type".to_string(), "text/plain".to_string()), - ("Set-Cookie".to_string(), String::new()), - ], - "Failed to sign out of account.", - ) -} - -pub async fn remove_tag(jar: CookieJar) -> impl IntoResponse { - // check for cookie - // anonymous users cannot remove their own tag - if let Some(_) = jar.get("__Secure-Token") { - return ( - [ - ("Content-Type".to_string(), "text/plain".to_string()), - ( - "Set-Cookie2".to_string(), - "__Secure-Question-Tag=refresh; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=0".to_string() - ) - ], - "You have been signed out. You can now close this tab.", - ); - } - - // return - ( - [ - ("Content-Type".to_string(), "text/plain".to_string()), - ("Set-Cookie".to_string(), String::new()), - ], - "Failed to remove tag.", - ) -} diff --git a/crates/authbeam/src/api/relationships.rs b/crates/authbeam/src/api/relationships.rs new file mode 100644 index 00000000..9783fd63 --- /dev/null +++ b/crates/authbeam/src/api/relationships.rs @@ -0,0 +1,422 @@ +use crate::database::Database; +use crate::model::{DatabaseError, UserFollow, RelationshipStatus}; +use databeam::DefaultReturn; + +use axum::response::IntoResponse; +use axum::{ + extract::{Path, State}, + Json, +}; +use axum_extra::extract::cookie::CookieJar; + +/// Toggle following on the given user +pub async fn follow_request( + jar: CookieJar, + Path(id): Path, + State(database): State, +) -> impl IntoResponse { + // get user from token + let auth_user = match jar.get("__Secure-Token") { + Some(c) => match database + .get_profile_by_unhashed(c.value_trimmed().to_string()) + .await + { + Ok(ua) => ua, + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }); + } + }, + None => { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: (), + }); + } + }; + + // check block status + let attempting_to_follow = match database.get_profile(id.to_owned()).await { + Ok(ua) => ua, + Err(_) => { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotFound.to_string(), + payload: (), + }) + } + }; + + let relationship = database + .get_user_relationship(attempting_to_follow.id.clone(), auth_user.id.clone()) + .await + .0; + + if relationship == RelationshipStatus::Blocked { + // blocked users cannot follow the people who blocked them! + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: (), + }); + } + + // return + match database + .toggle_user_follow(&mut UserFollow { + user: auth_user.id, + following: attempting_to_follow.id, + }) + .await + { + Ok(_) => Json(DefaultReturn { + success: true, + message: "Follow toggled".to_string(), + payload: (), + }), + Err(e) => Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }), + } +} + +/// Send/accept a friend request to/from another user +pub async fn friend_request( + jar: CookieJar, + Path(id): Path, + State(database): State, +) -> impl IntoResponse { + // get user from token + let auth_user = match jar.get("__Secure-Token") { + Some(c) => match database + .get_profile_by_unhashed(c.value_trimmed().to_string()) + .await + { + Ok(ua) => ua, + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: None, + }); + } + }, + None => { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: None, + }); + } + }; + + // ... + let other_user = match database.get_profile(id.to_owned()).await { + Ok(ua) => ua, + Err(_) => { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotFound.to_string(), + payload: None, + }) + } + }; + + // get current relationship + let current = database + .get_user_relationship(auth_user.id.clone(), other_user.id.clone()) + .await; + + if current.0 == RelationshipStatus::Blocked && auth_user.id != current.1 { + // cannot change relationship if we're blocked and we aren't the user that did the blocking + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: None, + }); + } + + let current = current.0; + + // return + if current == RelationshipStatus::Unknown { + // send request + match database + .set_user_relationship_status( + auth_user.id, + other_user.id, + RelationshipStatus::Pending, + false, + ) + .await + { + Ok(export) => { + return Json(DefaultReturn { + success: true, + message: "Friend request sent!".to_string(), + payload: Some(export), + }) + } + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: None, + }) + } + } + } else if current == RelationshipStatus::Pending { + // accept request + match database + .set_user_relationship_status( + auth_user.id, + other_user.id, + RelationshipStatus::Friends, + false, + ) + .await + { + Ok(export) => { + return Json(DefaultReturn { + success: true, + message: "Friend request accepted!".to_string(), + payload: Some(export), + }) + } + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: None, + }) + } + } + } else { + // no clue, remove friendship? + match database + .set_user_relationship_status( + auth_user.id, + other_user.id, + RelationshipStatus::Unknown, + false, + ) + .await + { + Ok(export) => { + return Json(DefaultReturn { + success: true, + message: "Friendship removed".to_string(), + payload: Some(export), + }) + } + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: None, + }) + } + } + } +} + +/// Block another user +pub async fn block_request( + jar: CookieJar, + Path(id): Path, + State(database): State, +) -> impl IntoResponse { + // get user from token + let auth_user = match jar.get("__Secure-Token") { + Some(c) => match database + .get_profile_by_unhashed(c.value_trimmed().to_string()) + .await + { + Ok(ua) => ua, + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: None, + }); + } + }, + None => { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: None, + }); + } + }; + + // ... + let other_user = match database.get_profile(id.to_owned()).await { + Ok(ua) => ua, + Err(_) => { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotFound.to_string(), + payload: None, + }) + } + }; + + // get current relationship + let current = database + .get_user_relationship(auth_user.id.clone(), other_user.id.clone()) + .await; + + if current.0 == RelationshipStatus::Blocked && auth_user.id != current.1 { + // cannot change relationship if we're blocked and we aren't the user that did the blocking + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: None, + }); + } + + // force unfollow + if let Err(e) = database + .force_remove_user_follow(&mut UserFollow { + user: auth_user.id.clone(), + following: other_user.id.clone(), + }) + .await + { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: None, + }); + } + + if let Err(e) = database + .force_remove_user_follow(&mut UserFollow { + user: other_user.id.clone(), + following: auth_user.id.clone(), + }) + .await + { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: None, + }); + } + + // return + match database + .set_user_relationship_status( + auth_user.id, + other_user.id, + RelationshipStatus::Blocked, + false, + ) + .await + { + Ok(export) => { + return Json(DefaultReturn { + success: true, + message: "User blocked!".to_string(), + payload: Some(export), + }) + } + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: None, + }) + } + } +} + +/// Remove relationship with another user +pub async fn delete_request( + jar: CookieJar, + Path(id): Path, + State(database): State, +) -> impl IntoResponse { + // get user from token + let auth_user = match jar.get("__Secure-Token") { + Some(c) => match database + .get_profile_by_unhashed(c.value_trimmed().to_string()) + .await + { + Ok(ua) => ua, + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: None, + }); + } + }, + None => { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: None, + }); + } + }; + + // ... + let other_user = match database.get_profile(id.to_owned()).await { + Ok(ua) => ua, + Err(_) => { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotFound.to_string(), + payload: None, + }) + } + }; + + // get current relationship + let current = database + .get_user_relationship(auth_user.id.clone(), other_user.id.clone()) + .await; + + if current.0 == RelationshipStatus::Blocked && auth_user.id != current.1 { + // cannot remove relationship if we're blocked and we aren't the user that did the blocking + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: None, + }); + } + + // return + match database + .set_user_relationship_status( + auth_user.id, + other_user.id, + RelationshipStatus::Unknown, + false, + ) + .await + { + Ok(export) => { + return Json(DefaultReturn { + success: true, + message: "Relationship removed!".to_string(), + payload: Some(export), + }) + } + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: None, + }) + } + } +} diff --git a/crates/authbeam/src/api/warnings.rs b/crates/authbeam/src/api/warnings.rs new file mode 100644 index 00000000..33521f73 --- /dev/null +++ b/crates/authbeam/src/api/warnings.rs @@ -0,0 +1,100 @@ +use crate::database::Database; +use crate::model::{DatabaseError, WarningCreate}; +use databeam::DefaultReturn; + +use axum::response::IntoResponse; +use axum::{ + extract::{Path, State}, + Json, +}; +use axum_extra::extract::cookie::CookieJar; + +/// Create a warning +pub async fn create_request( + jar: CookieJar, + State(database): State, + Json(props): Json, +) -> impl IntoResponse { + // get user from token + let auth_user = match jar.get("__Secure-Token") { + Some(c) => match database + .get_profile_by_unhashed(c.value_trimmed().to_string()) + .await + { + Ok(ua) => ua, + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }); + } + }, + None => { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: (), + }); + } + }; + + // return + match database.create_warning(props, auth_user).await { + Ok(_) => Json(DefaultReturn { + success: true, + message: "Acceptable".to_string(), + payload: (), + }), + Err(e) => Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }), + } +} + +/// Delete a warning +pub async fn delete_request( + jar: CookieJar, + Path(id): Path, + State(database): State, +) -> impl IntoResponse { + // get user from token + let auth_user = match jar.get("__Secure-Token") { + Some(c) => match database + .get_profile_by_unhashed(c.value_trimmed().to_string()) + .await + { + Ok(ua) => ua, + Err(e) => { + return Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }); + } + }, + None => { + return Json(DefaultReturn { + success: false, + message: DatabaseError::NotAllowed.to_string(), + payload: (), + }); + } + }; + + // return + match database.delete_warning(id, auth_user).await { + Ok(_) => Json(DefaultReturn { + success: true, + message: "Acceptable".to_string(), + payload: (), + }), + Err(e) => Json(DefaultReturn { + success: false, + message: e.to_string(), + payload: (), + }), + } +} diff --git a/crates/authbeam/src/database.rs b/crates/authbeam/src/database.rs index 9ac362e4..072742d2 100644 --- a/crates/authbeam/src/database.rs +++ b/crates/authbeam/src/database.rs @@ -1,6 +1,6 @@ use crate::model::{ - AuthError, IpBan, IpBanCreate, IpBlock, IpBlockCreate, Profile, ProfileCreate, ProfileMetadata, - RelationshipStatus, TokenContext, Warning, WarningCreate, + DatabaseError, IpBan, IpBanCreate, IpBlock, IpBlockCreate, Profile, ProfileCreate, + ProfileMetadata, RelationshipStatus, TokenContext, Warning, WarningCreate, }; use crate::model::{Group, Notification, NotificationCreate, Permission, UserFollow}; @@ -9,7 +9,7 @@ use reqwest::Client as HttpClient; use serde::{Deserialize, Serialize}; use databeam::{query as sqlquery, utility}; -pub type Result = std::result::Result; +pub type Result = std::result::Result; #[derive(Clone, Serialize, Deserialize, Debug)] pub struct HCaptchaConfig { @@ -33,14 +33,28 @@ impl Default for HCaptchaConfig { } } -#[derive(Clone, Debug)] +#[derive(Serialize, Deserialize, Clone, Debug)] pub struct ServerOptions { /// If new registrations are enabled + #[serde(default)] pub registration_enabled: bool, /// HCaptcha configuration + #[serde(default)] pub captcha: HCaptchaConfig, /// The header to read user IP from + #[serde(default)] pub real_ip_header: Option, + /// The directory to serve static assets from + #[serde(default)] + pub static_dir: String, + /// The origin of the public server (ex: "https://rainbeam.net") + /// + /// Used in embeds and links. + #[serde(default)] + pub host: String, + /// A list of image hosts that are blocked + #[serde(default)] + pub blocked_hosts: Vec, } impl Default for ServerOptions { @@ -49,6 +63,9 @@ impl Default for ServerOptions { registration_enabled: true, captcha: HCaptchaConfig::default(), real_ip_header: Option::None, + static_dir: String::new(), + host: String::new(), + blocked_hosts: Vec::new(), } } } @@ -198,6 +215,27 @@ impl Database { .await; } + // util + + /// Create a moderator audit log entry + pub async fn audit(&self, actor_id: String, content: String) -> Result<()> { + match self + .create_notification( + NotificationCreate { + title: format!("[{actor_id}](/+u/{actor_id})"), + content, + address: format!("/+u/{actor_id}"), + recipient: "*(audit)".to_string(), // all staff, audit registry + }, + None, + ) + .await + { + Ok(_) => Ok(()), + Err(_) => Err(DatabaseError::Other), + } + } + // profiles // GET @@ -229,13 +267,13 @@ impl Database { if id.len() <= 32 { return match self.get_profile_by_username(id).await { Ok(ua) => Ok(ua), - Err(_) => Err(AuthError::Other), + Err(_) => Err(DatabaseError::Other), }; } match self.get_profile_by_id(id).await { Ok(ua) => Ok(ua), - Err(_) => Err(AuthError::Other), + Err(_) => Err(DatabaseError::Other), } } @@ -258,7 +296,7 @@ impl Database { .await { Ok(u) => self.base.textify_row(u, Vec::new()).0, - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; // return @@ -269,23 +307,23 @@ impl Database { salt: row.get("salt").unwrap_or(&"".to_string()).to_string(), tokens: match serde_json::from_str(row.get("tokens").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, ips: match serde_json::from_str(row.get("ips").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, token_context: match serde_json::from_str(row.get("token_context").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, metadata: match serde_json::from_str(row.get("metadata").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, badges: match serde_json::from_str(row.get("badges").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, group: row.get("gid").unwrap().parse::().unwrap_or(0), joined: row.get("joined").unwrap().parse::().unwrap(), @@ -321,7 +359,7 @@ impl Database { .await { Ok(u) => self.base.textify_row(u, Vec::new()).0, - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; // return @@ -332,23 +370,23 @@ impl Database { salt: row.get("salt").unwrap_or(&"".to_string()).to_string(), tokens: match serde_json::from_str(row.get("tokens").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, ips: match serde_json::from_str(row.get("ips").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, token_context: match serde_json::from_str(row.get("token_context").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, metadata: match serde_json::from_str(row.get("metadata").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, badges: match serde_json::from_str(row.get("badges").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, group: row.get("gid").unwrap().parse::().unwrap_or(0), joined: row.get("joined").unwrap().parse::().unwrap(), @@ -382,7 +420,7 @@ impl Database { .await { Ok(r) => self.base.textify_row(r, Vec::new()).0, - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; // return @@ -393,23 +431,23 @@ impl Database { salt: row.get("salt").unwrap_or(&"".to_string()).to_string(), tokens: match serde_json::from_str(row.get("tokens").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, ips: match serde_json::from_str(row.get("ips").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, token_context: match serde_json::from_str(row.get("token_context").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, metadata: match serde_json::from_str(row.get("metadata").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, badges: match serde_json::from_str(row.get("badges").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, group: row.get("gid").unwrap().parse::().unwrap_or(0), joined: row.get("joined").unwrap().parse::().unwrap(), @@ -457,7 +495,7 @@ impl Database { .await { Ok(r) => self.base.textify_row(r, Vec::new()).0, - Err(_) => return Err(AuthError::NotFound), + Err(_) => return Err(DatabaseError::NotFound), }; // store in cache @@ -468,23 +506,23 @@ impl Database { salt: row.get("salt").unwrap_or(&"".to_string()).to_string(), tokens: match serde_json::from_str(row.get("tokens").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, ips: match serde_json::from_str(row.get("ips").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, token_context: match serde_json::from_str(row.get("token_context").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, metadata: match serde_json::from_str(row.get("metadata").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, badges: match serde_json::from_str(row.get("badges").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, group: row.get("gid").unwrap().parse::().unwrap_or(0), joined: row.get("joined").unwrap().parse::().unwrap(), @@ -539,7 +577,7 @@ impl Database { let c = &self.base.db.client; let row = match sqlquery(query).bind::<&String>(&id).fetch_one(c).await { Ok(r) => self.base.textify_row(r, Vec::new()).0, - Err(_) => return Err(AuthError::NotFound), + Err(_) => return Err(DatabaseError::NotFound), }; // store in cache @@ -550,23 +588,23 @@ impl Database { salt: row.get("salt").unwrap_or(&"".to_string()).to_string(), tokens: match serde_json::from_str(row.get("tokens").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, ips: match serde_json::from_str(row.get("ips").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, token_context: match serde_json::from_str(row.get("token_context").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, metadata: match serde_json::from_str(row.get("metadata").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, badges: match serde_json::from_str(row.get("badges").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, group: row.get("gid").unwrap().parse::().unwrap_or(0), joined: row.get("joined").unwrap().parse::().unwrap(), @@ -611,15 +649,15 @@ impl Database { .unwrap(); if regex.captures(&username).is_some() { - return Err(AuthError::ValueError); + return Err(DatabaseError::ValueError); } if (username.len() < 2) | (username.len() > 500) { - return Err(AuthError::ValueError); + return Err(DatabaseError::ValueError); } if banned_usernames.contains(&username.as_str()) { - return Err(AuthError::ValueError); + return Err(DatabaseError::ValueError); } Ok(()) @@ -633,7 +671,7 @@ impl Database { /// * `user_ip` - the ip address of the user registering pub async fn create_profile(&self, props: ProfileCreate, user_ip: String) -> Result { if self.config.registration_enabled == false { - return Err(AuthError::NotAllowed); + return Err(DatabaseError::NotAllowed); } // ... @@ -645,12 +683,12 @@ impl Database { .valid_response(&self.config.captcha.secret, None) .await { - return Err(AuthError::NotAllowed); + return Err(DatabaseError::NotAllowed); } // make sure user doesn't already exists if let Ok(_) = &self.get_profile_by_username(username.clone()).await { - return Err(AuthError::MustBeUnique); + return Err(DatabaseError::MustBeUnique); }; // check username @@ -693,7 +731,7 @@ impl Database { .await { Ok(_) => Ok(user_token_unhashed), - Err(_) => Err(AuthError::Other), + Err(_) => Err(DatabaseError::Other), } } @@ -745,14 +783,14 @@ impl Database { ] } - /// Update a [`Profile`]'s metadata by its `username` - pub async fn edit_profile_metadata_by_name( + /// Update a [`Profile`]'s metadata by its `id` + pub async fn edit_profile_metadata_by_id( &self, - name: String, + id: String, mut metadata: ProfileMetadata, ) -> Result<()> { // make sure user exists - let profile = match self.get_profile_by_username(name.clone()).await { + let profile = match self.get_profile(id.clone()).await { Ok(ua) => ua, Err(e) => return Err(e), }; @@ -767,28 +805,28 @@ impl Database { } if !metadata.check() { - return Err(AuthError::TooLong); + return Err(DatabaseError::TooLong); } // update user let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") { - "UPDATE \"xprofiles\" SET \"metadata\" = ? WHERE \"username\" = ?" + "UPDATE \"xprofiles\" SET \"metadata\" = ? WHERE \"id\" = ?" } else { - "UPDATE \"xprofiles\" SET (\"metadata\") = ($1) WHERE \"username\" = $2" + "UPDATE \"xprofiles\" SET (\"metadata\") = ($1) WHERE \"id\" = $2" }; let c = &self.base.db.client; let meta = &serde_json::to_string(&metadata).unwrap(); match sqlquery(query) .bind::<&String>(meta) - .bind::<&String>(&name) + .bind::<&String>(&id) .execute(c) .await { Ok(_) => { self.base .cachedb - .remove(format!("rbeam.auth.profile:{}", name)) + .remove(format!("rbeam.auth.profile:{}", profile.username)) .await; self.base @@ -798,7 +836,7 @@ impl Database { Ok(()) } - Err(_) => Err(AuthError::Other), + Err(_) => Err(DatabaseError::Other), } } @@ -850,27 +888,27 @@ impl Database { Ok(()) } - Err(_) => Err(AuthError::Other), + Err(_) => Err(DatabaseError::Other), } } - /// Update a [`Profile`]'s badges by its `username` - pub async fn edit_profile_badges_by_name( + /// Update a [`Profile`]'s badges by its `id` + pub async fn edit_profile_badges_by_id( &self, - name: String, + id: String, badges: Vec<(String, String, String)>, ) -> Result<()> { // make sure user exists - let ua = match self.get_profile_by_username(name.clone()).await { + let ua = match self.get_profile(id.clone()).await { Ok(ua) => ua, Err(e) => return Err(e), }; // update user let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") { - "UPDATE \"xprofiles\" SET \"badges\" = ? WHERE \"username\" = ?" + "UPDATE \"xprofiles\" SET \"badges\" = ? WHERE \"id\" = ?" } else { - "UPDATE \"xprofiles\" SET (\"badges\") = ($1) WHERE \"username\" = $2" + "UPDATE \"xprofiles\" SET (\"badges\") = ($1) WHERE \"id\" = $2" }; let c = &self.base.db.client; @@ -878,14 +916,14 @@ impl Database { match sqlquery(query) .bind::<&String>(badges) - .bind::<&String>(&name) + .bind::<&String>(&id) .execute(c) .await { Ok(_) => { self.base .cachedb - .remove(format!("rbeam.auth.profile:{}", name)) + .remove(format!("rbeam.auth.profile:{}", id)) .await; self.base @@ -895,7 +933,7 @@ impl Database { Ok(()) } - Err(_) => Err(AuthError::Other), + Err(_) => Err(DatabaseError::Other), } } @@ -909,9 +947,9 @@ impl Database { // update user let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") { - "UPDATE \"xprofiles\" SET \"tier\" = ? WHERE \"username\" = ?" + "UPDATE \"xprofiles\" SET \"tier\" = ? WHERE \"id\" = ?" } else { - "UPDATE \"xprofiles\" SET (\"tier\") = ($1) WHERE \"username\" = $2" + "UPDATE \"xprofiles\" SET (\"tier\") = ($1) WHERE \"id\" = $2" }; let c = &self.base.db.client; @@ -924,7 +962,7 @@ impl Database { Ok(_) => { self.base .cachedb - .remove(format!("rbeam.auth.profile:{}", id)) + .remove(format!("rbeam.auth.profile:{}", ua.username)) .await; self.base @@ -934,11 +972,11 @@ impl Database { Ok(()) } - Err(_) => Err(AuthError::Other), + Err(_) => Err(DatabaseError::Other), } } - /// Update a [`Profile`]'s `gid` by its `username` + /// Update a [`Profile`]'s `gid` by its `id` pub async fn edit_profile_group(&self, id: String, group: i32) -> Result<()> { // make sure user exists let ua = match self.get_profile(id.clone()).await { @@ -948,9 +986,9 @@ impl Database { // update user let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") { - "UPDATE \"xprofiles\" SET \"gid\" = ? WHERE \"username\" = ?" + "UPDATE \"xprofiles\" SET \"gid\" = ? WHERE \"id\" = ?" } else { - "UPDATE \"xprofiles\" SET (\"gid\") = ($1) WHERE \"username\" = $2" + "UPDATE \"xprofiles\" SET (\"gid\") = ($1) WHERE \"id\" = $2" }; let c = &self.base.db.client; @@ -973,7 +1011,7 @@ impl Database { Ok(()) } - Err(_) => Err(AuthError::Other), + Err(_) => Err(DatabaseError::Other), } } @@ -996,7 +1034,7 @@ impl Database { let password_hashed = shared::hash::hash_salted(password, ua.salt); if password_hashed != ua.password { - return Err(AuthError::NotAllowed); + return Err(DatabaseError::NotAllowed); } } @@ -1030,28 +1068,28 @@ impl Database { Ok(()) } - Err(_) => Err(AuthError::Other), + Err(_) => Err(DatabaseError::Other), } } - /// Update a [`Profile`]'s `username` by its name and password - pub async fn edit_profile_username_by_name( + /// Update a [`Profile`]'s `username` by its id and password + pub async fn edit_profile_username_by_id( &self, - name: String, + id: String, password: String, mut new_name: String, ) -> Result<()> { new_name = new_name.to_lowercase(); // make sure user exists - let ua = match self.get_profile_by_username(name.clone()).await { + let ua = match self.get_profile(id.clone()).await { Ok(ua) => ua, Err(e) => return Err(e), }; // make sure username isn't in use if let Ok(_) = self.get_profile_by_username(new_name.clone()).await { - return Err(AuthError::MustBeUnique); + return Err(DatabaseError::MustBeUnique); } // check username @@ -1063,27 +1101,27 @@ impl Database { let password_hashed = shared::hash::hash_salted(password, ua.salt); if password_hashed != ua.password { - return Err(AuthError::NotAllowed); + return Err(DatabaseError::NotAllowed); } // update user let query: &str = if (self.base.db.r#type == "sqlite") | (self.base.db.r#type == "mysql") { - "UPDATE \"xprofiles\" SET \"username\" = ? WHERE \"username\" = ?" + "UPDATE \"xprofiles\" SET \"username\" = ? WHERE \"id\" = ?" } else { - "UPDATE \"xprofiles\" SET (\"username\") = ($1) WHERE \"username\" = $2" + "UPDATE \"xprofiles\" SET (\"username\") = ($1) WHERE \"id\" = $2" }; let c = &self.base.db.client; match sqlquery(query) .bind::<&String>(&new_name) - .bind::<&String>(&name) + .bind::<&String>(&id) .execute(c) .await { Ok(_) => { self.base .cachedb - .remove(format!("rbeam.auth.profile:{}", name)) + .remove(format!("rbeam.auth.profile:{}", ua.username)) .await; self.base @@ -1093,7 +1131,7 @@ impl Database { Ok(()) } - Err(_) => Err(AuthError::Other), + Err(_) => Err(DatabaseError::Other), } } @@ -1121,7 +1159,7 @@ impl Database { }; if let Err(_) = sqlquery(query).bind::<&String>(&id).execute(c).await { - return Err(AuthError::Other); + return Err(DatabaseError::Other); }; let query: &str = @@ -1132,7 +1170,7 @@ impl Database { }; if let Err(_) = sqlquery(query).bind::<&String>(&id).execute(c).await { - return Err(AuthError::Other); + return Err(DatabaseError::Other); }; let query: &str = @@ -1148,7 +1186,7 @@ impl Database { .execute(c) .await { - return Err(AuthError::Other); + return Err(DatabaseError::Other); }; // rainbeam crate stuff @@ -1161,7 +1199,7 @@ impl Database { }; if let Err(_) = sqlquery(query).bind::<&String>(&id).execute(c).await { - return Err(AuthError::Other); + return Err(DatabaseError::Other); }; // questions by user @@ -1173,7 +1211,7 @@ impl Database { }; if let Err(_) = sqlquery(query).bind::<&String>(&id).execute(c).await { - return Err(AuthError::Other); + return Err(DatabaseError::Other); }; // responses by user @@ -1185,7 +1223,7 @@ impl Database { }; if let Err(_) = sqlquery(query).bind::<&String>(&id).execute(c).await { - return Err(AuthError::Other); + return Err(DatabaseError::Other); }; // responses to questions by user @@ -1201,7 +1239,7 @@ impl Database { .execute(c) .await { - return Err(AuthError::Other); + return Err(DatabaseError::Other); }; self.base @@ -1223,7 +1261,7 @@ impl Database { }; if let Err(_) = sqlquery(query).bind::<&String>(&id).execute(c).await { - return Err(AuthError::Other); + return Err(DatabaseError::Other); }; // user circle memberships @@ -1235,7 +1273,7 @@ impl Database { }; if let Err(_) = sqlquery(query).bind::<&String>(&id).execute(c).await { - return Err(AuthError::Other); + return Err(DatabaseError::Other); }; // pages by user @@ -1247,7 +1285,7 @@ impl Database { }; if let Err(_) = sqlquery(query).bind::<&String>(&id).execute(c).await { - return Err(AuthError::Other); + return Err(DatabaseError::Other); }; // relationships involving user @@ -1264,7 +1302,7 @@ impl Database { .execute(c) .await { - return Err(AuthError::Other); + return Err(DatabaseError::Other); }; self.base @@ -1281,7 +1319,7 @@ impl Database { }; if let Err(_) = sqlquery(query).bind::<&String>(&id).execute(c).await { - return Err(AuthError::Other); + return Err(DatabaseError::Other); }; // ... @@ -1312,7 +1350,7 @@ impl Database { Ok(()) } - Err(_) => Err(AuthError::Other), + Err(_) => Err(DatabaseError::Other), } } @@ -1326,11 +1364,11 @@ impl Database { // make sure they aren't a manager let group = match self.get_group_by_id(user.group).await { Ok(g) => g, - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; if group.permissions.contains(&Permission::Manager) { - return Err(AuthError::NotAllowed); + return Err(DatabaseError::NotAllowed); } // delete @@ -1375,7 +1413,7 @@ impl Database { id: row.get("id").unwrap().parse::().unwrap(), permissions: match serde_json::from_str(row.get("permissions").unwrap()) { Ok(m) => m, - Err(_) => return Err(AuthError::ValueError), + Err(_) => return Err(DatabaseError::ValueError), }, }; @@ -1415,7 +1453,7 @@ impl Database { .await { Ok(u) => self.base.textify_row(u, Vec::new()).0, - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; // return @@ -1466,7 +1504,7 @@ impl Database { out } - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; // return @@ -1534,7 +1572,7 @@ impl Database { out } - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; // return @@ -1623,7 +1661,7 @@ impl Database { out } - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; // return @@ -1691,7 +1729,7 @@ impl Database { out } - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; // return @@ -1739,7 +1777,7 @@ impl Database { pub async fn toggle_user_follow(&self, props: &mut UserFollow) -> Result<()> { // users cannot be the same if props.user == props.following { - return Err(AuthError::Other); + return Err(DatabaseError::Other); } // make sure both users exist @@ -1790,7 +1828,7 @@ impl Database { return Ok(()); } - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; } @@ -1844,7 +1882,7 @@ impl Database { // return Ok(()) } - Err(_) => Err(AuthError::Other), + Err(_) => Err(DatabaseError::Other), } } @@ -1855,7 +1893,7 @@ impl Database { pub async fn force_remove_user_follow(&self, props: &mut UserFollow) -> Result<()> { // users cannot be the same if props.user == props.following { - return Err(AuthError::Other); + return Err(DatabaseError::Other); } // check if follow exists @@ -1892,7 +1930,7 @@ impl Database { return Ok(()); } - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; } @@ -1932,7 +1970,7 @@ impl Database { let c = &self.base.db.client; let res = match sqlquery(&query).bind::<&String>(&id).fetch_one(c).await { Ok(p) => self.base.textify_row(p, Vec::new()).0, - Err(_) => return Err(AuthError::NotFound), + Err(_) => return Err(DatabaseError::NotFound), }; // return @@ -1998,7 +2036,7 @@ impl Database { out } - Err(_) => return Err(AuthError::NotFound), + Err(_) => return Err(DatabaseError::NotFound), }; // return @@ -2079,7 +2117,7 @@ impl Database { out } - Err(_) => return Err(AuthError::NotFound), + Err(_) => return Err(DatabaseError::NotFound), }; // return @@ -2142,7 +2180,7 @@ impl Database { // ... return Ok(()); } - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; } @@ -2165,11 +2203,11 @@ impl Database { // check permission let group = match self.get_group_by_id(user.group).await { Ok(g) => g, - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; if !group.permissions.contains(&Permission::Helper) { - return Err(AuthError::NotAllowed); + return Err(DatabaseError::NotAllowed); } } @@ -2203,7 +2241,7 @@ impl Database { // return return Ok(()); } - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; } @@ -2228,11 +2266,11 @@ impl Database { // check permission let group = match self.get_group_by_id(user.group).await { Ok(g) => g, - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; if !group.permissions.contains(&Permission::Helper) { - return Err(AuthError::NotAllowed); + return Err(DatabaseError::NotAllowed); } } @@ -2270,7 +2308,7 @@ impl Database { // return return Ok(()); } - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; } @@ -2305,7 +2343,7 @@ impl Database { let c = &self.base.db.client; let res = match sqlquery(&query).bind::<&String>(&id).fetch_one(c).await { Ok(p) => self.base.textify_row(p, Vec::new()).0, - Err(_) => return Err(AuthError::NotFound), + Err(_) => return Err(DatabaseError::NotFound), }; // return @@ -2349,11 +2387,11 @@ impl Database { // make sure user is a manager let group = match self.get_group_by_id(user.group).await { Ok(g) => g, - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; if !group.permissions.contains(&Permission::Helper) { - return Err(AuthError::NotAllowed); + return Err(DatabaseError::NotAllowed); } // pull from database @@ -2393,7 +2431,7 @@ impl Database { out } - Err(_) => return Err(AuthError::NotFound), + Err(_) => return Err(DatabaseError::NotFound), }; // return @@ -2410,11 +2448,11 @@ impl Database { // make sure user is a manager let group = match self.get_group_by_id(user.group).await { Ok(g) => g, - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; if !group.permissions.contains(&Permission::Helper) { - return Err(AuthError::NotAllowed); + return Err(DatabaseError::NotAllowed); } // ... @@ -2465,7 +2503,7 @@ impl Database { // ... return Ok(()); } - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; } @@ -2488,11 +2526,11 @@ impl Database { // check permission let group = match self.get_group_by_id(user.group).await { Ok(g) => g, - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; if !group.permissions.contains(&Permission::Manager) { - return Err(AuthError::NotAllowed); + return Err(DatabaseError::NotAllowed); } } @@ -2517,7 +2555,7 @@ impl Database { // return return Ok(()); } - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; } @@ -2552,7 +2590,7 @@ impl Database { let c = &self.base.db.client; let res = match sqlquery(&query).bind::<&String>(&id).fetch_one(c).await { Ok(p) => self.base.textify_row(p, Vec::new()).0, - Err(_) => return Err(AuthError::NotFound), + Err(_) => return Err(DatabaseError::NotFound), }; // return @@ -2600,7 +2638,7 @@ impl Database { let c = &self.base.db.client; let res = match sqlquery(&query).bind::<&String>(&ip).fetch_one(c).await { Ok(p) => self.base.textify_row(p, Vec::new()).0, - Err(_) => return Err(AuthError::NotFound), + Err(_) => return Err(DatabaseError::NotFound), }; // return @@ -2630,11 +2668,11 @@ impl Database { // make sure user is a manager let group = match self.get_group_by_id(user.group).await { Ok(g) => g, - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; if !group.permissions.contains(&Permission::Helper) { - return Err(AuthError::NotAllowed); + return Err(DatabaseError::NotAllowed); } // pull from database @@ -2670,7 +2708,7 @@ impl Database { out } - Err(_) => return Err(AuthError::NotFound), + Err(_) => return Err(DatabaseError::NotFound), }; // return @@ -2687,11 +2725,11 @@ impl Database { // make sure user is a helper let group = match self.get_group_by_id(user.group).await { Ok(g) => g, - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; if !group.permissions.contains(&Permission::Helper) { - return Err(AuthError::NotAllowed); + return Err(DatabaseError::NotAllowed); } else { let actor_id = user.id.clone(); if let Err(e) = self @@ -2712,7 +2750,7 @@ impl Database { // make sure this ip isn't already banned if self.get_ipban_by_ip(props.ip.clone()).await.is_ok() { - return Err(AuthError::MustBeUnique); + return Err(DatabaseError::MustBeUnique); } // ... @@ -2744,7 +2782,7 @@ impl Database { .await { Ok(_) => return Ok(()), - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; } @@ -2765,11 +2803,11 @@ impl Database { // check permission let group = match self.get_group_by_id(user.group).await { Ok(g) => g, - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; if !group.permissions.contains(&Permission::Manager) { - return Err(AuthError::NotAllowed); + return Err(DatabaseError::NotAllowed); } else { let actor_id = user.id.clone(); if let Err(e) = self @@ -2810,7 +2848,7 @@ impl Database { // return return Ok(()); } - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; } @@ -2908,7 +2946,7 @@ impl Database { .execute(c) .await { - return Err(AuthError::Other); + return Err(DatabaseError::Other); }; relationship.0 = RelationshipStatus::Unknown; // act like it never happened @@ -2948,7 +2986,7 @@ impl Database { .execute(c) .await { - return Err(AuthError::Other); + return Err(DatabaseError::Other); }; } else { // add @@ -2969,7 +3007,7 @@ impl Database { .execute(c) .await { - return Err(AuthError::Other); + return Err(DatabaseError::Other); }; } } @@ -2978,7 +3016,7 @@ impl Database { if utwo.metadata.is_true("sparkler:limited_friend_requests") { // make sure utwo is following uone if let Err(_) = self.get_follow(utwo.id.clone(), uone.id.clone()).await { - return Err(AuthError::NotAllowed); + return Err(DatabaseError::NotAllowed); } } @@ -3000,7 +3038,7 @@ impl Database { .execute(c) .await { - return Err(AuthError::Other); + return Err(DatabaseError::Other); }; // create notification @@ -3020,7 +3058,7 @@ impl Database { ) .await { - return Err(AuthError::Other); + return Err(DatabaseError::Other); }; }; } @@ -3042,7 +3080,7 @@ impl Database { .execute(c) .await { - return Err(AuthError::Other); + return Err(DatabaseError::Other); }; self.base @@ -3072,7 +3110,7 @@ impl Database { ) .await { - return Err(AuthError::Other); + return Err(DatabaseError::Other); }; }; } @@ -3093,7 +3131,7 @@ impl Database { .execute(c) .await { - return Err(AuthError::Other); + return Err(DatabaseError::Other); }; if relationship.0 == RelationshipStatus::Friends { @@ -3156,7 +3194,7 @@ impl Database { Ok(out) } - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), } } @@ -3208,7 +3246,7 @@ impl Database { Ok(out) } - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), } } @@ -3263,7 +3301,7 @@ impl Database { Ok(out) } - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), } } @@ -3320,7 +3358,7 @@ impl Database { Ok(out) } - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), } } @@ -3385,7 +3423,7 @@ impl Database { let c = &self.base.db.client; let res = match sqlquery(&query).bind::<&String>(&id).fetch_one(c).await { Ok(p) => self.base.textify_row(p, Vec::new()).0, - Err(_) => return Err(AuthError::NotFound), + Err(_) => return Err(DatabaseError::NotFound), }; // return @@ -3433,7 +3471,7 @@ impl Database { .await { Ok(p) => self.base.textify_row(p, Vec::new()).0, - Err(_) => return Err(AuthError::NotFound), + Err(_) => return Err(DatabaseError::NotFound), }; // return @@ -3485,7 +3523,7 @@ impl Database { out } - Err(_) => return Err(AuthError::NotFound), + Err(_) => return Err(DatabaseError::NotFound), }; // return @@ -3505,7 +3543,7 @@ impl Database { .await .is_ok() { - return Err(AuthError::MustBeUnique); + return Err(DatabaseError::MustBeUnique); } // ... @@ -3537,7 +3575,7 @@ impl Database { .await { Ok(_) => return Ok(()), - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; } @@ -3558,11 +3596,11 @@ impl Database { // check permission let group = match self.get_group_by_id(user.group).await { Ok(g) => g, - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; if !group.permissions.contains(&Permission::Manager) { - return Err(AuthError::NotAllowed); + return Err(DatabaseError::NotAllowed); } else { let actor_id = user.id.clone(); if let Err(e) = self @@ -3603,7 +3641,7 @@ impl Database { // return return Ok(()); } - Err(_) => return Err(AuthError::Other), + Err(_) => return Err(DatabaseError::Other), }; } } diff --git a/crates/authbeam/src/model.rs b/crates/authbeam/src/model.rs index 09a330a7..bcd5ebe6 100644 --- a/crates/authbeam/src/model.rs +++ b/crates/authbeam/src/model.rs @@ -483,7 +483,7 @@ pub struct IpBlockCreate { /// General API errors #[derive(Debug)] -pub enum AuthError { +pub enum DatabaseError { MustBeUnique, OutOfScope, NotAllowed, @@ -493,9 +493,9 @@ pub enum AuthError { Other, } -impl AuthError { +impl DatabaseError { pub fn to_string(&self) -> String { - use AuthError::*; + use DatabaseError::*; match self { MustBeUnique => String::from("One of the given values must be unique. (MustBeUnique)"), OutOfScope => String::from( @@ -510,9 +510,9 @@ impl AuthError { } } -impl IntoResponse for AuthError { +impl IntoResponse for DatabaseError { fn into_response(self) -> Response { - use crate::model::AuthError::*; + use crate::model::DatabaseError::*; match self { NotAllowed => ( StatusCode::UNAUTHORIZED, diff --git a/crates/rainbeam/Cargo.toml b/crates/rainbeam/Cargo.toml index 311dc313..a4735792 100644 --- a/crates/rainbeam/Cargo.toml +++ b/crates/rainbeam/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rainbeam" -version = "1.14.2" +version = "1.15.0" edition = "2021" authors = ["trisuaso", "swmff"] description = "Ask, share, socialize!" @@ -28,7 +28,6 @@ toml = "0.8.14" tower-http = { version = "0.5.2", features = ["fs", "trace"] } serde_json = "1.0.120" regex = "1.10.5" -mime_guess = "2.0.5" hcaptcha = "2.4.6" ammonia = "4.0.0" futures-util = "0.3.30" @@ -41,6 +40,7 @@ shared = { path = "../shared" } databeam = { path = "../databeam", default-features = false } authbeam = { path = "../authbeam", default-features = false } mimalloc = "0.1.43" +mime_guess = "2.0.5" [[bin]] path = "src/main.rs" diff --git a/crates/rainbeam/src/config.rs b/crates/rainbeam/src/config.rs index bebaad9b..ea0da155 100644 --- a/crates/rainbeam/src/config.rs +++ b/crates/rainbeam/src/config.rs @@ -63,7 +63,7 @@ pub struct Config { /// If new profile registration is enabled #[serde(default)] pub registration_enabled: bool, - /// The origin of the public server (ex: "") + /// The origin of the public server (ex: "https://rainbeam.net") /// /// Used in embeds and links. #[serde(default)] diff --git a/crates/rainbeam/src/database.rs b/crates/rainbeam/src/database.rs index 0102c6bb..d37ca424 100644 --- a/crates/rainbeam/src/database.rs +++ b/crates/rainbeam/src/database.rs @@ -334,20 +334,8 @@ impl Database { /// Create a moderator audit log entry pub async fn audit(&self, actor_id: String, content: String) -> Result<()> { - match self - .auth - .create_notification( - NotificationCreate { - title: format!("[{actor_id}](/+u/{actor_id})"), - content, - address: format!("/+u/{actor_id}"), - recipient: "*(audit)".to_string(), // all staff, audit - }, - None, - ) - .await - { - Ok(_) => Ok(()), + match self.auth.audit(actor_id, content).await { + Ok(r) => Ok(r), Err(_) => Err(DatabaseError::Other), } } diff --git a/crates/rainbeam/src/main.rs b/crates/rainbeam/src/main.rs index ec2ef25e..5498c818 100644 --- a/crates/rainbeam/src/main.rs +++ b/crates/rainbeam/src/main.rs @@ -48,6 +48,9 @@ pub async fn main() { captcha: config.captcha.clone(), registration_enabled: config.registration_enabled, real_ip_header: config.real_ip_header.clone(), + static_dir: config.static_dir.clone(), + host: config.host.clone(), + blocked_hosts: config.blocked_hosts.clone(), }, ) .await; diff --git a/crates/rainbeam/src/model.rs b/crates/rainbeam/src/model.rs index 1e1d4b9e..b311a2e8 100644 --- a/crates/rainbeam/src/model.rs +++ b/crates/rainbeam/src/model.rs @@ -662,8 +662,8 @@ impl Into> for DatabaseError { } } -impl From for DatabaseError { - fn from(_: authbeam::model::AuthError) -> Self { +impl From for DatabaseError { + fn from(_: authbeam::model::DatabaseError) -> Self { Self::Other } } diff --git a/crates/rainbeam/src/routing/api/circles.rs b/crates/rainbeam/src/routing/api/circles.rs index 13c17d94..e365abe9 100644 --- a/crates/rainbeam/src/routing/api/circles.rs +++ b/crates/rainbeam/src/routing/api/circles.rs @@ -13,7 +13,7 @@ use axum::{ Json, Router, }; -use super::profiles::read_image; +use authbeam::api::profile::read_image; use axum_extra::extract::cookie::CookieJar; pub fn routes(database: Database) -> Router { diff --git a/crates/rainbeam/src/routing/api/profiles.rs b/crates/rainbeam/src/routing/api/profiles.rs index db3010e4..9d2f44d9 100644 --- a/crates/rainbeam/src/routing/api/profiles.rs +++ b/crates/rainbeam/src/routing/api/profiles.rs @@ -1,50 +1,28 @@ use crate::database::Database; -use crate::model::{DataExportOptions, DatabaseError, RelationshipStatus}; +use crate::model::{DataExportOptions, DatabaseError}; use axum::extract::Query; use axum::http::{HeaderMap, HeaderValue}; use axum_extra::extract::CookieJar; use hcaptcha::Hcaptcha; -use std::{fs::File, io::Read}; -use authbeam::model::{NotificationCreate, Permission, SetProfileGroup, UserFollow}; +use authbeam::model::{NotificationCreate, Permission}; use databeam::DefaultReturn; use axum::{ - body::Body, extract::{Path, State}, response::{IntoResponse, Redirect}, - routing::{delete, get, post}, + routing::{get, post}, Json, Router, }; pub fn routes(database: Database) -> Router { Router::new() - .route("/:username/avatar", get(avatar_request)) - .route("/:username/banner", get(banner_request)) .route("/:username/report", post(report_request)) - .route("/:username/follow", post(follow_request)) .route("/:username/export", get(export_request)) // staff - .route("/:username/group", post(change_group_request)) // staff - .route("/:username/relationship/friend", post(friend_request)) - .route("/:username/relationship/block", post(block_request)) - .route("/:username/relationship", delete(breakup_request)) // ... .with_state(database) } -pub fn read_image(static_dir: String, image: String) -> Vec { - let mut bytes = Vec::new(); - - for byte in File::open(format!("{static_dir}/images/{image}",)) - .unwrap() - .bytes() - { - bytes.push(byte.unwrap()) - } - - bytes -} - // routes /// Redirect an ID to a full username @@ -87,208 +65,6 @@ pub async fn expand_ip_request( } } -/// Get a profile's avatar image -pub async fn avatar_request( - Path(username): Path, - State(database): State, -) -> impl IntoResponse { - // get user - let auth_user = match database.auth.get_profile_by_username(username).await { - Ok(ua) => ua, - Err(_) => { - return ( - [("Content-Type", "image/svg+xml")], - Body::from(read_image( - database.server_options.static_dir, - "default-avatar.svg".to_string(), - )), - ); - } - }; - - // ... - let avatar_url = match auth_user.metadata.kv.get("sparkler:avatar_url") { - Some(r) => r, - None => "", - }; - - if avatar_url.starts_with(&database.server_options.host) { - return ( - [("Content-Type", "image/svg+xml")], - Body::from(read_image( - database.server_options.static_dir, - "default-avatar.svg".to_string(), - )), - ); - } - - for host in database.server_options.blocked_hosts { - if avatar_url.starts_with(&host) { - return ( - [("Content-Type", "image/svg+xml")], - Body::from(read_image( - database.server_options.static_dir, - "default-avatar.svg".to_string(), - )), - ); - } - } - - // get profile image - if avatar_url.is_empty() { - return ( - [("Content-Type", "image/svg+xml")], - Body::from(read_image( - database.server_options.static_dir, - "default-avatar.svg".to_string(), - )), - ); - } - - let guessed_mime = mime_guess::from_path(avatar_url) - .first_raw() - .unwrap_or("application/octet-stream"); - - match database.auth.http.get(avatar_url).send().await { - Ok(stream) => { - if let Some(ct) = stream.headers().get("Content-Type") { - if !ct.to_str().unwrap().starts_with("image/") { - // if we failed to load the image, we might get back text/html or something - // we're going to return the default image if we got something that isn't - // an image (or has an incorrect mime) - return ( - [("Content-Type", "image/svg+xml")], - Body::from(read_image( - database.server_options.static_dir, - "default-avatar.svg".to_string(), - )), - ); - } - } - - ( - [( - "Content-Type", - if guessed_mime == "text/html" { - "text/plain" - } else { - guessed_mime - }, - )], - Body::from_stream(stream.bytes_stream()), - ) - } - Err(_) => ( - [("Content-Type", "image/svg+xml")], - Body::from(read_image( - database.server_options.static_dir, - "default-avatar.svg".to_string(), - )), - ), - } -} - -/// Get a profile's banner image -pub async fn banner_request( - Path(username): Path, - State(database): State, -) -> impl IntoResponse { - // get user - let auth_user = match database.auth.get_profile_by_username(username).await { - Ok(ua) => ua, - Err(_) => { - return ( - [("Content-Type", "image/svg+xml")], - Body::from(read_image( - database.server_options.static_dir, - "default-banner.svg".to_string(), - )), - ); - } - }; - - // ... - let banner_url = match auth_user.metadata.kv.get("sparkler:banner_url") { - Some(r) => r, - None => "", - }; - - if banner_url.starts_with(&database.server_options.host) { - return ( - [("Content-Type", "image/svg+xml")], - Body::from(read_image( - database.server_options.static_dir, - "default-banner.svg".to_string(), - )), - ); - } - - for host in database.server_options.blocked_hosts { - if banner_url.starts_with(&host) { - return ( - [("Content-Type", "image/svg+xml")], - Body::from(read_image( - database.server_options.static_dir, - "default-banner.svg".to_string(), - )), - ); - } - } - - // get profile image - if banner_url.is_empty() { - return ( - [("Content-Type", "image/svg+xml")], - Body::from(read_image( - database.server_options.static_dir, - "default-banner.svg".to_string(), - )), - ); - } - - let guessed_mime = mime_guess::from_path(banner_url) - .first_raw() - .unwrap_or("application/octet-stream"); - - match database.auth.http.get(banner_url).send().await { - Ok(stream) => { - if let Some(ct) = stream.headers().get("Content-Type") { - if !ct.to_str().unwrap().starts_with("image/") { - // if we failed to load the image, we might get back text/html or something - // we're going to return the default image if we got something that isn't - // an image (or has an incorrect mime) - return ( - [("Content-Type", "image/svg+xml")], - Body::from(read_image( - database.server_options.static_dir, - "default-banner.svg".to_string(), - )), - ); - } - } - - ( - [( - "Content-Type", - if guessed_mime == "text/html" { - "text/plain" - } else { - guessed_mime - }, - )], - Body::from_stream(stream.bytes_stream()), - ) - } - Err(_) => ( - [("Content-Type", "image/svg+xml")], - Body::from(read_image( - database.server_options.static_dir, - "default-banner.svg".to_string(), - )), - ), - } -} - /// Report a user profile pub async fn report_request( headers: HeaderMap, @@ -370,90 +146,6 @@ pub async fn report_request( } } -/// Toggle following on the given user -pub async fn follow_request( - jar: CookieJar, - Path(username): Path, - State(database): State, -) -> impl IntoResponse { - // get user from token - let auth_user = match jar.get("__Secure-Token") { - Some(c) => match database - .auth - .get_profile_by_unhashed(c.value_trimmed().to_string()) - .await - { - Ok(ua) => ua, - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }); - } - }, - None => { - return Json(DefaultReturn { - success: false, - message: DatabaseError::NotAllowed.to_string(), - payload: (), - }); - } - }; - - // check block status - let attempting_to_follow = match database - .auth - .get_profile_by_username(username.to_owned()) - .await - { - Ok(ua) => ua, - Err(_) => { - return Json(DefaultReturn { - success: false, - message: DatabaseError::NotFound.to_string(), - payload: (), - }) - } - }; - - let relationship = database - .auth - .get_user_relationship(attempting_to_follow.id.clone(), auth_user.id.clone()) - .await - .0; - - if relationship == RelationshipStatus::Blocked { - // blocked users cannot follow the people who blocked them! - return Json(DefaultReturn { - success: false, - message: DatabaseError::NotAllowed.to_string(), - payload: (), - }); - } - - // return - match database - .auth - .toggle_user_follow(&mut UserFollow { - user: auth_user.id, - following: attempting_to_follow.id, - }) - .await - { - Ok(_) => Json(DefaultReturn { - success: true, - message: "Acceptable".to_string(), - payload: (), - }), - Err(e) => Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: (), - }), - } -} - /// Create a data export of the given user pub async fn export_request( jar: CookieJar, @@ -539,488 +231,3 @@ pub async fn export_request( } } } - -/// Change a profile's group -pub async fn change_group_request( - jar: CookieJar, - Path(username): Path, - State(database): State, - Json(props): Json, -) -> impl IntoResponse { - // get user from token - let auth_user = match jar.get("__Secure-Token") { - Some(c) => match database - .auth - .get_profile_by_unhashed(c.value_trimmed().to_string()) - .await - { - Ok(ua) => ua, - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: None, - }); - } - }, - None => { - return Json(DefaultReturn { - success: false, - message: DatabaseError::NotAllowed.to_string(), - payload: None, - }); - } - }; - - // check permission - let group = match database.auth.get_group_by_id(auth_user.group).await { - Ok(g) => g, - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: None, - }) - } - }; - - if !group.permissions.contains(&Permission::Manager) { - // we must have the "Manager" permission to edit other users - return Json(DefaultReturn { - success: false, - message: DatabaseError::NotAllowed.to_string(), - payload: None, - }); - } - - // get other user - let other_user = match database.get_profile(username.clone()).await { - Ok(ua) => ua, - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: None, - }); - } - }; - - // check permission - let group = match database.auth.get_group_by_id(other_user.group).await { - Ok(g) => g, - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: None, - }) - } - }; - - if group.permissions.contains(&Permission::Manager) { - // we cannot manager other managers - return Json(DefaultReturn { - success: false, - message: DatabaseError::NotAllowed.to_string(), - payload: None, - }); - } - - // push update - // TODO: try not to clone - if let Err(e) = database - .auth - .edit_profile_group(username, props.group) - .await - { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: None, - }); - } - - // return - if let Err(e) = database - .audit( - auth_user.id, - format!( - "Changed user group: [{}](/+u/{})", - other_user.id, other_user.id - ), - ) - .await - { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: None, - }); - }; - - Json(DefaultReturn { - success: true, - message: "Acceptable".to_string(), - payload: Some(props.group), - }) -} - -/// Send/accept a friend request to/from another user -pub async fn friend_request( - jar: CookieJar, - Path(username): Path, - State(database): State, -) -> impl IntoResponse { - // get user from token - let auth_user = match jar.get("__Secure-Token") { - Some(c) => match database - .auth - .get_profile_by_unhashed(c.value_trimmed().to_string()) - .await - { - Ok(ua) => ua, - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: None, - }); - } - }, - None => { - return Json(DefaultReturn { - success: false, - message: DatabaseError::NotAllowed.to_string(), - payload: None, - }); - } - }; - - // ... - let other_user = match database - .auth - .get_profile_by_username(username.to_owned()) - .await - { - Ok(ua) => ua, - Err(_) => { - return Json(DefaultReturn { - success: false, - message: DatabaseError::NotFound.to_string(), - payload: None, - }) - } - }; - - // get current relationship - let current = database - .auth - .get_user_relationship(auth_user.id.clone(), other_user.id.clone()) - .await; - - if current.0 == RelationshipStatus::Blocked && auth_user.id != current.1 { - // cannot change relationship if we're blocked and we aren't the user that did the blocking - return Json(DefaultReturn { - success: false, - message: DatabaseError::NotAllowed.to_string(), - payload: None, - }); - } - - let current = current.0; - - // return - if current == RelationshipStatus::Unknown { - // send request - match database - .auth - .set_user_relationship_status( - auth_user.id, - other_user.id, - RelationshipStatus::Pending, - false, - ) - .await - { - Ok(export) => { - return Json(DefaultReturn { - success: true, - message: "Friend request sent!".to_string(), - payload: Some(export), - }) - } - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: None, - }) - } - } - } else if current == RelationshipStatus::Pending { - // accept request - match database - .auth - .set_user_relationship_status( - auth_user.id, - other_user.id, - RelationshipStatus::Friends, - false, - ) - .await - { - Ok(export) => { - return Json(DefaultReturn { - success: true, - message: "Friend request accepted!".to_string(), - payload: Some(export), - }) - } - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: None, - }) - } - } - } else { - // no clue, remove friendship? - match database - .auth - .set_user_relationship_status( - auth_user.id, - other_user.id, - RelationshipStatus::Unknown, - false, - ) - .await - { - Ok(export) => { - return Json(DefaultReturn { - success: true, - message: "Friendship removed".to_string(), - payload: Some(export), - }) - } - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: None, - }) - } - } - } -} - -/// Block another user -pub async fn block_request( - jar: CookieJar, - Path(username): Path, - State(database): State, -) -> impl IntoResponse { - // get user from token - let auth_user = match jar.get("__Secure-Token") { - Some(c) => match database - .auth - .get_profile_by_unhashed(c.value_trimmed().to_string()) - .await - { - Ok(ua) => ua, - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: None, - }); - } - }, - None => { - return Json(DefaultReturn { - success: false, - message: DatabaseError::NotAllowed.to_string(), - payload: None, - }); - } - }; - - // ... - let other_user = match database - .auth - .get_profile_by_username(username.to_owned()) - .await - { - Ok(ua) => ua, - Err(_) => { - return Json(DefaultReturn { - success: false, - message: DatabaseError::NotFound.to_string(), - payload: None, - }) - } - }; - - // get current relationship - let current = database - .auth - .get_user_relationship(auth_user.id.clone(), other_user.id.clone()) - .await; - - if current.0 == RelationshipStatus::Blocked && auth_user.id != current.1 { - // cannot change relationship if we're blocked and we aren't the user that did the blocking - return Json(DefaultReturn { - success: false, - message: DatabaseError::NotAllowed.to_string(), - payload: None, - }); - } - - // force unfollow - if let Err(e) = database - .auth - .force_remove_user_follow(&mut UserFollow { - user: auth_user.id.clone(), - following: other_user.id.clone(), - }) - .await - { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: None, - }); - } - - if let Err(e) = database - .auth - .force_remove_user_follow(&mut UserFollow { - user: other_user.id.clone(), - following: auth_user.id.clone(), - }) - .await - { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: None, - }); - } - - // return - match database - .auth - .set_user_relationship_status( - auth_user.id, - other_user.id, - RelationshipStatus::Blocked, - false, - ) - .await - { - Ok(export) => { - return Json(DefaultReturn { - success: true, - message: "User blocked!".to_string(), - payload: Some(export), - }) - } - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: None, - }) - } - } -} - -/// Remove relationship with another user -pub async fn breakup_request( - jar: CookieJar, - Path(username): Path, - State(database): State, -) -> impl IntoResponse { - // get user from token - let auth_user = match jar.get("__Secure-Token") { - Some(c) => match database - .auth - .get_profile_by_unhashed(c.value_trimmed().to_string()) - .await - { - Ok(ua) => ua, - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: None, - }); - } - }, - None => { - return Json(DefaultReturn { - success: false, - message: DatabaseError::NotAllowed.to_string(), - payload: None, - }); - } - }; - - // ... - let other_user = match database - .auth - .get_profile_by_username(username.to_owned()) - .await - { - Ok(ua) => ua, - Err(_) => { - return Json(DefaultReturn { - success: false, - message: DatabaseError::NotFound.to_string(), - payload: None, - }) - } - }; - - // get current relationship - let current = database - .auth - .get_user_relationship(auth_user.id.clone(), other_user.id.clone()) - .await; - - if current.0 == RelationshipStatus::Blocked && auth_user.id != current.1 { - // cannot remove relationship if we're blocked and we aren't the user that did the blocking - return Json(DefaultReturn { - success: false, - message: DatabaseError::NotAllowed.to_string(), - payload: None, - }); - } - - // return - match database - .auth - .set_user_relationship_status( - auth_user.id, - other_user.id, - RelationshipStatus::Unknown, - false, - ) - .await - { - Ok(export) => { - return Json(DefaultReturn { - success: true, - message: "Relationship removed!".to_string(), - payload: Some(export), - }) - } - Err(e) => { - return Json(DefaultReturn { - success: false, - message: e.to_string(), - payload: None, - }) - } - } -} diff --git a/crates/rainbeam/src/routing/api/util.rs b/crates/rainbeam/src/routing/api/util.rs index 6603849a..c79fbcff 100644 --- a/crates/rainbeam/src/routing/api/util.rs +++ b/crates/rainbeam/src/routing/api/util.rs @@ -1,4 +1,4 @@ -use super::profiles::read_image; +use authbeam::api::profile::read_image; use crate::database::Database; use askama_axum::IntoResponse; use axum::{ diff --git a/crates/rainbeam/src/routing/pages/mod.rs b/crates/rainbeam/src/routing/pages/mod.rs index 02e8991f..384fed63 100644 --- a/crates/rainbeam/src/routing/pages/mod.rs +++ b/crates/rainbeam/src/routing/pages/mod.rs @@ -1924,8 +1924,6 @@ pub async fn ipbans_request(jar: CookieJar, State(database): State) -> struct ReportTemplate { config: Config, profile: Option, - unread: usize, - notifs: usize, } /// GET /intents/report @@ -1942,30 +1940,10 @@ pub async fn report_request(jar: CookieJar, State(database): State) -> None => None, }; - let unread = if let Some(ref ua) = auth_user { - match database.get_questions_by_recipient(ua.id.to_owned()).await { - Ok(unread) => unread.len(), - Err(_) => 0, - } - } else { - 0 - }; - - let notifs = if let Some(ref ua) = auth_user { - database - .auth - .get_notification_count_by_recipient(ua.id.to_owned()) - .await - } else { - 0 - }; - Html( ReportTemplate { config: database.server_options.clone(), profile: auth_user, - unread, - notifs, } .render() .unwrap(), diff --git a/crates/rainbeam/static/style.css b/crates/rainbeam/static/style.css index 977e2a82..1e7be9d9 100644 --- a/crates/rainbeam/static/style.css +++ b/crates/rainbeam/static/style.css @@ -331,28 +331,36 @@ a.button.secondary:hover { background: var(--color-raised); } -#backtotop { +button.floating, +.button.floating { position: fixed; bottom: 0.5rem; left: 0.5rem; border-radius: var(--circle); width: 48px !important; - height: 48px !important; transition: all 0.15s; opacity: 0%; transform: scale(0); padding: 0 !important; + aspect-ratio: 1 / 1; + z-index: 3; +} + +button.floating.right, +.button.floating.right { + left: unset; + right: 0.5rem; } -#backtotop svg { +button.floating svg, +.button.floating svg { margin: 0; } -@media screen and (max-width: 900px) { - #backtotop:is(html[data-scroll-500="true"] *) { - opacity: 100%; - transform: scale(1); - } +button.floating:is(html[data-scroll-500="true"] *), +.button.floating:is(html[data-scroll-500="true"] *) { + opacity: 100%; + transform: scale(1); } .card button.secondary, @@ -1779,20 +1787,6 @@ table ol { margin-bottom: 0; } -#cover { - position: fixed; - top: 0; - left: 0; - background: black; - opacity: 0%; - pointer-events: none; - user-select: none; - transition: opacity 0.15s; - width: 100%; - height: 100%; - z-index: 2; -} - /* app */ html:not(.legacy), body:not(.legacy *) { @@ -1822,7 +1816,7 @@ body:not(.legacy *) { } .dark:not(.legacy), -.dark:not(.legacy) * { +.dark:not(.legacy *) * { --hue: 0; --sat: 0%; --lit: 10%; @@ -1878,10 +1872,6 @@ nav:not(.legacy *) a.button.title:hover::after { box-shadow: 0 0 8px 2px hsla(var(--color-primary-hsl), 25%); } -body:not(.legacy *):has(a.button.title:hover) #cover { - opacity: 25%; -} - .dropdown:not(.legacy *) .inner { padding: 0.25rem; } @@ -1936,10 +1926,15 @@ body:not(.legacy *):has(a.button.title:hover) #cover { @media screen and (max-width: 900px) { nav:not(.legacy *) { - background: var(--color-raised); - border-bottom: unset; - border-top: solid 1px var(--color-super-lowered); padding: 0.5rem 0.25rem; + margin-bottom: 0; + backdrop-filter: none; + border-bottom: solid 1px var(--color-super-lowered); + } + + nav:not(.legacy *):not([data-scroll="0"] *), + nav:not(.legacy *):is(html:not([data-scroll]) *) { + background: var(--color-raised); } nav:not(.legacy *) button, @@ -1973,32 +1968,6 @@ body:not(.legacy *):has(a.button.title:hover) #cover { display: contents; } - /* bottom nav */ - nav:not(.legacy *) { - position: fixed; - top: unset; - bottom: 0; - margin-bottom: 0 !important; - } - - nav:not(.legacy *) .dropdown .inner { - top: unset; - bottom: calc(100% + 5px); - } - - nav:not(.legacy *) .nav_side { - display: contents; - } - - article:not(.legacy *) { - margin-top: 0; - margin-bottom: 5rem; - } - - #backtotop:not(.legacy *) { - bottom: calc(0.5rem + 65px); - } - /* chat page fixes */ body:has(#is_chat_page) article:not(.legacy *) { margin-top: 0 !important; @@ -2031,11 +2000,17 @@ body:not(.legacy *):has(a.button.title:hover) #cover { } .pillmenu:not(.legacy *) { - top: 0; + top: 65px; position: sticky; z-index: 1; } + .pillmenu:not(.legacy *):not([data-scroll="0"] *), + .pillmenu:not(.legacy *):is(html:not([data-scroll]) *) { + border-bottom: solid 1px var(--color-super-lowered); + box-shadow: 0 0 4px var(--color-shadow); + } + /* toast to snackbar */ .toast:not(.legacy *) { width: 100% !important; diff --git a/crates/rainbeam/templates/base.html b/crates/rainbeam/templates/base.html index 69da6b6e..89aa3211 100644 --- a/crates/rainbeam/templates/base.html +++ b/crates/rainbeam/templates/base.html @@ -96,7 +96,6 @@
- {% if let Some(user) = profile %} {% if user.group == -1 %}
{% if let Some(profile) = profile %} @{{ member.username }} @{{ user.username }} @{{ user.username }} ${await ( await fetch(`/@${username}/_app/card.html`) - ).text(); + ).text()}
`; root.innerHTML += ``; // adoptedStyleSheets is so stupid, this is much easier + root.children[0].setAttribute( + "class", + document.documentElement.getAttribute("class"), + ); // steal dialog and put it in the root document so it works const dialog = root.querySelector("dialog"); diff --git a/crates/rainbeam/templates/components/global_question.html b/crates/rainbeam/templates/components/global_question.html index 0d0d036a..c856a7a4 100644 --- a/crates/rainbeam/templates/components/global_question.html +++ b/crates/rainbeam/templates/components/global_question.html @@ -15,7 +15,7 @@ > + + + + Home + +{% endblock %} {% block content %}
diff --git a/crates/rainbeam/templates/intents/report.html b/crates/rainbeam/templates/intents/report.html index 4aac18a0..8f9fdd10 100644 --- a/crates/rainbeam/templates/intents/report.html +++ b/crates/rainbeam/templates/intents/report.html @@ -1,8 +1,8 @@ {% extends "base.html" %} {% block title %}{{ config.name }}{% endblock %} {% block head %} -{% endblock %} {% block nav_left %} {% if profile.is_some() %} - +{% endblock %} {% block nav_left %} + - Timeline + Home - - - - - - Inbox {% if unread != 0 %} - {{ unread }} - {% endif %} - -{% endif %} {% endblock %} {% block nav_right %} {% if profile.is_some() %} - - - - - {% if notifs != 0 %} - {{ notifs }} - {% endif %} - -{% endif %} {% endblock %} {% block content %} +{% endblock %} {% block content %}
@@ -67,21 +67,6 @@ {% endif %} {% endblock %} {% block nav_right %} {% if profile.is_some() %} - - {% endif %} {% endblock %} {% block content %}
+ +
{% if let Some(fit) = other.metadata.kv.get("sparkler:banner_fit") %} @@ -404,7 +404,7 @@

{{ othe