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