Compare commits

..

13 commits

Author SHA1 Message Date
e663c1bbae style: spelling fix
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-08-07 23:32:22 +02:00
6afa78c8e8 audit log on ban and unban
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-08-05 12:46:17 +02:00
dee248e02d feat: audid log on invite creation 2025-08-05 12:36:27 +02:00
55ef6ddde2 feat: audit log on role creation 2025-08-05 12:30:39 +02:00
362e4bc2e8 feat: added audit log on channel update
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-08-05 12:21:48 +02:00
d84b7cf126 feat: added audit log to channel create 2025-08-05 12:07:17 +02:00
9d6ec5286b feat: added audit log to channel delete 2025-08-05 12:01:17 +02:00
95ef27c32d fix: spitt the new function, and added an AuditLogId Enum 2025-08-05 12:00:47 +02:00
2b9a44c4f0 style: cargo clippy --fix && cargo fmt
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-08-05 10:54:24 +02:00
e1d3a73687 feat: finished auditlog object and added endpoint to get all auditlogs 2025-08-05 10:52:22 +02:00
6017b7087f Merge origin/main into wip/auditlog. Needed pagination
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-08-05 10:02:30 +02:00
b49e5036be feat: started on auditlog object
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-08-05 09:58:37 +02:00
e8de96b2d0 feat: added audit logs table to the database
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-08-04 22:51:05 +02:00
16 changed files with 251 additions and 36 deletions

View file

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE audit_logs;

View file

@ -0,0 +1,14 @@
-- Your SQL goes here
CREATE TABLE audit_logs (
uuid UUID PRIMARY KEY NOT NULL,
guild_uuid UUID NOT NULL,
action_id INT2 NOT NULL,
by_uuid UUID NOT NULL REFERENCES guild_members(uuid),
channel_uuid UUID REFERENCES channels(uuid) DEFAULT NULL,
user_uuid UUID REFERENCES users(uuid) DEFAULT NULL,
message_uuid UUID REFERENCES messages(uuid) DEFAULT NULL,
role_uuid UUID REFERENCES roles(uuid) DEFAULT NULL,
audit_message VARCHAR(200) DEFAULT NULL,
changed_from VARCHAR(200) DEFAULT NULL,
changed_to VARCHAR(200) DEFAULT NULL
);

View file

@ -6,11 +6,7 @@ pub mod socket;
use std::sync::Arc;
use crate::{
AppState,
api::v1::auth::CurrentUser,
error::Error,
objects::{Channel, Member, Permissions},
utils::global_checks,
api::v1::auth::CurrentUser, error::Error, objects::{AuditLog, AuditLogId, Channel, Member, Permissions}, utils::global_checks, AppState
};
use axum::{
Extension, Json,
@ -55,7 +51,9 @@ pub async fn delete(
.check_permission(&mut conn, &app_state.cache_pool, Permissions::ManageChannel)
.await?;
let log_entry = AuditLog::new(channel.guild_uuid, AuditLogId::ChannelDelete as i16, member.uuid, None, None, None, None, Some(channel.name.clone()), None, None).await;
channel.delete(&mut conn, &app_state.cache_pool).await?;
log_entry.push(&mut conn).await?;
Ok(StatusCode::OK)
}
@ -117,12 +115,15 @@ pub async fn patch(
.await?;
if let Some(new_name) = &new_info.name {
let log_entry = AuditLog::new(channel.guild_uuid, AuditLogId::ChannelUpdateName as i16, member.uuid, Some(channel_uuid), None, None, None, None, Some(channel.name.clone()), Some(new_name.clone())).await;
channel
.set_name(&mut conn, &app_state.cache_pool, new_name.to_string())
.await?;
log_entry.push(&mut conn).await?;
}
if let Some(new_description) = &new_info.description {
let log_entry = AuditLog::new(channel.guild_uuid, AuditLogId::ChannelUpdateDescripiton as i16, member.uuid, Some(channel_uuid), None, None, None, None, Some(channel.description.clone().unwrap_or("".to_string())), Some(new_description.clone())).await;
channel
.set_description(
&mut conn,
@ -130,6 +131,7 @@ pub async fn patch(
new_description.to_string(),
)
.await?;
log_entry.push(&mut conn).await?;
}
if let Some(new_is_above) = &new_info.is_above {

View file

@ -0,0 +1,37 @@
use std::sync::Arc;
use ::uuid::Uuid;
use axum::{
Extension, Json,
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
};
use crate::{
AppState,
api::v1::auth::CurrentUser,
error::Error,
objects::{AuditLog, Member, PaginationRequest, Permissions},
utils::global_checks,
};
pub async fn get(
State(app_state): State<Arc<AppState>>,
Path(guild_uuid): Path<Uuid>,
Query(pagination): Query<PaginationRequest>,
Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
) -> Result<impl IntoResponse, Error> {
let mut conn = app_state.pool.get().await?;
global_checks(&mut conn, &app_state.config, uuid).await?;
let caller = Member::check_membership(&mut conn, uuid, guild_uuid).await?;
caller
.check_permission(&mut conn, &app_state.cache_pool, Permissions::ManageGuild)
.await?;
let logs = AuditLog::fetch_page(&mut conn, guild_uuid, pagination).await?;
Ok((StatusCode::OK, Json(logs)))
}

View file

@ -9,11 +9,7 @@ use axum::{
use uuid::Uuid;
use crate::{
AppState,
api::v1::auth::CurrentUser,
error::Error,
objects::{GuildBan, Member, Permissions},
utils::global_checks,
api::v1::auth::CurrentUser, error::Error, objects::{AuditLog, AuditLogId, GuildBan, Member, Permissions}, utils::global_checks, AppState
};
pub async fn get(
@ -51,7 +47,9 @@ pub async fn unban(
let ban = GuildBan::fetch_one(&mut conn, guild_uuid, user_uuid).await?;
let log_entry = AuditLog::new(guild_uuid, AuditLogId::MemberUnban as i16, caller.uuid, None, Some(ban.user_uuid), None, None, None, None, None).await;
ban.unban(&mut conn).await?;
log_entry.push(&mut conn).await?;
Ok(StatusCode::OK)
}

View file

@ -10,11 +10,7 @@ use axum::{
use serde::Deserialize;
use crate::{
AppState,
api::v1::auth::CurrentUser,
error::Error,
objects::{Channel, Member, Permissions},
utils::{CacheFns, global_checks, order_by_is_above},
api::v1::auth::CurrentUser, error::Error, objects::{AuditLog, AuditLogId, Channel, Member, Permissions}, utils::{global_checks, order_by_is_above, CacheFns}, AppState
};
#[derive(Deserialize)]
@ -83,5 +79,7 @@ pub async fn create(
)
.await?;
AuditLog::new(guild_uuid, AuditLogId::ChannelCreate as i16, member.uuid, Some(channel.uuid), None, None, None, Some(channel.name.clone()), None, None).await.push(&mut conn).await?;
Ok((StatusCode::OK, Json(channel)))
}

View file

@ -10,11 +10,7 @@ use serde::Deserialize;
use uuid::Uuid;
use crate::{
AppState,
api::v1::auth::CurrentUser,
error::Error,
objects::{Guild, Member, Permissions},
utils::global_checks,
api::v1::auth::CurrentUser, error::Error, objects::{AuditLog, AuditLogId, Guild, Member, Permissions}, utils::global_checks, AppState
};
#[derive(Deserialize)]
@ -62,5 +58,7 @@ pub async fn create(
.create_invite(&mut conn, uuid, invite_request.custom_id.clone())
.await?;
AuditLog::new(guild_uuid, AuditLogId::InviteCreate as i16, member.uuid, None, None, None, None, Some(invite.id.clone()), None, None).await.push(&mut conn).await?;
Ok((StatusCode::OK, Json(invite)))
}

View file

@ -12,6 +12,7 @@ use axum::{
use bytes::Bytes;
use uuid::Uuid;
mod auditlogs;
mod bans;
mod channels;
mod invites;
@ -46,6 +47,8 @@ pub fn router() -> Router<Arc<AppState>> {
// Bans
.route("/bans", get(bans::get))
.route("/bans/{uuid}", delete(bans::unban))
// Audit Logs
.route("/auditlogs", get(auditlogs::get))
}
/// `GET /api/v1/guilds/{uuid}` DESCRIPTION

View file

@ -10,11 +10,7 @@ use axum::{
use serde::Deserialize;
use crate::{
AppState,
api::v1::auth::CurrentUser,
error::Error,
objects::{Member, Permissions, Role},
utils::{CacheFns, global_checks, order_by_is_above},
api::v1::auth::CurrentUser, error::Error, objects::{AuditLog, AuditLogId, Member, Permissions, Role}, utils::{global_checks, order_by_is_above, CacheFns}, AppState
};
pub mod uuid;
@ -71,7 +67,10 @@ pub async fn create(
.check_permission(&mut conn, &app_state.cache_pool, Permissions::ManageRole)
.await?;
// TODO: roles permission
let role = Role::new(&mut conn, guild_uuid, role_info.name.clone()).await?;
AuditLog::new(guild_uuid, AuditLogId::RoleCreate as i16, member.uuid, None, None, None, Some(role.uuid), Some(role_info.name.clone()) , None, None).await.push(&mut conn).await?;
Ok((StatusCode::OK, Json(role)).into_response())
}

View file

@ -9,11 +9,7 @@ use axum::{
use serde::Deserialize;
use crate::{
AppState,
api::v1::auth::CurrentUser,
error::Error,
objects::{Member, Permissions},
utils::global_checks,
api::v1::auth::CurrentUser, error::Error, objects::{AuditLog, AuditLogId, Member, Permissions}, utils::global_checks, AppState
};
use uuid::Uuid;
@ -42,7 +38,9 @@ pub async fn post(
.check_permission(&mut conn, &app_state.cache_pool, Permissions::BanMember)
.await?;
let log_entry = AuditLog::new(member.guild_uuid, AuditLogId::MemberBan as i16, caller.uuid, None, Some(member.user_uuid), None, None, Some(payload.reason.clone()), None, None).await;
member.ban(&mut conn, &payload.reason).await?;
log_entry.push(&mut conn).await?;
Ok(StatusCode::OK)
}

View file

@ -54,9 +54,9 @@ pub async fn delete(
Member::fetch_one_with_uuid(&mut conn, &app_state.cache_pool, Some(&me), member_uuid)
.await?;
let deleter = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?;
let caller = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?;
deleter
caller
.check_permission(&mut conn, &app_state.cache_pool, Permissions::KickMember)
.await?;

138
src/objects/auditlog.rs Normal file
View file

@ -0,0 +1,138 @@
use crate::{
Conn,
error::Error,
objects::{Pagination, PaginationRequest, load_or_empty},
schema::audit_logs,
};
use diesel::{
ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into,
};
use diesel_async::RunQueryDsl;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub enum AuditLogId {
MessageDelete = 100,
MessageEdit = 101,
ChannelCreate = 200,
ChannelDelete = 201,
ChannelUpdateName = 202,
ChannelUpdateDescripiton = 203,
RoleCreate = 300,
RoleDelete = 301,
RoleUpdateName = 302,
RoleUpdatePermission = 303,
InviteCreate = 304,
InviteDelete = 305,
MemberKick = 400,
MemberBan = 401,
MemberUnban = 402,
MemberUpdateNickname = 403,
MemberUpdateRoles = 404,
}
#[derive(Insertable, Selectable, Queryable, Serialize, Deserialize, Clone)]
#[diesel(table_name = audit_logs)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct AuditLog {
pub uuid: Uuid,
pub guild_uuid: Uuid,
pub action_id: i16,
pub by_uuid: Uuid,
pub channel_uuid: Option<Uuid>,
pub user_uuid: Option<Uuid>,
pub message_uuid: Option<Uuid>,
pub role_uuid: Option<Uuid>,
pub audit_message: Option<String>,
pub changed_from: Option<String>,
pub changed_to: Option<String>,
}
impl AuditLog {
pub async fn count(conn: &mut Conn, guild_uuid: Uuid) -> Result<i64, Error> {
use audit_logs::dsl;
let count: i64 = dsl::audit_logs
.filter(dsl::guild_uuid.eq(guild_uuid))
.count()
.get_result(conn)
.await?;
Ok(count)
}
pub async fn fetch_page(
conn: &mut Conn,
guild_uuid: Uuid,
pagination: PaginationRequest,
) -> Result<Pagination<AuditLog>, Error> {
// TODO: Maybe add cache, but I do not know how
let per_page = pagination.per_page.unwrap_or(20);
let offset = (pagination.page - 1) * per_page;
if !(10..=100).contains(&per_page) {
return Err(Error::BadRequest(
"Invalid amount per page requested".to_string(),
));
}
use audit_logs::dsl;
let logs: Vec<AuditLog> = load_or_empty(
dsl::audit_logs
.filter(dsl::guild_uuid.eq(guild_uuid))
.limit(per_page.into())
.offset(offset as i64)
.select(AuditLog::as_select())
.load(conn)
.await,
)?;
let pages = (AuditLog::count(conn, guild_uuid).await? as f32 / per_page as f32).ceil();
let paginated_logs = Pagination::<AuditLog> {
objects: logs.clone(),
amount: logs.len() as i32,
pages: pages as i32,
page: pagination.page,
};
Ok(paginated_logs)
}
pub async fn new(
guild_uuid: Uuid,
action_id: i16,
by_uuid: Uuid,
channel_uuid: Option<Uuid>,
user_uuid: Option<Uuid>,
message_uuid: Option<Uuid>,
role_uuid: Option<Uuid>,
audit_message: Option<String>,
changed_from: Option<String>,
changed_to: Option<String>,
) -> AuditLog{
AuditLog {
uuid: Uuid::now_v7(),
guild_uuid,
action_id,
by_uuid,
channel_uuid,
user_uuid,
message_uuid,
role_uuid,
audit_message,
changed_from,
changed_to,
}
}
pub async fn push(self, conn: &mut Conn) -> Result<(), Error>{
insert_into(audit_logs::table)
.values(self.clone())
.execute(conn)
.await?;
Ok(())
}
}

View file

@ -52,8 +52,8 @@ impl ChannelBuilder {
pub struct Channel {
pub uuid: Uuid,
pub guild_uuid: Uuid,
name: String,
description: Option<String>,
pub name: String,
pub description: Option<String>,
pub is_above: Option<Uuid>,
pub permissions: Vec<ChannelPermission>,
}

View file

@ -7,6 +7,7 @@ use log::debug;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
mod auditlog;
mod bans;
mod channel;
mod email_token;
@ -20,6 +21,8 @@ mod password_reset_token;
mod role;
mod user;
pub use auditlog::AuditLog;
pub use auditlog::AuditLogId;
pub use bans::GuildBan;
pub use channel::Channel;
pub use email_token::EmailToken;

View file

@ -21,9 +21,9 @@ use super::{HasIsAbove, HasUuid, load_or_empty, member::MemberBuilder};
#[diesel(primary_key(uuid))]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Role {
uuid: Uuid,
pub uuid: Uuid,
guild_uuid: Uuid,
name: String,
pub name: String,
color: i32,
is_above: Option<Uuid>,
pub permissions: i64,

View file

@ -11,6 +11,25 @@ diesel::table! {
}
}
diesel::table! {
audit_logs (uuid) {
uuid -> Uuid,
guild_uuid -> Uuid,
action_id -> Int2,
by_uuid -> Uuid,
channel_uuid -> Nullable<Uuid>,
user_uuid -> Nullable<Uuid>,
message_uuid -> Nullable<Uuid>,
role_uuid -> Nullable<Uuid>,
#[max_length = 200]
audit_message -> Nullable<Varchar>,
#[max_length = 200]
changed_from -> Nullable<Varchar>,
#[max_length = 200]
changed_to -> Nullable<Varchar>,
}
}
diesel::table! {
channel_permissions (channel_uuid, role_uuid) {
channel_uuid -> Uuid,
@ -163,6 +182,11 @@ diesel::table! {
diesel::joinable!(access_tokens -> refresh_tokens (refresh_token));
diesel::joinable!(access_tokens -> users (uuid));
diesel::joinable!(audit_logs -> channels (channel_uuid));
diesel::joinable!(audit_logs -> guild_members (by_uuid));
diesel::joinable!(audit_logs -> messages (message_uuid));
diesel::joinable!(audit_logs -> roles (role_uuid));
diesel::joinable!(audit_logs -> users (user_uuid));
diesel::joinable!(channel_permissions -> channels (channel_uuid));
diesel::joinable!(channel_permissions -> roles (role_uuid));
diesel::joinable!(channels -> guilds (guild_uuid));
@ -182,6 +206,7 @@ diesel::joinable!(roles -> guilds (guild_uuid));
diesel::allow_tables_to_appear_in_same_query!(
access_tokens,
audit_logs,
channel_permissions,
channels,
friend_requests,