diff --git a/migrations/2025-07-22-195121_add_ban/down.sql b/migrations/2025-07-22-195121_add_ban/down.sql new file mode 100644 index 0000000..62fe554 --- /dev/null +++ b/migrations/2025-07-22-195121_add_ban/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE guild_bans; diff --git a/migrations/2025-07-22-195121_add_ban/up.sql b/migrations/2025-07-22-195121_add_ban/up.sql new file mode 100644 index 0000000..020a1b0 --- /dev/null +++ b/migrations/2025-07-22-195121_add_ban/up.sql @@ -0,0 +1,7 @@ +-- Your SQL goes here +CREATE TABLE guild_bans ( + guild_uuid uuid NOT NULL REFERENCES guilds(uuid) ON DELETE CASCADE, + user_uuid uuid NOT NULL REFERENCES users(uuid), + reason VARCHAR(200) DEFAULT NULL, + PRIMARY KEY (user_uuid, guild_uuid) +); diff --git a/src/api/v1/members/mod.rs b/src/api/v1/members/mod.rs index 34b6938..59ceac2 100644 --- a/src/api/v1/members/mod.rs +++ b/src/api/v1/members/mod.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use axum::{ Router, - routing::{delete, get}, + routing::{delete, get, post}, }; use crate::AppState; @@ -13,4 +13,5 @@ pub fn router() -> Router> { Router::new() .route("/{uuid}", get(uuid::get)) .route("/{uuid}", delete(uuid::delete)) + .route("/{uuid}/ban", post(uuid::ban::post)) } diff --git a/src/api/v1/members/uuid/ban.rs b/src/api/v1/members/uuid/ban.rs new file mode 100644 index 0000000..986b98d --- /dev/null +++ b/src/api/v1/members/uuid/ban.rs @@ -0,0 +1,50 @@ +use std::sync::Arc; + +use axum::{ + Extension, + extract::{Json, Path, State}, + http::StatusCode, + response::IntoResponse, +}; +use serde::Deserialize; + +use crate::{ + AppState, + api::v1::auth::CurrentUser, + error::Error, + objects::{Member, Permissions}, + utils::global_checks, +}; + +use uuid::Uuid; + +#[derive(Deserialize)] +pub struct RequstBody { + reason: String, +} + +pub async fn post( + State(app_state): State>, + Path(member_uuid): Path, + Extension(CurrentUser(uuid)): Extension>, + Json(payload): Json, +) -> Result { + global_checks(&app_state, uuid).await?; + + let mut conn = app_state.pool.get().await?; + + let member = Member::fetch_one_with_member(&app_state, None, member_uuid).await?; + + if member.is_owner { + return Err(Error::Forbidden("Not allowed".to_string())); + } + + let baner = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; + baner + .check_permission(&app_state, Permissions::ManageMember) + .await?; + + member.ban(&mut conn, &payload.reason).await?; + + Ok(StatusCode::OK) +} diff --git a/src/api/v1/members/uuid/mod.rs b/src/api/v1/members/uuid/mod.rs index 244f5f8..7c39bff 100644 --- a/src/api/v1/members/uuid/mod.rs +++ b/src/api/v1/members/uuid/mod.rs @@ -1,5 +1,7 @@ //! `/api/v1/members/{uuid}` Member specific endpoints +pub mod ban; + use std::sync::Arc; use crate::{ @@ -29,7 +31,7 @@ pub async fn get( let me = Me::get(&mut conn, uuid).await?; - let member = Member::fetch_one_with_member(&app_state, &me, member_uuid).await?; + let member = Member::fetch_one_with_member(&app_state, Some(&me), member_uuid).await?; Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; Ok((StatusCode::OK, Json(member))) @@ -46,7 +48,7 @@ pub async fn delete( let me = Me::get(&mut conn, uuid).await?; - let member = Member::fetch_one_with_member(&app_state, &me, member_uuid).await?; + let member = Member::fetch_one_with_member(&app_state, Some(&me), member_uuid).await?; let deleter = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?; diff --git a/src/objects/member.rs b/src/objects/member.rs index 8678f4a..d07b701 100644 --- a/src/objects/member.rs +++ b/src/objects/member.rs @@ -10,6 +10,7 @@ use crate::{ AppState, Conn, error::Error, objects::{Me, Permissions, Role}, + schema::guild_bans, schema::guild_members, }; @@ -73,6 +74,13 @@ pub struct Member { user: User, } +#[derive(Serialize, Deserialize)] +pub struct GuildBan { + pub guild_uuid: Uuid, + pub user_uuid: Uuid, + pub reason: String, +} + impl Member { pub async fn count(conn: &mut Conn, guild_uuid: Uuid) -> Result { use guild_members::dsl; @@ -122,7 +130,7 @@ impl Member { pub async fn fetch_one_with_member( app_state: &AppState, - me: &Me, + me: Option<&Me>, uuid: Uuid, ) -> Result { let mut conn = app_state.pool.get().await?; @@ -134,7 +142,7 @@ impl Member { .get_result(&mut conn) .await?; - member.build(app_state, Some(me)).await + member.build(app_state, me).await } pub async fn fetch_all( @@ -169,6 +177,18 @@ impl Member { ) -> Result { let mut conn = app_state.pool.get().await?; + use guild_bans::dsl; + let banned = dsl::guild_bans + .filter(guild_bans::guild_uuid.eq(guild_uuid)) + .filter(guild_bans::user_uuid.eq(user_uuid)) + .execute(&mut conn) + .await; + match banned { + Ok(_) => Err(Error::Forbidden("User banned".to_string())), + Err(diesel::result::Error::NotFound) => Ok(()), + Err(e) => Err(e.into()), + }?; + let member_uuid = Uuid::now_v7(); let member = MemberBuilder { @@ -195,4 +215,20 @@ impl Member { Ok(()) } + + pub async fn ban(self, conn: &mut Conn, reason: &String) -> Result<(), Error> { + use guild_bans::dsl; + insert_into(guild_bans::table) + .values(( + dsl::guild_uuid.eq(self.guild_uuid), + dsl::user_uuid.eq(self.user_uuid), + dsl::reason.eq(reason), + )) + .execute(conn) + .await?; + + self.delete(conn).await?; + + Ok(()) + } } diff --git a/src/schema.rs b/src/schema.rs index 4095dcd..54284d6 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -47,6 +47,15 @@ diesel::table! { } } +diesel::table! { + guild_bans (user_uuid, guild_uuid) { + guild_uuid -> Uuid, + user_uuid -> Uuid, + #[max_length = 200] + reason -> Nullable, + } +} + diesel::table! { guild_members (uuid) { uuid -> Uuid, @@ -154,6 +163,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!(guild_bans -> guilds (guild_uuid)); +diesel::joinable!(guild_bans -> users (user_uuid)); diesel::joinable!(guild_members -> guilds (guild_uuid)); diesel::joinable!(guild_members -> users (user_uuid)); diesel::joinable!(instance_permissions -> users (uuid)); @@ -171,6 +182,7 @@ diesel::allow_tables_to_appear_in_same_query!( channels, friend_requests, friends, + guild_bans, guild_members, guilds, instance_permissions,