1 use once_cell::sync::Lazy;
2 use serde::de::DeserializeOwned;
3 use serde_json::Value;
4 use std::env;
5
6 use rocket::{
7 http::{Cookie, Cookies, SameSite, Status},
8 request::{self, FlashMessage, Form, FromRequest, Outcome, Request},
9 response::{content::Html, Flash, Redirect},
10 Route,
11 };
12 use rocket_contrib::json::Json;
13
14 use crate::{
15 api::{ApiResult, EmptyResult, JsonResult, NumberOrString},
16 auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp},
17 config::ConfigBuilder,
18 db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType},
19 error::{Error, MapResult},
20 mail,
21 util::{
22 docker_base_image, format_naive_datetime_local, get_display_size, get_reqwest_client, is_running_in_docker,
23 },
24 CONFIG,
25 };
26
routes() -> Vec<Route>27 pub fn routes() -> Vec<Route> {
28 if !CONFIG.disable_admin_token() && !CONFIG.is_admin_token_set() {
29 return routes![admin_disabled];
30 }
31
32 routes![
33 admin_login,
34 get_users_json,
35 get_user_json,
36 post_admin_login,
37 admin_page,
38 invite_user,
39 logout,
40 delete_user,
41 deauth_user,
42 disable_user,
43 enable_user,
44 remove_2fa,
45 update_user_org_type,
46 update_revision_users,
47 post_config,
48 delete_config,
49 backup_db,
50 test_smtp,
51 users_overview,
52 organizations_overview,
53 delete_organization,
54 diagnostics,
55 get_diagnostics_config
56 ]
57 }
58
59 static DB_TYPE: Lazy<&str> = Lazy::new(|| {
60 DbConnType::from_url(&CONFIG.database_url())
61 .map(|t| match t {
62 DbConnType::sqlite => "SQLite",
63 DbConnType::mysql => "MySQL",
64 DbConnType::postgresql => "PostgreSQL",
65 })
66 .unwrap_or("Unknown")
67 });
68
69 static CAN_BACKUP: Lazy<bool> =
70 Lazy::new(|| DbConnType::from_url(&CONFIG.database_url()).map(|t| t == DbConnType::sqlite).unwrap_or(false));
71
72 #[get("/")]
admin_disabled() -> &'static str73 fn admin_disabled() -> &'static str {
74 "The admin panel is disabled, please configure the 'ADMIN_TOKEN' variable to enable it"
75 }
76
77 const COOKIE_NAME: &str = "BWRS_ADMIN";
78 const ADMIN_PATH: &str = "/admin";
79
80 const BASE_TEMPLATE: &str = "admin/base";
81 const VERSION: Option<&str> = option_env!("BWRS_VERSION");
82
admin_path() -> String83 fn admin_path() -> String {
84 format!("{}{}", CONFIG.domain_path(), ADMIN_PATH)
85 }
86
87 struct Referer(Option<String>);
88
89 impl<'a, 'r> FromRequest<'a, 'r> for Referer {
90 type Error = ();
91
from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error>92 fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
93 Outcome::Success(Referer(request.headers().get_one("Referer").map(str::to_string)))
94 }
95 }
96
97 #[derive(Debug)]
98 struct IpHeader(Option<String>);
99
100 impl<'a, 'r> FromRequest<'a, 'r> for IpHeader {
101 type Error = ();
102
from_request(req: &'a Request<'r>) -> Outcome<Self, Self::Error>103 fn from_request(req: &'a Request<'r>) -> Outcome<Self, Self::Error> {
104 if req.headers().get_one(&CONFIG.ip_header()).is_some() {
105 Outcome::Success(IpHeader(Some(CONFIG.ip_header())))
106 } else if req.headers().get_one("X-Client-IP").is_some() {
107 Outcome::Success(IpHeader(Some(String::from("X-Client-IP"))))
108 } else if req.headers().get_one("X-Real-IP").is_some() {
109 Outcome::Success(IpHeader(Some(String::from("X-Real-IP"))))
110 } else if req.headers().get_one("X-Forwarded-For").is_some() {
111 Outcome::Success(IpHeader(Some(String::from("X-Forwarded-For"))))
112 } else {
113 Outcome::Success(IpHeader(None))
114 }
115 }
116 }
117
118 /// Used for `Location` response headers, which must specify an absolute URI
119 /// (see https://tools.ietf.org/html/rfc2616#section-14.30).
admin_url(referer: Referer) -> String120 fn admin_url(referer: Referer) -> String {
121 // If we get a referer use that to make it work when, DOMAIN is not set
122 if let Some(mut referer) = referer.0 {
123 if let Some(start_index) = referer.find(ADMIN_PATH) {
124 referer.truncate(start_index + ADMIN_PATH.len());
125 return referer;
126 }
127 }
128
129 if CONFIG.domain_set() {
130 // Don't use CONFIG.domain() directly, since the user may want to keep a
131 // trailing slash there, particularly when running under a subpath.
132 format!("{}{}{}", CONFIG.domain_origin(), CONFIG.domain_path(), ADMIN_PATH)
133 } else {
134 // Last case, when no referer or domain set, technically invalid but better than nothing
135 ADMIN_PATH.to_string()
136 }
137 }
138
139 #[get("/", rank = 2)]
admin_login(flash: Option<FlashMessage>) -> ApiResult<Html<String>>140 fn admin_login(flash: Option<FlashMessage>) -> ApiResult<Html<String>> {
141 // If there is an error, show it
142 let msg = flash.map(|msg| format!("{}: {}", msg.name(), msg.msg()));
143 let json = json!({
144 "page_content": "admin/login",
145 "version": VERSION,
146 "error": msg,
147 "urlpath": CONFIG.domain_path()
148 });
149
150 // Return the page
151 let text = CONFIG.render_template(BASE_TEMPLATE, &json)?;
152 Ok(Html(text))
153 }
154
155 #[derive(FromForm)]
156 struct LoginForm {
157 token: String,
158 }
159
160 #[post("/", data = "<data>")]
post_admin_login( data: Form<LoginForm>, mut cookies: Cookies, ip: ClientIp, referer: Referer, ) -> Result<Redirect, Flash<Redirect>>161 fn post_admin_login(
162 data: Form<LoginForm>,
163 mut cookies: Cookies,
164 ip: ClientIp,
165 referer: Referer,
166 ) -> Result<Redirect, Flash<Redirect>> {
167 let data = data.into_inner();
168
169 // If the token is invalid, redirect to login page
170 if !_validate_token(&data.token) {
171 error!("Invalid admin token. IP: {}", ip.ip);
172 Err(Flash::error(Redirect::to(admin_url(referer)), "Invalid admin token, please try again."))
173 } else {
174 // If the token received is valid, generate JWT and save it as a cookie
175 let claims = generate_admin_claims();
176 let jwt = encode_jwt(&claims);
177
178 let cookie = Cookie::build(COOKIE_NAME, jwt)
179 .path(admin_path())
180 .max_age(time::Duration::minutes(20))
181 .same_site(SameSite::Strict)
182 .http_only(true)
183 .finish();
184
185 cookies.add(cookie);
186 Ok(Redirect::to(admin_url(referer)))
187 }
188 }
189
_validate_token(token: &str) -> bool190 fn _validate_token(token: &str) -> bool {
191 match CONFIG.admin_token().as_ref() {
192 None => false,
193 Some(t) => crate::crypto::ct_eq(t.trim(), token.trim()),
194 }
195 }
196
197 #[derive(Serialize)]
198 struct AdminTemplateData {
199 page_content: String,
200 version: Option<&'static str>,
201 page_data: Option<Value>,
202 config: Value,
203 can_backup: bool,
204 logged_in: bool,
205 urlpath: String,
206 }
207
208 impl AdminTemplateData {
new() -> Self209 fn new() -> Self {
210 Self {
211 page_content: String::from("admin/settings"),
212 version: VERSION,
213 config: CONFIG.prepare_json(),
214 can_backup: *CAN_BACKUP,
215 logged_in: true,
216 urlpath: CONFIG.domain_path(),
217 page_data: None,
218 }
219 }
220
with_data(page_content: &str, page_data: Value) -> Self221 fn with_data(page_content: &str, page_data: Value) -> Self {
222 Self {
223 page_content: String::from(page_content),
224 version: VERSION,
225 page_data: Some(page_data),
226 config: CONFIG.prepare_json(),
227 can_backup: *CAN_BACKUP,
228 logged_in: true,
229 urlpath: CONFIG.domain_path(),
230 }
231 }
232
render(self) -> Result<String, Error>233 fn render(self) -> Result<String, Error> {
234 CONFIG.render_template(BASE_TEMPLATE, &self)
235 }
236 }
237
238 #[get("/", rank = 1)]
admin_page(_token: AdminToken) -> ApiResult<Html<String>>239 fn admin_page(_token: AdminToken) -> ApiResult<Html<String>> {
240 let text = AdminTemplateData::new().render()?;
241 Ok(Html(text))
242 }
243
244 #[derive(Deserialize, Debug)]
245 #[allow(non_snake_case)]
246 struct InviteData {
247 email: String,
248 }
249
get_user_or_404(uuid: &str, conn: &DbConn) -> ApiResult<User>250 fn get_user_or_404(uuid: &str, conn: &DbConn) -> ApiResult<User> {
251 if let Some(user) = User::find_by_uuid(uuid, conn) {
252 Ok(user)
253 } else {
254 err_code!("User doesn't exist", Status::NotFound.code);
255 }
256 }
257
258 #[post("/invite", data = "<data>")]
invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -> JsonResult259 fn invite_user(data: Json<InviteData>, _token: AdminToken, conn: DbConn) -> JsonResult {
260 let data: InviteData = data.into_inner();
261 let email = data.email.clone();
262 if User::find_by_mail(&data.email, &conn).is_some() {
263 err_code!("User already exists", Status::Conflict.code)
264 }
265
266 let mut user = User::new(email);
267
268 // TODO: After try_blocks is stabilized, this can be made more readable
269 // See: https://github.com/rust-lang/rust/issues/31436
270 (|| {
271 if CONFIG.mail_enabled() {
272 mail::send_invite(&user.email, &user.uuid, None, None, &CONFIG.invitation_org_name(), None)?;
273 } else {
274 let invitation = Invitation::new(user.email.clone());
275 invitation.save(&conn)?;
276 }
277
278 user.save(&conn)
279 })()
280 .map_err(|e| e.with_code(Status::InternalServerError.code))?;
281
282 Ok(Json(user.to_json(&conn)))
283 }
284
285 #[post("/test/smtp", data = "<data>")]
test_smtp(data: Json<InviteData>, _token: AdminToken) -> EmptyResult286 fn test_smtp(data: Json<InviteData>, _token: AdminToken) -> EmptyResult {
287 let data: InviteData = data.into_inner();
288
289 if CONFIG.mail_enabled() {
290 mail::send_test(&data.email)
291 } else {
292 err!("Mail is not enabled")
293 }
294 }
295
296 #[get("/logout")]
logout(mut cookies: Cookies, referer: Referer) -> Redirect297 fn logout(mut cookies: Cookies, referer: Referer) -> Redirect {
298 cookies.remove(Cookie::named(COOKIE_NAME));
299 Redirect::to(admin_url(referer))
300 }
301
302 #[get("/users")]
get_users_json(_token: AdminToken, conn: DbConn) -> Json<Value>303 fn get_users_json(_token: AdminToken, conn: DbConn) -> Json<Value> {
304 let users = User::get_all(&conn);
305 let users_json: Vec<Value> = users.iter().map(|u| u.to_json(&conn)).collect();
306
307 Json(Value::Array(users_json))
308 }
309
310 #[get("/users/overview")]
users_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>>311 fn users_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> {
312 let users = User::get_all(&conn);
313 let dt_fmt = "%Y-%m-%d %H:%M:%S %Z";
314 let users_json: Vec<Value> = users
315 .iter()
316 .map(|u| {
317 let mut usr = u.to_json(&conn);
318 usr["cipher_count"] = json!(Cipher::count_owned_by_user(&u.uuid, &conn));
319 usr["attachment_count"] = json!(Attachment::count_by_user(&u.uuid, &conn));
320 usr["attachment_size"] = json!(get_display_size(Attachment::size_by_user(&u.uuid, &conn) as i32));
321 usr["user_enabled"] = json!(u.enabled);
322 usr["created_at"] = json!(format_naive_datetime_local(&u.created_at, dt_fmt));
323 usr["last_active"] = match u.last_active(&conn) {
324 Some(dt) => json!(format_naive_datetime_local(&dt, dt_fmt)),
325 None => json!("Never"),
326 };
327 usr
328 })
329 .collect();
330
331 let text = AdminTemplateData::with_data("admin/users", json!(users_json)).render()?;
332 Ok(Html(text))
333 }
334
335 #[get("/users/<uuid>")]
get_user_json(uuid: String, _token: AdminToken, conn: DbConn) -> JsonResult336 fn get_user_json(uuid: String, _token: AdminToken, conn: DbConn) -> JsonResult {
337 let user = get_user_or_404(&uuid, &conn)?;
338
339 Ok(Json(user.to_json(&conn)))
340 }
341
342 #[post("/users/<uuid>/delete")]
delete_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult343 fn delete_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
344 let user = get_user_or_404(&uuid, &conn)?;
345 user.delete(&conn)
346 }
347
348 #[post("/users/<uuid>/deauth")]
deauth_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult349 fn deauth_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
350 let mut user = get_user_or_404(&uuid, &conn)?;
351 Device::delete_all_by_user(&user.uuid, &conn)?;
352 user.reset_security_stamp();
353
354 user.save(&conn)
355 }
356
357 #[post("/users/<uuid>/disable")]
disable_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult358 fn disable_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
359 let mut user = get_user_or_404(&uuid, &conn)?;
360 Device::delete_all_by_user(&user.uuid, &conn)?;
361 user.reset_security_stamp();
362 user.enabled = false;
363
364 user.save(&conn)
365 }
366
367 #[post("/users/<uuid>/enable")]
enable_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult368 fn enable_user(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
369 let mut user = get_user_or_404(&uuid, &conn)?;
370 user.enabled = true;
371
372 user.save(&conn)
373 }
374
375 #[post("/users/<uuid>/remove-2fa")]
remove_2fa(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult376 fn remove_2fa(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
377 let mut user = get_user_or_404(&uuid, &conn)?;
378 TwoFactor::delete_all_by_user(&user.uuid, &conn)?;
379 user.totp_recover = None;
380 user.save(&conn)
381 }
382
383 #[derive(Deserialize, Debug)]
384 struct UserOrgTypeData {
385 user_type: NumberOrString,
386 user_uuid: String,
387 org_uuid: String,
388 }
389
390 #[post("/users/org_type", data = "<data>")]
update_user_org_type(data: Json<UserOrgTypeData>, _token: AdminToken, conn: DbConn) -> EmptyResult391 fn update_user_org_type(data: Json<UserOrgTypeData>, _token: AdminToken, conn: DbConn) -> EmptyResult {
392 let data: UserOrgTypeData = data.into_inner();
393
394 let mut user_to_edit = match UserOrganization::find_by_user_and_org(&data.user_uuid, &data.org_uuid, &conn) {
395 Some(user) => user,
396 None => err!("The specified user isn't member of the organization"),
397 };
398
399 let new_type = match UserOrgType::from_str(&data.user_type.into_string()) {
400 Some(new_type) => new_type as i32,
401 None => err!("Invalid type"),
402 };
403
404 if user_to_edit.atype == UserOrgType::Owner && new_type != UserOrgType::Owner {
405 // Removing owner permmission, check that there are at least another owner
406 let num_owners = UserOrganization::find_by_org_and_type(&data.org_uuid, UserOrgType::Owner as i32, &conn).len();
407
408 if num_owners <= 1 {
409 err!("Can't change the type of the last owner")
410 }
411 }
412
413 user_to_edit.atype = new_type as i32;
414 user_to_edit.save(&conn)
415 }
416
417 #[post("/users/update_revision")]
update_revision_users(_token: AdminToken, conn: DbConn) -> EmptyResult418 fn update_revision_users(_token: AdminToken, conn: DbConn) -> EmptyResult {
419 User::update_all_revisions(&conn)
420 }
421
422 #[get("/organizations/overview")]
organizations_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>>423 fn organizations_overview(_token: AdminToken, conn: DbConn) -> ApiResult<Html<String>> {
424 let organizations = Organization::get_all(&conn);
425 let organizations_json: Vec<Value> = organizations
426 .iter()
427 .map(|o| {
428 let mut org = o.to_json();
429 org["user_count"] = json!(UserOrganization::count_by_org(&o.uuid, &conn));
430 org["cipher_count"] = json!(Cipher::count_by_org(&o.uuid, &conn));
431 org["attachment_count"] = json!(Attachment::count_by_org(&o.uuid, &conn));
432 org["attachment_size"] = json!(get_display_size(Attachment::size_by_org(&o.uuid, &conn) as i32));
433 org
434 })
435 .collect();
436
437 let text = AdminTemplateData::with_data("admin/organizations", json!(organizations_json)).render()?;
438 Ok(Html(text))
439 }
440
441 #[post("/organizations/<uuid>/delete")]
delete_organization(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult442 fn delete_organization(uuid: String, _token: AdminToken, conn: DbConn) -> EmptyResult {
443 let org = Organization::find_by_uuid(&uuid, &conn).map_res("Organization doesn't exist")?;
444 org.delete(&conn)
445 }
446
447 #[derive(Deserialize)]
448 struct WebVaultVersion {
449 version: String,
450 }
451
452 #[derive(Deserialize)]
453 struct GitRelease {
454 tag_name: String,
455 }
456
457 #[derive(Deserialize)]
458 struct GitCommit {
459 sha: String,
460 }
461
get_github_api<T: DeserializeOwned>(url: &str) -> Result<T, Error>462 fn get_github_api<T: DeserializeOwned>(url: &str) -> Result<T, Error> {
463 let github_api = get_reqwest_client();
464
465 Ok(github_api.get(url).send()?.error_for_status()?.json::<T>()?)
466 }
467
has_http_access() -> bool468 fn has_http_access() -> bool {
469 let http_access = get_reqwest_client();
470
471 match http_access.head("https://github.com/dani-garcia/vaultwarden").send() {
472 Ok(r) => r.status().is_success(),
473 _ => false,
474 }
475 }
476
477 #[get("/diagnostics")]
diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResult<Html<String>>478 fn diagnostics(_token: AdminToken, ip_header: IpHeader, conn: DbConn) -> ApiResult<Html<String>> {
479 use crate::util::read_file_string;
480 use chrono::prelude::*;
481 use std::net::ToSocketAddrs;
482
483 // Get current running versions
484 let web_vault_version: WebVaultVersion =
485 match read_file_string(&format!("{}/{}", CONFIG.web_vault_folder(), "bwrs-version.json")) {
486 Ok(s) => serde_json::from_str(&s)?,
487 _ => match read_file_string(&format!("{}/{}", CONFIG.web_vault_folder(), "version.json")) {
488 Ok(s) => serde_json::from_str(&s)?,
489 _ => WebVaultVersion {
490 version: String::from("Version file missing"),
491 },
492 },
493 };
494
495 // Execute some environment checks
496 let running_within_docker = is_running_in_docker();
497 let has_http_access = has_http_access();
498 let uses_proxy = env::var_os("HTTP_PROXY").is_some()
499 || env::var_os("http_proxy").is_some()
500 || env::var_os("HTTPS_PROXY").is_some()
501 || env::var_os("https_proxy").is_some();
502
503 // Check if we are able to resolve DNS entries
504 let dns_resolved = match ("github.com", 0).to_socket_addrs().map(|mut i| i.next()) {
505 Ok(Some(a)) => a.ip().to_string(),
506 _ => "Could not resolve domain name.".to_string(),
507 };
508
509 // If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway.
510 // TODO: Maybe we need to cache this using a LazyStatic or something. Github only allows 60 requests per hour, and we use 3 here already.
511 let (latest_release, latest_commit, latest_web_build) = if has_http_access {
512 (
513 match get_github_api::<GitRelease>("https://api.github.com/repos/dani-garcia/vaultwarden/releases/latest") {
514 Ok(r) => r.tag_name,
515 _ => "-".to_string(),
516 },
517 match get_github_api::<GitCommit>("https://api.github.com/repos/dani-garcia/vaultwarden/commits/main") {
518 Ok(mut c) => {
519 c.sha.truncate(8);
520 c.sha
521 }
522 _ => "-".to_string(),
523 },
524 // Do not fetch the web-vault version when running within Docker.
525 // The web-vault version is embedded within the container it self, and should not be updated manually
526 if running_within_docker {
527 "-".to_string()
528 } else {
529 match get_github_api::<GitRelease>(
530 "https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest",
531 ) {
532 Ok(r) => r.tag_name.trim_start_matches('v').to_string(),
533 _ => "-".to_string(),
534 }
535 },
536 )
537 } else {
538 ("-".to_string(), "-".to_string(), "-".to_string())
539 };
540
541 let ip_header_name = match &ip_header.0 {
542 Some(h) => h,
543 _ => "",
544 };
545
546 let diagnostics_json = json!({
547 "dns_resolved": dns_resolved,
548 "latest_release": latest_release,
549 "latest_commit": latest_commit,
550 "web_vault_enabled": &CONFIG.web_vault_enabled(),
551 "web_vault_version": web_vault_version.version,
552 "latest_web_build": latest_web_build,
553 "running_within_docker": running_within_docker,
554 "docker_base_image": docker_base_image(),
555 "has_http_access": has_http_access,
556 "ip_header_exists": &ip_header.0.is_some(),
557 "ip_header_match": ip_header_name == CONFIG.ip_header(),
558 "ip_header_name": ip_header_name,
559 "ip_header_config": &CONFIG.ip_header(),
560 "uses_proxy": uses_proxy,
561 "db_type": *DB_TYPE,
562 "db_version": get_sql_server_version(&conn),
563 "admin_url": format!("{}/diagnostics", admin_url(Referer(None))),
564 "overrides": &CONFIG.get_overrides().join(", "),
565 "server_time_local": Local::now().format("%Y-%m-%d %H:%M:%S %Z").to_string(),
566 "server_time": Utc::now().format("%Y-%m-%d %H:%M:%S UTC").to_string(), // Run the date/time check as the last item to minimize the difference
567 });
568
569 let text = AdminTemplateData::with_data("admin/diagnostics", diagnostics_json).render()?;
570 Ok(Html(text))
571 }
572
573 #[get("/diagnostics/config")]
get_diagnostics_config(_token: AdminToken) -> Json<Value>574 fn get_diagnostics_config(_token: AdminToken) -> Json<Value> {
575 let support_json = CONFIG.get_support_json();
576 Json(support_json)
577 }
578
579 #[post("/config", data = "<data>")]
post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult580 fn post_config(data: Json<ConfigBuilder>, _token: AdminToken) -> EmptyResult {
581 let data: ConfigBuilder = data.into_inner();
582 CONFIG.update_config(data)
583 }
584
585 #[post("/config/delete")]
delete_config(_token: AdminToken) -> EmptyResult586 fn delete_config(_token: AdminToken) -> EmptyResult {
587 CONFIG.delete_user_config()
588 }
589
590 #[post("/config/backup_db")]
backup_db(_token: AdminToken, conn: DbConn) -> EmptyResult591 fn backup_db(_token: AdminToken, conn: DbConn) -> EmptyResult {
592 if *CAN_BACKUP {
593 backup_database(&conn)
594 } else {
595 err!("Can't back up current DB (Only SQLite supports this feature)");
596 }
597 }
598
599 pub struct AdminToken {}
600
601 impl<'a, 'r> FromRequest<'a, 'r> for AdminToken {
602 type Error = &'static str;
603
from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error>604 fn from_request(request: &'a Request<'r>) -> request::Outcome<Self, Self::Error> {
605 if CONFIG.disable_admin_token() {
606 Outcome::Success(AdminToken {})
607 } else {
608 let mut cookies = request.cookies();
609
610 let access_token = match cookies.get(COOKIE_NAME) {
611 Some(cookie) => cookie.value(),
612 None => return Outcome::Forward(()), // If there is no cookie, redirect to login
613 };
614
615 let ip = match request.guard::<ClientIp>() {
616 Outcome::Success(ip) => ip.ip,
617 _ => err_handler!("Error getting Client IP"),
618 };
619
620 if decode_admin(access_token).is_err() {
621 // Remove admin cookie
622 cookies.remove(Cookie::named(COOKIE_NAME));
623 error!("Invalid or expired admin JWT. IP: {}.", ip);
624 return Outcome::Forward(());
625 }
626
627 Outcome::Success(AdminToken {})
628 }
629 }
630 }
631