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        super::PaginatedQueryBuilder::new(
125            Self::columns_sql(None),
126            r#"
127            FROM server_activities
128            LEFT JOIN users ON users.uuid = server_activities.user_uuid
129            LEFT JOIN roles ON roles.uuid = users.role_uuid
130            WHERE server_activities.server_uuid = $1 AND ($2 IS NULL OR server_activities.event ILIKE '%' || $2 || '%' OR users.username ILIKE '%' || $2 || '%')
131            "#,
132            "server_activities.created DESC",
133        )
134        .bind(server_uuid)
135        .bind(search)
136        .fetch(database, page, per_page)
137        .await
138    }
139
140    pub async fn by_server_uuid_user_uuid_with_pagination(
141        database: &crate::database::Database,
142        server_uuid: uuid::Uuid,
143        user_uuid: uuid::Uuid,
144        page: i64,
145        per_page: i64,
146        search: Option<&str>,
147    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
148        super::PaginatedQueryBuilder::new(
149            Self::columns_sql(None),
150            r#"
151            FROM server_activities
152            LEFT JOIN users ON users.uuid = server_activities.user_uuid
153            LEFT JOIN roles ON roles.uuid = users.role_uuid
154            WHERE server_activities.server_uuid = $1 AND server_activities.user_uuid = $2
155                AND ($3 IS NULL OR server_activities.event ILIKE '%' || $3 || '%')
156            "#,
157            "server_activities.created DESC",
158        )
159        .bind(server_uuid)
160        .bind(user_uuid)
161        .bind(search)
162        .fetch(database, page, per_page)
163        .await
164    }
165
166    pub async fn delete_older_than(
167        database: &crate::database::Database,
168        cutoff: chrono::DateTime<chrono::Utc>,
169    ) -> Result<u64, crate::database::DatabaseError> {
170        let result = sqlx::query(
171            r#"
172            DELETE FROM server_activities
173            WHERE server_activities.created < $1
174            "#,
175        )
176        .bind(cutoff.naive_utc())
177        .execute(database.write())
178        .await?;
179
180        Ok(result.rows_affected())
181    }
182
183    pub async fn retain_latest_logs_per_server(
184        database: &crate::database::Database,
185        keep_count: i64,
186    ) -> Result<u64, crate::database::DatabaseError> {
187        let result = sqlx::query(
188            r#"
189            DELETE FROM server_activities
190            WHERE ctid IN (
191                SELECT ctid 
192                FROM (
193                    SELECT
194                        ctid, 
195                        ROW_NUMBER() OVER (PARTITION BY server_activities.server_uuid ORDER BY server_activities.created DESC) rn
196                    FROM server_activities
197                ) sub
198                WHERE sub.rn > $1
199            )
200            "#,
201        )
202        .bind(keep_count)
203        .execute(database.write())
204        .await?;
205
206        Ok(result.rows_affected())
207    }
208}
209
210#[async_trait::async_trait]
211impl IntoApiObject for ServerActivity {
212    type ApiObject = ApiServerActivity;
213    type ExtraArgs<'a> = (&'a crate::storage::StorageUrlRetriever<'a>, bool);
214
215    async fn into_api_object<'a>(
216        self,
217        state: &crate::State,
218        (storage_url_retriever, show_ip): Self::ExtraArgs<'a>,
219    ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
220        let api_object = ApiServerActivity::init_hooks(&self, state).await?;
221
222        let user = if let Some(user) = self.user {
223            Some(user.into_api_object(state, storage_url_retriever).await?)
224        } else {
225            None
226        };
227
228        let impersonator = if let Some(impersonator) = self.impersonator {
229            Some(
230                impersonator
231                    .fetch_cached(&state.database)
232                    .await?
233                    .into_api_object(state, storage_url_retriever)
234                    .await?,
235            )
236        } else {
237            None
238        };
239
240        let api_object = finish_extendible!(
241            ApiServerActivity {
242                user,
243                impersonator,
244                event: self.event,
245                ip: if show_ip {
246                    self.ip.map(|ip| ip.ip().to_compact_string())
247                } else {
248                    None
249                },
250                data: self.data,
251                is_api: self.api_key.is_some(),
252                is_schedule: self.schedule.is_some(),
253                created: self.created.and_utc(),
254            },
255            api_object,
256            state
257        )?;
258
259        Ok(api_object)
260    }
261}
262
263#[derive(ToSchema, Deserialize, Validate)]
264pub struct CreateServerActivityOptions {
265    #[garde(skip)]
266    pub server_uuid: uuid::Uuid,
267    #[garde(skip)]
268    pub user_uuid: Option<uuid::Uuid>,
269    #[garde(skip)]
270    pub impersonator_uuid: Option<uuid::Uuid>,
271    #[garde(skip)]
272    pub api_key_uuid: Option<uuid::Uuid>,
273    #[garde(skip)]
274    pub schedule_uuid: Option<uuid::Uuid>,
275    #[garde(length(chars, min = 1, max = 255))]
276    #[schema(min_length = 1, max_length = 255)]
277    pub event: compact_str::CompactString,
278    #[garde(skip)]
279    #[schema(value_type = Option<String>)]
280    pub ip: Option<sqlx::types::ipnetwork::IpNetwork>,
281    #[garde(skip)]
282    pub data: serde_json::Value,
283
284    #[garde(skip)]
285    pub created: Option<chrono::NaiveDateTime>,
286}
287
288#[async_trait::async_trait]
289impl CreatableModel for ServerActivity {
290    type CreateOptions<'a> = CreateServerActivityOptions;
291    type CreateResult = ();
292
293    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
294        static CREATE_LISTENERS: LazyLock<CreateListenerList<ServerActivity>> =
295            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
296
297        &CREATE_LISTENERS
298    }
299
300    async fn create_with_transaction(
301        state: &crate::State,
302        mut options: Self::CreateOptions<'_>,
303        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
304    ) -> Result<Self::CreateResult, crate::database::DatabaseError> {
305        options.validate()?;
306
307        let mut query_builder = InsertQueryBuilder::new("server_activities");
308
309        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
310
311        query_builder
312            .set("server_uuid", options.server_uuid)
313            .set("user_uuid", options.user_uuid)
314            .set("impersonator_uuid", options.impersonator_uuid)
315            .set("api_key_uuid", options.api_key_uuid)
316            .set("schedule_uuid", options.schedule_uuid)
317            .set("event", &options.event)
318            .set("ip", options.ip)
319            .set("data", &options.data);
320
321        if let Some(created) = options.created {
322            query_builder.set("created", created);
323        }
324
325        query_builder.execute(&mut **transaction).await?;
326
327        let mut result = ();
328
329        Self::run_after_create_handlers(&mut result, &options, state, transaction).await?;
330
331        Ok(result)
332    }
333}
334
335#[schema_extension_derive::extendible]
336#[init_args(ServerActivity, crate::State)]
337#[hook_args(crate::State)]
338#[derive(ToSchema, Serialize)]
339#[schema(title = "ServerActivity")]
340pub struct ApiServerActivity {
341    pub user: Option<super::user::ApiUser>,
342    pub impersonator: Option<super::user::ApiUser>,
343
344    pub event: compact_str::CompactString,
345    pub ip: Option<compact_str::CompactString>,
346    pub data: serde_json::Value,
347
348    pub is_api: bool,
349    pub is_schedule: bool,
350
351    pub created: chrono::DateTime<chrono::Utc>,
352}