diff --git a/.gitignore b/.gitignore index 060148d..7cd509b 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,3 @@ Cargo.lock # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ /config.toml -/key.pem diff --git a/Cargo.toml b/Cargo.toml index 7ac8754..1c5f34b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,15 +27,15 @@ regex = "1.11" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" simple_logger = "5.0.0" -redis = { version = "0.31.0", features= ["tokio-comp"] } -tokio-tungstenite = { version = "0.26", features = ["native-tls", "url"] } +redis = { version = "0.32", features= ["tokio-comp"] } +tokio-tungstenite = { version = "0.27", 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 = "0.3.0" +bunny-api-tokio = { version = "0.4", features = ["edge_storage"], default-features = false } bindet = "0.3.2" deadpool = "0.12" diesel = { version = "2.2", features = ["uuid", "chrono"], default-features = false } @@ -43,9 +43,8 @@ diesel-async = { version = "0.5", features = ["deadpool", "postgres", "async-con diesel_migrations = { version = "2.2.0", features = ["postgres"] } thiserror = "2.0.12" actix-multipart = "0.7.2" -lettre = { version = "0.11.16", features = ["tokio1", "tokio1-native-tls"] } +lettre = { version = "0.11", features = ["tokio1", "tokio1-native-tls"] } chrono = { version = "0.4.41", features = ["serde"] } -ed25519-dalek = { version = "2.1.1", features = ["pem", "pkcs8", "rand_core"] } [dependencies.tokio] version = "1.45" diff --git a/entrypoint.sh b/entrypoint.sh index 98729db..38ba890 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -65,4 +65,4 @@ rotate_log "/gorb/logs/backend.log" # Give the DB time to start up before connecting sleep 5 -/usr/bin/gorb-backend --config /gorb/config/config.toml --private-key /gorb/config/federation-privkey.pem 2>&1 | tee /gorb/logs/backend.log +/usr/bin/gorb-backend --config /gorb/config/config.toml 2>&1 | tee /gorb/logs/backend.log diff --git a/migrations/2025-06-03-123458_federated_instances/down.sql b/migrations/2025-06-03-123458_federated_instances/down.sql deleted file mode 100644 index b9ca2c0..0000000 --- a/migrations/2025-06-03-123458_federated_instances/down.sql +++ /dev/null @@ -1,3 +0,0 @@ --- This file should undo anything in `up.sql` -DROP TABLE federated_users; -DROP TABLE instances; diff --git a/migrations/2025-06-03-123458_federated_instances/up.sql b/migrations/2025-06-03-123458_federated_instances/up.sql deleted file mode 100644 index 4113798..0000000 --- a/migrations/2025-06-03-123458_federated_instances/up.sql +++ /dev/null @@ -1,10 +0,0 @@ --- Your SQL goes here -CREATE TABLE instances ( - instance_url VARCHAR(8000) PRIMARY KEY NOT NULL, - public_key VARCHAR(500) UNIQUE NOT NULL -); - -CREATE TABLE federated_users ( - uuid UUID PRIMARY KEY NOT NULL, - instance_url VARCHAR(8000) NOT NULL REFERENCES instances(instance_url) -); diff --git a/migrations/2025-06-06-145916_guild_ownership_changes/down.sql b/migrations/2025-06-06-145916_guild_ownership_changes/down.sql new file mode 100644 index 0000000..21a08c9 --- /dev/null +++ b/migrations/2025-06-06-145916_guild_ownership_changes/down.sql @@ -0,0 +1,14 @@ +-- 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 new file mode 100644 index 0000000..b94323f --- /dev/null +++ b/migrations/2025-06-06-145916_guild_ownership_changes/up.sql @@ -0,0 +1,14 @@ +-- 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 e190c2f..ac6c1ad 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_access_token, generate_refresh_token, new_refresh_token_cookie, + PASSWORD_REGEX, generate_token, new_refresh_token_cookie, user_uuid_from_identifier, }, }; @@ -59,8 +59,8 @@ pub async fn response( )); } - let refresh_token = generate_refresh_token()?; - let access_token = generate_access_token()?; + let refresh_token = generate_token::<32>()?; + let access_token = generate_token::<16>()?; 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 63e150e..1f4f406 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_access_token, generate_refresh_token, new_refresh_token_cookie}, + utils::{generate_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_refresh_token()?; + let new_refresh_token = generate_token::<32>()?; 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()?; 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 66e2989..1d28088 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_access_token, generate_refresh_token, + EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX, generate_token, new_refresh_token_cookie, }, }; @@ -120,8 +120,8 @@ pub async fn res( .execute(&mut conn) .await?; - let refresh_token = generate_refresh_token()?; - let access_token = generate_access_token()?; + let refresh_token = generate_token::<32>()?; + let access_token = generate_token::<16>()?; let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; diff --git a/src/api/v1/channels/uuid/mod.rs b/src/api/v1/channels/uuid/mod.rs index 1cb20c7..bece6ed 100644 --- a/src/api/v1/channels/uuid/mod.rs +++ b/src/api/v1/channels/uuid/mod.rs @@ -4,11 +4,7 @@ pub mod messages; pub mod socket; use crate::{ - Data, - api::v1::auth::check_access_token, - error::Error, - objects::{Channel, Member}, - utils::{get_auth_header, global_checks}, + api::v1::auth::check_access_token, error::Error, objects::{Channel, Member, Permissions}, utils::{get_auth_header, global_checks}, Data }; use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web}; use serde::Deserialize; @@ -59,7 +55,9 @@ pub async fn delete( let channel = Channel::fetch_one(&data, channel_uuid).await?; - Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; + let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; + + member.check_permission(&data, Permissions::DeleteChannel).await?; channel.delete(&data).await?; @@ -125,7 +123,9 @@ pub async fn patch( let mut channel = Channel::fetch_one(&data, channel_uuid).await?; - Member::check_membership(&mut conn, uuid, channel.guild_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?; diff --git a/src/api/v1/federation/mod.rs b/src/api/v1/federation/mod.rs deleted file mode 100644 index 16901d4..0000000 --- a/src/api/v1/federation/mod.rs +++ /dev/null @@ -1,44 +0,0 @@ -use actix_web::{post, web, HttpRequest, Scope}; - -use crate::{objects::Signature, Data}; - -mod pubkey; - -pub fn web() -> Scope { - web::scope("/federation") - .service(pubkey::get) - .service(post) -} - -#[post("")] -pub async fn post( - req: HttpRequest, - channel_info: web::Json<>, - data: web::Data, -) -> Result { - let headers = req.headers(); - - let signature = Signature::from_signature_header(headers)?; - - let guild_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?; - - Member::check_membership(&mut conn, uuid, guild_uuid).await?; - - // FIXME: Logic to check permissions, should probably be done in utils.rs - - let channel = Channel::new( - data.clone(), - guild_uuid, - channel_info.name.clone(), - channel_info.description.clone(), - ) - .await?; - - Ok(HttpResponse::Ok().json(channel)) -} diff --git a/src/api/v1/federation/pubkey.rs b/src/api/v1/federation/pubkey.rs deleted file mode 100644 index 3ce9f8d..0000000 --- a/src/api/v1/federation/pubkey.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! `/api/v1/users/{uuid}` Specific user endpoints - -use actix_web::{HttpResponse, get, web}; -use ed25519_dalek::pkcs8::{spki::der::pem::LineEnding, EncodePublicKey}; - -use crate::{ - Data, - error::Error, -}; - -/// `GET /api/v1/users/{uuid}` Returns user with the given UUID -/// -/// requires auth: yes -/// -/// requires relation: yes -/// -/// ### Response Example -/// ``` -/// "" -/// ``` -/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps -#[get("/pubkey")] -pub async fn get( - data: web::Data, -) -> Result { - let pubkey = data.signing_key.verifying_key().to_public_key_pem(LineEnding::LF)?; - - Ok(HttpResponse::Ok().body(pubkey)) -} diff --git a/src/api/v1/guilds/uuid/channels.rs b/src/api/v1/guilds/uuid/channels.rs index 083553a..db895e4 100644 --- a/src/api/v1/guilds/uuid/channels.rs +++ b/src/api/v1/guilds/uuid/channels.rs @@ -1,9 +1,5 @@ use crate::{ - Data, - api::v1::auth::check_access_token, - error::Error, - objects::{Channel, Member}, - utils::{get_auth_header, global_checks, order_by_is_above}, + api::v1::auth::check_access_token, error::Error, objects::{Channel, Member, Permissions}, utils::{get_auth_header, global_checks, order_by_is_above}, Data }; use ::uuid::Uuid; use actix_web::{HttpRequest, HttpResponse, get, post, web}; @@ -74,9 +70,9 @@ pub async fn create( global_checks(&data, uuid).await?; - Member::check_membership(&mut conn, uuid, guild_uuid).await?; + let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; - // FIXME: Logic to check permissions, should probably be done in utils.rs + member.check_permission(&data, Permissions::CreateChannel).await?; 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 5025416..0860435 100644 --- a/src/api/v1/guilds/uuid/icon.rs +++ b/src/api/v1/guilds/uuid/icon.rs @@ -5,11 +5,7 @@ use futures_util::StreamExt as _; use uuid::Uuid; use crate::{ - Data, - api::v1::auth::check_access_token, - error::Error, - objects::{Guild, Member}, - utils::{get_auth_header, global_checks}, + api::v1::auth::check_access_token, error::Error, objects::{Guild, Member, Permissions}, utils::{get_auth_header, global_checks}, Data }; /// `PUT /api/v1/guilds/{uuid}/icon` Icon upload @@ -36,7 +32,9 @@ pub async fn upload( global_checks(&data, uuid).await?; - Member::check_membership(&mut conn, uuid, guild_uuid).await?; + let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; + + member.check_permission(&data, Permissions::ManageServer).await?; let mut guild = Guild::fetch_one(&mut conn, guild_uuid).await?; @@ -47,7 +45,7 @@ pub async fn upload( guild .set_icon( - &data.bunny_cdn, + &data.bunny_storage, &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 bb8269c..eb8d2ce 100644 --- a/src/api/v1/guilds/uuid/invites/mod.rs +++ b/src/api/v1/guilds/uuid/invites/mod.rs @@ -3,11 +3,7 @@ use serde::Deserialize; use uuid::Uuid; use crate::{ - Data, - api::v1::auth::check_access_token, - error::Error, - objects::{Guild, Member}, - utils::{get_auth_header, global_checks}, + api::v1::auth::check_access_token, error::Error, objects::{Guild, Member, Permissions}, utils::{get_auth_header, global_checks}, Data }; #[derive(Deserialize)] @@ -61,7 +57,9 @@ pub async fn create( global_checks(&data, uuid).await?; - Member::check_membership(&mut conn, uuid, guild_uuid).await?; + let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; + + member.check_permission(&data, Permissions::CreateInvite).await?; let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; diff --git a/src/api/v1/guilds/uuid/roles/mod.rs b/src/api/v1/guilds/uuid/roles/mod.rs index 717b30b..c33f144 100644 --- a/src/api/v1/guilds/uuid/roles/mod.rs +++ b/src/api/v1/guilds/uuid/roles/mod.rs @@ -3,11 +3,7 @@ use actix_web::{HttpRequest, HttpResponse, get, post, web}; use serde::Deserialize; use crate::{ - Data, - api::v1::auth::check_access_token, - error::Error, - objects::{Member, Role}, - utils::{get_auth_header, global_checks, order_by_is_above}, + api::v1::auth::check_access_token, error::Error, objects::{Member, Permissions, Role}, utils::{get_auth_header, global_checks, order_by_is_above}, Data }; pub mod uuid; @@ -70,9 +66,9 @@ pub async fn create( global_checks(&data, uuid).await?; - Member::check_membership(&mut conn, uuid, guild_uuid).await?; + let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; - // FIXME: Logic to check permissions, should probably be done in utils.rs + member.check_permission(&data, Permissions::CreateRole).await?; let role = Role::new(&mut conn, guild_uuid, role_info.name.clone()).await?; diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index 9422fe4..6c2df0b 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -9,7 +9,6 @@ mod invites; mod me; mod stats; mod users; -mod federation; pub fn web() -> Scope { web::scope("/v1") @@ -20,5 +19,4 @@ pub fn web() -> Scope { .service(guilds::web()) .service(invites::web()) .service(me::web()) - .service(federation::web()) } diff --git a/src/error.rs b/src/error.rs index 860400c..1b1bfba 100644 --- a/src/error.rs +++ b/src/error.rs @@ -12,7 +12,6 @@ use bunny_api_tokio::error::Error as BunnyError; use deadpool::managed::{BuildError, PoolError}; use diesel::{ConnectionError, result::Error as DieselError}; use diesel_async::pooled_connection::PoolError as DieselPoolError; -use ed25519_dalek::pkcs8::{self, spki}; use lettre::{ address::AddressError, error::Error as EmailError, transport::smtp::Error as SmtpError, }; @@ -64,10 +63,6 @@ pub enum Error { SmtpError(#[from] SmtpError), #[error(transparent)] SmtpAddressError(#[from] AddressError), - #[error(transparent)] - Pkcs8Error(#[from] pkcs8::Error), - #[error(transparent)] - SpkiError(#[from] spki::Error), #[error("{0}")] PasswordHashError(String), #[error("{0}")] diff --git a/src/main.rs b/src/main.rs index 71fa846..47794e3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,12 @@ use actix_cors::Cors; use actix_web::{App, HttpServer, web}; -use argon2::{password_hash::rand_core::OsRng, Argon2}; +use argon2::Argon2; use clap::Parser; use diesel_async::pooled_connection::AsyncDieselConnectionManager; use diesel_async::pooled_connection::deadpool::Pool; -use ed25519_dalek::{pkcs8::{spki::der::pem::LineEnding, DecodePrivateKey, EncodePrivateKey}, SigningKey}; use error::Error; use objects::MailClient; use simple_logger::SimpleLogger; -use tokio::{fs::{read_to_string, File}, io::AsyncWriteExt}; use std::time::SystemTime; mod config; use config::{Config, ConfigBuilder}; @@ -30,8 +28,6 @@ pub mod utils; struct Args { #[arg(short, long, default_value_t = String::from("/etc/gorb/config.toml"))] config: String, - #[arg(short, long, default_value_t = String::from("/etc/gorb/privkey.pem"))] - private_key: String, } #[derive(Clone)] @@ -44,9 +40,8 @@ pub struct Data { pub config: Config, pub argon2: Argon2<'static>, pub start_time: SystemTime, - pub bunny_cdn: bunny_api_tokio::Client, + pub bunny_storage: bunny_api_tokio::EdgeStorageClient, pub mail_client: MailClient, - pub signing_key: SigningKey, } #[tokio::main] @@ -70,14 +65,9 @@ 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(); - bunny_cdn - .storage - .init(bunny.api_key, bunny.endpoint, bunny.storage_zone) - .await?; + let bunny_storage = bunny_api_tokio::EdgeStorageClient::new(bunny.api_key, bunny.endpoint, bunny.storage_zone).await?; let mail = config.mail.clone(); @@ -103,18 +93,6 @@ async fn main() -> Result<(), Error> { .await? .unwrap(); - let signing_key; - - if let Ok(content) = read_to_string(&args.private_key).await { - signing_key = SigningKey::from_pkcs8_pem(&content)?; - } else { - let mut csprng = OsRng; - signing_key = tokio::task::spawn_blocking(move || SigningKey::generate(&mut csprng)).await?; - - let mut file = File::create(args.private_key).await?; - file.write_all(signing_key.to_pkcs8_pem(LineEnding::LF)?.as_bytes()).await?; - } - /* **Stored for later possible use** @@ -139,9 +117,8 @@ 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_cdn, + bunny_storage, mail_client, - signing_key, }; HttpServer::new(move || { diff --git a/src/objects/channel.rs b/src/objects/channel.rs index 9b756f2..4d52353 100644 --- a/src/objects/channel.rs +++ b/src/objects/channel.rs @@ -198,15 +198,46 @@ impl Channel { 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(()) } diff --git a/src/objects/email_token.rs b/src/objects/email_token.rs index f55de8c..4ec6b7e 100644 --- a/src/objects/email_token.rs +++ b/src/objects/email_token.rs @@ -3,7 +3,7 @@ use lettre::message::MultiPart; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{Data, error::Error, utils::generate_refresh_token}; +use crate::{Data, error::Error, utils::generate_token}; use super::Me; @@ -23,7 +23,7 @@ impl EmailToken { #[allow(clippy::new_ret_no_self)] pub async fn new(data: &Data, me: Me) -> Result<(), Error> { - let token = generate_refresh_token()?; + let token = generate_token::<32>()?; let email_token = EmailToken { user_uuid: me.uuid, diff --git a/src/objects/guild.rs b/src/objects/guild.rs index f5e973d..47058ee 100644 --- a/src/objects/guild.rs +++ b/src/objects/guild.rs @@ -26,7 +26,6 @@ pub struct GuildBuilder { name: String, description: Option, icon: Option, - owner_uuid: Uuid, } impl GuildBuilder { @@ -40,7 +39,6 @@ impl GuildBuilder { name: self.name, description: self.description, icon: self.icon.and_then(|i| i.parse().ok()), - owner_uuid: self.owner_uuid, roles, member_count, }) @@ -53,7 +51,6 @@ pub struct Guild { name: String, description: Option, icon: Option, - owner_uuid: Uuid, pub roles: Vec, member_count: i64, } @@ -110,7 +107,6 @@ impl Guild { name: name.clone(), description: None, icon: None, - owner_uuid, }; insert_into(guilds::table) @@ -125,6 +121,7 @@ impl Guild { nickname: None, user_uuid: owner_uuid, guild_uuid, + is_owner: true, }; insert_into(guild_members::table) @@ -137,7 +134,6 @@ impl Guild { name, description: None, icon: None, - owner_uuid, roles: vec![], member_count: 1, }) @@ -192,7 +188,7 @@ impl Guild { // FIXME: Horrible security pub async fn set_icon( &mut self, - bunny_cdn: &bunny_api_tokio::Client, + bunny_storage: &bunny_api_tokio::EdgeStorageClient, conn: &mut Conn, cdn_url: Url, icon: BytesMut, @@ -203,12 +199,12 @@ impl Guild { if let Some(icon) = &self.icon { let relative_url = icon.path().trim_start_matches('/'); - bunny_cdn.storage.delete(relative_url).await?; + bunny_storage.delete(relative_url).await?; } let path = format!("icons/{}/icon.{}", self.uuid, image_type); - bunny_cdn.storage.upload(path.clone(), icon.into()).await?; + bunny_storage.upload(path.clone(), icon.into()).await?; let icon_url = cdn_url.join(&path)?; diff --git a/src/objects/me.rs b/src/objects/me.rs index 6af5bce..e183c5d 100644 --- a/src/objects/me.rs +++ b/src/objects/me.rs @@ -85,13 +85,12 @@ impl Me { let relative_url = avatar_url.path().trim_start_matches('/'); - data.bunny_cdn.storage.delete(relative_url).await?; + data.bunny_storage.delete(relative_url).await?; } let path = format!("avatar/{}/avatar.{}", self.uuid, image_type); - data.bunny_cdn - .storage + data.bunny_storage .upload(path.clone(), avatar.into()) .await?; diff --git a/src/objects/member.rs b/src/objects/member.rs index f18e726..20bc848 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -5,7 +5,7 @@ use diesel_async::RunQueryDsl; use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{Conn, Data, error::Error, schema::guild_members}; +use crate::{error::Error, objects::{Permissions, Role}, schema::guild_members, Conn, Data}; use super::{User, load_or_empty}; @@ -17,10 +17,11 @@ pub struct MemberBuilder { pub nickname: Option, pub user_uuid: Uuid, pub guild_uuid: Uuid, + pub is_owner: bool, } impl MemberBuilder { - async fn build(&self, data: &Data) -> Result { + pub async fn build(&self, data: &Data) -> Result { let user = User::fetch_one(data, self.user_uuid).await?; Ok(Member { @@ -28,9 +29,22 @@ impl MemberBuilder { 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)] @@ -39,6 +53,7 @@ pub struct Member { pub nickname: Option, pub user_uuid: Uuid, pub guild_uuid: Uuid, + pub is_owner: bool, user: User, } @@ -58,16 +73,16 @@ impl Member { conn: &mut Conn, user_uuid: Uuid, guild_uuid: Uuid, - ) -> Result<(), Error> { + ) -> Result { use guild_members::dsl; - dsl::guild_members + 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(()) + Ok(member_builder) } pub async fn fetch_one(data: &Data, user_uuid: Uuid, guild_uuid: Uuid) -> Result { @@ -113,6 +128,7 @@ impl Member { guild_uuid, user_uuid, nickname: None, + is_owner: false, }; insert_into(guild_members::table) diff --git a/src/objects/mod.rs b/src/objects/mod.rs index 8731ed5..30a0a64 100644 --- a/src/objects/mod.rs +++ b/src/objects/mod.rs @@ -17,7 +17,6 @@ mod message; mod password_reset_token; mod role; mod user; -mod signature; pub use channel::Channel; pub use email_token::EmailToken; @@ -28,8 +27,8 @@ 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; -pub use signature::Signature; use crate::error::Error; @@ -113,44 +112,6 @@ impl MailClient { } } -#[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(Deserialize)] pub struct StartAmountQuery { pub start: Option, diff --git a/src/objects/password_reset_token.rs b/src/objects/password_reset_token.rs index 0376d88..e14d25a 100644 --- a/src/objects/password_reset_token.rs +++ b/src/objects/password_reset_token.rs @@ -12,10 +12,10 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::{ - Data, error::Error, schema::users, - utils::{PASSWORD_REGEX, generate_refresh_token, global_checks, user_uuid_from_identifier}, + utils::{generate_token, global_checks, user_uuid_from_identifier, PASSWORD_REGEX}, + Data }; #[derive(Serialize, Deserialize)] @@ -48,7 +48,7 @@ impl PasswordResetToken { #[allow(clippy::new_ret_no_self)] pub async fn new(data: &Data, identifier: String) -> Result<(), Error> { - let token = generate_refresh_token()?; + let token = generate_token::<32>()?; let mut conn = data.pool.get().await?; diff --git a/src/objects/role.rs b/src/objects/role.rs index f67dc6d..a78798a 100644 --- a/src/objects/role.rs +++ b/src/objects/role.rs @@ -3,14 +3,14 @@ use diesel::{ update, }; use diesel_async::RunQueryDsl; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::{Conn, error::Error, schema::roles, utils::order_by_is_above}; +use crate::{error::Error, schema::{role_members, roles}, utils::order_by_is_above, Conn, Data}; use super::{HasIsAbove, HasUuid, load_or_empty}; -#[derive(Serialize, Clone, Queryable, Selectable, Insertable)] +#[derive(Deserialize, Serialize, Clone, Queryable, Selectable, Insertable)] #[diesel(table_name = roles)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct Role { @@ -19,7 +19,28 @@ pub struct Role { name: String, color: i32, is_above: Option, - permissions: i64, + 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 { @@ -48,6 +69,33 @@ impl Role { 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 @@ -59,6 +107,10 @@ impl Role { 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(); @@ -94,3 +146,41 @@ impl Role { 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/signature.rs b/src/objects/signature.rs deleted file mode 100644 index a333906..0000000 --- a/src/objects/signature.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::collections::HashMap; - -use actix_web::http::header::HeaderMap; -use url::Url; - -use crate::error::Error; - -pub struct Signature { - pub url: Url, - pub signature: String, -} - -impl Signature { - pub fn from_signature_header(headers: &HeaderMap) -> Result { - let signature_header = headers.get(actix_web::http::header::HeaderName::from_static("signature")); - - if signature_header.is_none() { - return Err(Error::Unauthorized( - "No signature header provided".to_string(), - )); - } - - let signature_raw = signature_header.unwrap().to_str()?; - - let key_values = signature_raw.split_whitespace(); - - let mut hash_map = HashMap::new(); - - let results: Result, Error> = key_values.map(|kv| { - let mut kv_split = kv.split('='); - let key = kv_split.next().unwrap().to_string(); - let value = kv_split.next().ok_or(Error::BadRequest(format!(r#"Expected key="value", found {}"#, key)))?.trim_matches('"').to_string(); - - hash_map.insert(key, value); - - Ok::<(), Error>(()) - }).collect(); - - results?; - - let key_id = hash_map.get("keyId"); - let algorithm = hash_map.get("algorithm"); - let signature = hash_map.get("signature"); - - if key_id.is_none() { - return Err(Error::BadRequest("No keyId was provided".to_string())) - } - - - if algorithm.is_none() { - return Err(Error::BadRequest("No key algorithm was provided".to_string())) - } - - if signature.is_none() { - return Err(Error::BadRequest("No signature was provided".to_string())) - } - - let key_id = key_id.unwrap(); - let algorithm = algorithm.unwrap(); - let signature = signature.unwrap(); - - if algorithm != "ed25519" { - return Err(Error::BadRequest(format!("Unsupported signature {}, please use ed25519", algorithm))) - } - - Ok(Signature { url: key_id.parse()?, signature: signature.clone() }) - } -} diff --git a/src/schema.rs b/src/schema.rs index 7422f48..c7a350c 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -31,15 +31,6 @@ diesel::table! { } } -diesel::table! { - federated_users (uuid) { - uuid -> Uuid, - email_verified -> Bool, - #[max_length = 8000] - instance_url -> Varchar, - } -} - diesel::table! { guild_members (uuid) { uuid -> Uuid, @@ -47,13 +38,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] @@ -70,15 +61,6 @@ diesel::table! { } } -diesel::table! { - instances (instance_url) { - #[max_length = 8000] - instance_url -> Varchar, - #[max_length = 500] - public_key -> Varchar, - } -} - diesel::table! { invites (id) { #[max_length = 32] @@ -155,10 +137,8 @@ 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!(federated_users -> instances (instance_url)); 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)); @@ -172,11 +152,9 @@ diesel::allow_tables_to_appear_in_same_query!( access_tokens, channel_permissions, channels, - federated_users, guild_members, guilds, instance_permissions, - instances, invites, messages, refresh_tokens, diff --git a/src/utils.rs b/src/utils.rs index b3de7ad..7a5581a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, sync::LazyLock}; +use std::sync::LazyLock; use actix_web::{ cookie::{Cookie, SameSite, time::Duration}, @@ -115,14 +115,8 @@ pub fn new_refresh_token_cookie(config: &Config, refresh_token: String) -> Cooki .finish() } -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]; +pub fn generate_token() -> Result { + let mut buf = [0u8; N]; fill(&mut buf)?; Ok(encode(buf)) }