Skip to main content

systemprompt_users/repository/
sso_login_events.rs

1//! Repository for `sso_login_events` — the audit row every browser-redirect
2//! SSO sign-in leaves, accepted or refused.
3//!
4//! Queries are built with `sqlx::query` rather than the checked macros so the
5//! table needs no offline query cache entry.
6//!
7//! Copyright (c) systemprompt.io — Business Source License 1.1.
8//! See <https://systemprompt.io> for licensing details.
9
10use std::net::IpAddr;
11use std::sync::Arc;
12
13use chrono::{DateTime, Utc};
14use sqlx::PgPool;
15use systemprompt_database::DbPool;
16use systemprompt_identifiers::UserId;
17
18use crate::error::Result;
19
20const MAX_LIST: i64 = 500;
21
22#[derive(Debug, Clone, Copy)]
23pub struct NewSsoLoginEvent<'a> {
24    pub provider_id: &'a str,
25    pub issuer: &'a str,
26    pub outcome: &'a str,
27    pub reason: Option<&'a str>,
28    pub user_id: Option<&'a UserId>,
29    pub email: Option<&'a str>,
30    pub external_sub: Option<&'a str>,
31    pub ip: Option<IpAddr>,
32    pub groups_count: Option<i32>,
33}
34
35#[derive(Debug, Clone)]
36pub struct SsoLoginEvent {
37    pub id: i64,
38    pub occurred_at: DateTime<Utc>,
39    pub provider_id: String,
40    pub issuer: String,
41    pub outcome: String,
42    pub reason: Option<String>,
43    pub user_id: Option<UserId>,
44    pub email: Option<String>,
45    pub external_sub: Option<String>,
46    pub ip: Option<String>,
47    pub groups_count: Option<i32>,
48}
49
50#[derive(Debug, Clone)]
51pub struct SsoLoginEventRepository {
52    pool: Arc<PgPool>,
53    write_pool: Arc<PgPool>,
54}
55
56impl SsoLoginEventRepository {
57    pub fn new(db: &DbPool) -> Self {
58        Self {
59            pool: db.pool(),
60            write_pool: db.write_pool(),
61        }
62    }
63
64    pub async fn insert_sso_login_event(&self, event: &NewSsoLoginEvent<'_>) -> Result<()> {
65        sqlx::query!(
66            "INSERT INTO sso_login_events
67             (provider_id, issuer, outcome, reason, user_id, email, external_sub, ip,
68              groups_count)
69             VALUES ($1, $2, $3, $4, $5, $6, $7, CAST($8::TEXT AS INET), $9)",
70            event.provider_id,
71            event.issuer,
72            event.outcome,
73            event.reason,
74            event.user_id.map(UserId::as_str),
75            event.email,
76            event.external_sub,
77            event.ip.map(|ip| ip.to_string()),
78            event.groups_count
79        )
80        .execute(&*self.write_pool)
81        .await?;
82        Ok(())
83    }
84
85    pub async fn list_recent_sso_login_events(&self, limit: i64) -> Result<Vec<SsoLoginEvent>> {
86        let rows = sqlx::query!(
87            r#"SELECT id, occurred_at, provider_id, issuer, outcome, reason, user_id, email,
88                    external_sub, host(ip) AS "ip?", groups_count
89               FROM sso_login_events
90              ORDER BY occurred_at DESC, id DESC
91              LIMIT $1"#,
92            limit.clamp(1, MAX_LIST)
93        )
94        .fetch_all(&*self.pool)
95        .await?;
96
97        Ok(rows
98            .into_iter()
99            .map(|r| SsoLoginEvent {
100                id: r.id,
101                occurred_at: r.occurred_at,
102                provider_id: r.provider_id,
103                issuer: r.issuer,
104                outcome: r.outcome,
105                reason: r.reason,
106                user_id: r.user_id.map(UserId::new),
107                email: r.email,
108                external_sub: r.external_sub,
109                ip: r.ip,
110                groups_count: r.groups_count,
111            })
112            .collect())
113    }
114}