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}