Skip to main content

tuwunel_api/oidc/account/
session_list.rs

1use std::cmp;
2
3use const_str::format as const_format;
4use futures::StreamExt;
5use ruma::{MilliSecondsSinceUnixEpoch, UserId};
6use tuwunel_core::{Result, utils::html::escape as html_escape};
7use tuwunel_service::Services;
8
9use super::{ACCOUNT_HEAD, ACCOUNT_JS_INCLUDE, ts_cell, url_encode};
10
11pub(super) async fn sessions_list_html(services: &Services, user_id: &UserId) -> Result<String> {
12	let mut devices: Vec<_> = services
13		.users
14		.all_devices_metadata(user_id)
15		.collect()
16		.await;
17
18	// Newest sessions first (highest last_seen_ts at top, None treated as oldest)
19	devices.sort_by_key(|b| cmp::Reverse(b.last_seen_ts));
20
21	let mut rows = Vec::new();
22	for device in &devices {
23		let device_display_name = device
24			.display_name
25			.as_deref()
26			.unwrap_or("Unknown device");
27
28		let name = html_escape(device_display_name);
29		let id_enc = url_encode(device.device_id.as_str());
30		let id = html_escape(device.device_id.as_str());
31		let ip = html_escape(device.last_seen_ip.as_deref().unwrap_or("—"));
32		let ts_cell = device
33			.last_seen_ts
34			.as_ref()
35			.map(MilliSecondsSinceUnixEpoch::as_secs)
36			.map(u64::from)
37			.map(ts_cell)
38			.unwrap_or_default();
39
40		rows.push(format!(
41			r#"
42			<tr>
43				<td>{name}</td>
44				<td><code>{id}</code></td>
45				<td>{ip}</td>
46				<td>{ts_cell}</td>
47				<td class="center">
48					<a href="/_tuwunel/oidc/account?action=org.matrix.session_view&device_id={id_enc}">
49						View
50					</a>
51					<span class="sep"> | </span>
52					<a
53						href="/_tuwunel/oidc/account?action=org.matrix.session_end&device_id={id_enc}"
54						class="err"
55					>
56						Sign out
57					</a>
58				</td>
59			</tr>"#
60		));
61	}
62
63	Ok(PAGE_HTML
64		.replace("{uid}", &html_escape(user_id.as_str()))
65		.replace("{dlen}", &devices.len().to_string())
66		.replace("{rows}", &rows.join("")))
67}
68
69static PAGE_HTML: &str = const_format!(
70	r#"
71<!DOCTYPE html>
72<html lang="en">
73	<head>
74		{ACCOUNT_HEAD}
75		<title>Active Sessions</title>
76	</head>
77	<body class="wide">
78		<h1>Active Sessions</h1>
79		<p>
80			Signed in as <strong>{{uid}}</strong>. {{dlen}} active session(s).
81		</p>
82		<table>
83			<tr>
84				<th>Name</th>
85				<th>Device ID</th>
86				<th>Last seen IP</th>
87				<th>Last seen</th>
88				<th class="center">Actions</th>
89			</tr>
90			{{rows}}
91		</table>
92		<div class="nav">
93			<a href="/_tuwunel/oidc/account?action=org.matrix.profile">View Profile</a>
94		</div>
95		{ACCOUNT_JS_INCLUDE}
96	</body>
97</html>"#
98);