use revolt_database::{ util::{permissions::DatabasePermissionQuery, reference::Reference}, AuditLogEntry, AuditLogQuery, Database, User, }; use revolt_models::v0; use revolt_permissions::{calculate_server_permissions, ChannelPermission}; use revolt_result::{create_error, Result}; use rocket::{serde::json::Json, State}; use validator::Validate; /// # Audit Log Query /// /// Queries a server's audit logs. #[openapi(tag = "Audit Logs")] #[get("//audit_logs?")] pub async fn query( db: &State, user: User, target: Reference<'_>, options: v0::OptionsAuditLogQuery, ) -> Result> { options.validate().map_err(|error| { create_error!(FailedValidation { error: error.to_string() }) })?; let server = target.as_server(db).await?; let mut query = DatabasePermissionQuery::new(db, &user).server(&server); calculate_server_permissions(&mut query) .await .throw_if_lacking_channel_permission(ChannelPermission::ViewAuditLogs)?; let v0::OptionsAuditLogQuery { user: user_filter, target, r#type, before, after, limit, } = options; let audit_logs = db .get_server_audit_logs( &server.id, AuditLogQuery { user: user_filter, target, r#type, before, after, limit: limit.unwrap_or(50), }, ) .await?; let (users, members) = AuditLogEntry::with_users(db, &server.id, &user, &audit_logs).await?; Ok(Json(v0::AuditLogQueryResponse { audit_logs: audit_logs.into_iter().map(Into::into).collect(), users, members, })) } #[cfg(test)] mod test { use revolt_database::{Member, Server}; use revolt_models::v0; use rocket::http::{Header, Status}; use crate::util::test::TestHarness; #[rocket::async_test] async fn audit_log_query() { let harness = TestHarness::new().await; let (_, session, user) = harness.new_user().await; let (server, channels) = Server::create( &harness.db, v0::DataCreateServer { name: "Test Server".to_string(), ..Default::default() }, &user, true, ) .await .expect("Failed to create test server."); Member::create(&harness.db, &server, &user, None).await.unwrap(); let channel = &channels[0]; let status = harness .client .patch(format!("/channels/{}", channel.id())) .header(Header::new("X-Audit-Log-Reason", "Test Reason 1")) .header(Header::new("x-session-token", session.token.clone())) .json(&v0::DataEditChannel { description: Some("General chat channel.".to_string()), name: None, owner: None, icon: None, nsfw: None, archived: None, voice: None, slowmode: None, remove: Vec::new(), }) .dispatch() .await .status(); assert_eq!(status, Status::Ok); let status = harness .client .patch(format!("/channels/{}", channel.id())) .header(Header::new("X-Audit-Log-Reason", "Test Reason 2")) .header(Header::new("x-session-token", session.token.clone())) .json(&v0::DataEditChannel { description: Some("New description.".to_string()), name: None, owner: None, icon: None, nsfw: None, archived: None, voice: None, slowmode: None, remove: Vec::new(), }) .dispatch() .await .status(); assert_eq!(status, Status::Ok); let status = harness .client .delete(format!("/channels/{}", channel.id())) .header(Header::new("X-Audit-Log-Reason", "Test Reason 3")) .header(Header::new("x-session-token", session.token.clone())) .dispatch() .await .status(); assert_eq!(status, Status::NoContent); let response = harness .client .get(format!( "/servers/{}/audit_logs?include_users=true", &server.id )) .header(Header::new("x-session-token", session.token.clone())) .dispatch() .await .into_json::() .await .expect("Failed to deserialise audit_logs response"); let v0::AuditLogQueryResponse { audit_logs: entries, users, members, } = response; assert_eq!(entries.len(), 3); assert_eq!(users.len(), 1); assert_eq!(members.len(), 1); assert_eq!(&users[0].id, &user.id); let entry = entries .iter() .find(|entry| entry.reason.as_deref() == Some("Test Reason 3")) .expect("Missing channel deletion audit record"); assert_eq!(entry.reason.as_deref(), Some("Test Reason 3")); assert_eq!(&entry.server, &server.id); assert_eq!(&entry.user, &user.id); assert_eq!( &entry.action, &v0::AuditLogEntryAction::ChannelDelete { channel: channel.id().to_string(), name: "General".to_string() } ); let entry = entries .iter() .find(|entry| entry.reason.as_deref() == Some("Test Reason 2")) .expect("Missing channel description edit audit record"); assert_eq!(entry.reason.as_deref(), Some("Test Reason 2")); assert_eq!(&entry.server, &server.id); assert_eq!(&entry.user, &user.id); assert_eq!( &entry.action, &v0::AuditLogEntryAction::ChannelEdit { channel: channel.id().to_string(), before: v0::PartialChannel { description: Some("General chat channel.".to_string()), ..Default::default() }, after: v0::PartialChannel { description: Some("New description.".to_string()), ..Default::default() } } ); let entry = entries .iter() .find(|entry| entry.reason.as_deref() == Some("Test Reason 1")) .expect("Missing channel edit audit record"); assert_eq!(entry.reason.as_deref(), Some("Test Reason 1")); assert_eq!(&entry.server, &server.id); assert_eq!(&entry.user, &user.id); assert_eq!( &entry.action, &v0::AuditLogEntryAction::ChannelEdit { channel: channel.id().to_string(), before: v0::PartialChannel { description: None, ..Default::default() }, after: v0::PartialChannel { description: Some("General chat channel.".to_string()), ..Default::default() } } ); } }