systemprompt_analytics/repository/fingerprint/
mutations.rs1use anyhow::Result;
2use systemprompt_identifiers::UserId;
3
4use super::FingerprintRepository;
5use crate::models::{FingerprintReputation, FlagReason};
6
7impl FingerprintRepository {
8 pub async fn upsert_fingerprint(
9 &self,
10 fingerprint_hash: &str,
11 ip_address: Option<&str>,
12 user_agent: Option<&str>,
13 user_id: Option<&UserId>,
14 ) -> Result<FingerprintReputation> {
15 let user_ids = user_id.map_or_else(Vec::new, |u| vec![u.as_str().to_string()]);
16
17 let row = sqlx::query_as!(
18 FingerprintReputation,
19 r#"
20 INSERT INTO fingerprint_reputation (
21 fingerprint_hash, last_ip_address, last_user_agent,
22 associated_user_ids, total_session_count
23 )
24 VALUES ($1, $2, $3, $4, 1)
25 ON CONFLICT (fingerprint_hash) DO UPDATE SET
26 last_seen_at = CURRENT_TIMESTAMP,
27 last_ip_address = COALESCE($2, fingerprint_reputation.last_ip_address),
28 last_user_agent = COALESCE($3, fingerprint_reputation.last_user_agent),
29 total_session_count = fingerprint_reputation.total_session_count + 1,
30 associated_user_ids = CASE
31 WHEN array_length($4, 1) > 0 AND NOT ($4[1] = ANY(fingerprint_reputation.associated_user_ids))
32 THEN array_cat(fingerprint_reputation.associated_user_ids, $4)
33 ELSE fingerprint_reputation.associated_user_ids
34 END,
35 updated_at = CURRENT_TIMESTAMP
36 RETURNING
37 fingerprint_hash,
38 first_seen_at,
39 last_seen_at,
40 total_session_count,
41 active_session_count,
42 total_request_count,
43 requests_last_hour,
44 peak_requests_per_minute,
45 sustained_high_velocity_minutes,
46 is_flagged,
47 flag_reason,
48 flagged_at,
49 reputation_score,
50 abuse_incidents,
51 last_abuse_at,
52 last_ip_address,
53 last_user_agent,
54 associated_user_ids,
55 updated_at
56 "#,
57 fingerprint_hash,
58 ip_address,
59 user_agent,
60 &user_ids[..],
61 )
62 .fetch_one(&*self.write_pool)
63 .await?;
64
65 Ok(row)
66 }
67
68 pub async fn flag_fingerprint(
69 &self,
70 fingerprint_hash: &str,
71 reason: FlagReason,
72 new_score: i32,
73 ) -> Result<()> {
74 sqlx::query!(
75 r#"
76 UPDATE fingerprint_reputation
77 SET is_flagged = TRUE,
78 flag_reason = $2,
79 flagged_at = CURRENT_TIMESTAMP,
80 reputation_score = $3,
81 abuse_incidents = abuse_incidents + 1,
82 last_abuse_at = CURRENT_TIMESTAMP,
83 updated_at = CURRENT_TIMESTAMP
84 WHERE fingerprint_hash = $1
85 "#,
86 fingerprint_hash,
87 reason.as_str(),
88 new_score,
89 )
90 .execute(&*self.write_pool)
91 .await?;
92
93 Ok(())
94 }
95
96 pub async fn update_velocity_metrics(
97 &self,
98 fingerprint_hash: &str,
99 requests_last_hour: i32,
100 peak_requests_per_minute: f32,
101 sustained_high_velocity_minutes: i32,
102 ) -> Result<()> {
103 sqlx::query!(
104 r#"
105 UPDATE fingerprint_reputation
106 SET requests_last_hour = $2,
107 peak_requests_per_minute = $3,
108 sustained_high_velocity_minutes = $4,
109 total_request_count = total_request_count + 1,
110 updated_at = CURRENT_TIMESTAMP
111 WHERE fingerprint_hash = $1
112 "#,
113 fingerprint_hash,
114 requests_last_hour,
115 peak_requests_per_minute,
116 sustained_high_velocity_minutes,
117 )
118 .execute(&*self.write_pool)
119 .await?;
120
121 Ok(())
122 }
123
124 pub async fn update_active_session_count(
125 &self,
126 fingerprint_hash: &str,
127 active_count: i32,
128 ) -> Result<()> {
129 sqlx::query!(
130 r#"
131 UPDATE fingerprint_reputation
132 SET active_session_count = $2,
133 updated_at = CURRENT_TIMESTAMP
134 WHERE fingerprint_hash = $1
135 "#,
136 fingerprint_hash,
137 active_count,
138 )
139 .execute(&*self.write_pool)
140 .await?;
141
142 Ok(())
143 }
144
145 pub async fn increment_request_count(&self, fingerprint_hash: &str) -> Result<()> {
146 sqlx::query!(
147 r#"
148 UPDATE fingerprint_reputation
149 SET total_request_count = total_request_count + 1,
150 updated_at = CURRENT_TIMESTAMP
151 WHERE fingerprint_hash = $1
152 "#,
153 fingerprint_hash,
154 )
155 .execute(&*self.write_pool)
156 .await?;
157
158 Ok(())
159 }
160
161 pub async fn clear_flag(&self, fingerprint_hash: &str) -> Result<()> {
162 sqlx::query!(
163 r#"
164 UPDATE fingerprint_reputation
165 SET is_flagged = FALSE,
166 flag_reason = NULL,
167 flagged_at = NULL,
168 updated_at = CURRENT_TIMESTAMP
169 WHERE fingerprint_hash = $1
170 "#,
171 fingerprint_hash,
172 )
173 .execute(&*self.write_pool)
174 .await?;
175
176 Ok(())
177 }
178
179 pub async fn adjust_reputation_score(&self, fingerprint_hash: &str, delta: i32) -> Result<i32> {
180 let row = sqlx::query_scalar!(
181 r#"
182 UPDATE fingerprint_reputation
183 SET reputation_score = GREATEST(0, LEAST(100, reputation_score + $2)),
184 updated_at = CURRENT_TIMESTAMP
185 WHERE fingerprint_hash = $1
186 RETURNING reputation_score as "reputation_score!"
187 "#,
188 fingerprint_hash,
189 delta,
190 )
191 .fetch_one(&*self.write_pool)
192 .await?;
193
194 Ok(row)
195 }
196}