Skip to main content

shared/models/
server_activity.rs

1use crate::{models::InsertQueryBuilder, prelude::*};
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    extension_data: super::ModelExtensionData,
26}
27
28impl BaseModel for ServerActivity {
29    const NAME: &'static str = "server_activity";
30
31    fn get_extension_list() -> &'static super::ModelExtensionList {
32        static EXTENSIONS: LazyLock<super::ModelExtensionList> =
33            LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
34
35        &EXTENSIONS
36    }
37
38    fn get_extension_data(&self) -> &super::ModelExtensionData {
39        &self.extension_data
40    }
41
42    #[inline]
43    fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
44        let prefix = prefix.unwrap_or_default();
45
46        let mut columns = BTreeMap::from([
47            (
48                "server_activities.impersonator_uuid",
49                compact_str::format_compact!("{prefix}impersonator_uuid"),
50            ),
51            (
52                "server_activities.api_key_uuid",
53                compact_str::format_compact!("{prefix}api_key_uuid"),
54            ),
55            (
56                "server_activities.schedule_uuid",
57                compact_str::format_compact!("{prefix}schedule_uuid"),
58            ),
59            (
60                "server_activities.event",
61                compact_str::format_compact!("{prefix}event"),
62            ),
63            (
64                "server_activities.ip",
65                compact_str::format_compact!("{prefix}ip"),
66            ),
67            (
68                "server_activities.data",
69                compact_str::format_compact!("{prefix}data"),
70            ),
71            (
72                "server_activities.created",
73                compact_str::format_compact!("{prefix}created"),
74            ),
75        ]);
76
77        columns.extend(super::user::User::base_columns(Some("user_")));
78
79        columns
80    }
81
82    #[inline]
83    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
84        let prefix = prefix.unwrap_or_default();
85
86        Ok(Self {
87            user: if row
88                .try_get::<uuid::Uuid, _>("user_uuid".to_string().as_str())
89                .is_ok()
90            {
91                Some(super::user::User::map(Some("user_"), row)?)
92            } else {
93                None
94            },
95            impersonator: super::user::User::get_fetchable_from_row(
96                row,
97                compact_str::format_compact!("{prefix}impersonator_uuid"),
98            ),
99            api_key: super::user_api_key::UserApiKey::get_fetchable_from_row(
100                row,
101                compact_str::format_compact!("{prefix}api_key_uuid"),
102            ),
103            schedule: super::server_schedule::ServerSchedule::get_fetchable_from_row(
104                row,
105                compact_str::format_compact!("{prefix}schedule_uuid"),
106            ),
107            event: row.try_get(compact_str::format_compact!("{prefix}event").as_str())?,
108            ip: row.try_get(compact_str::format_compact!("{prefix}ip").as_str())?,
109            data: row.try_get(compact_str::format_compact!("{prefix}data").as_str())?,
110            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
111            extension_data: Self::map_extensions(prefix, row)?,
112        })
113    }
114}
115
116impl ServerActivity {
117    pub async fn by_server_uuid_with_pagination(
118        database: &crate::database::Database,
119        server_uuid: uuid::Uuid,
120        page: i64,
121        per_page: i64,
122        search: Option<&str>,
123    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
124        let offset = (page - 1) * per_page;
125
126        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
127            r#"
128            SELECT {}, COUNT(*) OVER() AS total_count
129            FROM server_activities
130            LEFT JOIN users ON users.uuid = server_activities.user_uuid
131            LEFT JOIN roles ON roles.uuid = users.role_uuid
132            WHERE server_activities.server_uuid = $1 AND ($2 IS NULL OR server_activities.event ILIKE '%' || $2 || '%' OR users.username ILIKE '%' || $2 || '%')
133            ORDER BY server_activities.created DESC
134            LIMIT $3 OFFSET $4
135            "#,
136            Self::columns_sql(None)
137        )))
138        .bind(server_uuid)
139        .bind(search)
140        .bind(per_page)
141        .bind(offset)
142        .fetch_all(database.read())
143        .await?;
144
145        Ok(super::Pagination {
146            total: rows
147                .first()
148                .map_or(Ok(0), |row| row.try_get("total_count"))?,
149            per_page,
150            page,
151            data: rows
152                .into_iter()
153                .map(|row| Self::map(None, &row))
154                .try_collect_vec()?,
155        })
156    }
157
158    pub async fn by_server_uuid_user_uuid_with_pagination(
159        database: &crate::database::Database,
160        server_uuid: uuid::Uuid,
161        user_uuid: uuid::Uuid,
162        page: i64,
163        per_page: i64,
164        search: Option<&str>,
165    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
166        let offset = (page - 1) * per_page;
167
168        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
169            r#"
170            SELECT {}, COUNT(*) OVER() AS total_count
171            FROM server_activities
172            LEFT JOIN users ON users.uuid = server_activities.user_uuid
173            LEFT JOIN roles ON roles.uuid = users.role_uuid
174            WHERE server_activities.server_uuid = $1 AND server_activities.user_uuid = $2
175                AND ($3 IS NULL OR server_activities.event ILIKE '%' || $3 || '%')
176            ORDER BY server_activities.created DESC
177            LIMIT $4 OFFSET $5
178            "#,
179            Self::columns_sql(None)
180        )))
181        .bind(server_uuid)
182        .bind(user_uuid)
183        .bind(search)
184        .bind(per_page)
185        .bind(offset)
186        .fetch_all(database.read())
187        .await?;
188
189        Ok(super::Pagination {
190            total: rows
191                .first()
192                .map_or(Ok(0), |row| row.try_get("total_count"))?,
193            per_page,
194            page,
195            data: rows
196                .into_iter()
197                .map(|row| Self::map(None, &row))
198                .try_collect_vec()?,
199        })
200    }
201
202    pub async fn delete_older_than(
203        database: &crate::database::Database,
204        cutoff: chrono::DateTime<chrono::Utc>,
205    ) -> Result<u64, crate::database::DatabaseError> {
206        let result = sqlx::query(
207            r#"
208            DELETE FROM server_activities
209            WHERE server_activities.created < $1
210            "#,
211        )
212        .bind(cutoff.naive_utc())
213        .execute(database.write())
214        .await?;
215
216        Ok(result.rows_affected())
217    }
218
219    pub async fn retain_latest_logs_per_server(
220        database: &crate::database::Database,
221        keep_count: i64,
222    ) -> Result<u64, crate::database::DatabaseError> {
223        let result = sqlx::query(
224            r#"
225            DELETE FROM server_activities
226            WHERE ctid IN (
227                SELECT ctid 
228                FROM (
229                    SELECT
230                        ctid, 
231                        ROW_NUMBER() OVER (PARTITION BY server_activities.server_uuid ORDER BY server_activities.created DESC) rn
232                    FROM server_activities
233                ) sub
234                WHERE sub.rn > $1
235            )
236            "#,
237        )
238        .bind(keep_count)
239        .execute(database.write())
240        .await?;
241
242        Ok(result.rows_affected())
243    }
244}
245
246#[async_trait::async_trait]
247impl IntoApiObject for ServerActivity {
248    type ApiObject = ApiServerActivity;
249    type ExtraArgs<'a> = (&'a crate::storage::StorageUrlRetriever<'a>, bool);
250
251    async fn into_api_object<'a>(
252        self,
253        state: &crate::State,
254        (storage_url_retriever, show_ip): Self::ExtraArgs<'a>,
255    ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
256        let api_object = ApiServerActivity::init_hooks(&self, state).await?;
257
258        let user = if let Some(user) = self.user {
259            Some(user.into_api_object(state, storage_url_retriever).await?)
260        } else {
261            None
262        };
263
264        let impersonator = if let Some(impersonator) = self.impersonator {
265            Some(
266                impersonator
267                    .fetch_cached(&state.database)
268                    .await?
269                    .into_api_object(state, storage_url_retriever)
270                    .await?,
271            )
272        } else {
273            None
274        };
275
276        let api_object = finish_extendible!(
277            ApiServerActivity {
278                user,
279                impersonator,
280                event: self.event,
281                ip: if show_ip {
282                    self.ip.map(|ip| ip.ip().to_compact_string())
283                } else {
284                    None
285                },
286                data: self.data,
287                is_api: self.api_key.is_some(),
288                is_schedule: self.schedule.is_some(),
289                created: self.created.and_utc(),
290            },
291            api_object,
292            state
293        )?;
294
295        Ok(api_object)
296    }
297}
298
299#[derive(ToSchema, Deserialize, Validate)]
300pub struct CreateServerActivityOptions {
301    #[garde(skip)]
302    pub server_uuid: uuid::Uuid,
303    #[garde(skip)]
304    pub user_uuid: Option<uuid::Uuid>,
305    #[garde(skip)]
306    pub impersonator_uuid: Option<uuid::Uuid>,
307    #[garde(skip)]
308    pub api_key_uuid: Option<uuid::Uuid>,
309    #[garde(skip)]
310    pub schedule_uuid: Option<uuid::Uuid>,
311    #[garde(length(chars, min = 1, max = 255))]
312    #[schema(min_length = 1, max_length = 255)]
313    pub event: compact_str::CompactString,
314    #[garde(skip)]
315    #[schema(value_type = Option<String>)]
316    pub ip: Option<sqlx::types::ipnetwork::IpNetwork>,
317    #[garde(skip)]
318    pub data: serde_json::Value,
319
320    #[garde(skip)]
321    pub created: Option<chrono::NaiveDateTime>,
322}
323
324#[async_trait::async_trait]
325impl CreatableModel for ServerActivity {
326    type CreateOptions<'a> = CreateServerActivityOptions;
327    type CreateResult = ();
328
329    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
330        static CREATE_LISTENERS: LazyLock<CreateListenerList<ServerActivity>> =
331            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
332
333        &CREATE_LISTENERS
334    }
335
336    async fn create_with_transaction(
337        state: &crate::State,
338        mut options: Self::CreateOptions<'_>,
339        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
340    ) -> Result<Self::CreateResult, crate::database::DatabaseError> {
341        options.validate()?;
342
343        let mut query_builder = InsertQueryBuilder::new("server_activities");
344
345        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
346
347        query_builder
348            .set("server_uuid", options.server_uuid)
349            .set("user_uuid", options.user_uuid)
350            .set("impersonator_uuid", options.impersonator_uuid)
351            .set("api_key_uuid", options.api_key_uuid)
352            .set("schedule_uuid", options.schedule_uuid)
353            .set("event", &options.event)
354            .set("ip", options.ip)
355            .set("data", &options.data);
356
357        if let Some(created) = options.created {
358            query_builder.set("created", created);
359        }
360
361        query_builder.execute(&mut **transaction).await?;
362
363        let mut result = ();
364
365        Self::run_after_create_handlers(&mut result, &options, state, transaction).await?;
366
367        Ok(result)
368    }
369}
370
371#[schema_extension_derive::extendible]
372#[init_args(ServerActivity, crate::State)]
373#[hook_args(crate::State)]
374#[derive(ToSchema, Serialize)]
375#[schema(title = "ServerActivity")]
376pub struct ApiServerActivity {
377    pub user: Option<super::user::ApiUser>,
378    pub impersonator: Option<super::user::ApiUser>,
379
380    pub event: compact_str::CompactString,
381    pub ip: Option<compact_str::CompactString>,
382    pub data: serde_json::Value,
383
384    pub is_api: bool,
385    pub is_schedule: bool,
386
387    pub created: chrono::DateTime<chrono::Utc>,
388}