Files
tranquil-pds/crates/tranquil-api/src/server/trusted_devices.rs
T
2026-07-12 00:25:08 +03:00

220 lines
5.9 KiB
Rust

use 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
.revoke_device_trust(&input.device_id, &auth.did)
.await
.log_db_err("revoking device trust")?;
info!(did = %&auth.did, device_id = %input.device_id, "Trusted device revoked");
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
}