Something went wrong. Try again.
A downstream fork of Tranquil PDS for use in margin.cafe
Something went wrong. Try again.
7.8 kB · 280 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281use axum::{Json, extract::State};use chrono::{DateTime, Duration, Utc};use serde::{Deserialize, Serialize};use tracing::{error, info};use tranquil_db_traits::OAuthRepository;use tranquil_pds::api::SuccessResponse;use tranquil_pds::api::error::{ApiError, DbResultExt};use tranquil_types::DeviceId;
use tranquil_pds::auth::{Active, Auth};use tranquil_pds::state::AppState;
const TRUST_DURATION_DAYS: i64 = 30;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]#[serde(rename_all = "lowercase")]pub enum DeviceTrustState { Untrusted, Trusted, Expired,}
impl DeviceTrustState { pub fn from_timestamps( trusted_at: Option<DateTime<Utc>>, trusted_until: Option<DateTime<Utc>>, ) -> Self { match (trusted_at, trusted_until) { (Some(_), Some(until)) if until > Utc::now() => Self::Trusted, (Some(_), Some(_)) => Self::Expired, _ => Self::Untrusted, } }
pub fn is_trusted(&self) -> bool { matches!(self, Self::Trusted) }
pub fn is_expired(&self) -> bool { matches!(self, Self::Expired) }
pub fn as_str(&self) -> &'static str { match self { Self::Untrusted => "untrusted", Self::Trusted => "trusted", Self::Expired => "expired", } }}
#[derive(Serialize)]#[serde(rename_all = "camelCase")]pub struct TrustedDevice { pub id: DeviceId, pub user_agent: Option<String>, pub friendly_name: Option<String>, pub trusted_at: Option<DateTime<Utc>>, pub trusted_until: Option<DateTime<Utc>>, pub last_seen_at: DateTime<Utc>, pub trust_state: DeviceTrustState,}
#[derive(Serialize)]#[serde(rename_all = "camelCase")]pub struct ListTrustedDevicesOutput { pub devices: Vec<TrustedDevice>,}
pub async fn list_trusted_devices( State(state): State<AppState>, auth: Auth<Active>,) -> Result<Json<ListTrustedDevicesOutput>, ApiError> { let rows = state .repos .oauth .list_trusted_devices(&auth.did) .await .log_db_err("listing trusted devices")?;
let devices = rows .into_iter() .map(|row| { let trust_state = DeviceTrustState::from_timestamps(row.trusted_at, row.trusted_until); TrustedDevice { id: row.id, user_agent: row.user_agent, friendly_name: row.friendly_name, trusted_at: row.trusted_at, trusted_until: row.trusted_until, last_seen_at: row.last_seen_at, trust_state, } }) .collect();
Ok(Json(ListTrustedDevicesOutput { devices }))}
#[derive(Deserialize)]#[serde(rename_all = "camelCase")]pub struct RevokeTrustedDeviceInput { pub device_id: DeviceId,}
pub async fn revoke_trusted_device( State(state): State<AppState>, auth: Auth<Active>, Json(input): Json<RevokeTrustedDeviceInput>,) -> Result<Json<SuccessResponse>, ApiError> { match state .repos .oauth .device_belongs_to_user(&input.device_id, &auth.did) .await { Ok(true) => {} Ok(false) => { return Err(ApiError::DeviceNotFound); } Err(e) => { error!("DB error: {:?}", e); return Err(ApiError::InternalError(None)); } }
state .repos .oauth .delete_account_device(&auth.did, &input.device_id) .await .log_db_err("forgetting remembered device")?;
info!(did = %&auth.did, device_id = %input.device_id, "Remembered device forgotten"); Ok(Json(SuccessResponse { success: true }))}
#[derive(Serialize)]#[serde(rename_all = "camelCase")]pub struct RememberedAppChoice { pub client_id: tranquil_types::ClientId, pub scopes: Vec<String>,}
#[derive(Serialize)]#[serde(rename_all = "camelCase")]pub struct ListRememberedAppChoicesOutput { pub choices: Vec<RememberedAppChoice>,}
pub async fn list_remembered_app_choices( State(state): State<AppState>, auth: Auth<Active>,) -> Result<Json<ListRememberedAppChoicesOutput>, ApiError> { let rows = state .repos .oauth .list_scope_preference_clients(&auth.did) .await .log_db_err("listing remembered app choices")?;
Ok(Json(ListRememberedAppChoicesOutput { choices: rows .into_iter() .map(|row| RememberedAppChoice { client_id: row.client_id, scopes: row .preferences .into_iter() .filter(|preference| preference.granted) .map(|preference| preference.scope) .collect(), }) .collect(), }))}
#[derive(Deserialize)]#[serde(rename_all = "camelCase")]pub struct ForgetRememberedAppChoiceInput { pub client_id: tranquil_types::ClientId,}
pub async fn forget_remembered_app_choice( State(state): State<AppState>, auth: Auth<Active>, Json(input): Json<ForgetRememberedAppChoiceInput>,) -> Result<Json<SuccessResponse>, ApiError> { state .repos .oauth .delete_scope_preferences(&auth.did, &input.client_id) .await .log_db_err("forgetting remembered app choice")?;
Ok(Json(SuccessResponse { success: true }))}
#[derive(Deserialize)]#[serde(rename_all = "camelCase")]pub struct UpdateTrustedDeviceInput { pub device_id: DeviceId, pub friendly_name: Option<String>,}
pub async fn update_trusted_device( State(state): State<AppState>, auth: Auth<Active>, Json(input): Json<UpdateTrustedDeviceInput>,) -> Result<Json<SuccessResponse>, ApiError> { match state .repos .oauth .device_belongs_to_user(&input.device_id, &auth.did) .await { Ok(true) => {} Ok(false) => { return Err(ApiError::DeviceNotFound); } Err(e) => { error!("DB error: {:?}", e); return Err(ApiError::InternalError(None)); } }
state .repos .oauth .update_device_friendly_name(&input.device_id, &auth.did, input.friendly_name.as_deref()) .await .log_db_err("updating device friendly name")?;
info!(did = %auth.did, device_id = %input.device_id, "Trusted device updated"); Ok(Json(SuccessResponse { success: true }))}
pub async fn get_device_trust_state( oauth_repo: &dyn OAuthRepository, device_id: &DeviceId, did: &tranquil_types::Did,) -> DeviceTrustState { match oauth_repo.get_device_trust_info(device_id, did).await { Ok(Some(info)) => DeviceTrustState::from_timestamps(info.trusted_at, info.trusted_until), _ => DeviceTrustState::Untrusted, }}
pub async fn is_device_trusted( oauth_repo: &dyn OAuthRepository, device_id: &DeviceId, did: &tranquil_types::Did,) -> bool { get_device_trust_state(oauth_repo, device_id, did) .await .is_trusted()}
pub async fn trust_device( oauth_repo: &dyn OAuthRepository, device_id: &DeviceId, did: &tranquil_types::Did,) -> Result<(), tranquil_db_traits::DbError> { let now = Utc::now(); let trusted_until = now + Duration::days(TRUST_DURATION_DAYS); oauth_repo .trust_device(device_id, did, now, trusted_until) .await}
pub async fn extend_device_trust( oauth_repo: &dyn OAuthRepository, device_id: &DeviceId, did: &tranquil_types::Did,) -> Result<(), tranquil_db_traits::DbError> { let trusted_until = Utc::now() + Duration::days(TRUST_DURATION_DAYS); oauth_repo .extend_device_trust(device_id, did, trusted_until) .await}