-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhome.rs
More file actions
206 lines (179 loc) · 7.67 KB
/
Copy pathhome.rs
File metadata and controls
206 lines (179 loc) · 7.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
//! Admin home page handler.
//!
//! Displays the main admin dashboard with configuration properties.
use std::net::SocketAddr;
use std::sync::Arc;
use axum::{
extract::{ConnectInfo, State},
http::HeaderMap,
response::{Html, IntoResponse, Redirect, Response},
};
use tower_cookies::Cookies;
use super::{get_base_styles, get_navbar_css, get_navbar_html, is_admin_enabled, is_authenticated};
use crate::pds::db::{PdsDb, StatisticKey};
use crate::pds::server::PdsState;
use crate::pds::xrpc::{get_caller_info};
/// Handle GET /admin/ - Show admin home page.
pub async fn admin_home(
State(state): State<Arc<PdsState>>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
headers: HeaderMap,
cookies: Cookies,
) -> impl IntoResponse {
// Extract caller info first for IP-based session validation
let (ip_address, user_agent) = get_caller_info(&headers, Some(addr));
// Check if admin dashboard is enabled
if !is_admin_enabled(&state.db) {
return Response::builder()
.status(403)
.header("Content-Type", "text/html")
.body("Admin dashboard is disabled. Set FeatureEnabled_AdminDashboard=1 in ConfigProperty table.".to_string())
.unwrap()
.into_response();
}
// Check authentication with IP verification
if !is_authenticated(&state.db, &cookies, &ip_address) {
return Redirect::to("/admin/login").into_response();
}
// Increment statistics
let stat_key = StatisticKey {
name: "admin/home".to_string(),
ip_address,
user_agent,
};
let _ = state.db.increment_statistic_for_endpoint(&stat_key);
// Get hostname for title
let hostname = state
.db
.get_config_property("PdsHostname")
.unwrap_or_else(|_| "(PdsHostname not set)".to_string());
let html = format!(
r#"<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin - Home - {hostname}</title>
<style>
{base_styles}
{navbar_css}
</style>
</head>
<body>
<div class="container">
{navbar}
<h1>Admin Dashboard</h1>
<p>
Welcome to the rustproto PDS Admin Dashboard.
Below is the configuration for this PDS. You can edit the config on the Config page.
</p>
<h2>Configuration</h2>
<table>
<tr>
<th>Key</th>
<th>Value</th>
<th>Description</th>
</tr>
{config_rows}
</table>
</div>
</body>
</html>"#,
hostname = html_encode(&hostname),
base_styles = get_base_styles(),
navbar_css = get_navbar_css(),
navbar = get_navbar_html("home"),
config_rows = build_config_table(&state.db),
);
Html(html).into_response()
}
/// Build the configuration table matching dnproto's Admin_Home layout.
fn build_config_table(db: &PdsDb) -> String {
let get_value = |key: &str| -> String {
if is_sensitive_key(key) {
"<span class=\"dimmed\">[hidden]</span>".to_string()
} else {
match db.get_config_property(key) {
Ok(v) if !v.is_empty() => html_encode(&v),
_ => "<span class=\"dimmed\">empty</span>".to_string(),
}
}
};
let get_bool_value = |key: &str| -> String {
match db.get_config_property_bool(key) {
Ok(true) => "enabled".to_string(),
Ok(false) => "<span class=\"dimmed\">disabled</span>".to_string(),
Err(_) => "<span class=\"dimmed\">empty</span>".to_string(),
}
};
let section = |name: &str| -> String {
format!(r#"<tr class="section-header"><td colspan="3">{}</td></tr>"#, name)
};
let row = |key: &str, value: String, desc: &str| -> String {
format!(
r#"<tr>
<td class="key-name">{}</td>
<td>{}</td>
<td>{}</td>
</tr>"#,
key, value, desc
)
};
let mut rows = Vec::new();
// Server section
rows.push(section("Server"));
rows.push(row("ServerListenScheme", get_value("ServerListenScheme"), "http or https?"));
rows.push(row("ServerListenHost", get_value("ServerListenHost"), "Hostname that server listens on. Can be localhost for reverse proxy."));
rows.push(row("ServerListenPort", get_value("ServerListenPort"), "Port that server listens on."));
// Features section
rows.push(section("Features"));
rows.push(row("FeatureEnabled_AdminDashboard", get_bool_value("FeatureEnabled_AdminDashboard"), "Is the admin dashboard enabled?"));
rows.push(row("FeatureEnabled_Oauth", get_bool_value("FeatureEnabled_Oauth"), "Is OAuth enabled? This is a global flag that turns it off/on."));
rows.push(row("FeatureEnabled_Passkeys", get_bool_value("FeatureEnabled_Passkeys"), "Are passkeys enabled?"));
rows.push(row("FeatureEnabled_RequestCrawl", get_bool_value("FeatureEnabled_RequestCrawl"), "If enabled, will periodically request a crawl from the crawlers."));
rows.push(row("FeatureEnabled_Spaces", get_bool_value("FeatureEnabled_Spaces"), "Are atproto permissioned spaces enabled?"));
// PDS section
rows.push(section("PDS"));
rows.push(row("PdsCrawlers", get_value("PdsCrawlers"), "Comma-separated list of relays to request crawl from. (ex: bsky.network)"));
rows.push(row("PdsDid", get_value("PdsDid"), "DID for the PDS (ex: did:web:thisisyourpdshost.com)"));
rows.push(row("PdsHostname", get_value("PdsHostname"), "Hostname for the PDS. What goes in your DID doc."));
rows.push(row("PdsAvailableUserDomain", get_value("PdsAvailableUserDomain"), "A single domain that is the available user domains, prefixed with ."));
// User section
rows.push(section("User"));
rows.push(row("UserHandle", get_value("UserHandle"), "Handle for the user (this is a single-user PDS)."));
rows.push(row("UserDid", get_value("UserDid"), "DID for the user (ex: did:web:______)."));
rows.push(row("UserEmail", get_value("UserEmail"), "User's email address."));
rows.push(row("UserIsActive", get_bool_value("UserIsActive"), "Is the user active?"));
// Deployment section
rows.push(section("Deployment"));
rows.push(row("LogRetentionDays", get_value("LogRetentionDays"), "Number of days to keep logs before deleting."));
rows.push(row("SystemctlServiceName", get_value("SystemctlServiceName"), "systemctl service name. Gets used during deployment/restart."));
rows.push(row("CaddyAccessLogFilePath", get_value("CaddyAccessLogFilePath"), "Access log for caddy."));
// Security section
rows.push(section("Security"));
rows.push(row("AtprotoProxyAllowedDids", get_value("AtprotoProxyAllowedDids"), "Comma-separated list of DIDs allowed for Atproto-Proxy header."));
rows.push(row("OauthAllowedRedirectUris", get_value("OauthAllowedRedirectUris"), "Comma-separated list of allowed OAuth redirect URIs."));
// App View section
rows.push(section("App View"));
rows.push(row("AppViewHostName", get_value("AppViewHostName"), "Host name for the App View service (ex: public.api.bsky.app)."));
// Debugging section
rows.push(section("Debugging"));
rows.push(row("LogXrpcEndpoints", get_value("LogXrpcEndpoints"), "Comma-separated list of XRPC endpoints (nsids) to log in full detail at info level."));
rows.join("\n")
}
/// Check if a config key contains sensitive data.
fn is_sensitive_key(key: &str) -> bool {
let lower = key.to_lowercase();
lower.contains("password")
|| lower.contains("secret")
|| lower.contains("key")
|| lower.contains("jwt")
}
/// HTML encode a string to prevent XSS.
fn html_encode(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}