shared/models/
server_activity.rs

1use crate::{models::InsertQueryBuilder, prelude::*, storage::StorageUrlRetriever};
2use compact_str::ToCompactString;
3use garde::Validate;
4use serde::{Deserialize, Serialize};
5use sqlx::{Row, postgres::PgRow};
6use std::{
7    collections::BTreeMap,
8    sync::{Arc, LazyLock},
9};
10use utoipa::ToSchema;
11
12#[derive(Serialize, Deserialize)]
13pub struct ServerActivity {
14    pub user: Option<super::user::User>,
15    pub impersonator: Option<Fetchable<super::user::User>>,
16    pub api_key: Option<Fetchable<super::user_api_key::UserApiKey>>,
17    pub schedule: Option<Fetchable<super::server_schedule::ServerSchedule>>,
18
19    pub event: compact_str::CompactString,
20    pub ip: Option<sqlx::types::ipnetwork::IpNetwork>,
21    pub data: serde_json::Value,
22
23    pub created: chrono::NaiveDateTime,
24}
25
26impl BaseModel for ServerActivity {
27    const NAME: &'static str = "server_activity";
28
29    #[inline]
30    fn columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
31        let prefix = prefix.unwrap_or_default();
32
33        let mut columns = BTreeMap::from([
34            (
35                "server_activities.impersonator_uuid",
36                compact_str::format_compact!("{prefix}impersonator_uuid"),
37            ),
38            (
39                "server_activities.api_key_uuid",
40                compact_str::format_compact!("{prefix}api_key_uuid"),
41            ),
42            (
43                "server_activities.schedule_uuid",
44                compact_str::format_compact!("{prefix}schedule_uuid"),
45            ),
46            (
47                "server_activities.event",
48                compact_str::format_compact!("{prefix}event"),
49            ),
50            (
51                "server_activities.ip",
52                compact_str::format_compact!("{prefix}ip"),
53            ),
54            (
55                "server_activities.data",
56                compact_str::format_compact!("{prefix}data"),
57            ),
58            (
59                "server_activities.created",
60                compact_str::format_compact!("{prefix}created"),
61            ),
62        ]);
63
64        columns.extend(super::user::User::columns(Some("user_")));
65
66        columns
67    }
68
69    #[inline]
70    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
71        let prefix = prefix.unwrap_or_default();
72
73        Ok(Self {
74            user: if row
75                .try_get::<uuid::Uuid, _>("user_uuid".to_string().as_str())
76                .is_ok()
77            {
78                Some(super::user::User::map(Some("user_"), row)?)
79            } else {
80                None
81            },
82            impersonator: super::user::User::get_fetchable_from_row(
83                row,
84                compact_str::format_compact!("{prefix}impersonator_uuid"),
85            ),
86            api_key: super::user_api_key::UserApiKey::get_fetchable_from_row(
87                row,
88                compact_str::format_compact!("{prefix}api_key_uuid"),
89            ),
90            schedule: super::server_schedule::ServerSchedule::get_fetchable_from_row(
91                row,
92                compact_str::format_compact!("{prefix}schedule_uuid"),
93            ),
94            event: row.try_get(compact_str::format_compact!("{prefix}event").as_str())?,
95            ip: row.try_get(compact_str::format_compact!("{prefix}ip").as_str())?,
96            data: row.try_get(compact_str::format_compact!("{prefix}data").as_str())?,
97            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
98        })
99    }
100}
101
102impl ServerActivity {
103    pub async fn by_server_uuid_with_pagination(
104        database: &crate::database::Database,
105        server_uuid: uuid::Uuid,
106        page: i64,
107        per_page: i64,
108        search: Option<&str>,
109    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
110        let offset = (page - 1) * per_page;
111
112        let rows = sqlx::query(&format!(
113            r#"
114            SELECT {}, COUNT(*) OVER() AS total_count
115            FROM server_activities
116            LEFT JOIN users ON users.uuid = server_activities.user_uuid
117            LEFT JOIN roles ON roles.uuid = users.role_uuid
118            WHERE server_activities.server_uuid = $1 AND ($2 IS NULL OR server_activities.event ILIKE '%' || $2 || '%' OR users.username ILIKE '%' || $2 || '%')
119            ORDER BY server_activities.created DESC
120            LIMIT $3 OFFSET $4
121            "#,
122            Self::columns_sql(None)
123        ))
124        .bind(server_uuid)
125        .bind(search)
126        .bind(per_page)
127        .bind(offset)
128        .fetch_all(database.read())
129        .await?;
130
131        Ok(super::Pagination {
132            total: rows
133                .first()
134                .map_or(Ok(0), |row| row.try_get("total_count"))?,
135            per_page,
136            page,
137            data: rows
138                .into_iter()
139                .map(|row| Self::map(None, &row))
140                .try_collect_vec()?,
141        })
142    }
143
144    pub async fn delete_older_than(
145        database: &crate::database::Database,
146        cutoff: chrono::DateTime<chrono::Utc>,
147    ) -> Result<u64, crate::database::DatabaseError> {
148        let result = sqlx::query(
149            r#"
150            DELETE FROM server_activities
151            WHERE created < $1
152            "#,
153        )
154        .bind(cutoff.naive_utc())
155        .execute(database.write())
156        .await?;
157
158        Ok(result.rows_affected())
159    }
160
161    #[inline]
162    pub async fn into_api_object(
163        self,
164        database: &crate::database::Database,
165        storage_url_retriever: &StorageUrlRetriever<'_>,
166    ) -> Result<ApiServerActivity, anyhow::Error> {
167        Ok(ApiServerActivity {
168            user: self
169                .user
170                .map(|user| user.into_api_object(storage_url_retriever)),
171            impersonator: if let Some(impersonator) = self.impersonator {
172                Some(
173                    impersonator
174                        .fetch_cached(database)
175                        .await?
176                        .into_api_object(storage_url_retriever),
177                )
178            } else {
179                None
180            },
181            event: self.event,
182            ip: self.ip.map(|ip| ip.ip().to_compact_string()),
183            data: self.data,
184            is_api: self.api_key.is_some(),
185            is_schedule: self.schedule.is_some(),
186            created: self.created.and_utc(),
187        })
188    }
189}
190
191#[derive(ToSchema, Deserialize, Validate)]
192pub struct CreateServerActivityOptions {
193    #[garde(skip)]
194    pub server_uuid: uuid::Uuid,
195    #[garde(skip)]
196    pub user_uuid: Option<uuid::Uuid>,
197    #[garde(skip)]
198    pub impersonator_uuid: Option<uuid::Uuid>,
199    #[garde(skip)]
200    pub api_key_uuid: Option<uuid::Uuid>,
201    #[garde(skip)]
202    pub schedule_uuid: Option<uuid::Uuid>,
203    #[garde(length(chars, min = 1, max = 255))]
204    #[schema(min_length = 1, max_length = 255)]
205    pub event: compact_str::CompactString,
206    #[garde(skip)]
207    #[schema(value_type = Option<String>)]
208    pub ip: Option<sqlx::types::ipnetwork::IpNetwork>,
209    #[garde(skip)]
210    pub data: serde_json::Value,
211
212    #[garde(skip)]
213    pub created: Option<chrono::NaiveDateTime>,
214}
215
216#[async_trait::async_trait]
217impl CreatableModel for ServerActivity {
218    type CreateOptions<'a> = CreateServerActivityOptions;
219    type CreateResult = ();
220
221    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
222        static CREATE_LISTENERS: LazyLock<CreateListenerList<ServerActivity>> =
223            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
224
225        &CREATE_LISTENERS
226    }
227
228    async fn create(
229        state: &crate::State,
230        mut options: Self::CreateOptions<'_>,
231    ) -> Result<Self::CreateResult, crate::database::DatabaseError> {
232        options.validate()?;
233
234        let mut transaction = state.database.write().begin().await?;
235
236        let mut query_builder = InsertQueryBuilder::new("server_activities");
237
238        Self::run_create_handlers(&mut options, &mut query_builder, state, &mut transaction)
239            .await?;
240
241        query_builder
242            .set("server_uuid", options.server_uuid)
243            .set("user_uuid", options.user_uuid)
244            .set("impersonator_uuid", options.impersonator_uuid)
245            .set("api_key_uuid", options.api_key_uuid)
246            .set("schedule_uuid", options.schedule_uuid)
247            .set("event", &options.event)
248            .set("ip", options.ip)
249            .set("data", options.data);
250
251        if let Some(created) = options.created {
252            query_builder.set("created", created);
253        }
254
255        query_builder.execute(&mut *transaction).await?;
256
257        transaction.commit().await?;
258
259        Ok(())
260    }
261}
262
263#[derive(ToSchema, Serialize)]
264#[schema(title = "ServerActivity")]
265pub struct ApiServerActivity {
266    pub user: Option<super::user::ApiUser>,
267    pub impersonator: Option<super::user::ApiUser>,
268
269    pub event: compact_str::CompactString,
270    pub ip: Option<compact_str::CompactString>,
271    pub data: serde_json::Value,
272
273    pub is_api: bool,
274    pub is_schedule: bool,
275
276    pub created: chrono::DateTime<chrono::Utc>,
277}