use axum::{ Extension, Router, extract::{Query, State}, response::Redirect, routing::get, }; use conduwuit_core::utils::{IterStream, ReadyExt, stream::TryExpect}; use conduwuit_service::threepid::EmailRequirement; use futures::StreamExt; use ruma::{ OwnedClientSecret, OwnedDeviceId, OwnedSessionId, api::client::discovery::get_authorization_server_metadata::v1::AccountManagementAction, }; use serde::{Deserialize, Serialize}; use crate::{ WebError, extract::Expect, pages::{ Result, TemplateContext, components::{DeviceCard, DeviceCardStyle, UserCard}, }, response, session::{LoginTarget, User}, template, }; pub(crate) mod cross_signing_reset; pub(crate) mod deactivate; pub(crate) mod device; pub(crate) mod email; pub(crate) mod login; pub(crate) mod password; pub(crate) mod register; pub(crate) fn build() -> Router { #[allow(clippy::wildcard_imports)] use self::*; Router::new() .route("/", get(get_account)) .route("/deeplink", get(get_account_deeplink)) .merge(login::build()) .nest("/password/", password::build()) .nest("/email/", email::build()) .nest("/cross_signing_reset", cross_signing_reset::build()) .nest("/deactivate", deactivate::build()) .nest("/device/", device::build()) .nest("/register/", register::build()) } #[derive(Deserialize, Serialize)] struct ThreepidQuery { client_secret: OwnedClientSecret, session_id: OwnedSessionId, } template! { struct Account use "account.html.j2" { user_card: UserCard, body: AccountBody } } #[derive(Debug)] enum AccountBody { Unlocked { suspended: bool, email_requirement: EmailRequirement, email: Option, devices: Vec, }, Locked, } async fn get_account( State(services): State, Extension(context): Extension, user: User, ) -> Result { let user_id = user.expect(LoginTarget::Account)?; let user_card = UserCard::for_local_user(&services, user_id.clone()).await; if services.users.is_locked(&user_id).await.unwrap() { return response!(Account::new(context, user_card, AccountBody::Locked)); } let email_requirement = services.threepid.email_requirement(); let email = services .threepid .get_email_for_localpart(user_id.localpart()) .await .map(|address| address.to_string()); let dehydrated_device_id = services.users.get_dehydrated_device_id(&user_id).await.ok(); let mut devices: Vec<_> = services .users .all_device_ids(&user_id) .then(async |device_id| { services .users .get_device_metadata(&user_id, &device_id) .await }) .expect_ok() .ready_filter(|device| { dehydrated_device_id .as_ref() .is_none_or(|id| device.device_id != *id) }) .collect() .await; devices.sort_unstable_by(|a, b| a.last_seen_ts.cmp(&b.last_seen_ts).reverse()); let device_cards = devices .into_iter() .stream() .then(async |device| { DeviceCard::for_device(&services, &user_id, device, DeviceCardStyle::Minimal).await }) .collect() .await; let suspended = services.users.is_suspended(&user_id).await.unwrap(); response!(Account::new(context, user_card, AccountBody::Unlocked { suspended, email_requirement, email, devices: device_cards })) } #[derive(Deserialize)] struct AccountDeeplinkQuery { action: Option, device_id: Option, } async fn get_account_deeplink( Expect(Query(query)): Expect>, ) -> Result { let redirect_target = match query.action.unwrap_or(AccountManagementAction::Profile) { | AccountManagementAction::AccountDeactivate => "deactivate".to_owned(), | AccountManagementAction::CrossSigningReset => "cross_signing_reset".to_owned(), | AccountManagementAction::DeviceDelete => { let Some(device_id) = query.device_id else { return response!(WebError::BadRequest( "A device ID is required for this action".to_owned() )); }; format!("device/{device_id}/delete") }, | AccountManagementAction::DeviceView => { let Some(device_id) = query.device_id else { return response!(WebError::BadRequest( "A device ID is required for this action".to_owned() )); }; format!("device/{device_id}/") }, | AccountManagementAction::DevicesList => "#devices".to_owned(), | AccountManagementAction::Profile => String::new(), | _ => return response!(WebError::BadRequest("Unknown action".to_owned())), }; response!(Redirect::to(&format!("{}/account/{}", crate::ROUTE_PREFIX, redirect_target))) }