admin.rs 21 KB

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