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}