Skip to main content

tuwunel_api/oidc/account/
session_view.rs

1use const_str::format as const_format;
2use ruma::{OwnedDeviceId, UserId};
3use tuwunel_core::{Err, Result, err, utils::html::escape as html_escape};
4use tuwunel_service::Services;
5
6use super::{ACCOUNT_HEAD, ACCOUNT_JS_INCLUDE, ts_cell, url_encode};
7
8pub(super) async fn session_view_html(
9	services: &Services,
10	user_id: &UserId,
11	device_id: &str,
12	login_token: &str,
13) -> Result<String> {
14	if device_id.is_empty() {
15		return Err!(Request(InvalidParam("device_id is required")));
16	}
17
18	let device_id_owned: OwnedDeviceId = device_id.into();
19	let device = services
20		.users
21		.get_device_metadata(user_id, &device_id_owned)
22		.await
23		.map_err(|_| err!(Request(NotFound("Session not found"))))?;
24
25	let device_display_name = device
26		.display_name
27		.as_deref()
28		.unwrap_or("Unknown device");
29
30	let name = html_escape(device_display_name);
31	let tok = html_escape(login_token);
32	let ip = html_escape(device.last_seen_ip.as_deref().unwrap_or("—"));
33	let id = html_escape(device.device_id.as_str());
34	let id_enc = url_encode(device.device_id.as_str());
35
36	let ts_cell = device
37		.last_seen_ts
38		.map(|t| u64::from(t.as_secs()))
39		.map(ts_cell)
40		.unwrap_or_default();
41
42	// Link directly to account_callback (skips SSO) using the peeked login_token
43	// so the user doesn't have to re-authenticate just to sign out a session.
44	Ok(PAGE_HTML
45		.replace("{name}", &name)
46		.replace("{tok}", &tok)
47		.replace("{ip}", &ip)
48		.replace("{id}", &id)
49		.replace("{id_enc}", &id_enc)
50		.replace("{ts_cell}", &ts_cell)
51		.replace("{uid}", &html_escape(user_id.as_str())))
52}
53
54static PAGE_HTML: &str = const_format!(
55	r#"
56<!DOCTYPE html>
57<html lang="en">
58	<head>
59		{ACCOUNT_HEAD}
60		<title>Session: {{name}}</title>
61	</head>
62	<body>
63		<h1>Session Details</h1>
64		<p>
65			Signed in as <strong>{{uid}}</strong>.
66		</p>
67		<dl>
68			<dt>Name</dt><dd>{{name}}</dd>
69			<dt>Device ID</dt><dd><code>{{id}}</code></dd>
70			<dt>Last seen IP</dt><dd>{{ip}}</dd>
71			<dt>Last seen</dt><dd>{{ts_cell}}</dd>
72		</dl>
73		<div class="actions">
74			<a href="/_tuwunel/oidc/account?action=org.matrix.sessions_list">
75				Back to sessions
76			</a>
77			<a
78				href="/_tuwunel/oidc/account_callback?action=org.matrix.session_end&device_id={{id_enc}}&loginToken={{tok}}"
79				class="err"
80			>
81				Sign out this session
82			</a>
83		</div>
84		{ACCOUNT_JS_INCLUDE}
85	</body>
86</html>
87"#
88);