add: audit log and reports table

TODO: audit log/reports UIs
This commit is contained in:
trisua 2025-04-01 23:16:09 -04:00
parent 9a9b72bdbb
commit b2df2739a7
16 changed files with 387 additions and 6 deletions

View file

@ -0,0 +1,61 @@
use crate::{
State, get_user_from_token,
model::{ApiReturn, Error},
routes::api::v1::CreateIpBan,
};
use axum::{Extension, Json, extract::Path, response::IntoResponse};
use axum_extra::extract::CookieJar;
use tetratto_core::model::{auth::IpBan, permissions::FinePermission};
/// Create a new IP ban.
pub async fn create_request(
jar: CookieJar,
Path(ip): Path<String>,
Extension(data): Extension<State>,
Json(req): Json<CreateIpBan>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
if !user.permissions.check(FinePermission::MANAGE_BANS) {
return Json(Error::NotAllowed.into());
}
match data.create_ipban(IpBan::new(ip, user.id, req.reason)).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "IP ban deleted".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
}
/// Delete the given IP ban.
pub async fn delete_request(
jar: CookieJar,
Path(id): Path<usize>,
Extension(data): Extension<State>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
if !user.permissions.check(FinePermission::MANAGE_BANS) {
return Json(Error::NotAllowed.into());
}
match data.delete_ipban(id, user).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "IP ban deleted".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
}

View file

@ -1,4 +1,5 @@
pub mod images;
pub mod ipbans;
pub mod profile;
pub mod social;

View file

@ -172,6 +172,9 @@ pub fn routes() -> Router {
"/communities/{cid}/memberships/{uid}/role",
post(communities::communities::update_membership_role),
)
// ipbans
.route("/bans/{ip}", post(auth::ipbans::create_request))
.route("/bans/id/{id}", delete(auth::ipbans::delete_request))
}
#[derive(Deserialize)]
@ -265,3 +268,8 @@ pub struct UpdateMembershipRole {
pub struct DeleteUser {
pub password: String,
}
#[derive(Deserialize)]
pub struct CreateIpBan {
pub reason: String,
}

View file

@ -0,0 +1,84 @@
use super::*;
use crate::cache::Cache;
use crate::model::{
Error, Result, auth::User, moderation::AuditLogEntry, permissions::FinePermission,
};
use crate::{auto_method, execute, get, query_row};
#[cfg(feature = "sqlite")]
use rusqlite::Row;
#[cfg(feature = "postgres")]
use tokio_postgres::Row;
impl DataManager {
/// Get an [`AuditLogEntry`] from an SQL row.
pub(crate) fn get_auditlog_entry_from_row(
#[cfg(feature = "sqlite")] x: &Row<'_>,
#[cfg(feature = "postgres")] x: &Row,
) -> AuditLogEntry {
AuditLogEntry {
id: get!(x->0(isize)) as usize,
created: get!(x->1(isize)) as usize,
moderator: get!(x->2(isize)) as usize,
content: get!(x->3(String)),
}
}
auto_method!(get_auditlog_entry_by_id(usize)@get_auditlog_entry_from_row -> "SELECT * FROM auditlog WHERE id = $1" --name="audit log entry" --returns=AuditLogEntry --cache-key-tmpl="atto.auditlog:{}");
/// Create a new audit log entry in the database.
///
/// # Arguments
/// * `data` - a mock [`AuditLogEntry`] object to insert
pub async fn create_auditlog_entry(&self, data: AuditLogEntry) -> Result<()> {
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = execute!(
&conn,
"INSERT INTO auditlog VALUES ($1, $2, $3, $4)",
&[
&data.id.to_string().as_str(),
&data.created.to_string().as_str(),
&data.moderator.to_string().as_str(),
&data.content.as_str(),
]
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
// return
Ok(())
}
pub async fn delete_auditlog_entry(&self, id: usize, user: User) -> Result<()> {
if !user.permissions.check(FinePermission::MANAGE_AUDITLOG) {
return Err(Error::NotAllowed);
}
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = execute!(
&conn,
"DELETE FROM auditlog WHERE id = $1",
&[&id.to_string()]
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
self.2.remove(format!("atto.auditlog:{}", id)).await;
// return
Ok(())
}
}

View file

@ -22,6 +22,8 @@ impl DataManager {
execute!(&conn, common::CREATE_TABLE_USERFOLLOWS).unwrap();
execute!(&conn, common::CREATE_TABLE_USERBLOCKS).unwrap();
execute!(&conn, common::CREATE_TABLE_IPBANS).unwrap();
execute!(&conn, common::CREATE_TABLE_AUDITLOG).unwrap();
execute!(&conn, common::CREATE_TABLE_REPORTS).unwrap();
Ok(())
}
@ -136,6 +138,11 @@ macro_rules! auto_method {
if user.id != y.owner {
if !user.permissions.check(FinePermission::$permission) {
return Err(Error::NotAllowed);
} else {
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
user.id,
format!("invoked `{}` with x value `{id}`", stringify!($name)),
))
}
}
@ -161,6 +168,11 @@ macro_rules! auto_method {
if user.id != y.owner {
if !user.permissions.check(FinePermission::$permission) {
return Err(Error::NotAllowed);
} else {
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
user.id,
format!("invoked `{}` with x value `{id}`", stringify!($name)),
))
}
}
@ -188,6 +200,12 @@ macro_rules! auto_method {
if user.id != y.owner {
if !user.permissions.check(FinePermission::$permission) {
return Err(Error::NotAllowed);
} else {
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
user.id,
format!("invoked `{}` with x value `{id}`", stringify!($name)),
))
.await?
}
}
@ -213,6 +231,12 @@ macro_rules! auto_method {
if user.id != y.owner {
if !user.permissions.check(FinePermission::$permission) {
return Err(Error::NotAllowed);
} else {
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
user.id,
format!("invoked `{}` with x value `{x}`", stringify!($name)),
))
.await?
}
}
@ -240,6 +264,12 @@ macro_rules! auto_method {
if user.id != y.owner {
if !user.permissions.check(FinePermission::$permission) {
return Err(Error::NotAllowed);
} else {
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
user.id,
format!("invoked `{}` with x value `{id}`", stringify!($name), id),
))
.await?
}
}
@ -269,6 +299,12 @@ macro_rules! auto_method {
if user.id != y.owner {
if !user.permissions.check(FinePermission::$permission) {
return Err(Error::NotAllowed);
} else {
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
user.id,
format!("invoked `{}` with x value `{x:?}`", stringify!($name)),
))
.await?
}
}
@ -418,6 +454,12 @@ macro_rules! auto_method {
if user.id != y.owner {
if !user.permissions.check(FinePermission::$permission) {
return Err(Error::NotAllowed);
} else {
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
user.id,
format!("invoked `{}` with x value `{id}`", stringify!($name)),
))
.await?
}
}
@ -445,6 +487,12 @@ macro_rules! auto_method {
if user.id != y.owner {
if !user.permissions.check(FinePermission::$permission) {
return Err(Error::NotAllowed);
} else {
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
user.id,
format!("invoked `{}` with x value `{x}`", stringify!($name)),
))
.await?
}
}
@ -497,6 +545,12 @@ macro_rules! auto_method {
if user.id != y.owner {
if !user.permissions.check(FinePermission::$permission) {
return Err(Error::NotAllowed);
} else {
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
user.id,
format!("invoked `{}` with x value `{x:?}`", stringify!($name)),
))
.await?
}
}

View file

@ -223,6 +223,12 @@ impl DataManager {
if user.id != y.owner {
if !user.permissions.check(FinePermission::MANAGE_COMMUNITIES) {
return Err(Error::NotAllowed);
} else {
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
user.id,
format!("invoked `delete_community` with x value `{id}`"),
))
.await?
}
}

View file

@ -7,3 +7,5 @@ pub const CREATE_TABLE_NOTIFICATIONS: &str = include_str!("./sql/create_notifica
pub const CREATE_TABLE_USERFOLLOWS: &str = include_str!("./sql/create_userfollows.sql");
pub const CREATE_TABLE_USERBLOCKS: &str = include_str!("./sql/create_userblocks.sql");
pub const CREATE_TABLE_IPBANS: &str = include_str!("./sql/create_ipbans.sql");
pub const CREATE_TABLE_AUDITLOG: &str = include_str!("./sql/create_auditlog.sql");
pub const CREATE_TABLE_REPORTS: &str = include_str!("./sql/create_reports.sql");

View file

@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS auditlog (
ip TEXT NOT NULL,
created INTEGER NOT NULL PRIMARY KEY,
moderator TEXT NOT NULL,
content TEXT NOT NULL
)

View file

@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS reports (
ip TEXT NOT NULL,
created INTEGER NOT NULL PRIMARY KEY,
owner TEXT NOT NULL,
content TEXT NOT NULL,
asset INTEGER NOT NULL,
asset_type TEXT NOT NULL
)

View file

@ -1,3 +1,4 @@
mod audit_log;
mod auth;
mod common;
mod communities;
@ -7,6 +8,7 @@ mod memberships;
mod notifications;
mod posts;
mod reactions;
mod reports;
mod userblocks;
mod userfollows;

View file

@ -395,6 +395,12 @@ impl DataManager {
if user.id != y.owner {
if !user.permissions.check(FinePermission::MANAGE_POSTS) {
return Err(Error::NotAllowed);
} else {
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
user.id,
format!("invoked `delete_post` with x value `{id}`"),
))
.await?
}
}

View file

@ -0,0 +1,86 @@
use super::*;
use crate::cache::Cache;
use crate::model::{Error, Result, auth::User, moderation::Report, permissions::FinePermission};
use crate::{auto_method, execute, get, query_row};
#[cfg(feature = "sqlite")]
use rusqlite::Row;
#[cfg(feature = "postgres")]
use tokio_postgres::Row;
impl DataManager {
/// Get a [`Report`] from an SQL row.
pub(crate) fn get_report_from_row(
#[cfg(feature = "sqlite")] x: &Row<'_>,
#[cfg(feature = "postgres")] x: &Row,
) -> Report {
Report {
id: get!(x->0(isize)) as usize,
created: get!(x->1(isize)) as usize,
owner: get!(x->2(isize)) as usize,
content: get!(x->3(String)),
asset: get!(x->4(isize)) as usize,
asset_type: serde_json::from_str(&get!(x->5(String))).unwrap(),
}
}
auto_method!(get_report_by_id(usize)@get_report_from_row -> "SELECT * FROM reports WHERE id = $1" --name="report" --returns=Report --cache-key-tmpl="atto.reports:{}");
/// Create a new report in the database.
///
/// # Arguments
/// * `data` - a mock [`Report`] object to insert
pub async fn create_report(&self, data: Report) -> Result<()> {
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = execute!(
&conn,
"INSERT INTO reports VALUES ($1, $2, $3, $4, $5, $6)",
&[
&data.id.to_string().as_str(),
&data.created.to_string().as_str(),
&data.owner.to_string().as_str(),
&data.content.as_str(),
&data.asset.to_string().as_str(),
&serde_json::to_string(&data.asset_type).unwrap().as_str(),
]
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
// return
Ok(())
}
pub async fn delete_report(&self, id: usize, user: User) -> Result<()> {
if !user.permissions.check(FinePermission::MANAGE_REPORTS) {
return Err(Error::NotAllowed);
}
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = execute!(
&conn,
"DELETE FROM reports WHERE id = $1",
&[&id.to_string()]
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
self.2.remove(format!("atto.report:{}", id)).await;
// return
Ok(())
}
}

View file

@ -70,7 +70,7 @@ impl Community {
}
}
#[derive(Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
pub struct CommunityContext {
pub display_name: String,
pub description: String,
@ -86,7 +86,7 @@ impl Default for CommunityContext {
}
/// Who can read a [`Community`].
#[derive(Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum CommunityReadAccess {
/// Everybody can view the community.
Everybody,
@ -101,7 +101,7 @@ impl Default for CommunityReadAccess {
}
/// Who can write to a [`Community`].
#[derive(Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum CommunityWriteAccess {
/// Everybody.
Everybody,
@ -120,7 +120,7 @@ impl Default for CommunityWriteAccess {
}
/// Who can join a [`Community`].
#[derive(Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum CommunityJoinAccess {
/// Joins are closed. Nobody can join the community.
Nobody,
@ -136,7 +136,7 @@ impl Default for CommunityJoinAccess {
}
}
#[derive(Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CommunityMembership {
pub id: usize,
pub created: usize,
@ -161,7 +161,7 @@ impl CommunityMembership {
}
}
#[derive(Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
pub struct PostContext {
pub comments_enabled: bool,
}

View file

@ -1,6 +1,7 @@
pub mod auth;
pub mod communities;
pub mod communities_permissions;
pub mod moderation;
pub mod permissions;
pub mod reactions;

View file

@ -0,0 +1,54 @@
use serde::{Deserialize, Serialize};
use tetratto_shared::{snow::AlmostSnowflake, unix_epoch_timestamp};
use super::reactions::AssetType;
#[derive(Serialize, Deserialize)]
pub struct AuditLogEntry {
pub id: usize,
pub created: usize,
pub moderator: usize,
pub content: String,
}
impl AuditLogEntry {
/// Create a new [`AuditLogEntry`].
pub fn new(moderator: usize, content: String) -> Self {
Self {
id: AlmostSnowflake::new(1234567890)
.to_string()
.parse::<usize>()
.unwrap(),
created: unix_epoch_timestamp() as usize,
moderator,
content,
}
}
}
#[derive(Serialize, Deserialize)]
pub struct Report {
pub id: usize,
pub created: usize,
pub owner: usize,
pub content: String,
pub asset: usize,
pub asset_type: AssetType,
}
impl Report {
/// Create a new [`Report`].
pub fn new(owner: usize, content: String, asset: usize, asset_type: AssetType) -> Self {
Self {
id: AlmostSnowflake::new(1234567890)
.to_string()
.parse::<usize>()
.unwrap(),
created: unix_epoch_timestamp() as usize,
owner,
content,
asset,
asset_type,
}
}
}

View file

@ -23,6 +23,8 @@ bitflags! {
const MANAGE_REACTIONS = 1 << 12;
const MANAGE_FOLLOWS = 1 << 13;
const MANAGE_VERIFIED = 1 << 14;
const MANAGE_AUDITLOG = 1 << 15;
const MANAGE_REPORTS = 1 << 16;
const _ = !0;
}