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