this repo has no description
1use axum::{
2 Json,
3 extract::State,
4 http::StatusCode,
5 response::{IntoResponse, Response},
6};
7use chrono::{DateTime, Duration, Utc};
8use serde::{Deserialize, Serialize};
9use serde_json::json;
10use sqlx::PgPool;
11use tracing::{error, info};
12
13use crate::auth::BearerAuth;
14use crate::state::AppState;
15
16const TRUST_DURATION_DAYS: i64 = 30;
17
18#[derive(Serialize)]
19#[serde(rename_all = "camelCase")]
20pub struct TrustedDevice {
21 pub id: String,
22 pub user_agent: Option<String>,
23 pub friendly_name: Option<String>,
24 pub trusted_at: Option<DateTime<Utc>>,
25 pub trusted_until: Option<DateTime<Utc>>,
26 pub last_seen_at: DateTime<Utc>,
27}
28
29#[derive(Serialize)]
30#[serde(rename_all = "camelCase")]
31pub struct ListTrustedDevicesResponse {
32 pub devices: Vec<TrustedDevice>,
33}
34
35pub async fn list_trusted_devices(State(state): State<AppState>, auth: BearerAuth) -> Response {
36 let devices = sqlx::query!(
37 r#"SELECT od.id, od.user_agent, od.friendly_name, od.trusted_at, od.trusted_until, od.last_seen_at
38 FROM oauth_device od
39 JOIN oauth_account_device oad ON od.id = oad.device_id
40 WHERE oad.did = $1 AND od.trusted_until IS NOT NULL AND od.trusted_until > NOW()
41 ORDER BY od.last_seen_at DESC"#,
42 auth.0.did
43 )
44 .fetch_all(&state.db)
45 .await;
46
47 match devices {
48 Ok(rows) => {
49 let devices = rows
50 .into_iter()
51 .map(|row| TrustedDevice {
52 id: row.id,
53 user_agent: row.user_agent,
54 friendly_name: row.friendly_name,
55 trusted_at: row.trusted_at,
56 trusted_until: row.trusted_until,
57 last_seen_at: row.last_seen_at,
58 })
59 .collect();
60 Json(ListTrustedDevicesResponse { devices }).into_response()
61 }
62 Err(e) => {
63 error!("DB error: {:?}", e);
64 (
65 StatusCode::INTERNAL_SERVER_ERROR,
66 Json(json!({"error": "InternalError"})),
67 )
68 .into_response()
69 }
70 }
71}
72
73#[derive(Deserialize)]
74#[serde(rename_all = "camelCase")]
75pub struct RevokeTrustedDeviceInput {
76 pub device_id: String,
77}
78
79pub async fn revoke_trusted_device(
80 State(state): State<AppState>,
81 auth: BearerAuth,
82 Json(input): Json<RevokeTrustedDeviceInput>,
83) -> Response {
84 let device_exists = sqlx::query_scalar!(
85 r#"SELECT 1 as one FROM oauth_device od
86 JOIN oauth_account_device oad ON od.id = oad.device_id
87 WHERE oad.did = $1 AND od.id = $2"#,
88 auth.0.did,
89 input.device_id
90 )
91 .fetch_optional(&state.db)
92 .await;
93
94 match device_exists {
95 Ok(Some(_)) => {}
96 Ok(None) => {
97 return (
98 StatusCode::NOT_FOUND,
99 Json(json!({"error": "DeviceNotFound", "message": "Device not found or not owned by this account"})),
100 )
101 .into_response();
102 }
103 Err(e) => {
104 error!("DB error: {:?}", e);
105 return (
106 StatusCode::INTERNAL_SERVER_ERROR,
107 Json(json!({"error": "InternalError"})),
108 )
109 .into_response();
110 }
111 }
112
113 let result = sqlx::query!(
114 "UPDATE oauth_device SET trusted_at = NULL, trusted_until = NULL WHERE id = $1",
115 input.device_id
116 )
117 .execute(&state.db)
118 .await;
119
120 match result {
121 Ok(_) => {
122 info!(did = %auth.0.did, device_id = %input.device_id, "Trusted device revoked");
123 Json(json!({"success": true})).into_response()
124 }
125 Err(e) => {
126 error!("DB error: {:?}", e);
127 (
128 StatusCode::INTERNAL_SERVER_ERROR,
129 Json(json!({"error": "InternalError"})),
130 )
131 .into_response()
132 }
133 }
134}
135
136#[derive(Deserialize)]
137#[serde(rename_all = "camelCase")]
138pub struct UpdateTrustedDeviceInput {
139 pub device_id: String,
140 pub friendly_name: Option<String>,
141}
142
143pub async fn update_trusted_device(
144 State(state): State<AppState>,
145 auth: BearerAuth,
146 Json(input): Json<UpdateTrustedDeviceInput>,
147) -> Response {
148 let device_exists = sqlx::query_scalar!(
149 r#"SELECT 1 as one FROM oauth_device od
150 JOIN oauth_account_device oad ON od.id = oad.device_id
151 WHERE oad.did = $1 AND od.id = $2"#,
152 auth.0.did,
153 input.device_id
154 )
155 .fetch_optional(&state.db)
156 .await;
157
158 match device_exists {
159 Ok(Some(_)) => {}
160 Ok(None) => {
161 return (
162 StatusCode::NOT_FOUND,
163 Json(json!({"error": "DeviceNotFound", "message": "Device not found or not owned by this account"})),
164 )
165 .into_response();
166 }
167 Err(e) => {
168 error!("DB error: {:?}", e);
169 return (
170 StatusCode::INTERNAL_SERVER_ERROR,
171 Json(json!({"error": "InternalError"})),
172 )
173 .into_response();
174 }
175 }
176
177 let result = sqlx::query!(
178 "UPDATE oauth_device SET friendly_name = $1 WHERE id = $2",
179 input.friendly_name,
180 input.device_id
181 )
182 .execute(&state.db)
183 .await;
184
185 match result {
186 Ok(_) => {
187 info!(did = %auth.0.did, device_id = %input.device_id, "Trusted device updated");
188 Json(json!({"success": true})).into_response()
189 }
190 Err(e) => {
191 error!("DB error: {:?}", e);
192 (
193 StatusCode::INTERNAL_SERVER_ERROR,
194 Json(json!({"error": "InternalError"})),
195 )
196 .into_response()
197 }
198 }
199}
200
201pub async fn is_device_trusted(db: &PgPool, device_id: &str, did: &str) -> bool {
202 let result = sqlx::query_scalar!(
203 r#"SELECT trusted_until FROM oauth_device od
204 JOIN oauth_account_device oad ON od.id = oad.device_id
205 WHERE od.id = $1 AND oad.did = $2"#,
206 device_id,
207 did
208 )
209 .fetch_optional(db)
210 .await;
211
212 match result {
213 Ok(Some(Some(trusted_until))) => trusted_until > Utc::now(),
214 _ => false,
215 }
216}
217
218pub async fn trust_device(db: &PgPool, device_id: &str) -> Result<(), sqlx::Error> {
219 let now = Utc::now();
220 let trusted_until = now + Duration::days(TRUST_DURATION_DAYS);
221
222 sqlx::query!(
223 "UPDATE oauth_device SET trusted_at = $1, trusted_until = $2 WHERE id = $3",
224 now,
225 trusted_until,
226 device_id
227 )
228 .execute(db)
229 .await?;
230
231 Ok(())
232}
233
234pub async fn extend_device_trust(db: &PgPool, device_id: &str) -> Result<(), sqlx::Error> {
235 let trusted_until = Utc::now() + Duration::days(TRUST_DURATION_DAYS);
236
237 sqlx::query!(
238 "UPDATE oauth_device SET trusted_until = $1 WHERE id = $2 AND trusted_until IS NOT NULL",
239 trusted_until,
240 device_id
241 )
242 .execute(db)
243 .await?;
244
245 Ok(())
246}