this repo has no description
at main 7.5 kB view raw
1use crate::api::SuccessResponse; 2use crate::api::error::ApiError; 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 = 91 DeviceTrustState::from_timestamps(row.trusted_at, row.trusted_until); 92 TrustedDevice { 93 id: row.id, 94 user_agent: row.user_agent, 95 friendly_name: row.friendly_name, 96 trusted_at: row.trusted_at, 97 trusted_until: row.trusted_until, 98 last_seen_at: row.last_seen_at, 99 trust_state, 100 } 101 }) 102 .collect(); 103 Json(ListTrustedDevicesResponse { devices }).into_response() 104 } 105 Err(e) => { 106 error!("DB error: {:?}", e); 107 ApiError::InternalError(None).into_response() 108 } 109 } 110} 111 112#[derive(Deserialize)] 113#[serde(rename_all = "camelCase")] 114pub struct RevokeTrustedDeviceInput { 115 pub device_id: String, 116} 117 118pub async fn revoke_trusted_device( 119 State(state): State<AppState>, 120 auth: BearerAuth, 121 Json(input): Json<RevokeTrustedDeviceInput>, 122) -> Response { 123 let device_exists = sqlx::query_scalar!( 124 r#"SELECT 1 as one FROM oauth_device od 125 JOIN oauth_account_device oad ON od.id = oad.device_id 126 WHERE oad.did = $1 AND od.id = $2"#, 127 &auth.0.did, 128 input.device_id 129 ) 130 .fetch_optional(&state.db) 131 .await; 132 133 match device_exists { 134 Ok(Some(_)) => {} 135 Ok(None) => { 136 return ApiError::DeviceNotFound.into_response(); 137 } 138 Err(e) => { 139 error!("DB error: {:?}", e); 140 return ApiError::InternalError(None).into_response(); 141 } 142 } 143 144 let result = sqlx::query!( 145 "UPDATE oauth_device SET trusted_at = NULL, trusted_until = NULL WHERE id = $1", 146 input.device_id 147 ) 148 .execute(&state.db) 149 .await; 150 151 match result { 152 Ok(_) => { 153 info!(did = %&auth.0.did, device_id = %input.device_id, "Trusted device revoked"); 154 SuccessResponse::ok().into_response() 155 } 156 Err(e) => { 157 error!("DB error: {:?}", e); 158 ApiError::InternalError(None).into_response() 159 } 160 } 161} 162 163#[derive(Deserialize)] 164#[serde(rename_all = "camelCase")] 165pub struct UpdateTrustedDeviceInput { 166 pub device_id: String, 167 pub friendly_name: Option<String>, 168} 169 170pub async fn update_trusted_device( 171 State(state): State<AppState>, 172 auth: BearerAuth, 173 Json(input): Json<UpdateTrustedDeviceInput>, 174) -> Response { 175 let device_exists = sqlx::query_scalar!( 176 r#"SELECT 1 as one FROM oauth_device od 177 JOIN oauth_account_device oad ON od.id = oad.device_id 178 WHERE oad.did = $1 AND od.id = $2"#, 179 &auth.0.did, 180 input.device_id 181 ) 182 .fetch_optional(&state.db) 183 .await; 184 185 match device_exists { 186 Ok(Some(_)) => {} 187 Ok(None) => { 188 return ApiError::DeviceNotFound.into_response(); 189 } 190 Err(e) => { 191 error!("DB error: {:?}", e); 192 return ApiError::InternalError(None).into_response(); 193 } 194 } 195 196 let result = sqlx::query!( 197 "UPDATE oauth_device SET friendly_name = $1 WHERE id = $2", 198 input.friendly_name, 199 input.device_id 200 ) 201 .execute(&state.db) 202 .await; 203 204 match result { 205 Ok(_) => { 206 info!(did = %auth.0.did, device_id = %input.device_id, "Trusted device updated"); 207 SuccessResponse::ok().into_response() 208 } 209 Err(e) => { 210 error!("DB error: {:?}", e); 211 ApiError::InternalError(None).into_response() 212 } 213 } 214} 215 216pub async fn get_device_trust_state(db: &PgPool, device_id: &str, did: &str) -> DeviceTrustState { 217 let result = sqlx::query!( 218 r#"SELECT trusted_at, trusted_until FROM oauth_device od 219 JOIN oauth_account_device oad ON od.id = oad.device_id 220 WHERE od.id = $1 AND oad.did = $2"#, 221 device_id, 222 did 223 ) 224 .fetch_optional(db) 225 .await; 226 227 match result { 228 Ok(Some(row)) => DeviceTrustState::from_timestamps(row.trusted_at, row.trusted_until), 229 _ => DeviceTrustState::Untrusted, 230 } 231} 232 233pub async fn is_device_trusted(db: &PgPool, device_id: &str, did: &str) -> bool { 234 get_device_trust_state(db, device_id, did) 235 .await 236 .is_trusted() 237} 238 239pub async fn trust_device(db: &PgPool, device_id: &str) -> Result<(), sqlx::Error> { 240 let now = Utc::now(); 241 let trusted_until = now + Duration::days(TRUST_DURATION_DAYS); 242 243 sqlx::query!( 244 "UPDATE oauth_device SET trusted_at = $1, trusted_until = $2 WHERE id = $3", 245 now, 246 trusted_until, 247 device_id 248 ) 249 .execute(db) 250 .await?; 251 252 Ok(()) 253} 254 255pub async fn extend_device_trust(db: &PgPool, device_id: &str) -> Result<(), sqlx::Error> { 256 let trusted_until = Utc::now() + Duration::days(TRUST_DURATION_DAYS); 257 258 sqlx::query!( 259 "UPDATE oauth_device SET trusted_until = $1 WHERE id = $2 AND trusted_until IS NOT NULL", 260 trusted_until, 261 device_id 262 ) 263 .execute(db) 264 .await?; 265 266 Ok(()) 267}