add: audit log, reports

add: theme preference setting
This commit is contained in:
trisua 2025-04-02 11:39:51 -04:00
parent b2df2739a7
commit d3d0c41334
38 changed files with 925 additions and 169 deletions

View file

@ -3,7 +3,7 @@ use crate::cache::Cache;
use crate::model::{
Error, Result, auth::User, moderation::AuditLogEntry, permissions::FinePermission,
};
use crate::{auto_method, execute, get, query_row};
use crate::{auto_method, execute, get, query_row, query_rows};
#[cfg(feature = "sqlite")]
use rusqlite::Row;
@ -13,7 +13,7 @@ use tokio_postgres::Row;
impl DataManager {
/// Get an [`AuditLogEntry`] from an SQL row.
pub(crate) fn get_auditlog_entry_from_row(
pub(crate) fn get_audit_log_entry_from_row(
#[cfg(feature = "sqlite")] x: &Row<'_>,
#[cfg(feature = "postgres")] x: &Row,
) -> AuditLogEntry {
@ -25,13 +25,42 @@ impl DataManager {
}
}
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:{}");
auto_method!(get_audit_log_entry_by_id(usize)@get_audit_log_entry_from_row -> "SELECT * FROM audit_log WHERE id = $1" --name="audit log entry" --returns=AuditLogEntry --cache-key-tmpl="atto.audit_log:{}");
/// Get all audit log entries (paginated).
///
/// # Arguments
/// * `batch` - the limit of items in each page
/// * `page` - the page number
pub async fn get_audit_log_entries(
&self,
batch: usize,
page: usize,
) -> Result<Vec<AuditLogEntry>> {
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_rows!(
&conn,
"SELECT * FROM audit_log ORDER BY created DESC LIMIT $1 OFFSET $2",
&[&(batch as isize), &((page * batch) as isize)],
|x| { Self::get_audit_log_entry_from_row(x) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("audit log entry".to_string()));
}
Ok(res.unwrap())
}
/// 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<()> {
pub async fn create_audit_log_entry(&self, data: AuditLogEntry) -> Result<()> {
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
@ -39,7 +68,7 @@ impl DataManager {
let res = execute!(
&conn,
"INSERT INTO auditlog VALUES ($1, $2, $3, $4)",
"INSERT INTO audit_log VALUES ($1, $2, $3, $4)",
&[
&data.id.to_string().as_str(),
&data.created.to_string().as_str(),
@ -56,7 +85,7 @@ impl DataManager {
Ok(())
}
pub async fn delete_auditlog_entry(&self, id: usize, user: User) -> Result<()> {
pub async fn delete_audit_log_entry(&self, id: usize, user: User) -> Result<()> {
if !user.permissions.check(FinePermission::MANAGE_AUDITLOG) {
return Err(Error::NotAllowed);
}
@ -68,7 +97,7 @@ impl DataManager {
let res = execute!(
&conn,
"DELETE FROM auditlog WHERE id = $1",
"DELETE FROM audit_log WHERE id = $1",
&[&id.to_string()]
);
@ -76,7 +105,7 @@ impl DataManager {
return Err(Error::DatabaseError(e.to_string()));
}
self.2.remove(format!("atto.auditlog:{}", id)).await;
self.2.remove(format!("atto.audit_log:{}", id)).await;
// return
Ok(())

View file

@ -1,5 +1,6 @@
use super::*;
use crate::cache::Cache;
use crate::model::moderation::AuditLogEntry;
use crate::model::{
Error, Result,
auth::{Token, User, UserSettings},
@ -262,6 +263,17 @@ impl DataManager {
self.cache_clear_user(&other_user).await;
// create audit log entry
self.create_audit_log_entry(AuditLogEntry::new(
user.id,
format!(
"invoked `update_user_verified_status` with x value `{}` and y value `{}`",
other_user.id, x
),
))
.await?;
// ...
Ok(())
}
@ -326,6 +338,67 @@ impl DataManager {
Ok(())
}
pub async fn update_user_role(
&self,
id: usize,
role: FinePermission,
user: User,
) -> Result<()> {
// check permission
if !user.permissions.check(FinePermission::MANAGE_USERS) {
return Err(Error::NotAllowed);
}
let other_user = self.get_user_by_id(id).await?;
if other_user.permissions.check_manager() && !user.permissions.check_admin() {
return Err(Error::MiscError(
"Cannot manage the role of other managers".to_string(),
));
}
if other_user.permissions == user.permissions {
return Err(Error::MiscError(
"Cannot manage users of equal level to you".to_string(),
));
}
// ...
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = execute!(
&conn,
"UPDATE users SET permissions = $1 WHERE id = $2",
&[
&(role.bits()).to_string().as_str(),
&id.to_string().as_str()
]
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
self.cache_clear_user(&other_user).await;
// create audit log entry
self.create_audit_log_entry(AuditLogEntry::new(
user.id,
format!(
"invoked `update_user_role` with x value `{}` and y value `{}`",
other_user.id,
role.bits()
),
))
.await?;
// ...
Ok(())
}
pub async fn cache_clear_user(&self, user: &User) {
self.2.remove(format!("atto.user:{}", user.id)).await;
self.2.remove(format!("atto.user:{}", user.username)).await;

View file

@ -22,7 +22,7 @@ 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_AUDIT_LOG).unwrap();
execute!(&conn, common::CREATE_TABLE_REPORTS).unwrap();
Ok(())
@ -139,7 +139,7 @@ macro_rules! auto_method {
if !user.permissions.check(FinePermission::$permission) {
return Err(Error::NotAllowed);
} else {
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
user.id,
format!("invoked `{}` with x value `{id}`", stringify!($name)),
))
@ -169,7 +169,7 @@ macro_rules! auto_method {
if !user.permissions.check(FinePermission::$permission) {
return Err(Error::NotAllowed);
} else {
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
user.id,
format!("invoked `{}` with x value `{id}`", stringify!($name)),
))
@ -201,7 +201,7 @@ macro_rules! auto_method {
if !user.permissions.check(FinePermission::$permission) {
return Err(Error::NotAllowed);
} else {
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
user.id,
format!("invoked `{}` with x value `{id}`", stringify!($name)),
))
@ -232,7 +232,7 @@ macro_rules! auto_method {
if !user.permissions.check(FinePermission::$permission) {
return Err(Error::NotAllowed);
} else {
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
user.id,
format!("invoked `{}` with x value `{x}`", stringify!($name)),
))
@ -265,7 +265,7 @@ macro_rules! auto_method {
if !user.permissions.check(FinePermission::$permission) {
return Err(Error::NotAllowed);
} else {
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
user.id,
format!("invoked `{}` with x value `{id}`", stringify!($name), id),
))
@ -300,7 +300,7 @@ macro_rules! auto_method {
if !user.permissions.check(FinePermission::$permission) {
return Err(Error::NotAllowed);
} else {
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
user.id,
format!("invoked `{}` with x value `{x:?}`", stringify!($name)),
))
@ -455,7 +455,7 @@ macro_rules! auto_method {
if !user.permissions.check(FinePermission::$permission) {
return Err(Error::NotAllowed);
} else {
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
user.id,
format!("invoked `{}` with x value `{id}`", stringify!($name)),
))
@ -488,7 +488,7 @@ macro_rules! auto_method {
if !user.permissions.check(FinePermission::$permission) {
return Err(Error::NotAllowed);
} else {
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
user.id,
format!("invoked `{}` with x value `{x}`", stringify!($name)),
))
@ -546,7 +546,7 @@ macro_rules! auto_method {
if !user.permissions.check(FinePermission::$permission) {
return Err(Error::NotAllowed);
} else {
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
user.id,
format!("invoked `{}` with x value `{x:?}`", stringify!($name)),
))

View file

@ -224,7 +224,7 @@ impl DataManager {
if !user.permissions.check(FinePermission::MANAGE_COMMUNITIES) {
return Err(Error::NotAllowed);
} else {
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
user.id,
format!("invoked `delete_community` with x value `{id}`"),
))

View file

@ -7,5 +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_AUDIT_LOG: &str = include_str!("./sql/create_audit_log.sql");
pub const CREATE_TABLE_REPORTS: &str = include_str!("./sql/create_reports.sql");

View file

@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER NOT NULL PRIMARY KEY,
created INTEGER NOT NULL,
owner INTEGER NOT NULL,
content TEXT NOT NULL
)

View file

@ -1,6 +0,0 @@
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

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

View file

@ -1,5 +1,6 @@
use super::*;
use crate::cache::Cache;
use crate::model::moderation::AuditLogEntry;
use crate::model::{Error, Result, auth::IpBan, auth::User, permissions::FinePermission};
use crate::{auto_method, execute, get, query_row};
@ -57,11 +58,18 @@ impl DataManager {
return Err(Error::DatabaseError(e.to_string()));
}
// create audit log entry
self.create_audit_log_entry(AuditLogEntry::new(
user.id,
format!("invoked `create_ipban` with x value `{}`", data.ip),
))
.await?;
// return
Ok(())
}
pub async fn delete_ipban(&self, id: usize, user: User) -> Result<()> {
pub async fn delete_ipban(&self, ip: &str, user: User) -> Result<()> {
// ONLY moderators can manage ip bans
if !user.permissions.check(FinePermission::MANAGE_BANS) {
return Err(Error::NotAllowed);
@ -72,17 +80,20 @@ impl DataManager {
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = execute!(
&conn,
"DELETE FROM ipbans WHERE id = $1",
&[&id.to_string()]
);
let res = execute!(&conn, "DELETE FROM ipbans WHERE ip = $1", &[ip]);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
self.2.remove(format!("atto.ipban:{}", id)).await;
self.2.remove(format!("atto.ipban:{}", ip)).await;
// create audit log entry
self.create_audit_log_entry(AuditLogEntry::new(
user.id,
format!("invoked `delete_ipban` with x value `{ip}`"),
))
.await?;
// return
Ok(())

View file

@ -331,6 +331,31 @@ impl DataManager {
);
}
// incr comment count
if let Some(id) = data.replying_to {
self.incr_post_comments(id).await.unwrap();
// send notification
let rt = self.get_post_by_id(id).await?;
if data.owner != rt.owner {
let owner = self.get_user_by_id(rt.owner).await?;
self.create_notification(Notification::new(
"Your post has received a new comment!".to_string(),
format!(
"[@{}](/api/v1/auth/profile/find/{}) has commented on your [post](/post/{}).",
owner.username, owner.id, rt.id
),
rt.owner,
))
.await?;
if rt.context.comments_enabled == false {
return Err(Error::NotAllowed);
}
}
}
// ...
let conn = match self.connect().await {
Ok(c) => c,
@ -364,27 +389,6 @@ impl DataManager {
return Err(Error::DatabaseError(e.to_string()));
}
// incr comment count
if let Some(id) = data.replying_to {
self.incr_post_comments(id).await.unwrap();
// send notification
let rt = self.get_post_by_id(id).await?;
if data.owner != rt.owner {
let owner = self.get_user_by_id(rt.owner).await?;
self.create_notification(Notification::new(
"Your post has received a new comment!".to_string(),
format!(
"[@{}](/api/v1/auth/profile/find/{}) has commented on your [post](/post/{}).",
owner.username, owner.id, rt.id
),
rt.owner,
))
.await?;
}
}
// return
Ok(data.id)
}
@ -396,7 +400,7 @@ impl DataManager {
if !user.permissions.check(FinePermission::MANAGE_POSTS) {
return Err(Error::NotAllowed);
} else {
self.create_auditlog_entry(crate::model::moderation::AuditLogEntry::new(
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
user.id,
format!("invoked `delete_post` with x value `{id}`"),
))

View file

@ -144,6 +144,9 @@ impl DataManager {
}
}
}
AssetType::User => {
return Err(Error::NotAllowed);
}
};
// return
@ -200,6 +203,9 @@ impl DataManager {
return Err(e);
}
}
AssetType::User => {
return Err(Error::NotAllowed);
}
};
// return

View file

@ -1,7 +1,8 @@
use super::*;
use crate::cache::Cache;
use crate::model::moderation::AuditLogEntry;
use crate::model::{Error, Result, auth::User, moderation::Report, permissions::FinePermission};
use crate::{auto_method, execute, get, query_row};
use crate::{auto_method, execute, get, query_row, query_rows};
#[cfg(feature = "sqlite")]
use rusqlite::Row;
@ -27,6 +28,31 @@ impl DataManager {
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:{}");
/// Get all reports (paginated).
///
/// # Arguments
/// * `batch` - the limit of items in each page
/// * `page` - the page number
pub async fn get_reports(&self, batch: usize, page: usize) -> Result<Vec<Report>> {
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = query_rows!(
&conn,
"SELECT * FROM reports ORDER BY created DESC LIMIT $1 OFFSET $2",
&[&(batch as isize), &((page * batch) as isize)],
|x| { Self::get_report_from_row(x) }
);
if res.is_err() {
return Err(Error::GeneralNotFound("report".to_string()));
}
Ok(res.unwrap())
}
/// Create a new report in the database.
///
/// # Arguments
@ -80,6 +106,13 @@ impl DataManager {
self.2.remove(format!("atto.report:{}", id)).await;
// create audit log entry
self.create_audit_log_entry(AuditLogEntry::new(
user.id,
format!("invoked `delete_report` with x value `{id}`"),
))
.await?;
// return
Ok(())
}

View file

@ -25,6 +25,19 @@ pub struct User {
pub following_count: usize,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum ThemePreference {
Auto,
Dark,
Light,
}
impl Default for ThemePreference {
fn default() -> Self {
Self::Auto
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct UserSettings {
#[serde(default)]
@ -35,6 +48,8 @@ pub struct UserSettings {
pub private_profile: bool,
#[serde(default)]
pub private_communities: bool,
#[serde(default)]
pub theme_preference: ThemePreference,
}
impl Default for UserSettings {
@ -44,6 +59,7 @@ impl Default for UserSettings {
biography: String::new(),
private_profile: false,
private_communities: false,
theme_preference: ThemePreference::default(),
}
}
}
@ -88,6 +104,15 @@ impl User {
}
}
/// Banned user profile.
pub fn banned() -> Self {
Self {
username: "<banned>".to_string(),
id: 0,
..Default::default()
}
}
/// Create a new token
///
/// # Returns

View file

@ -25,6 +25,7 @@ bitflags! {
const MANAGE_VERIFIED = 1 << 14;
const MANAGE_AUDITLOG = 1 << 15;
const MANAGE_REPORTS = 1 << 16;
const BANNED = 1 << 17;
const _ = !0;
}
@ -101,6 +102,9 @@ impl FinePermission {
if (self & FinePermission::ADMINISTRATOR) == FinePermission::ADMINISTRATOR {
// has administrator permission, meaning everything else is automatically true
return true;
} else if self.check_banned() {
// has banned permission, meaning everything else is automatically false
return false;
}
(self & permission) == permission
@ -118,7 +122,17 @@ impl FinePermission {
/// Check if the given [`FinePermission`] qualifies as "Manager" status.
pub fn check_manager(self) -> bool {
self.check_helper() && self.check(FinePermission::ADMINISTRATOR)
self.check_helper() && self.check(FinePermission::MANAGE_USERS)
}
/// Check if the given [`FinePermission`] qualifies as "Administrator" status.
pub fn check_admin(self) -> bool {
self.check_manager() && self.check(FinePermission::ADMINISTRATOR)
}
/// Check if the given [`FinePermission`] qualifies as "Banned" status.
pub fn check_banned(self) -> bool {
(self & FinePermission::BANNED) == FinePermission::BANNED
}
}

View file

@ -4,8 +4,12 @@ use tetratto_shared::{snow::AlmostSnowflake, unix_epoch_timestamp};
/// All of the items which support reactions.
#[derive(Serialize, Deserialize)]
pub enum AssetType {
#[serde(alias = "community")]
Community,
#[serde(alias = "post")]
Post,
#[serde(alias = "user")]
User,
}
#[derive(Serialize, Deserialize)]