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