diff --git a/.woodpecker/build-and-publish.yml b/.woodpecker/build-and-publish.yml index 57f2761..3643953 100644 --- a/.woodpecker/build-and-publish.yml +++ b/.woodpecker/build-and-publish.yml @@ -21,10 +21,6 @@ steps: - name: container-build-and-publish image: docker commands: - - docker login --username radical --password $PASSWORD git.gorb.app - - docker buildx build --platform linux/amd64,linux/arm64 --rm --push -t git.gorb.app/gorb/backend:main . - environment: - PASSWORD: - from_secret: docker_password + - docker buildx build --platform linux/amd64,linux/arm64 --rm -t gorb/backend:main . volumes: - /var/run/podman/podman.sock:/var/run/docker.sock diff --git a/.woodpecker/publish-docs.yml b/.woodpecker/publish-docs.yml deleted file mode 100644 index e6ce482..0000000 --- a/.woodpecker/publish-docs.yml +++ /dev/null @@ -1,19 +0,0 @@ -when: - - event: push - branch: main - -steps: - - name: build-docs - image: rust:bookworm - commands: - - cargo doc --release --no-deps - - - name: publish-docs - image: debian:12 - commands: - - apt update -y && apt install -y rsync openssh-client - - printf "Host *\n StrictHostKeyChecking no" >> /etc/ssh/ssh_config - - ssh-agent bash -c "ssh-add <(echo '$KEY' | base64 -d) && rsync --archive --verbose --compress --hard-links --delete-during --partial --progress ./target/doc/ root@gorb.app:/var/www/docs.gorb.app/api && ssh root@gorb.app systemctl reload caddy.service" - environment: - KEY: - from_secret: ssh_key diff --git a/Cargo.toml b/Cargo.toml index 1c5f34b..492a284 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,12 +8,6 @@ strip = true lto = true codegen-units = 1 -# Speed up compilation to make dev bearable -[profile.dev] -debug = 0 -strip = "debuginfo" -codegen-units = 512 - [dependencies] actix-cors = "0.7.1" actix-web = "4.11" @@ -27,23 +21,23 @@ regex = "1.11" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" simple_logger = "5.0.0" -redis = { version = "0.32", features= ["tokio-comp"] } -tokio-tungstenite = { version = "0.27", features = ["native-tls", "url"] } +redis = { version = "0.31.0", features= ["tokio-comp"] } +tokio-tungstenite = { version = "0.26", features = ["native-tls", "url"] } toml = "0.8" url = { version = "2.5", features = ["serde"] } uuid = { version = "1.17", features = ["serde", "v7"] } random-string = "1.1" actix-ws = "0.3.0" futures-util = "0.3.31" -bunny-api-tokio = { version = "0.4", features = ["edge_storage"], default-features = false } +bunny-api-tokio = "0.3.0" bindet = "0.3.2" deadpool = "0.12" -diesel = { version = "2.2", features = ["uuid", "chrono"], default-features = false } +diesel = { version = "2.2", features = ["uuid", "chrono"] } diesel-async = { version = "0.5", features = ["deadpool", "postgres", "async-connection-wrapper"] } diesel_migrations = { version = "2.2.0", features = ["postgres"] } thiserror = "2.0.12" actix-multipart = "0.7.2" -lettre = { version = "0.11", features = ["tokio1", "tokio1-native-tls"] } +lettre = { version = "0.11.16", features = ["tokio1", "tokio1-native-tls"] } chrono = { version = "0.4.41", features = ["serde"] } [dependencies.tokio] diff --git a/migrations/2025-06-01-143713_add_about_to_users/down.sql b/migrations/2025-06-01-143713_add_about_to_users/down.sql deleted file mode 100644 index de48d07..0000000 --- a/migrations/2025-06-01-143713_add_about_to_users/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE users DROP COLUMN about; \ No newline at end of file diff --git a/migrations/2025-06-01-143713_add_about_to_users/up.sql b/migrations/2025-06-01-143713_add_about_to_users/up.sql deleted file mode 100644 index 54b5449..0000000 --- a/migrations/2025-06-01-143713_add_about_to_users/up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Your SQL goes here -ALTER TABLE users ADD COLUMN about VARCHAR(200) DEFAULT NULL; diff --git a/migrations/2025-06-03-103311_remove_email_tokens/down.sql b/migrations/2025-06-03-103311_remove_email_tokens/down.sql deleted file mode 100644 index e8f0350..0000000 --- a/migrations/2025-06-03-103311_remove_email_tokens/down.sql +++ /dev/null @@ -1,7 +0,0 @@ --- This file should undo anything in `up.sql` -CREATE TABLE email_tokens ( - token VARCHAR(64) NOT NULL, - user_uuid uuid UNIQUE NOT NULL REFERENCES users(uuid), - created_at TIMESTAMPTZ NOT NULL, - PRIMARY KEY (token, user_uuid) -); diff --git a/migrations/2025-06-03-103311_remove_email_tokens/up.sql b/migrations/2025-06-03-103311_remove_email_tokens/up.sql deleted file mode 100644 index b41afe5..0000000 --- a/migrations/2025-06-03-103311_remove_email_tokens/up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Your SQL goes here -DROP TABLE email_tokens; diff --git a/migrations/2025-06-03-110142_remove_password_reset_tokens/down.sql b/migrations/2025-06-03-110142_remove_password_reset_tokens/down.sql deleted file mode 100644 index 009d9e4..0000000 --- a/migrations/2025-06-03-110142_remove_password_reset_tokens/down.sql +++ /dev/null @@ -1,7 +0,0 @@ --- This file should undo anything in `up.sql` -CREATE TABLE password_reset_tokens ( - token VARCHAR(64) NOT NULL, - user_uuid uuid UNIQUE NOT NULL REFERENCES users(uuid), - created_at TIMESTAMPTZ NOT NULL, - PRIMARY KEY (token, user_uuid) -); diff --git a/migrations/2025-06-03-110142_remove_password_reset_tokens/up.sql b/migrations/2025-06-03-110142_remove_password_reset_tokens/up.sql deleted file mode 100644 index 181d7c5..0000000 --- a/migrations/2025-06-03-110142_remove_password_reset_tokens/up.sql +++ /dev/null @@ -1,2 +0,0 @@ --- Your SQL goes here -DROP TABLE password_reset_tokens; diff --git a/migrations/2025-06-06-145916_guild_ownership_changes/down.sql b/migrations/2025-06-06-145916_guild_ownership_changes/down.sql deleted file mode 100644 index 21a08c9..0000000 --- a/migrations/2025-06-06-145916_guild_ownership_changes/down.sql +++ /dev/null @@ -1,14 +0,0 @@ --- This file should undo anything in `up.sql` -ALTER TABLE guilds -ADD COLUMN owner_uuid UUID REFERENCES users(uuid); - -UPDATE guilds g -SET owner_uuid = gm.user_uuid -FROM guild_members gm -WHERE gm.guild_uuid = g.uuid AND gm.is_owner = TRUE; - -ALTER TABLE guilds -ALTER COLUMN owner_uuid SET NOT NULL; - -ALTER TABLE guild_members -DROP COLUMN is_owner; diff --git a/migrations/2025-06-06-145916_guild_ownership_changes/up.sql b/migrations/2025-06-06-145916_guild_ownership_changes/up.sql deleted file mode 100644 index b94323f..0000000 --- a/migrations/2025-06-06-145916_guild_ownership_changes/up.sql +++ /dev/null @@ -1,14 +0,0 @@ --- Your SQL goes here -ALTER TABLE guild_members -ADD COLUMN is_owner BOOLEAN NOT NULL DEFAULT false; - -UPDATE guild_members gm -SET is_owner = true -FROM guilds g -WHERE gm.guild_uuid = g.uuid AND gm.user_uuid = g.owner_uuid; - -CREATE UNIQUE INDEX one_owner_per_guild ON guild_members (guild_uuid) -WHERE is_owner; - -ALTER TABLE guilds -DROP COLUMN owner_uuid; diff --git a/src/api/v1/auth/login.rs b/src/api/v1/auth/login.rs index ac6c1ad..e190c2f 100644 --- a/src/api/v1/auth/login.rs +++ b/src/api/v1/auth/login.rs @@ -11,7 +11,7 @@ use crate::{ error::Error, schema::*, utils::{ - PASSWORD_REGEX, generate_token, new_refresh_token_cookie, + PASSWORD_REGEX, generate_access_token, generate_refresh_token, new_refresh_token_cookie, user_uuid_from_identifier, }, }; @@ -59,8 +59,8 @@ pub async fn response( )); } - let refresh_token = generate_token::<32>()?; - let access_token = generate_token::<16>()?; + let refresh_token = generate_refresh_token()?; + let access_token = generate_access_token()?; let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; diff --git a/src/api/v1/auth/refresh.rs b/src/api/v1/auth/refresh.rs index 1f4f406..63e150e 100644 --- a/src/api/v1/auth/refresh.rs +++ b/src/api/v1/auth/refresh.rs @@ -11,7 +11,7 @@ use crate::{ access_tokens::{self, dsl}, refresh_tokens::{self, dsl as rdsl}, }, - utils::{generate_token, new_refresh_token_cookie}, + utils::{generate_access_token, generate_refresh_token, new_refresh_token_cookie}, }; use super::Response; @@ -55,7 +55,7 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result 1987200 { - let new_refresh_token = generate_token::<32>()?; + let new_refresh_token = generate_refresh_token()?; match update(refresh_tokens::table) .filter(rdsl::token.eq(&refresh_token)) @@ -75,7 +75,7 @@ pub async fn res(req: HttpRequest, data: web::Data) -> Result()?; + let access_token = generate_access_token()?; update(access_tokens::table) .filter(dsl::refresh_token.eq(&refresh_token)) diff --git a/src/api/v1/auth/register.rs b/src/api/v1/auth/register.rs index 1d28088..66e2989 100644 --- a/src/api/v1/auth/register.rs +++ b/src/api/v1/auth/register.rs @@ -20,7 +20,7 @@ use crate::{ users::{self, dsl as udsl}, }, utils::{ - EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX, generate_token, + EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX, generate_access_token, generate_refresh_token, new_refresh_token_cookie, }, }; @@ -120,8 +120,8 @@ pub async fn res( .execute(&mut conn) .await?; - let refresh_token = generate_token::<32>()?; - let access_token = generate_token::<16>()?; + let refresh_token = generate_refresh_token()?; + let access_token = generate_access_token()?; let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; diff --git a/src/api/v1/auth/reset_password.rs b/src/api/v1/auth/reset_password.rs index 444266c..8240fbd 100644 --- a/src/api/v1/auth/reset_password.rs +++ b/src/api/v1/auth/reset_password.rs @@ -4,7 +4,7 @@ use actix_web::{HttpResponse, get, post, web}; use chrono::{Duration, Utc}; use serde::Deserialize; -use crate::{Data, error::Error, objects::PasswordResetToken}; +use crate::{Data, error::Error, structs::PasswordResetToken}; #[derive(Deserialize)] struct Query { @@ -26,11 +26,13 @@ struct Query { /// #[get("/reset-password")] pub async fn get(query: web::Query, data: web::Data) -> Result { + let mut conn = data.pool.get().await?; + if let Ok(password_reset_token) = - PasswordResetToken::get_with_identifier(&data, query.identifier.clone()).await + PasswordResetToken::get_with_identifier(&mut conn, query.identifier.clone()).await { if Utc::now().signed_duration_since(password_reset_token.created_at) > Duration::hours(1) { - password_reset_token.delete(&data).await?; + password_reset_token.delete(&mut conn).await?; } else { return Err(Error::TooManyRequests( "Please allow 1 hour before sending a new email".to_string(), @@ -72,8 +74,15 @@ pub async fn post( reset_password: web::Json, data: web::Data, ) -> Result { + let mut conn = data.pool.get().await?; + let password_reset_token = - PasswordResetToken::get(&data, reset_password.token.clone()).await?; + PasswordResetToken::get(&mut conn, reset_password.token.clone()).await?; + + if Utc::now().signed_duration_since(password_reset_token.created_at) > Duration::hours(24) { + password_reset_token.delete(&mut conn).await?; + return Ok(HttpResponse::Gone().finish()); + } password_reset_token .set_password(&data, reset_password.password.clone()) diff --git a/src/api/v1/auth/verify_email.rs b/src/api/v1/auth/verify_email.rs index e596500..c5c9097 100644 --- a/src/api/v1/auth/verify_email.rs +++ b/src/api/v1/auth/verify_email.rs @@ -8,7 +8,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - objects::{EmailToken, Me}, + structs::{EmailToken, Me}, utils::get_auth_header, }; @@ -46,15 +46,20 @@ pub async fn get( let me = Me::get(&mut conn, uuid).await?; - let email_token = EmailToken::get(&data, me.uuid).await?; + let email_token = EmailToken::get(&mut conn, me.uuid).await?; if query.token != email_token.token { return Ok(HttpResponse::Unauthorized().finish()); } + if Utc::now().signed_duration_since(email_token.created_at) > Duration::hours(24) { + email_token.delete(&mut conn).await?; + return Ok(HttpResponse::Gone().finish()); + } + me.verify_email(&mut conn).await?; - email_token.delete(&data).await?; + email_token.delete(&mut conn).await?; Ok(HttpResponse::Ok().finish()) } @@ -85,9 +90,9 @@ pub async fn post(req: HttpRequest, data: web::Data) -> Result Duration::hours(1) { - email_token.delete(&data).await?; + email_token.delete(&mut conn).await?; } else { return Err(Error::TooManyRequests( "Please allow 1 hour before sending a new email".to_string(), diff --git a/src/api/v1/channels/mod.rs b/src/api/v1/channels/mod.rs index e9558c9..999bb23 100644 --- a/src/api/v1/channels/mod.rs +++ b/src/api/v1/channels/mod.rs @@ -6,7 +6,6 @@ pub fn web() -> Scope { web::scope("/channels") .service(uuid::get) .service(uuid::delete) - .service(uuid::patch) .service(uuid::messages::get) .service(uuid::socket::ws) } diff --git a/src/api/v1/channels/uuid/messages.rs b/src/api/v1/channels/uuid/messages.rs index 9fdea0b..ddcc800 100644 --- a/src/api/v1/channels/uuid/messages.rs +++ b/src/api/v1/channels/uuid/messages.rs @@ -4,7 +4,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - objects::{Channel, Member}, + structs::{Channel, Member}, utils::{get_auth_header, global_checks}, }; use ::uuid::Uuid; diff --git a/src/api/v1/channels/uuid/mod.rs b/src/api/v1/channels/uuid/mod.rs index bece6ed..f429159 100644 --- a/src/api/v1/channels/uuid/mod.rs +++ b/src/api/v1/channels/uuid/mod.rs @@ -1,13 +1,14 @@ -//! `/api/v1/channels/{uuid}` Channel specific endpoints - pub mod messages; pub mod socket; use crate::{ - api::v1::auth::check_access_token, error::Error, objects::{Channel, Member, Permissions}, utils::{get_auth_header, global_checks}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{Channel, Member}, + utils::{get_auth_header, global_checks}, }; -use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web}; -use serde::Deserialize; +use actix_web::{HttpRequest, HttpResponse, delete, get, web}; use uuid::Uuid; #[get("/{uuid}")] @@ -55,93 +56,9 @@ pub async fn delete( let channel = Channel::fetch_one(&data, channel_uuid).await?; - let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; - - member.check_permission(&data, Permissions::DeleteChannel).await?; + Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; channel.delete(&data).await?; Ok(HttpResponse::Ok().finish()) } - -#[derive(Deserialize)] -struct NewInfo { - name: Option, - description: Option, - is_above: Option, -} - -/// `PATCH /api/v1/channels/{uuid}` Returns user with the given UUID -/// -/// requires auth: yes -/// -/// requires relation: yes -/// -/// ### Request Example -/// All fields are optional and can be nulled/dropped if only changing 1 value -/// ``` -/// json!({ -/// "name": "gaming-chat", -/// "description": "Gaming related topics.", -/// "is_above": "398f6d7b-752c-4348-9771-fe6024adbfb1" -/// }); -/// ``` -/// -/// ### Response Example -/// ``` -/// json!({ -/// uuid: "cdcac171-5add-4f88-9559-3a247c8bba2c", -/// guild_uuid: "383d2afa-082f-4dd3-9050-ca6ed91487b6", -/// name: "gaming-chat", -/// description: "Gaming related topics.", -/// is_above: "398f6d7b-752c-4348-9771-fe6024adbfb1", -/// permissions: { -/// role_uuid: "79cc0806-0f37-4a06-a468-6639c4311a2d", -/// permissions: 0 -/// } -/// }); -/// ``` -/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps -#[patch("/{uuid}")] -pub async fn patch( - req: HttpRequest, - path: web::Path<(Uuid,)>, - new_info: web::Json, - data: web::Data, -) -> Result { - let headers = req.headers(); - - let auth_header = get_auth_header(headers)?; - - let channel_uuid = path.into_inner().0; - - let mut conn = data.pool.get().await?; - - let uuid = check_access_token(auth_header, &mut conn).await?; - - global_checks(&data, uuid).await?; - - let mut channel = Channel::fetch_one(&data, channel_uuid).await?; - - let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; - - member.check_permission(&data, Permissions::ManageChannel).await?; - - if let Some(new_name) = &new_info.name { - channel.set_name(&data, new_name.to_string()).await?; - } - - if let Some(new_description) = &new_info.description { - channel - .set_description(&data, new_description.to_string()) - .await?; - } - - if let Some(new_is_above) = &new_info.is_above { - channel - .set_description(&data, new_is_above.to_string()) - .await?; - } - - Ok(HttpResponse::Ok().json(channel)) -} diff --git a/src/api/v1/channels/uuid/socket.rs b/src/api/v1/channels/uuid/socket.rs index b346e8e..556dca3 100644 --- a/src/api/v1/channels/uuid/socket.rs +++ b/src/api/v1/channels/uuid/socket.rs @@ -10,7 +10,7 @@ use uuid::Uuid; use crate::{ Data, api::v1::auth::check_access_token, - objects::{Channel, Member}, + structs::{Channel, Member}, utils::{get_ws_protocol_header, global_checks}, }; diff --git a/src/api/v1/guilds/mod.rs b/src/api/v1/guilds/mod.rs index ada5dc8..b7a7a7c 100644 --- a/src/api/v1/guilds/mod.rs +++ b/src/api/v1/guilds/mod.rs @@ -9,7 +9,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - objects::{Guild, StartAmountQuery}, + structs::{Guild, StartAmountQuery}, utils::{get_auth_header, global_checks}, }; diff --git a/src/api/v1/guilds/uuid/channels.rs b/src/api/v1/guilds/uuid/channels.rs index db895e4..0dd4566 100644 --- a/src/api/v1/guilds/uuid/channels.rs +++ b/src/api/v1/guilds/uuid/channels.rs @@ -1,5 +1,9 @@ use crate::{ - api::v1::auth::check_access_token, error::Error, objects::{Channel, Member, Permissions}, utils::{get_auth_header, global_checks, order_by_is_above}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{Channel, Member}, + utils::{get_auth_header, global_checks, order_by_is_above}, }; use ::uuid::Uuid; use actix_web::{HttpRequest, HttpResponse, get, post, web}; @@ -70,9 +74,9 @@ pub async fn create( global_checks(&data, uuid).await?; - let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; + Member::check_membership(&mut conn, uuid, guild_uuid).await?; - member.check_permission(&data, Permissions::CreateChannel).await?; + // FIXME: Logic to check permissions, should probably be done in utils.rs let channel = Channel::new( data.clone(), diff --git a/src/api/v1/guilds/uuid/icon.rs b/src/api/v1/guilds/uuid/icon.rs index 0860435..f2e15b6 100644 --- a/src/api/v1/guilds/uuid/icon.rs +++ b/src/api/v1/guilds/uuid/icon.rs @@ -5,7 +5,11 @@ use futures_util::StreamExt as _; use uuid::Uuid; use crate::{ - api::v1::auth::check_access_token, error::Error, objects::{Guild, Member, Permissions}, utils::{get_auth_header, global_checks}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{Guild, Member}, + utils::{get_auth_header, global_checks}, }; /// `PUT /api/v1/guilds/{uuid}/icon` Icon upload @@ -32,9 +36,7 @@ pub async fn upload( global_checks(&data, uuid).await?; - let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; - - member.check_permission(&data, Permissions::ManageServer).await?; + Member::check_membership(&mut conn, uuid, guild_uuid).await?; let mut guild = Guild::fetch_one(&mut conn, guild_uuid).await?; @@ -45,7 +47,7 @@ pub async fn upload( guild .set_icon( - &data.bunny_storage, + &data.bunny_cdn, &mut conn, data.config.bunny.cdn_url.clone(), bytes, diff --git a/src/api/v1/guilds/uuid/invites/mod.rs b/src/api/v1/guilds/uuid/invites/mod.rs index eb8d2ce..ea04529 100644 --- a/src/api/v1/guilds/uuid/invites/mod.rs +++ b/src/api/v1/guilds/uuid/invites/mod.rs @@ -3,12 +3,16 @@ use serde::Deserialize; use uuid::Uuid; use crate::{ - api::v1::auth::check_access_token, error::Error, objects::{Guild, Member, Permissions}, utils::{get_auth_header, global_checks}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{Guild, Member}, + utils::{get_auth_header, global_checks}, }; #[derive(Deserialize)] struct InviteRequest { - custom_id: Option, + custom_id: String, } #[get("{uuid}/invites")] @@ -42,7 +46,7 @@ pub async fn get( pub async fn create( req: HttpRequest, path: web::Path<(Uuid,)>, - invite_request: web::Json, + invite_request: web::Json>, data: web::Data, ) -> Result { let headers = req.headers(); @@ -57,13 +61,13 @@ pub async fn create( global_checks(&data, uuid).await?; - let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; - - member.check_permission(&data, Permissions::CreateInvite).await?; + Member::check_membership(&mut conn, uuid, guild_uuid).await?; let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; - let invite = guild.create_invite(&mut conn, uuid, invite_request.custom_id.clone()).await?; + let custom_id = invite_request.as_ref().map(|ir| ir.custom_id.clone()); + + let invite = guild.create_invite(&mut conn, uuid, custom_id).await?; Ok(HttpResponse::Ok().json(invite)) } diff --git a/src/api/v1/guilds/uuid/members.rs b/src/api/v1/guilds/uuid/members.rs index 972d862..d7ed0a5 100644 --- a/src/api/v1/guilds/uuid/members.rs +++ b/src/api/v1/guilds/uuid/members.rs @@ -2,7 +2,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - objects::Member, + structs::Member, utils::{get_auth_header, global_checks}, }; use ::uuid::Uuid; diff --git a/src/api/v1/guilds/uuid/mod.rs b/src/api/v1/guilds/uuid/mod.rs index 4c88d7a..c24e957 100644 --- a/src/api/v1/guilds/uuid/mod.rs +++ b/src/api/v1/guilds/uuid/mod.rs @@ -13,7 +13,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - objects::{Guild, Member}, + structs::{Guild, Member}, utils::{get_auth_header, global_checks}, }; diff --git a/src/api/v1/guilds/uuid/roles/mod.rs b/src/api/v1/guilds/uuid/roles/mod.rs index c33f144..8015384 100644 --- a/src/api/v1/guilds/uuid/roles/mod.rs +++ b/src/api/v1/guilds/uuid/roles/mod.rs @@ -3,7 +3,11 @@ use actix_web::{HttpRequest, HttpResponse, get, post, web}; use serde::Deserialize; use crate::{ - api::v1::auth::check_access_token, error::Error, objects::{Member, Permissions, Role}, utils::{get_auth_header, global_checks, order_by_is_above}, Data + Data, + api::v1::auth::check_access_token, + error::Error, + structs::{Member, Role}, + utils::{get_auth_header, global_checks, order_by_is_above}, }; pub mod uuid; @@ -66,9 +70,9 @@ pub async fn create( global_checks(&data, uuid).await?; - let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; + Member::check_membership(&mut conn, uuid, guild_uuid).await?; - member.check_permission(&data, Permissions::CreateRole).await?; + // FIXME: Logic to check permissions, should probably be done in utils.rs let role = Role::new(&mut conn, guild_uuid, role_info.name.clone()).await?; diff --git a/src/api/v1/guilds/uuid/roles/uuid.rs b/src/api/v1/guilds/uuid/roles/uuid.rs index f1a3206..0e7f306 100644 --- a/src/api/v1/guilds/uuid/roles/uuid.rs +++ b/src/api/v1/guilds/uuid/roles/uuid.rs @@ -2,7 +2,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - objects::{Member, Role}, + structs::{Member, Role}, utils::{get_auth_header, global_checks}, }; use ::uuid::Uuid; diff --git a/src/api/v1/invites/id.rs b/src/api/v1/invites/id.rs index 22e2868..687b825 100644 --- a/src/api/v1/invites/id.rs +++ b/src/api/v1/invites/id.rs @@ -4,7 +4,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - objects::{Guild, Invite, Member}, + structs::{Guild, Invite, Member}, utils::{get_auth_header, global_checks}, }; diff --git a/src/api/v1/me/guilds.rs b/src/api/v1/me/guilds.rs index 71cfca4..7fe02bd 100644 --- a/src/api/v1/me/guilds.rs +++ b/src/api/v1/me/guilds.rs @@ -6,7 +6,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - objects::Me, + structs::Me, utils::{get_auth_header, global_checks}, }; diff --git a/src/api/v1/me/mod.rs b/src/api/v1/me/mod.rs index da5c929..fc9e61b 100644 --- a/src/api/v1/me/mod.rs +++ b/src/api/v1/me/mod.rs @@ -6,7 +6,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - objects::Me, + structs::Me, utils::{get_auth_header, global_checks}, }; @@ -41,7 +41,6 @@ struct NewInfo { //password: Option, will probably be handled through a reset password link email: Option, pronouns: Option, - about: Option, } #[derive(Debug, MultipartForm)] @@ -65,7 +64,10 @@ pub async fn update( let uuid = check_access_token(auth_header, &mut conn).await?; - if form.avatar.is_some() || form.json.username.is_some() || form.json.display_name.is_some() { + if form.avatar.is_some() + || form.json.username.is_some() + || form.json.display_name.is_some() + { global_checks(&data, uuid).await?; } @@ -76,8 +78,12 @@ pub async fn update( let byte_slice: &[u8] = &bytes; - me.set_avatar(&data, data.config.bunny.cdn_url.clone(), byte_slice.into()) - .await?; + me.set_avatar( + &data, + data.config.bunny.cdn_url.clone(), + byte_slice.into(), + ) + .await?; } if let Some(username) = &form.json.username { @@ -96,9 +102,5 @@ pub async fn update( me.set_pronouns(&data, pronouns.clone()).await?; } - if let Some(about) = &form.json.about { - me.set_about(&data, about.clone()).await?; - } - Ok(HttpResponse::Ok().finish()) } diff --git a/src/api/v1/users/mod.rs b/src/api/v1/users/mod.rs index 334fd5f..fd3980d 100644 --- a/src/api/v1/users/mod.rs +++ b/src/api/v1/users/mod.rs @@ -6,7 +6,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - objects::{StartAmountQuery, User}, + structs::{StartAmountQuery, User}, utils::{get_auth_header, global_checks}, }; diff --git a/src/api/v1/users/uuid.rs b/src/api/v1/users/uuid.rs index 9e602a0..213afe5 100644 --- a/src/api/v1/users/uuid.rs +++ b/src/api/v1/users/uuid.rs @@ -7,7 +7,7 @@ use crate::{ Data, api::v1::auth::check_access_token, error::Error, - objects::User, + structs::User, utils::{get_auth_header, global_checks}, }; diff --git a/src/main.rs b/src/main.rs index 47794e3..d026f55 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,9 +5,9 @@ use clap::Parser; use diesel_async::pooled_connection::AsyncDieselConnectionManager; use diesel_async::pooled_connection::deadpool::Pool; use error::Error; -use objects::MailClient; use simple_logger::SimpleLogger; use std::time::SystemTime; +use structs::MailClient; mod config; use config::{Config, ConfigBuilder}; use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; @@ -19,8 +19,8 @@ type Conn = mod api; pub mod error; -pub mod objects; pub mod schema; +pub mod structs; pub mod utils; #[derive(Parser, Debug)] @@ -40,7 +40,7 @@ pub struct Data { pub config: Config, pub argon2: Argon2<'static>, pub start_time: SystemTime, - pub bunny_storage: bunny_api_tokio::EdgeStorageClient, + pub bunny_cdn: bunny_api_tokio::Client, pub mail_client: MailClient, } @@ -65,9 +65,14 @@ async fn main() -> Result<(), Error> { let cache_pool = redis::Client::open(config.cache_database.url())?; + let mut bunny_cdn = bunny_api_tokio::Client::new("").await?; + let bunny = config.bunny.clone(); - let bunny_storage = bunny_api_tokio::EdgeStorageClient::new(bunny.api_key, bunny.endpoint, bunny.storage_zone).await?; + bunny_cdn + .storage + .init(bunny.api_key, bunny.endpoint, bunny.storage_zone) + .await?; let mail = config.mail.clone(); @@ -117,7 +122,7 @@ async fn main() -> Result<(), Error> { // TODO: Possibly implement "pepper" into this (thinking it could generate one if it doesnt exist and store it on disk) argon2: Argon2::default(), start_time: SystemTime::now(), - bunny_storage, + bunny_cdn, mail_client, }; @@ -150,7 +155,7 @@ async fn main() -> Result<(), Error> { App::new() .app_data(web::Data::new(data.clone())) .wrap(cors) - .service(api::web(data.config.web.backend_url.path())) + .service(api::web(&data.config.web.backend_url.path())) }) .bind((web.ip, web.port))? .run() diff --git a/src/objects/channel.rs b/src/objects/channel.rs deleted file mode 100644 index 4d52353..0000000 --- a/src/objects/channel.rs +++ /dev/null @@ -1,384 +0,0 @@ -use diesel::{ - ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, delete, - insert_into, update, -}; -use diesel_async::{RunQueryDsl, pooled_connection::AsyncDieselConnectionManager}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use crate::{ - Conn, Data, - error::Error, - schema::{channel_permissions, channels, messages}, - utils::{CHANNEL_REGEX, order_by_is_above}, -}; - -use super::{HasIsAbove, HasUuid, Message, load_or_empty, message::MessageBuilder}; - -#[derive(Queryable, Selectable, Insertable, Clone, Debug)] -#[diesel(table_name = channels)] -#[diesel(check_for_backend(diesel::pg::Pg))] -struct ChannelBuilder { - uuid: Uuid, - guild_uuid: Uuid, - name: String, - description: Option, - is_above: Option, -} - -impl ChannelBuilder { - async fn build(self, conn: &mut Conn) -> Result { - use self::channel_permissions::dsl::*; - let channel_permission: Vec = load_or_empty( - channel_permissions - .filter(channel_uuid.eq(self.uuid)) - .select(ChannelPermission::as_select()) - .load(conn) - .await, - )?; - - Ok(Channel { - uuid: self.uuid, - guild_uuid: self.guild_uuid, - name: self.name, - description: self.description, - is_above: self.is_above, - permissions: channel_permission, - }) - } -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct Channel { - pub uuid: Uuid, - pub guild_uuid: Uuid, - name: String, - description: Option, - pub is_above: Option, - pub permissions: Vec, -} - -#[derive(Serialize, Deserialize, Clone, Queryable, Selectable, Debug)] -#[diesel(table_name = channel_permissions)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct ChannelPermission { - pub role_uuid: Uuid, - pub permissions: i64, -} - -impl HasUuid for Channel { - fn uuid(&self) -> &Uuid { - self.uuid.as_ref() - } -} - -impl HasIsAbove for Channel { - fn is_above(&self) -> Option<&Uuid> { - self.is_above.as_ref() - } -} - -impl Channel { - pub async fn fetch_all( - pool: &deadpool::managed::Pool< - AsyncDieselConnectionManager, - Conn, - >, - guild_uuid: Uuid, - ) -> Result, Error> { - let mut conn = pool.get().await?; - - use channels::dsl; - let channel_builders: Vec = load_or_empty( - dsl::channels - .filter(dsl::guild_uuid.eq(guild_uuid)) - .select(ChannelBuilder::as_select()) - .load(&mut conn) - .await, - )?; - - let channel_futures = channel_builders.iter().map(async move |c| { - let mut conn = pool.get().await?; - c.clone().build(&mut conn).await - }); - - futures::future::try_join_all(channel_futures).await - } - - pub async fn fetch_one(data: &Data, channel_uuid: Uuid) -> Result { - if let Ok(cache_hit) = data.get_cache_key(channel_uuid.to_string()).await { - return Ok(serde_json::from_str(&cache_hit)?); - } - - let mut conn = data.pool.get().await?; - - use channels::dsl; - let channel_builder: ChannelBuilder = dsl::channels - .filter(dsl::uuid.eq(channel_uuid)) - .select(ChannelBuilder::as_select()) - .get_result(&mut conn) - .await?; - - let channel = channel_builder.build(&mut conn).await?; - - data.set_cache_key(channel_uuid.to_string(), channel.clone(), 60) - .await?; - - Ok(channel) - } - - pub async fn new( - data: actix_web::web::Data, - guild_uuid: Uuid, - name: String, - description: Option, - ) -> Result { - if !CHANNEL_REGEX.is_match(&name) { - return Err(Error::BadRequest("Channel name is invalid".to_string())); - } - - let mut conn = data.pool.get().await?; - - let channel_uuid = Uuid::now_v7(); - - let channels = Self::fetch_all(&data.pool, guild_uuid).await?; - - let channels_ordered = order_by_is_above(channels).await?; - - let last_channel = channels_ordered.last(); - - let new_channel = ChannelBuilder { - uuid: channel_uuid, - guild_uuid, - name: name.clone(), - description: description.clone(), - is_above: None, - }; - - insert_into(channels::table) - .values(new_channel.clone()) - .execute(&mut conn) - .await?; - - if let Some(old_last_channel) = last_channel { - use channels::dsl; - update(channels::table) - .filter(dsl::uuid.eq(old_last_channel.uuid)) - .set(dsl::is_above.eq(new_channel.uuid)) - .execute(&mut conn) - .await?; - } - - // returns different object because there's no reason to build the channelbuilder (wastes 1 database request) - let channel = Self { - uuid: channel_uuid, - guild_uuid, - name, - description, - is_above: None, - permissions: vec![], - }; - - data.set_cache_key(channel_uuid.to_string(), channel.clone(), 1800) - .await?; - - if data - .get_cache_key(format!("{}_channels", guild_uuid)) - .await - .is_ok() - { - data.del_cache_key(format!("{}_channels", guild_uuid)) - .await?; - } - - Ok(channel) - } - - pub async fn delete(self, data: &Data) -> Result<(), Error> { - let mut conn = data.pool.get().await?; - - use channels::dsl; - match update(channels::table) - .filter(dsl::is_above.eq(self.uuid)) - .set(dsl::is_above.eq(None::)) - .execute(&mut conn) - .await - { - Ok(r) => Ok(r), - Err(diesel::result::Error::NotFound) => Ok(0), - Err(e) => Err(e), - }?; - - delete(channels::table) - .filter(dsl::uuid.eq(self.uuid)) - .execute(&mut conn) - .await?; - - match update(channels::table) - .filter(dsl::is_above.eq(self.uuid)) - .set(dsl::is_above.eq(self.is_above)) - .execute(&mut conn) - .await - { - Ok(r) => Ok(r), - Err(diesel::result::Error::NotFound) => Ok(0), - Err(e) => Err(e), - }?; - - if data.get_cache_key(self.uuid.to_string()).await.is_ok() { - data.del_cache_key(self.uuid.to_string()).await?; - } - - if data - .get_cache_key(format!("{}_channels", self.guild_uuid)) - .await - .is_ok() - { - data.del_cache_key(format!("{}_channels", self.guild_uuid)) - .await?; - } - - Ok(()) - } - - pub async fn fetch_messages( - &self, - data: &Data, - amount: i64, - offset: i64, - ) -> Result, Error> { - let mut conn = data.pool.get().await?; - - use messages::dsl; - let messages: Vec = load_or_empty( - dsl::messages - .filter(dsl::channel_uuid.eq(self.uuid)) - .select(MessageBuilder::as_select()) - .order(dsl::uuid.desc()) - .limit(amount) - .offset(offset) - .load(&mut conn) - .await, - )?; - - let message_futures = messages.iter().map(async move |b| b.build(data).await); - - futures::future::try_join_all(message_futures).await - } - - pub async fn new_message( - &self, - data: &Data, - user_uuid: Uuid, - message: String, - ) -> Result { - let message_uuid = Uuid::now_v7(); - - let message = MessageBuilder { - uuid: message_uuid, - channel_uuid: self.uuid, - user_uuid, - message, - }; - - let mut conn = data.pool.get().await?; - - insert_into(messages::table) - .values(message.clone()) - .execute(&mut conn) - .await?; - - message.build(data).await - } - - pub async fn set_name(&mut self, data: &Data, new_name: String) -> Result<(), Error> { - if !CHANNEL_REGEX.is_match(&new_name) { - return Err(Error::BadRequest("Channel name is invalid".to_string())); - } - - let mut conn = data.pool.get().await?; - - use channels::dsl; - update(channels::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::name.eq(&new_name)) - .execute(&mut conn) - .await?; - - self.name = new_name; - - Ok(()) - } - - pub async fn set_description( - &mut self, - data: &Data, - new_description: String, - ) -> Result<(), Error> { - let mut conn = data.pool.get().await?; - - use channels::dsl; - update(channels::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::description.eq(&new_description)) - .execute(&mut conn) - .await?; - - self.description = Some(new_description); - - Ok(()) - } - - pub async fn move_channel(&mut self, data: &Data, new_is_above: Uuid) -> Result<(), Error> { - let mut conn = data.pool.get().await?; - - use channels::dsl; - let old_above_uuid: Option = match dsl::channels - .filter(dsl::is_above.eq(self.uuid)) - .select(dsl::uuid) - .get_result(&mut conn) - .await - { - Ok(r) => Ok(Some(r)), - Err(diesel::result::Error::NotFound) => Ok(None), - Err(e) => Err(e), - }?; - - if let Some(uuid) = old_above_uuid { - update(channels::table) - .filter(dsl::uuid.eq(uuid)) - .set(dsl::is_above.eq(None::)) - .execute(&mut conn) - .await?; - } - - match update(channels::table) - .filter(dsl::is_above.eq(new_is_above)) - .set(dsl::is_above.eq(self.uuid)) - .execute(&mut conn) - .await - { - Ok(r) => Ok(r), - Err(diesel::result::Error::NotFound) => Ok(0), - Err(e) => Err(e), - }?; - - update(channels::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::is_above.eq(new_is_above)) - .execute(&mut conn) - .await?; - - if let Some(uuid) = old_above_uuid { - update(channels::table) - .filter(dsl::uuid.eq(uuid)) - .set(dsl::is_above.eq(self.is_above)) - .execute(&mut conn) - .await?; - } - - self.is_above = Some(new_is_above); - - Ok(()) - } -} diff --git a/src/objects/email_token.rs b/src/objects/email_token.rs deleted file mode 100644 index 4ec6b7e..0000000 --- a/src/objects/email_token.rs +++ /dev/null @@ -1,61 +0,0 @@ -use chrono::Utc; -use lettre::message::MultiPart; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use crate::{Data, error::Error, utils::generate_token}; - -use super::Me; - -#[derive(Serialize, Deserialize)] -pub struct EmailToken { - user_uuid: Uuid, - pub token: String, - pub created_at: chrono::DateTime, -} - -impl EmailToken { - pub async fn get(data: &Data, user_uuid: Uuid) -> Result { - let email_token = serde_json::from_str(&data.get_cache_key(format!("{}_email_verify", user_uuid)).await?)?; - - Ok(email_token) - } - - #[allow(clippy::new_ret_no_self)] - pub async fn new(data: &Data, me: Me) -> Result<(), Error> { - let token = generate_token::<32>()?; - - let email_token = EmailToken { - user_uuid: me.uuid, - token: token.clone(), - // TODO: Check if this can be replaced with something built into valkey - created_at: Utc::now() - }; - - data.set_cache_key(format!("{}_email_verify", me.uuid), email_token, 86400).await?; - - let mut verify_endpoint = data.config.web.frontend_url.join("verify-email")?; - - verify_endpoint.set_query(Some(&format!("token={}", token))); - - let email = data - .mail_client - .message_builder() - .to(me.email.parse()?) - .subject(format!("{} E-mail Verification", data.config.instance.name)) - .multipart(MultiPart::alternative_plain_html( - format!("Verify your {} account\n\nHello, {}!\nThanks for creating a new account on Gorb.\nThe final step to create your account is to verify your email address by visiting the page, within 24 hours.\n\n{}\n\nIf you didn't ask to verify this address, you can safely ignore this email\n\nThanks, The gorb team.", data.config.instance.name, me.username, verify_endpoint), - format!(r#"

Verify your {} Account

Hello, {}!

Thanks for creating a new account on Gorb.

The final step to create your account is to verify your email address by clicking the button below, within 24 hours.

VERIFY ACCOUNT

If you didn't ask to verify this address, you can safely ignore this email.

"#, data.config.instance.name, me.username, verify_endpoint) - ))?; - - data.mail_client.send_mail(email).await?; - - Ok(()) - } - - pub async fn delete(&self, data: &Data) -> Result<(), Error> { - data.del_cache_key(format!("{}_email_verify", self.user_uuid)).await?; - - Ok(()) - } -} diff --git a/src/objects/guild.rs b/src/objects/guild.rs deleted file mode 100644 index 47058ee..0000000 --- a/src/objects/guild.rs +++ /dev/null @@ -1,222 +0,0 @@ -use actix_web::web::BytesMut; -use diesel::{ - ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into, - update, -}; -use diesel_async::{RunQueryDsl, pooled_connection::AsyncDieselConnectionManager}; -use serde::Serialize; -use tokio::task; -use url::Url; -use uuid::Uuid; - -use crate::{ - Conn, - error::Error, - schema::{guild_members, guilds, invites}, - utils::image_check, -}; - -use super::{Invite, Member, Role, load_or_empty, member::MemberBuilder}; - -#[derive(Serialize, Queryable, Selectable, Insertable, Clone)] -#[diesel(table_name = guilds)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct GuildBuilder { - uuid: Uuid, - name: String, - description: Option, - icon: Option, -} - -impl GuildBuilder { - pub async fn build(self, conn: &mut Conn) -> Result { - let member_count = Member::count(conn, self.uuid).await?; - - let roles = Role::fetch_all(conn, self.uuid).await?; - - Ok(Guild { - uuid: self.uuid, - name: self.name, - description: self.description, - icon: self.icon.and_then(|i| i.parse().ok()), - roles, - member_count, - }) - } -} - -#[derive(Serialize)] -pub struct Guild { - pub uuid: Uuid, - name: String, - description: Option, - icon: Option, - pub roles: Vec, - member_count: i64, -} - -impl Guild { - pub async fn fetch_one(conn: &mut Conn, guild_uuid: Uuid) -> Result { - use guilds::dsl; - let guild_builder: GuildBuilder = dsl::guilds - .filter(dsl::uuid.eq(guild_uuid)) - .select(GuildBuilder::as_select()) - .get_result(conn) - .await?; - - guild_builder.build(conn).await - } - - pub async fn fetch_amount( - pool: &deadpool::managed::Pool< - AsyncDieselConnectionManager, - Conn, - >, - offset: i64, - amount: i64, - ) -> Result, Error> { - // Fetch guild data from database - let mut conn = pool.get().await?; - - use guilds::dsl; - let guild_builders: Vec = load_or_empty( - dsl::guilds - .select(GuildBuilder::as_select()) - .order_by(dsl::uuid) - .offset(offset) - .limit(amount) - .load(&mut conn) - .await, - )?; - - // Process each guild concurrently - let guild_futures = guild_builders.iter().map(async move |g| { - let mut conn = pool.get().await?; - g.clone().build(&mut conn).await - }); - - // Execute all futures concurrently and collect results - futures::future::try_join_all(guild_futures).await - } - - pub async fn new(conn: &mut Conn, name: String, owner_uuid: Uuid) -> Result { - let guild_uuid = Uuid::now_v7(); - - let guild_builder = GuildBuilder { - uuid: guild_uuid, - name: name.clone(), - description: None, - icon: None, - }; - - insert_into(guilds::table) - .values(guild_builder) - .execute(conn) - .await?; - - let member_uuid = Uuid::now_v7(); - - let member = MemberBuilder { - uuid: member_uuid, - nickname: None, - user_uuid: owner_uuid, - guild_uuid, - is_owner: true, - }; - - insert_into(guild_members::table) - .values(member) - .execute(conn) - .await?; - - Ok(Guild { - uuid: guild_uuid, - name, - description: None, - icon: None, - roles: vec![], - member_count: 1, - }) - } - - pub async fn get_invites(&self, conn: &mut Conn) -> Result, Error> { - use invites::dsl; - let invites = load_or_empty( - dsl::invites - .filter(dsl::guild_uuid.eq(self.uuid)) - .select(Invite::as_select()) - .load(conn) - .await, - )?; - - Ok(invites) - } - - pub async fn create_invite( - &self, - conn: &mut Conn, - user_uuid: Uuid, - custom_id: Option, - ) -> Result { - let invite_id; - - if let Some(id) = custom_id { - invite_id = id; - if invite_id.len() > 32 { - return Err(Error::BadRequest("MAX LENGTH".to_string())); - } - } else { - let charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - - invite_id = random_string::generate(8, charset); - } - - let invite = Invite { - id: invite_id, - user_uuid, - guild_uuid: self.uuid, - }; - - insert_into(invites::table) - .values(invite.clone()) - .execute(conn) - .await?; - - Ok(invite) - } - - // FIXME: Horrible security - pub async fn set_icon( - &mut self, - bunny_storage: &bunny_api_tokio::EdgeStorageClient, - conn: &mut Conn, - cdn_url: Url, - icon: BytesMut, - ) -> Result<(), Error> { - let icon_clone = icon.clone(); - let image_type = task::spawn_blocking(move || image_check(icon_clone)).await??; - - if let Some(icon) = &self.icon { - let relative_url = icon.path().trim_start_matches('/'); - - bunny_storage.delete(relative_url).await?; - } - - let path = format!("icons/{}/icon.{}", self.uuid, image_type); - - bunny_storage.upload(path.clone(), icon.into()).await?; - - let icon_url = cdn_url.join(&path)?; - - use guilds::dsl; - update(guilds::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::icon.eq(icon_url.as_str())) - .execute(conn) - .await?; - - self.icon = Some(icon_url); - - Ok(()) - } -} diff --git a/src/objects/invite.rs b/src/objects/invite.rs deleted file mode 100644 index 5e0827e..0000000 --- a/src/objects/invite.rs +++ /dev/null @@ -1,30 +0,0 @@ -use diesel::{ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper}; -use diesel_async::RunQueryDsl; -use serde::Serialize; -use uuid::Uuid; - -use crate::{Conn, error::Error, schema::invites}; - -/// Server invite struct -#[derive(Clone, Serialize, Queryable, Selectable, Insertable)] -pub struct Invite { - /// case-sensitive alphanumeric string with a fixed length of 8 characters, can be up to 32 characters for custom invites - pub id: String, - /// User that created the invite - pub user_uuid: Uuid, - /// UUID of the guild that the invite belongs to - pub guild_uuid: Uuid, -} - -impl Invite { - pub async fn fetch_one(conn: &mut Conn, invite_id: String) -> Result { - use invites::dsl; - let invite: Invite = dsl::invites - .filter(dsl::id.eq(invite_id)) - .select(Invite::as_select()) - .get_result(conn) - .await?; - - Ok(invite) - } -} diff --git a/src/objects/me.rs b/src/objects/me.rs deleted file mode 100644 index e183c5d..0000000 --- a/src/objects/me.rs +++ /dev/null @@ -1,231 +0,0 @@ -use actix_web::web::BytesMut; -use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper, update}; -use diesel_async::RunQueryDsl; -use serde::Serialize; -use tokio::task; -use url::Url; -use uuid::Uuid; - -use crate::{ - Conn, Data, - error::Error, - schema::{guild_members, guilds, users}, - utils::{EMAIL_REGEX, USERNAME_REGEX, image_check}, -}; - -use super::{Guild, guild::GuildBuilder, load_or_empty, member::MemberBuilder}; - -#[derive(Serialize, Queryable, Selectable)] -#[diesel(table_name = users)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct Me { - pub uuid: Uuid, - pub username: String, - pub display_name: Option, - avatar: Option, - pronouns: Option, - about: Option, - pub email: String, - pub email_verified: bool, -} - -impl Me { - pub async fn get(conn: &mut Conn, user_uuid: Uuid) -> Result { - use users::dsl; - let me: Me = dsl::users - .filter(dsl::uuid.eq(user_uuid)) - .select(Me::as_select()) - .get_result(conn) - .await?; - - Ok(me) - } - - pub async fn fetch_memberships(&self, conn: &mut Conn) -> Result, Error> { - use guild_members::dsl; - let memberships: Vec = load_or_empty( - dsl::guild_members - .filter(dsl::user_uuid.eq(self.uuid)) - .select(MemberBuilder::as_select()) - .load(conn) - .await, - )?; - - let mut guilds: Vec = vec![]; - - for membership in memberships { - use guilds::dsl; - guilds.push( - dsl::guilds - .filter(dsl::uuid.eq(membership.guild_uuid)) - .select(GuildBuilder::as_select()) - .get_result(conn) - .await? - .build(conn) - .await?, - ) - } - - Ok(guilds) - } - - pub async fn set_avatar( - &mut self, - data: &Data, - cdn_url: Url, - avatar: BytesMut, - ) -> Result<(), Error> { - let avatar_clone = avatar.clone(); - let image_type = task::spawn_blocking(move || image_check(avatar_clone)).await??; - - let mut conn = data.pool.get().await?; - - if let Some(avatar) = &self.avatar { - let avatar_url: Url = avatar.parse()?; - - let relative_url = avatar_url.path().trim_start_matches('/'); - - data.bunny_storage.delete(relative_url).await?; - } - - let path = format!("avatar/{}/avatar.{}", self.uuid, image_type); - - data.bunny_storage - .upload(path.clone(), avatar.into()) - .await?; - - let avatar_url = cdn_url.join(&path)?; - - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::avatar.eq(avatar_url.as_str())) - .execute(&mut conn) - .await?; - - if data.get_cache_key(self.uuid.to_string()).await.is_ok() { - data.del_cache_key(self.uuid.to_string()).await? - } - - self.avatar = Some(avatar_url.to_string()); - - Ok(()) - } - - pub async fn verify_email(&self, conn: &mut Conn) -> Result<(), Error> { - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::email_verified.eq(true)) - .execute(conn) - .await?; - - Ok(()) - } - - pub async fn set_username(&mut self, data: &Data, new_username: String) -> Result<(), Error> { - if !USERNAME_REGEX.is_match(&new_username) { - return Err(Error::BadRequest("Invalid username".to_string())); - } - - let mut conn = data.pool.get().await?; - - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::username.eq(new_username.as_str())) - .execute(&mut conn) - .await?; - - if data.get_cache_key(self.uuid.to_string()).await.is_ok() { - data.del_cache_key(self.uuid.to_string()).await? - } - - self.username = new_username; - - Ok(()) - } - - pub async fn set_display_name( - &mut self, - data: &Data, - new_display_name: String, - ) -> Result<(), Error> { - let mut conn = data.pool.get().await?; - - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(dsl::display_name.eq(new_display_name.as_str())) - .execute(&mut conn) - .await?; - - if data.get_cache_key(self.uuid.to_string()).await.is_ok() { - data.del_cache_key(self.uuid.to_string()).await? - } - - self.display_name = Some(new_display_name); - - Ok(()) - } - - pub async fn set_email(&mut self, data: &Data, new_email: String) -> Result<(), Error> { - if !EMAIL_REGEX.is_match(&new_email) { - return Err(Error::BadRequest("Invalid username".to_string())); - } - - let mut conn = data.pool.get().await?; - - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.uuid)) - .set(( - dsl::email.eq(new_email.as_str()), - dsl::email_verified.eq(false), - )) - .execute(&mut conn) - .await?; - - if data.get_cache_key(self.uuid.to_string()).await.is_ok() { - data.del_cache_key(self.uuid.to_string()).await? - } - - self.email = new_email; - - Ok(()) - } - - pub async fn set_pronouns(&mut self, data: &Data, new_pronouns: String) -> Result<(), Error> { - let mut conn = data.pool.get().await?; - - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.uuid)) - .set((dsl::pronouns.eq(new_pronouns.as_str()),)) - .execute(&mut conn) - .await?; - - if data.get_cache_key(self.uuid.to_string()).await.is_ok() { - data.del_cache_key(self.uuid.to_string()).await? - } - - Ok(()) - } - - pub async fn set_about(&mut self, data: &Data, new_about: String) -> Result<(), Error> { - let mut conn = data.pool.get().await?; - - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.uuid)) - .set((dsl::about.eq(new_about.as_str()),)) - .execute(&mut conn) - .await?; - - if data.get_cache_key(self.uuid.to_string()).await.is_ok() { - data.del_cache_key(self.uuid.to_string()).await? - } - - Ok(()) - } -} diff --git a/src/objects/member.rs b/src/objects/member.rs deleted file mode 100644 index 20bc848..0000000 --- a/src/objects/member.rs +++ /dev/null @@ -1,141 +0,0 @@ -use diesel::{ - ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into, -}; -use diesel_async::RunQueryDsl; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use crate::{error::Error, objects::{Permissions, Role}, schema::guild_members, Conn, Data}; - -use super::{User, load_or_empty}; - -#[derive(Serialize, Queryable, Selectable, Insertable)] -#[diesel(table_name = guild_members)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct MemberBuilder { - pub uuid: Uuid, - pub nickname: Option, - pub user_uuid: Uuid, - pub guild_uuid: Uuid, - pub is_owner: bool, -} - -impl MemberBuilder { - pub async fn build(&self, data: &Data) -> Result { - let user = User::fetch_one(data, self.user_uuid).await?; - - Ok(Member { - uuid: self.uuid, - nickname: self.nickname.clone(), - user_uuid: self.user_uuid, - guild_uuid: self.guild_uuid, - is_owner: self.is_owner, - user, - }) - } - - pub async fn check_permission(&self, data: &Data, permission: Permissions) -> Result<(), Error> { - if !self.is_owner { - let roles = Role::fetch_from_member(&data, self.uuid).await?; - let allowed = roles.iter().any(|r| r.permissions & permission as i64 != 0); - if !allowed { - return Err(Error::Forbidden("Not allowed".to_string())) - } - } - - Ok(()) - } -} - -#[derive(Serialize, Deserialize)] -pub struct Member { - pub uuid: Uuid, - pub nickname: Option, - pub user_uuid: Uuid, - pub guild_uuid: Uuid, - pub is_owner: bool, - user: User, -} - -impl Member { - pub async fn count(conn: &mut Conn, guild_uuid: Uuid) -> Result { - use guild_members::dsl; - let count: i64 = dsl::guild_members - .filter(dsl::guild_uuid.eq(guild_uuid)) - .count() - .get_result(conn) - .await?; - - Ok(count) - } - - pub async fn check_membership( - conn: &mut Conn, - user_uuid: Uuid, - guild_uuid: Uuid, - ) -> Result { - use guild_members::dsl; - let member_builder = dsl::guild_members - .filter(dsl::user_uuid.eq(user_uuid)) - .filter(dsl::guild_uuid.eq(guild_uuid)) - .select(MemberBuilder::as_select()) - .get_result(conn) - .await?; - - Ok(member_builder) - } - - pub async fn fetch_one(data: &Data, user_uuid: Uuid, guild_uuid: Uuid) -> Result { - let mut conn = data.pool.get().await?; - - use guild_members::dsl; - let member: MemberBuilder = dsl::guild_members - .filter(dsl::user_uuid.eq(user_uuid)) - .filter(dsl::guild_uuid.eq(guild_uuid)) - .select(MemberBuilder::as_select()) - .get_result(&mut conn) - .await?; - - member.build(data).await - } - - pub async fn fetch_all(data: &Data, guild_uuid: Uuid) -> Result, Error> { - let mut conn = data.pool.get().await?; - - use guild_members::dsl; - let member_builders: Vec = load_or_empty( - dsl::guild_members - .filter(dsl::guild_uuid.eq(guild_uuid)) - .select(MemberBuilder::as_select()) - .load(&mut conn) - .await, - )?; - - let member_futures = member_builders - .iter() - .map(async move |m| m.build(data).await); - - futures::future::try_join_all(member_futures).await - } - - pub async fn new(data: &Data, user_uuid: Uuid, guild_uuid: Uuid) -> Result { - let mut conn = data.pool.get().await?; - - let member_uuid = Uuid::now_v7(); - - let member = MemberBuilder { - uuid: member_uuid, - guild_uuid, - user_uuid, - nickname: None, - is_owner: false, - }; - - insert_into(guild_members::table) - .values(&member) - .execute(&mut conn) - .await?; - - member.build(data).await - } -} diff --git a/src/objects/message.rs b/src/objects/message.rs deleted file mode 100644 index 6c1700a..0000000 --- a/src/objects/message.rs +++ /dev/null @@ -1,40 +0,0 @@ -use diesel::{Insertable, Queryable, Selectable}; -use serde::Serialize; -use uuid::Uuid; - -use crate::{Data, error::Error, schema::messages}; - -use super::User; - -#[derive(Clone, Queryable, Selectable, Insertable)] -#[diesel(table_name = messages)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct MessageBuilder { - pub uuid: Uuid, - pub channel_uuid: Uuid, - pub user_uuid: Uuid, - pub message: String, -} - -impl MessageBuilder { - pub async fn build(&self, data: &Data) -> Result { - let user = User::fetch_one(data, self.user_uuid).await?; - - Ok(Message { - uuid: self.uuid, - channel_uuid: self.channel_uuid, - user_uuid: self.user_uuid, - message: self.message.clone(), - user, - }) - } -} - -#[derive(Clone, Serialize)] -pub struct Message { - uuid: Uuid, - channel_uuid: Uuid, - user_uuid: Uuid, - message: String, - user: User, -} diff --git a/src/objects/mod.rs b/src/objects/mod.rs deleted file mode 100644 index 30a0a64..0000000 --- a/src/objects/mod.rs +++ /dev/null @@ -1,119 +0,0 @@ -use lettre::{ - AsyncSmtpTransport, AsyncTransport, Message as Email, Tokio1Executor, - message::{Mailbox, MessageBuilder as EmailBuilder}, - transport::smtp::authentication::Credentials, -}; -use log::debug; -use serde::Deserialize; -use uuid::Uuid; - -mod channel; -mod email_token; -mod guild; -mod invite; -mod me; -mod member; -mod message; -mod password_reset_token; -mod role; -mod user; - -pub use channel::Channel; -pub use email_token::EmailToken; -pub use guild::Guild; -pub use invite::Invite; -pub use me::Me; -pub use member::Member; -pub use message::Message; -pub use password_reset_token::PasswordResetToken; -pub use role::Role; -pub use role::Permissions; -pub use user::User; - -use crate::error::Error; - -pub trait HasUuid { - fn uuid(&self) -> &Uuid; -} - -pub trait HasIsAbove { - fn is_above(&self) -> Option<&Uuid>; -} - -fn load_or_empty( - query_result: Result, diesel::result::Error>, -) -> Result, diesel::result::Error> { - match query_result { - Ok(vec) => Ok(vec), - Err(diesel::result::Error::NotFound) => Ok(Vec::new()), - Err(e) => Err(e), - } -} - -#[derive(PartialEq, Eq, Clone)] -pub enum MailTls { - StartTls, - Tls, -} - -impl From for MailTls { - fn from(value: String) -> Self { - match &*value.to_lowercase() { - "starttls" => Self::StartTls, - _ => Self::Tls, - } - } -} - -#[derive(Clone)] -pub struct MailClient { - creds: Credentials, - smtp_server: String, - mbox: Mailbox, - tls: MailTls, -} - -impl MailClient { - pub fn new>( - creds: Credentials, - smtp_server: String, - mbox: String, - tls: T, - ) -> Result { - Ok(Self { - creds, - smtp_server, - mbox: mbox.parse()?, - tls: tls.into(), - }) - } - - pub fn message_builder(&self) -> EmailBuilder { - Email::builder().from(self.mbox.clone()) - } - - pub async fn send_mail(&self, email: Email) -> Result<(), Error> { - let mailer: AsyncSmtpTransport = match self.tls { - MailTls::StartTls => { - AsyncSmtpTransport::::starttls_relay(&self.smtp_server)? - .credentials(self.creds.clone()) - .build() - } - MailTls::Tls => AsyncSmtpTransport::::relay(&self.smtp_server)? - .credentials(self.creds.clone()) - .build(), - }; - - let response = mailer.send(email).await?; - - debug!("mail sending response: {:?}", response); - - Ok(()) - } -} - -#[derive(Deserialize)] -pub struct StartAmountQuery { - pub start: Option, - pub amount: Option, -} diff --git a/src/objects/password_reset_token.rs b/src/objects/password_reset_token.rs deleted file mode 100644 index e14d25a..0000000 --- a/src/objects/password_reset_token.rs +++ /dev/null @@ -1,146 +0,0 @@ -use argon2::{ - PasswordHasher, - password_hash::{SaltString, rand_core::OsRng}, -}; -use chrono::Utc; -use diesel::{ - ExpressionMethods, QueryDsl, update, -}; -use diesel_async::RunQueryDsl; -use lettre::message::MultiPart; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use crate::{ - error::Error, - schema::users, - utils::{generate_token, global_checks, user_uuid_from_identifier, PASSWORD_REGEX}, - Data -}; - -#[derive(Serialize, Deserialize)] -pub struct PasswordResetToken { - user_uuid: Uuid, - pub token: String, - pub created_at: chrono::DateTime, -} - -impl PasswordResetToken { - pub async fn get(data: &Data, token: String) -> Result { - let user_uuid: Uuid = serde_json::from_str(&data.get_cache_key(format!("{}", token)).await?)?; - let password_reset_token = serde_json::from_str(&data.get_cache_key(format!("{}_password_reset", user_uuid)).await?)?; - - Ok(password_reset_token) - } - - pub async fn get_with_identifier( - data: &Data, - identifier: String, - ) -> Result { - let mut conn = data.pool.get().await?; - - let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?; - - let password_reset_token = serde_json::from_str(&data.get_cache_key(format!("{}_password_reset", user_uuid)).await?)?; - - Ok(password_reset_token) - } - - #[allow(clippy::new_ret_no_self)] - pub async fn new(data: &Data, identifier: String) -> Result<(), Error> { - let token = generate_token::<32>()?; - - let mut conn = data.pool.get().await?; - - let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?; - - global_checks(data, user_uuid).await?; - - use users::dsl as udsl; - let (username, email_address): (String, String) = udsl::users - .filter(udsl::uuid.eq(user_uuid)) - .select((udsl::username, udsl::email)) - .get_result(&mut conn) - .await?; - - let password_reset_token = PasswordResetToken { - user_uuid, - token: token.clone(), - created_at: Utc::now(), - }; - - data.set_cache_key(format!("{}_password_reset", user_uuid), password_reset_token, 86400).await?; - data.set_cache_key(token.clone(), user_uuid, 86400).await?; - - let mut reset_endpoint = data.config.web.frontend_url.join("reset-password")?; - - reset_endpoint.set_query(Some(&format!("token={}", token))); - - let email = data - .mail_client - .message_builder() - .to(email_address.parse()?) - .subject(format!("{} Password Reset", data.config.instance.name)) - .multipart(MultiPart::alternative_plain_html( - format!("{} Password Reset\n\nHello, {}!\nSomeone requested a password reset for your Gorb account.\nClick the button below within 24 hours to reset your password.\n\n{}\n\nIf you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.\n\nThanks, The gorb team.", data.config.instance.name, username, reset_endpoint), - format!(r#"

{} Password Reset

Hello, {}!

Someone requested a password reset for your Gorb account.

Click the button below within 24 hours to reset your password.

RESET PASSWORD

If you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.

"#, data.config.instance.name, username, reset_endpoint) - ))?; - - data.mail_client.send_mail(email).await?; - - Ok(()) - } - - pub async fn set_password(&self, data: &Data, password: String) -> Result<(), Error> { - if !PASSWORD_REGEX.is_match(&password) { - return Err(Error::BadRequest( - "Please provide a valid password".to_string(), - )); - } - - let salt = SaltString::generate(&mut OsRng); - - let hashed_password = data - .argon2 - .hash_password(password.as_bytes(), &salt) - .map_err(|e| Error::PasswordHashError(e.to_string()))?; - - let mut conn = data.pool.get().await?; - - use users::dsl; - update(users::table) - .filter(dsl::uuid.eq(self.user_uuid)) - .set(dsl::password.eq(hashed_password.to_string())) - .execute(&mut conn) - .await?; - - let (username, email_address): (String, String) = dsl::users - .filter(dsl::uuid.eq(self.user_uuid)) - .select((dsl::username, dsl::email)) - .get_result(&mut conn) - .await?; - - let login_page = data.config.web.frontend_url.join("login")?; - - let email = data - .mail_client - .message_builder() - .to(email_address.parse()?) - .subject(format!("Your {} Password has been Reset", data.config.instance.name)) - .multipart(MultiPart::alternative_plain_html( - format!("{} Password Reset Confirmation\n\nHello, {}!\nYour password has been successfully reset for your Gorb account.\nIf you did not initiate this change, please click the link below to reset your password immediately.\n\n{}\n\nThanks, The gorb team.", data.config.instance.name, username, login_page), - format!(r#"

{} Password Reset Confirmation

Hello, {}!

Your password has been successfully reset for your Gorb account.

If you did not initiate this change, please click the button below to reset your password immediately.

RESET PASSWORD
"#, data.config.instance.name, username, login_page) - ))?; - - data.mail_client.send_mail(email).await?; - - self.delete(&data).await - } - - pub async fn delete(&self, data: &Data) -> Result<(), Error> { - data.del_cache_key(format!("{}_password_reset", &self.user_uuid)).await?; - data.del_cache_key(format!("{}", &self.token)).await?; - - Ok(()) - } -} diff --git a/src/objects/role.rs b/src/objects/role.rs deleted file mode 100644 index a78798a..0000000 --- a/src/objects/role.rs +++ /dev/null @@ -1,186 +0,0 @@ -use diesel::{ - ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into, - update, -}; -use diesel_async::RunQueryDsl; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use crate::{error::Error, schema::{role_members, roles}, utils::order_by_is_above, Conn, Data}; - -use super::{HasIsAbove, HasUuid, load_or_empty}; - -#[derive(Deserialize, Serialize, Clone, Queryable, Selectable, Insertable)] -#[diesel(table_name = roles)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct Role { - uuid: Uuid, - guild_uuid: Uuid, - name: String, - color: i32, - is_above: Option, - pub permissions: i64, -} - -#[derive(Serialize, Clone, Queryable, Selectable, Insertable)] -#[diesel(table_name = role_members)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct RoleMember { - role_uuid: Uuid, - member_uuid: Uuid, -} - -impl RoleMember { - async fn fetch_role(&self, conn: &mut Conn) -> Result { - use roles::dsl; - let role: Role = dsl::roles - .filter(dsl::uuid.eq(self.role_uuid)) - .select(Role::as_select()) - .get_result(conn) - .await?; - - Ok(role) - } -} - -impl HasUuid for Role { - fn uuid(&self) -> &Uuid { - self.uuid.as_ref() - } -} - -impl HasIsAbove for Role { - fn is_above(&self) -> Option<&Uuid> { - self.is_above.as_ref() - } -} - -impl Role { - pub async fn fetch_all(conn: &mut Conn, guild_uuid: Uuid) -> Result, Error> { - use roles::dsl; - let roles: Vec = load_or_empty( - dsl::roles - .filter(dsl::guild_uuid.eq(guild_uuid)) - .select(Role::as_select()) - .load(conn) - .await, - )?; - - Ok(roles) - } - - pub async fn fetch_from_member(data: &Data, member_uuid: Uuid) -> Result, Error> { - if let Ok(roles) = data.get_cache_key(format!("{}_roles", member_uuid)).await { - return Ok(serde_json::from_str(&roles)?) - } - - let mut conn = data.pool.get().await?; - - use role_members::dsl; - let role_memberships: Vec = load_or_empty( - dsl::role_members - .filter(dsl::member_uuid.eq(member_uuid)) - .select(RoleMember::as_select()) - .load(&mut conn) - .await, - )?; - - let mut roles = vec![]; - - for membership in role_memberships { - roles.push(membership.fetch_role(&mut conn).await?); - } - - data.set_cache_key(format!("{}_roles", member_uuid), roles.clone(), 300).await?; - - Ok(roles) - } - - pub async fn fetch_one(conn: &mut Conn, role_uuid: Uuid) -> Result { - use roles::dsl; - let role: Role = dsl::roles - .filter(dsl::uuid.eq(role_uuid)) - .select(Role::as_select()) - .get_result(conn) - .await?; - - Ok(role) - } - - pub async fn fetch_permissions(&self) -> Vec { - Permissions::fetch_permissions(self.permissions.clone()) - } - - pub async fn new(conn: &mut Conn, guild_uuid: Uuid, name: String) -> Result { - let role_uuid = Uuid::now_v7(); - - let roles = Self::fetch_all(conn, guild_uuid).await?; - - let roles_ordered = order_by_is_above(roles).await?; - - let last_role = roles_ordered.last(); - - let new_role = Role { - uuid: role_uuid, - guild_uuid, - name, - color: 16777215, - is_above: None, - permissions: 0, - }; - - insert_into(roles::table) - .values(new_role.clone()) - .execute(conn) - .await?; - - if let Some(old_last_role) = last_role { - use roles::dsl; - update(roles::table) - .filter(dsl::uuid.eq(old_last_role.uuid)) - .set(dsl::is_above.eq(new_role.uuid)) - .execute(conn) - .await?; - } - - Ok(new_role) - } -} - -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum Permissions { - SendMessage = 1, - CreateChannel = 2, - DeleteChannel = 4, - ManageChannel = 8, - CreateRole = 16, - DeleteRole = 32, - ManageRole = 64, - CreateInvite = 128, - ManageInvite = 256, - ManageServer = 512, - ManageMember = 1024, -} - -impl Permissions { - pub fn fetch_permissions(permissions: i64) -> Vec { - let all_perms = vec![ - Self::SendMessage, - Self::CreateChannel, - Self::DeleteChannel, - Self::ManageChannel, - Self::CreateRole, - Self::DeleteRole, - Self::ManageRole, - Self::CreateInvite, - Self::ManageInvite, - Self::ManageServer, - Self::ManageMember, - ]; - - all_perms - .into_iter() - .filter(|p| permissions & (*p as i64) != 0) - .collect() - } -} diff --git a/src/objects/user.rs b/src/objects/user.rs deleted file mode 100644 index 98e5e80..0000000 --- a/src/objects/user.rs +++ /dev/null @@ -1,60 +0,0 @@ -use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper}; -use diesel_async::RunQueryDsl; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use crate::{Conn, Data, error::Error, schema::users}; - -use super::load_or_empty; - -#[derive(Deserialize, Serialize, Clone, Queryable, Selectable)] -#[diesel(table_name = users)] -#[diesel(check_for_backend(diesel::pg::Pg))] -pub struct User { - uuid: Uuid, - username: String, - display_name: Option, - avatar: Option, - pronouns: Option, - about: Option, -} - -impl User { - pub async fn fetch_one(data: &Data, user_uuid: Uuid) -> Result { - let mut conn = data.pool.get().await?; - - if let Ok(cache_hit) = data.get_cache_key(user_uuid.to_string()).await { - return Ok(serde_json::from_str(&cache_hit)?); - } - - use users::dsl; - let user: User = dsl::users - .filter(dsl::uuid.eq(user_uuid)) - .select(User::as_select()) - .get_result(&mut conn) - .await?; - - data.set_cache_key(user_uuid.to_string(), user.clone(), 1800) - .await?; - - Ok(user) - } - - pub async fn fetch_amount( - conn: &mut Conn, - offset: i64, - amount: i64, - ) -> Result, Error> { - use users::dsl; - let users: Vec = load_or_empty( - dsl::users - .limit(amount) - .offset(offset) - .select(User::as_select()) - .load(conn) - .await, - )?; - - Ok(users) - } -} diff --git a/src/schema.rs b/src/schema.rs index c7a350c..3be885a 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -31,6 +31,15 @@ diesel::table! { } } +diesel::table! { + email_tokens (token, user_uuid) { + #[max_length = 64] + token -> Varchar, + user_uuid -> Uuid, + created_at -> Timestamptz, + } +} + diesel::table! { guild_members (uuid) { uuid -> Uuid, @@ -38,13 +47,13 @@ diesel::table! { user_uuid -> Uuid, #[max_length = 100] nickname -> Nullable, - is_owner -> Bool, } } diesel::table! { guilds (uuid) { uuid -> Uuid, + owner_uuid -> Uuid, #[max_length = 100] name -> Varchar, #[max_length = 300] @@ -80,6 +89,15 @@ diesel::table! { } } +diesel::table! { + password_reset_tokens (token, user_uuid) { + #[max_length = 64] + token -> Varchar, + user_uuid -> Uuid, + created_at -> Timestamptz, + } +} + diesel::table! { refresh_tokens (token) { #[max_length = 64] @@ -128,8 +146,6 @@ diesel::table! { avatar -> Nullable, #[max_length = 32] pronouns -> Nullable, - #[max_length = 200] - about -> Nullable, } } @@ -137,13 +153,16 @@ diesel::joinable!(access_tokens -> refresh_tokens (refresh_token)); diesel::joinable!(access_tokens -> users (uuid)); diesel::joinable!(channel_permissions -> channels (channel_uuid)); diesel::joinable!(channels -> guilds (guild_uuid)); +diesel::joinable!(email_tokens -> users (user_uuid)); diesel::joinable!(guild_members -> guilds (guild_uuid)); diesel::joinable!(guild_members -> users (user_uuid)); +diesel::joinable!(guilds -> users (owner_uuid)); diesel::joinable!(instance_permissions -> users (uuid)); diesel::joinable!(invites -> guilds (guild_uuid)); diesel::joinable!(invites -> users (user_uuid)); diesel::joinable!(messages -> channels (channel_uuid)); diesel::joinable!(messages -> users (user_uuid)); +diesel::joinable!(password_reset_tokens -> users (user_uuid)); diesel::joinable!(refresh_tokens -> users (uuid)); diesel::joinable!(role_members -> guild_members (member_uuid)); diesel::joinable!(roles -> guilds (guild_uuid)); @@ -152,11 +171,13 @@ diesel::allow_tables_to_appear_in_same_query!( access_tokens, channel_permissions, channels, + email_tokens, guild_members, guilds, instance_permissions, invites, messages, + password_reset_tokens, refresh_tokens, role_members, roles, diff --git a/src/structs.rs b/src/structs.rs new file mode 100644 index 0000000..5e19dad --- /dev/null +++ b/src/structs.rs @@ -0,0 +1,1329 @@ +use actix_web::web::BytesMut; +use argon2::{ + PasswordHasher, + password_hash::{SaltString, rand_core::OsRng}, +}; +use chrono::Utc; +use diesel::{ + ExpressionMethods, QueryDsl, Selectable, SelectableHelper, delete, + dsl::now, + insert_into, + prelude::{Insertable, Queryable}, + update, +}; +use diesel_async::{RunQueryDsl, pooled_connection::AsyncDieselConnectionManager}; +use lettre::{ + AsyncSmtpTransport, AsyncTransport, Message as Email, Tokio1Executor, + message::{Mailbox, MessageBuilder as EmailBuilder, MultiPart}, + transport::smtp::authentication::Credentials, +}; +use log::debug; +use serde::{Deserialize, Serialize}; +use tokio::task; +use url::Url; +use uuid::Uuid; + +use crate::{ + Conn, Data, + error::Error, + schema::*, + utils::{ + EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX, generate_refresh_token, global_checks, + image_check, order_by_is_above, user_uuid_from_identifier, + }, +}; + +pub trait HasUuid { + fn uuid(&self) -> &Uuid; +} + +pub trait HasIsAbove { + fn is_above(&self) -> Option<&Uuid>; +} + +fn load_or_empty( + query_result: Result, diesel::result::Error>, +) -> Result, diesel::result::Error> { + match query_result { + Ok(vec) => Ok(vec), + Err(diesel::result::Error::NotFound) => Ok(Vec::new()), + Err(e) => Err(e), + } +} + +#[derive(PartialEq, Eq, Clone)] +pub enum MailTls { + StartTls, + Tls, +} + +impl From for MailTls { + fn from(value: String) -> Self { + match &*value.to_lowercase() { + "starttls" => Self::StartTls, + _ => Self::Tls, + } + } +} + +#[derive(Clone)] +pub struct MailClient { + creds: Credentials, + smtp_server: String, + mbox: Mailbox, + tls: MailTls, +} + +impl MailClient { + pub fn new>( + creds: Credentials, + smtp_server: String, + mbox: String, + tls: T, + ) -> Result { + Ok(Self { + creds, + smtp_server, + mbox: mbox.parse()?, + tls: tls.into(), + }) + } + + pub fn message_builder(&self) -> EmailBuilder { + Email::builder().from(self.mbox.clone()) + } + + pub async fn send_mail(&self, email: Email) -> Result<(), Error> { + let mailer: AsyncSmtpTransport = match self.tls { + MailTls::StartTls => { + AsyncSmtpTransport::::starttls_relay(&self.smtp_server)? + .credentials(self.creds.clone()) + .build() + } + MailTls::Tls => AsyncSmtpTransport::::relay(&self.smtp_server)? + .credentials(self.creds.clone()) + .build(), + }; + + let response = mailer.send(email).await?; + + debug!("mail sending response: {:?}", response); + + Ok(()) + } +} + +#[derive(Queryable, Selectable, Insertable, Clone, Debug)] +#[diesel(table_name = channels)] +#[diesel(check_for_backend(diesel::pg::Pg))] +struct ChannelBuilder { + uuid: Uuid, + guild_uuid: Uuid, + name: String, + description: Option, + is_above: Option, +} + +impl ChannelBuilder { + async fn build(self, conn: &mut Conn) -> Result { + use self::channel_permissions::dsl::*; + let channel_permission: Vec = load_or_empty( + channel_permissions + .filter(channel_uuid.eq(self.uuid)) + .select(ChannelPermission::as_select()) + .load(conn) + .await, + )?; + + Ok(Channel { + uuid: self.uuid, + guild_uuid: self.guild_uuid, + name: self.name, + description: self.description, + is_above: self.is_above, + permissions: channel_permission, + }) + } +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct Channel { + pub uuid: Uuid, + pub guild_uuid: Uuid, + name: String, + description: Option, + pub is_above: Option, + pub permissions: Vec, +} + +#[derive(Serialize, Deserialize, Clone, Queryable, Selectable, Debug)] +#[diesel(table_name = channel_permissions)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct ChannelPermission { + pub role_uuid: Uuid, + pub permissions: i64, +} + +impl HasUuid for Channel { + fn uuid(&self) -> &Uuid { + self.uuid.as_ref() + } +} + +impl HasIsAbove for Channel { + fn is_above(&self) -> Option<&Uuid> { + self.is_above.as_ref() + } +} + +impl Channel { + pub async fn fetch_all( + pool: &deadpool::managed::Pool< + AsyncDieselConnectionManager, + Conn, + >, + guild_uuid: Uuid, + ) -> Result, Error> { + let mut conn = pool.get().await?; + + use channels::dsl; + let channel_builders: Vec = load_or_empty( + dsl::channels + .filter(dsl::guild_uuid.eq(guild_uuid)) + .select(ChannelBuilder::as_select()) + .load(&mut conn) + .await, + )?; + + let channel_futures = channel_builders.iter().map(async move |c| { + let mut conn = pool.get().await?; + c.clone().build(&mut conn).await + }); + + futures::future::try_join_all(channel_futures).await + } + + pub async fn fetch_one(data: &Data, channel_uuid: Uuid) -> Result { + if let Ok(cache_hit) = data.get_cache_key(channel_uuid.to_string()).await { + return Ok(serde_json::from_str(&cache_hit)?); + } + + let mut conn = data.pool.get().await?; + + use channels::dsl; + let channel_builder: ChannelBuilder = dsl::channels + .filter(dsl::uuid.eq(channel_uuid)) + .select(ChannelBuilder::as_select()) + .get_result(&mut conn) + .await?; + + let channel = channel_builder.build(&mut conn).await?; + + data.set_cache_key(channel_uuid.to_string(), channel.clone(), 60) + .await?; + + Ok(channel) + } + + pub async fn new( + data: actix_web::web::Data, + guild_uuid: Uuid, + name: String, + description: Option, + ) -> Result { + let mut conn = data.pool.get().await?; + + let channel_uuid = Uuid::now_v7(); + + let channels = Self::fetch_all(&data.pool, guild_uuid).await?; + + let channels_ordered = order_by_is_above(channels).await?; + + let last_channel = channels_ordered.last(); + + let new_channel = ChannelBuilder { + uuid: channel_uuid, + guild_uuid, + name: name.clone(), + description: description.clone(), + is_above: None, + }; + + insert_into(channels::table) + .values(new_channel.clone()) + .execute(&mut conn) + .await?; + + if let Some(old_last_channel) = last_channel { + use channels::dsl; + update(channels::table) + .filter(dsl::uuid.eq(old_last_channel.uuid)) + .set(dsl::is_above.eq(new_channel.uuid)) + .execute(&mut conn) + .await?; + } + + // returns different object because there's no reason to build the channelbuilder (wastes 1 database request) + let channel = Self { + uuid: channel_uuid, + guild_uuid, + name, + description, + is_above: None, + permissions: vec![], + }; + + data.set_cache_key(channel_uuid.to_string(), channel.clone(), 1800) + .await?; + + if data + .get_cache_key(format!("{}_channels", guild_uuid)) + .await + .is_ok() + { + data.del_cache_key(format!("{}_channels", guild_uuid)) + .await?; + } + + Ok(channel) + } + + pub async fn delete(self, data: &Data) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + + use channels::dsl; + delete(channels::table) + .filter(dsl::uuid.eq(self.uuid)) + .execute(&mut conn) + .await?; + + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await?; + } + + Ok(()) + } + + pub async fn fetch_messages( + &self, + data: &Data, + amount: i64, + offset: i64, + ) -> Result, Error> { + let mut conn = data.pool.get().await?; + + use messages::dsl; + let messages: Vec = load_or_empty( + dsl::messages + .filter(dsl::channel_uuid.eq(self.uuid)) + .select(MessageBuilder::as_select()) + .order(dsl::uuid.desc()) + .limit(amount) + .offset(offset) + .load(&mut conn) + .await, + )?; + + let message_futures = messages.iter().map(async move |b| b.build(data).await); + + futures::future::try_join_all(message_futures).await + } + + pub async fn new_message( + &self, + data: &Data, + user_uuid: Uuid, + message: String, + ) -> Result { + let message_uuid = Uuid::now_v7(); + + let message = MessageBuilder { + uuid: message_uuid, + channel_uuid: self.uuid, + user_uuid, + message, + }; + + let mut conn = data.pool.get().await?; + + insert_into(messages::table) + .values(message.clone()) + .execute(&mut conn) + .await?; + + message.build(data).await + } +} + +#[derive(Clone, Copy)] +pub enum Permissions { + SendMessage = 1, + CreateChannel = 2, + DeleteChannel = 4, + ManageChannel = 8, + CreateRole = 16, + DeleteRole = 32, + ManageRole = 64, + CreateInvite = 128, + ManageInvite = 256, + ManageServer = 512, + ManageMember = 1024, +} + +impl Permissions { + pub fn fetch_permissions(permissions: i64) -> Vec { + let all_perms = vec![ + Self::SendMessage, + Self::CreateChannel, + Self::DeleteChannel, + Self::ManageChannel, + Self::CreateRole, + Self::DeleteRole, + Self::ManageRole, + Self::CreateInvite, + Self::ManageInvite, + Self::ManageServer, + Self::ManageMember, + ]; + + all_perms + .into_iter() + .filter(|p| permissions & (*p as i64) != 0) + .collect() + } +} + +#[derive(Serialize, Queryable, Selectable, Insertable, Clone)] +#[diesel(table_name = guilds)] +#[diesel(check_for_backend(diesel::pg::Pg))] +struct GuildBuilder { + uuid: Uuid, + name: String, + description: Option, + icon: Option, + owner_uuid: Uuid, +} + +impl GuildBuilder { + async fn build(self, conn: &mut Conn) -> Result { + let member_count = Member::count(conn, self.uuid).await?; + + let roles = Role::fetch_all(conn, self.uuid).await?; + + Ok(Guild { + uuid: self.uuid, + name: self.name, + description: self.description, + icon: self.icon.and_then(|i| i.parse().ok()), + owner_uuid: self.owner_uuid, + roles, + member_count, + }) + } +} + +#[derive(Serialize)] +pub struct Guild { + pub uuid: Uuid, + name: String, + description: Option, + icon: Option, + owner_uuid: Uuid, + pub roles: Vec, + member_count: i64, +} + +impl Guild { + pub async fn fetch_one(conn: &mut Conn, guild_uuid: Uuid) -> Result { + use guilds::dsl; + let guild_builder: GuildBuilder = dsl::guilds + .filter(dsl::uuid.eq(guild_uuid)) + .select(GuildBuilder::as_select()) + .get_result(conn) + .await?; + + guild_builder.build(conn).await + } + + pub async fn fetch_amount( + pool: &deadpool::managed::Pool< + AsyncDieselConnectionManager, + Conn, + >, + offset: i64, + amount: i64, + ) -> Result, Error> { + // Fetch guild data from database + let mut conn = pool.get().await?; + + use guilds::dsl; + let guild_builders: Vec = load_or_empty( + dsl::guilds + .select(GuildBuilder::as_select()) + .order_by(dsl::uuid) + .offset(offset) + .limit(amount) + .load(&mut conn) + .await, + )?; + + // Process each guild concurrently + let guild_futures = guild_builders.iter().map(async move |g| { + let mut conn = pool.get().await?; + g.clone().build(&mut conn).await + }); + + // Execute all futures concurrently and collect results + futures::future::try_join_all(guild_futures).await + } + + pub async fn new(conn: &mut Conn, name: String, owner_uuid: Uuid) -> Result { + let guild_uuid = Uuid::now_v7(); + + let guild_builder = GuildBuilder { + uuid: guild_uuid, + name: name.clone(), + description: None, + icon: None, + owner_uuid, + }; + + insert_into(guilds::table) + .values(guild_builder) + .execute(conn) + .await?; + + let member_uuid = Uuid::now_v7(); + + let member = MemberBuilder { + uuid: member_uuid, + nickname: None, + user_uuid: owner_uuid, + guild_uuid, + }; + + insert_into(guild_members::table) + .values(member) + .execute(conn) + .await?; + + Ok(Guild { + uuid: guild_uuid, + name, + description: None, + icon: None, + owner_uuid, + roles: vec![], + member_count: 1, + }) + } + + pub async fn get_invites(&self, conn: &mut Conn) -> Result, Error> { + use invites::dsl; + let invites = load_or_empty( + dsl::invites + .filter(dsl::guild_uuid.eq(self.uuid)) + .select(Invite::as_select()) + .load(conn) + .await, + )?; + + Ok(invites) + } + + pub async fn create_invite( + &self, + conn: &mut Conn, + user_uuid: Uuid, + custom_id: Option, + ) -> Result { + let invite_id; + + if let Some(id) = custom_id { + invite_id = id; + if invite_id.len() > 32 { + return Err(Error::BadRequest("MAX LENGTH".to_string())); + } + } else { + let charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + + invite_id = random_string::generate(8, charset); + } + + let invite = Invite { + id: invite_id, + user_uuid, + guild_uuid: self.uuid, + }; + + insert_into(invites::table) + .values(invite.clone()) + .execute(conn) + .await?; + + Ok(invite) + } + + // FIXME: Horrible security + pub async fn set_icon( + &mut self, + bunny_cdn: &bunny_api_tokio::Client, + conn: &mut Conn, + cdn_url: Url, + icon: BytesMut, + ) -> Result<(), Error> { + let icon_clone = icon.clone(); + let image_type = task::spawn_blocking(move || image_check(icon_clone)).await??; + + if let Some(icon) = &self.icon { + let relative_url = icon.path().trim_start_matches('/'); + + bunny_cdn.storage.delete(relative_url).await?; + } + + let path = format!("icons/{}/icon.{}", self.uuid, image_type); + + bunny_cdn.storage.upload(path.clone(), icon.into()).await?; + + let icon_url = cdn_url.join(&path)?; + + use guilds::dsl; + update(guilds::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::icon.eq(icon_url.as_str())) + .execute(conn) + .await?; + + self.icon = Some(icon_url); + + Ok(()) + } +} + +#[derive(Serialize, Clone, Queryable, Selectable, Insertable)] +#[diesel(table_name = roles)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Role { + uuid: Uuid, + guild_uuid: Uuid, + name: String, + color: i32, + is_above: Option, + permissions: i64, +} + +impl HasUuid for Role { + fn uuid(&self) -> &Uuid { + self.uuid.as_ref() + } +} + +impl HasIsAbove for Role { + fn is_above(&self) -> Option<&Uuid> { + self.is_above.as_ref() + } +} + +impl Role { + pub async fn fetch_all(conn: &mut Conn, guild_uuid: Uuid) -> Result, Error> { + use roles::dsl; + let roles: Vec = load_or_empty( + dsl::roles + .filter(dsl::guild_uuid.eq(guild_uuid)) + .select(Role::as_select()) + .load(conn) + .await, + )?; + + Ok(roles) + } + + pub async fn fetch_one(conn: &mut Conn, role_uuid: Uuid) -> Result { + use roles::dsl; + let role: Role = dsl::roles + .filter(dsl::uuid.eq(role_uuid)) + .select(Role::as_select()) + .get_result(conn) + .await?; + + Ok(role) + } + + pub async fn new(conn: &mut Conn, guild_uuid: Uuid, name: String) -> Result { + let role_uuid = Uuid::now_v7(); + + let roles = Self::fetch_all(conn, guild_uuid).await?; + + let roles_ordered = order_by_is_above(roles).await?; + + let last_role = roles_ordered.last(); + + let new_role = Role { + uuid: role_uuid, + guild_uuid, + name, + color: 16777215, + is_above: None, + permissions: 0, + }; + + insert_into(roles::table) + .values(new_role.clone()) + .execute(conn) + .await?; + + if let Some(old_last_role) = last_role { + use roles::dsl; + update(roles::table) + .filter(dsl::uuid.eq(old_last_role.uuid)) + .set(dsl::is_above.eq(new_role.uuid)) + .execute(conn) + .await?; + } + + Ok(new_role) + } +} + +#[derive(Serialize, Queryable, Selectable, Insertable)] +#[diesel(table_name = guild_members)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct MemberBuilder { + pub uuid: Uuid, + pub nickname: Option, + pub user_uuid: Uuid, + pub guild_uuid: Uuid, +} + +impl MemberBuilder { + async fn build(&self, data: &Data) -> Result { + let user = User::fetch_one(data, self.user_uuid).await?; + + Ok(Member { + uuid: self.uuid, + nickname: self.nickname.clone(), + user_uuid: self.user_uuid, + guild_uuid: self.guild_uuid, + user, + }) + } +} + +#[derive(Serialize, Deserialize)] +pub struct Member { + pub uuid: Uuid, + pub nickname: Option, + pub user_uuid: Uuid, + pub guild_uuid: Uuid, + user: User, +} + +impl Member { + async fn count(conn: &mut Conn, guild_uuid: Uuid) -> Result { + use guild_members::dsl; + let count: i64 = dsl::guild_members + .filter(dsl::guild_uuid.eq(guild_uuid)) + .count() + .get_result(conn) + .await?; + + Ok(count) + } + + pub async fn check_membership( + conn: &mut Conn, + user_uuid: Uuid, + guild_uuid: Uuid, + ) -> Result<(), Error> { + use guild_members::dsl; + dsl::guild_members + .filter(dsl::user_uuid.eq(user_uuid)) + .filter(dsl::guild_uuid.eq(guild_uuid)) + .select(MemberBuilder::as_select()) + .get_result(conn) + .await?; + + Ok(()) + } + + pub async fn fetch_one(data: &Data, user_uuid: Uuid, guild_uuid: Uuid) -> Result { + let mut conn = data.pool.get().await?; + + use guild_members::dsl; + let member: MemberBuilder = dsl::guild_members + .filter(dsl::user_uuid.eq(user_uuid)) + .filter(dsl::guild_uuid.eq(guild_uuid)) + .select(MemberBuilder::as_select()) + .get_result(&mut conn) + .await?; + + member.build(data).await + } + + pub async fn fetch_all(data: &Data, guild_uuid: Uuid) -> Result, Error> { + let mut conn = data.pool.get().await?; + + use guild_members::dsl; + let member_builders: Vec = load_or_empty( + dsl::guild_members + .filter(dsl::guild_uuid.eq(guild_uuid)) + .select(MemberBuilder::as_select()) + .load(&mut conn) + .await, + )?; + + let member_futures = member_builders + .iter() + .map(async move |m| m.build(data).await); + + futures::future::try_join_all(member_futures).await + } + + pub async fn new(data: &Data, user_uuid: Uuid, guild_uuid: Uuid) -> Result { + let mut conn = data.pool.get().await?; + + let member_uuid = Uuid::now_v7(); + + let member = MemberBuilder { + uuid: member_uuid, + guild_uuid, + user_uuid, + nickname: None, + }; + + insert_into(guild_members::table) + .values(&member) + .execute(&mut conn) + .await?; + + member.build(data).await + } +} + +#[derive(Clone, Queryable, Selectable, Insertable)] +#[diesel(table_name = messages)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct MessageBuilder { + uuid: Uuid, + channel_uuid: Uuid, + user_uuid: Uuid, + message: String, +} + +impl MessageBuilder { + pub async fn build(&self, data: &Data) -> Result { + let user = User::fetch_one(data, self.user_uuid).await?; + + Ok(Message { + uuid: self.uuid, + channel_uuid: self.channel_uuid, + user_uuid: self.user_uuid, + message: self.message.clone(), + user, + }) + } +} + +#[derive(Clone, Serialize)] +pub struct Message { + uuid: Uuid, + channel_uuid: Uuid, + user_uuid: Uuid, + message: String, + user: User, +} + +/// Server invite struct +#[derive(Clone, Serialize, Queryable, Selectable, Insertable)] +pub struct Invite { + /// case-sensitive alphanumeric string with a fixed length of 8 characters, can be up to 32 characters for custom invites + id: String, + /// User that created the invite + user_uuid: Uuid, + /// UUID of the guild that the invite belongs to + pub guild_uuid: Uuid, +} + +impl Invite { + pub async fn fetch_one(conn: &mut Conn, invite_id: String) -> Result { + use invites::dsl; + let invite: Invite = dsl::invites + .filter(dsl::id.eq(invite_id)) + .select(Invite::as_select()) + .get_result(conn) + .await?; + + Ok(invite) + } +} + +#[derive(Deserialize, Serialize, Clone, Queryable, Selectable)] +#[diesel(table_name = users)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct User { + uuid: Uuid, + username: String, + display_name: Option, + avatar: Option, + pronouns: Option, +} + +impl User { + pub async fn fetch_one(data: &Data, user_uuid: Uuid) -> Result { + let mut conn = data.pool.get().await?; + + if let Ok(cache_hit) = data.get_cache_key(user_uuid.to_string()).await { + return Ok(serde_json::from_str(&cache_hit)?); + } + + use users::dsl; + let user: User = dsl::users + .filter(dsl::uuid.eq(user_uuid)) + .select(User::as_select()) + .get_result(&mut conn) + .await?; + + data.set_cache_key(user_uuid.to_string(), user.clone(), 1800) + .await?; + + Ok(user) + } + + pub async fn fetch_amount( + conn: &mut Conn, + offset: i64, + amount: i64, + ) -> Result, Error> { + use users::dsl; + let users: Vec = load_or_empty( + dsl::users + .limit(amount) + .offset(offset) + .select(User::as_select()) + .load(conn) + .await, + )?; + + Ok(users) + } +} + +#[derive(Serialize, Queryable, Selectable)] +#[diesel(table_name = users)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct Me { + pub uuid: Uuid, + username: String, + display_name: Option, + avatar: Option, + pronouns: Option, + email: String, + pub email_verified: bool, +} + +impl Me { + pub async fn get(conn: &mut Conn, user_uuid: Uuid) -> Result { + use users::dsl; + let me: Me = dsl::users + .filter(dsl::uuid.eq(user_uuid)) + .select(Me::as_select()) + .get_result(conn) + .await?; + + Ok(me) + } + + pub async fn fetch_memberships(&self, conn: &mut Conn) -> Result, Error> { + use guild_members::dsl; + let memberships: Vec = load_or_empty( + dsl::guild_members + .filter(dsl::user_uuid.eq(self.uuid)) + .select(MemberBuilder::as_select()) + .load(conn) + .await + )?; + + let mut guilds: Vec = vec![]; + + for membership in memberships { + use guilds::dsl; + guilds.push( + dsl::guilds + .filter(dsl::uuid.eq(membership.guild_uuid)) + .select(GuildBuilder::as_select()) + .get_result(conn) + .await? + .build(conn) + .await?, + ) + } + + Ok(guilds) + } + + pub async fn set_avatar( + &mut self, + data: &Data, + cdn_url: Url, + avatar: BytesMut, + ) -> Result<(), Error> { + let avatar_clone = avatar.clone(); + let image_type = task::spawn_blocking(move || image_check(avatar_clone)).await??; + + let mut conn = data.pool.get().await?; + + if let Some(avatar) = &self.avatar { + let avatar_url: Url = avatar.parse()?; + + let relative_url = avatar_url.path().trim_start_matches('/'); + + data.bunny_cdn.storage.delete(relative_url).await?; + } + + let path = format!("avatar/{}/avatar.{}", self.uuid, image_type); + + data. + bunny_cdn + .storage + .upload(path.clone(), avatar.into()) + .await?; + + let avatar_url = cdn_url.join(&path)?; + + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::avatar.eq(avatar_url.as_str())) + .execute(&mut conn) + .await?; + + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await? + } + + self.avatar = Some(avatar_url.to_string()); + + Ok(()) + } + + pub async fn verify_email(&self, conn: &mut Conn) -> Result<(), Error> { + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::email_verified.eq(true)) + .execute(conn) + .await?; + + Ok(()) + } + + pub async fn set_username( + &mut self, + data: &Data, + new_username: String, + ) -> Result<(), Error> { + if !USERNAME_REGEX.is_match(&new_username) { + return Err(Error::BadRequest("Invalid username".to_string())); + } + + let mut conn = data.pool.get().await?; + + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::username.eq(new_username.as_str())) + .execute(&mut conn) + .await?; + + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await? + } + + self.username = new_username; + + Ok(()) + } + + pub async fn set_display_name( + &mut self, + data: &Data, + new_display_name: String, + ) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(dsl::display_name.eq(new_display_name.as_str())) + .execute(&mut conn) + .await?; + + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await? + } + + self.display_name = Some(new_display_name); + + Ok(()) + } + + pub async fn set_email(&mut self, data: &Data, new_email: String) -> Result<(), Error> { + if !EMAIL_REGEX.is_match(&new_email) { + return Err(Error::BadRequest("Invalid username".to_string())); + } + + let mut conn = data.pool.get().await?; + + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(( + dsl::email.eq(new_email.as_str()), + dsl::email_verified.eq(false), + )) + .execute(&mut conn) + .await?; + + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await? + } + + self.email = new_email; + + Ok(()) + } + + pub async fn set_pronouns(&mut self, data: &Data, new_pronouns: String) -> Result<(), Error> { + let mut conn = data.pool.get().await?; + + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.uuid)) + .set(( + dsl::pronouns.eq(new_pronouns.as_str()), + )) + .execute(&mut conn) + .await?; + + if data.get_cache_key(self.uuid.to_string()).await.is_ok() { + data.del_cache_key(self.uuid.to_string()).await? + } + + Ok(()) + } +} + +#[derive(Deserialize)] +pub struct StartAmountQuery { + pub start: Option, + pub amount: Option, +} + +#[derive(Selectable, Queryable)] +#[diesel(table_name = email_tokens)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct EmailToken { + user_uuid: Uuid, + pub token: String, + pub created_at: chrono::DateTime, +} + +impl EmailToken { + pub async fn get(conn: &mut Conn, user_uuid: Uuid) -> Result { + use email_tokens::dsl; + let email_token = dsl::email_tokens + .filter(dsl::user_uuid.eq(user_uuid)) + .select(EmailToken::as_select()) + .get_result(conn) + .await?; + + Ok(email_token) + } + + #[allow(clippy::new_ret_no_self)] + pub async fn new(data: &Data, me: Me) -> Result<(), Error> { + let token = generate_refresh_token()?; + + let mut conn = data.pool.get().await?; + + use email_tokens::dsl; + insert_into(email_tokens::table) + .values(( + dsl::user_uuid.eq(me.uuid), + dsl::token.eq(&token), + dsl::created_at.eq(now), + )) + .execute(&mut conn) + .await?; + + let mut verify_endpoint = data.config.web.frontend_url.join("verify-email")?; + + verify_endpoint.set_query(Some(&format!("token={}", token))); + + let email = data + .mail_client + .message_builder() + .to(me.email.parse()?) + .subject(format!("{} E-mail Verification", data.config.instance.name)) + .multipart(MultiPart::alternative_plain_html( + format!("Verify your {} account\n\nHello, {}!\nThanks for creating a new account on Gorb.\nThe final step to create your account is to verify your email address by visiting the page, within 24 hours.\n\n{}\n\nIf you didn't ask to verify this address, you can safely ignore this email\n\nThanks, The gorb team.", data.config.instance.name, me.username, verify_endpoint), + format!(r#"

Verify your {} Account

Hello, {}!

Thanks for creating a new account on Gorb.

The final step to create your account is to verify your email address by clicking the button below, within 24 hours.

VERIFY ACCOUNT

If you didn't ask to verify this address, you can safely ignore this email.

"#, data.config.instance.name, me.username, verify_endpoint) + ))?; + + data.mail_client.send_mail(email).await?; + + Ok(()) + } + + pub async fn delete(&self, conn: &mut Conn) -> Result<(), Error> { + use email_tokens::dsl; + delete(email_tokens::table) + .filter(dsl::user_uuid.eq(self.user_uuid)) + .filter(dsl::token.eq(&self.token)) + .execute(conn) + .await?; + + Ok(()) + } +} + +#[derive(Selectable, Queryable)] +#[diesel(table_name = password_reset_tokens)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct PasswordResetToken { + user_uuid: Uuid, + pub token: String, + pub created_at: chrono::DateTime, +} + +impl PasswordResetToken { + pub async fn get(conn: &mut Conn, token: String) -> Result { + use password_reset_tokens::dsl; + let password_reset_token = dsl::password_reset_tokens + .filter(dsl::token.eq(token)) + .select(PasswordResetToken::as_select()) + .get_result(conn) + .await?; + + Ok(password_reset_token) + } + + pub async fn get_with_identifier( + conn: &mut Conn, + identifier: String, + ) -> Result { + let user_uuid = user_uuid_from_identifier(conn, &identifier).await?; + + use password_reset_tokens::dsl; + let password_reset_token = dsl::password_reset_tokens + .filter(dsl::user_uuid.eq(user_uuid)) + .select(PasswordResetToken::as_select()) + .get_result(conn) + .await?; + + Ok(password_reset_token) + } + + #[allow(clippy::new_ret_no_self)] + pub async fn new(data: &Data, identifier: String) -> Result<(), Error> { + let token = generate_refresh_token()?; + + let mut conn = data.pool.get().await?; + + let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?; + + global_checks(data, user_uuid).await?; + + use users::dsl as udsl; + let (username, email_address): (String, String) = udsl::users + .filter(udsl::uuid.eq(user_uuid)) + .select((udsl::username, udsl::email)) + .get_result(&mut conn) + .await?; + + use password_reset_tokens::dsl; + insert_into(password_reset_tokens::table) + .values(( + dsl::user_uuid.eq(user_uuid), + dsl::token.eq(&token), + dsl::created_at.eq(now), + )) + .execute(&mut conn) + .await?; + + let mut reset_endpoint = data.config.web.frontend_url.join("reset-password")?; + + reset_endpoint.set_query(Some(&format!("token={}", token))); + + let email = data + .mail_client + .message_builder() + .to(email_address.parse()?) + .subject(format!("{} Password Reset", data.config.instance.name)) + .multipart(MultiPart::alternative_plain_html( + format!("{} Password Reset\n\nHello, {}!\nSomeone requested a password reset for your Gorb account.\nClick the button below within 24 hours to reset your password.\n\n{}\n\nIf you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.\n\nThanks, The gorb team.", data.config.instance.name, username, reset_endpoint), + format!(r#"

{} Password Reset

Hello, {}!

Someone requested a password reset for your Gorb account.

Click the button below within 24 hours to reset your password.

RESET PASSWORD

If you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.

"#, data.config.instance.name, username, reset_endpoint) + ))?; + + data.mail_client.send_mail(email).await?; + + Ok(()) + } + + pub async fn set_password(&self, data: &Data, password: String) -> Result<(), Error> { + if !PASSWORD_REGEX.is_match(&password) { + return Err(Error::BadRequest( + "Please provide a valid password".to_string(), + )); + } + + let salt = SaltString::generate(&mut OsRng); + + let hashed_password = data + .argon2 + .hash_password(password.as_bytes(), &salt) + .map_err(|e| Error::PasswordHashError(e.to_string()))?; + + let mut conn = data.pool.get().await?; + + use users::dsl; + update(users::table) + .filter(dsl::uuid.eq(self.user_uuid)) + .set(dsl::password.eq(hashed_password.to_string())) + .execute(&mut conn) + .await?; + + let (username, email_address): (String, String) = dsl::users + .filter(dsl::uuid.eq(self.user_uuid)) + .select((dsl::username, dsl::email)) + .get_result(&mut conn) + .await?; + + let login_page = data.config.web.frontend_url.join("login")?; + + let email = data + .mail_client + .message_builder() + .to(email_address.parse()?) + .subject(format!("Your {} Password has been Reset", data.config.instance.name)) + .multipart(MultiPart::alternative_plain_html( + format!("{} Password Reset Confirmation\n\nHello, {}!\nYour password has been successfully reset for your Gorb account.\nIf you did not initiate this change, please click the link below to reset your password immediately.\n\n{}\n\nThanks, The gorb team.", data.config.instance.name, username, login_page), + format!(r#"

{} Password Reset Confirmation

Hello, {}!

Your password has been successfully reset for your Gorb account.

If you did not initiate this change, please click the button below to reset your password immediately.

RESET PASSWORD
"#, data.config.instance.name, username, login_page) + ))?; + + data.mail_client.send_mail(email).await?; + + self.delete(&mut conn).await + } + + pub async fn delete(&self, conn: &mut Conn) -> Result<(), Error> { + use password_reset_tokens::dsl; + delete(password_reset_tokens::table) + .filter(dsl::user_uuid.eq(self.user_uuid)) + .filter(dsl::token.eq(&self.token)) + .execute(conn) + .await?; + + Ok(()) + } +} diff --git a/src/utils.rs b/src/utils.rs index 7a5581a..c9f3cb2 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -19,8 +19,8 @@ use crate::{ Conn, Data, config::Config, error::Error, - objects::{HasIsAbove, HasUuid}, schema::users, + structs::{HasIsAbove, HasUuid}, }; pub static EMAIL_REGEX: LazyLock = LazyLock::new(|| { @@ -30,9 +30,6 @@ pub static EMAIL_REGEX: LazyLock = LazyLock::new(|| { pub static USERNAME_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"^[a-z0-9_.-]+$").unwrap()); -pub static CHANNEL_REGEX: LazyLock = - LazyLock::new(|| Regex::new(r"^[a-z0-9_.-]+$").unwrap()); - // Password is expected to be hashed using SHA3-384 pub static PASSWORD_REGEX: LazyLock = LazyLock::new(|| Regex::new(r"[0-9a-f]{96}").unwrap()); @@ -115,8 +112,14 @@ pub fn new_refresh_token_cookie(config: &Config, refresh_token: String) -> Cooki .finish() } -pub fn generate_token() -> Result { - let mut buf = [0u8; N]; +pub fn generate_access_token() -> Result { + let mut buf = [0u8; 16]; + fill(&mut buf)?; + Ok(encode(buf)) +} + +pub fn generate_refresh_token() -> Result { + let mut buf = [0u8; 32]; fill(&mut buf)?; Ok(encode(buf)) }