mirror of
https://github.com/4cuck/nitter-status.git
synced 2026-10-27 00:57:39 +00:00
keep latest healthcheck errors, basic admin login
This commit is contained in:
1 parent
cc70134d9d
commit
759963e7b2
32 files changed
+1761
-308
No files matched your search
@@ -0,0 +1,367 @@
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::Form;
|
||||
use axum::Json;
|
||||
use axum::extract::Path;
|
||||
use axum::extract::State;
|
||||
use axum::response::Html;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::response::Redirect;
|
||||
use entities::check_errors;
|
||||
use entities::health_check;
|
||||
use entities::state::AppState;
|
||||
use hyper::HeaderMap;
|
||||
use hyper::StatusCode;
|
||||
use hyper::header::REFERER;
|
||||
use reqwest::Client;
|
||||
use reqwest::Url;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use sea_orm::EntityTrait;
|
||||
use sea_orm::QueryOrder;
|
||||
use sea_orm::QuerySelect;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use thiserror::Error;
|
||||
use tower_sessions::Session;
|
||||
use constant_time_eq::constant_time_eq;
|
||||
use sha2::{Sha256, Digest};
|
||||
use entities::host;
|
||||
use sea_orm::ColumnTrait;
|
||||
use sea_orm::QueryFilter;
|
||||
use trust_dns_resolver::AsyncResolver;
|
||||
use trust_dns_resolver::config::ResolverConfig;
|
||||
use trust_dns_resolver::config::ResolverOpts;
|
||||
|
||||
use crate::ADMIN_OVERVIEW_URL;
|
||||
use crate::Config;
|
||||
use crate::LOGIN_URL;
|
||||
use crate::Result;
|
||||
use crate::ServerError;
|
||||
|
||||
#[derive(Serialize, Deserialize, Default)]
|
||||
pub struct ActiveLogin {
|
||||
/// Hosts this session has access to.
|
||||
hosts: HashSet<i32>
|
||||
}
|
||||
const LOGIN_KEY: &'static str = "LOGIN";
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum LoginError {
|
||||
#[error("Couldn't get login file at '{0}': {1}")]
|
||||
HttpFailure(Url, reqwest::Error),
|
||||
#[error("Invalid instance URL '{0}'")]
|
||||
InstanceUrl(String),
|
||||
#[error("Invalid response at '{0}'")]
|
||||
InvalidResponse(Url,String),
|
||||
#[error("Key mismatch")]
|
||||
KeyMismatch,
|
||||
#[error("No public instance host found with domain '{0}'")]
|
||||
HostNotFound(String),
|
||||
#[error("Server responded with status code '{0}'")]
|
||||
ServerResponse(u16, String),
|
||||
#[error("Invalid hash found")]
|
||||
InvalidHash(String),
|
||||
#[error("Failed to fetch DNS TXT records")]
|
||||
DNSError(#[from] trust_dns_resolver::error::ResolveError),
|
||||
#[error("No valid DNS TXT entry found for your key")]
|
||||
DNSNoValidEntry(String),
|
||||
}
|
||||
type LoginResult<T> = std::result::Result<T, LoginError>;
|
||||
|
||||
pub async fn logout(session: Session) -> Result<axum::response::Response> {
|
||||
session.delete();
|
||||
Ok(Redirect::temporary(LOGIN_URL).into_response())
|
||||
}
|
||||
|
||||
pub async fn login(
|
||||
State(ref template): State<Arc<tera::Tera>>,
|
||||
State(ref config): State<Arc<crate::Config>>,
|
||||
State(ref login_client): State<Client>,
|
||||
State(ref db): State<DatabaseConnection>,
|
||||
session: Session,
|
||||
Form(input): Form<LoginInput>
|
||||
) -> Result<axum::response::Response> {
|
||||
tracing::debug!(login=?input);
|
||||
let host = host::Entity::find().filter(host::Column::Domain.eq(input.domain.trim()))
|
||||
.one(db)
|
||||
.await?;
|
||||
|
||||
if host.as_ref().map(|host|get_session_login(&session).map(|login|login.hosts.contains(&host.id)).is_ok()).unwrap_or_default() {
|
||||
// already logged in
|
||||
return Ok(Redirect::temporary(&input.referrer).into_response());
|
||||
}
|
||||
|
||||
match login_inner(config, login_client, &input, host).await {
|
||||
Ok(host) => {
|
||||
let session_value = match get_session_login(&session) {
|
||||
Ok(mut session) => {
|
||||
session.hosts.insert(host.id);
|
||||
session
|
||||
},
|
||||
Err(_) => {
|
||||
let mut ids = HashSet::with_capacity(1);
|
||||
ids.insert(host.id);
|
||||
ActiveLogin {
|
||||
hosts: ids,
|
||||
}
|
||||
}
|
||||
};
|
||||
session.insert(LOGIN_KEY,session_value)?;
|
||||
let referrer = input.referrer.trim();
|
||||
let location = match referrer.trim().is_empty() && referrer != LOGIN_URL {
|
||||
true => ADMIN_OVERVIEW_URL,
|
||||
false => input.referrer.trim(),
|
||||
};
|
||||
let mut res = Redirect::temporary(location).into_response();
|
||||
*res.status_mut() = StatusCode::FOUND;
|
||||
Ok(res)
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::debug!(login_error=?e);
|
||||
let mut context = tera::Context::new();
|
||||
context.insert("REFERRER", &input.referrer);
|
||||
context.insert("ERROR", &e.to_string());
|
||||
context.insert("LOGIN_METHOD", &input.verification_method);
|
||||
context.insert("DOMAIN", &input.domain);
|
||||
context.insert("VERIFY_TOKEN_NAME", &config.login_token_name);
|
||||
match e {
|
||||
LoginError::InvalidResponse(_, val)
|
||||
| LoginError::ServerResponse(_,val)
|
||||
| LoginError::DNSNoValidEntry(val)
|
||||
| LoginError::InvalidHash(val) => context.insert("QUOTE", &val),
|
||||
_ => (),
|
||||
}
|
||||
let mut res = Html(template.render("login.html.j2", &context)?).into_response();
|
||||
*res.status_mut() = StatusCode::FORBIDDEN;
|
||||
Ok(res.into_response())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn login_inner(
|
||||
config: &crate::Config,
|
||||
login_client: &Client,
|
||||
input: &LoginInput,
|
||||
host: Option<host::Model>,
|
||||
) -> LoginResult<host::Model> {
|
||||
let host = host.ok_or_else(||LoginError::HostNotFound(input.domain.clone()))?;
|
||||
|
||||
match input.verification_method {
|
||||
VerificationMethod::DNS => {
|
||||
let entries = fetch_host_dns(&host.domain, &config).await?;
|
||||
for entry in &entries {
|
||||
if let Ok(_) = verify_key(entry, &input.key) {
|
||||
return Ok(host);
|
||||
}
|
||||
}
|
||||
Err(LoginError::DNSNoValidEntry(entries.join(",")))
|
||||
},
|
||||
VerificationMethod::HTTP => {
|
||||
let fetched_key = fetch_host_txt(&host.url, login_client, &config).await?;
|
||||
verify_key(&fetched_key, &input.key).map(|_|host)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct LoginInput {
|
||||
domain: String,
|
||||
key: String,
|
||||
referrer: String,
|
||||
verification_method: VerificationMethod
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
enum VerificationMethod {
|
||||
DNS,
|
||||
HTTP
|
||||
}
|
||||
|
||||
async fn fetch_host_txt(instance_url: &str, client: &Client, config: &Config) -> LoginResult<String> {
|
||||
let mut request_url = Url::parse(&instance_url).map_err(|_|LoginError::InstanceUrl(instance_url.to_string()))?;
|
||||
request_url.set_path(&[".",&config.login_token_name].concat());
|
||||
request_url.set_query(None);
|
||||
let result = client.get(request_url.clone()).send().await.map_err(|e|LoginError::HttpFailure(request_url.clone(), e))?;
|
||||
match true { // result.status().is_success()
|
||||
true => Ok(result.text().await.map_err(|e: reqwest::Error|LoginError::InvalidResponse(request_url,e.to_string()))?),
|
||||
false => Err(LoginError::ServerResponse(result.status().as_u16(),result.text().await.unwrap_or_default())),
|
||||
}
|
||||
}
|
||||
async fn fetch_host_dns(instance_domain: &str, config: &Config) -> LoginResult<Vec<String>> {
|
||||
// TODO: cache resolver ?
|
||||
let resolver = AsyncResolver::tokio(ResolverConfig::cloudflare_tls(), ResolverOpts::default());
|
||||
let hashed_key = resolver
|
||||
.txt_lookup(format!("{}.{}.",&config.login_token_name, instance_domain))
|
||||
.await?;
|
||||
|
||||
let mut entries = Vec::with_capacity(2);
|
||||
for record in hashed_key.iter() {
|
||||
for data in record.iter() {
|
||||
match std::str::from_utf8(data) {
|
||||
Ok(val) => entries.push(val.to_owned()),
|
||||
Err(_) => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(entries)
|
||||
}
|
||||
/// Verify a key with its public available hash
|
||||
/// Key is in base16 (hex) and has to match the hash passed in after SHA2 encoding it.
|
||||
fn verify_key(hash: &str, key: &str) -> LoginResult<()> {
|
||||
let mut hasher: Sha256 = Sha256::new();
|
||||
hasher.update(key);
|
||||
let res = hasher.finalize();
|
||||
let mut decoded_hash = [0;32];
|
||||
base16ct::mixed::decode(hash, &mut decoded_hash).map_err(|e|{
|
||||
tracing::trace!(hash_error=?e);
|
||||
LoginError::InvalidHash(hash.to_owned())
|
||||
})?;
|
||||
match constant_time_eq(&res,&decoded_hash) {
|
||||
false => Err(LoginError::KeyMismatch),
|
||||
true => Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn login_view(
|
||||
State(ref template): State<Arc<tera::Tera>>,
|
||||
State(ref config): State<Arc<crate::Config>>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<axum::response::Response> {
|
||||
tracing::debug!(headers=?headers);
|
||||
let referrer = headers.get(REFERER).and_then(|v|v.to_str().ok());
|
||||
let mut context = tera::Context::new();
|
||||
context.insert("REFERRER", &referrer);// FIXME: won't work, handle this in the error part to extract the current situation
|
||||
context.insert("VERIFY_TOKEN_NAME", &config.login_token_name);
|
||||
let res = Html(template.render("login.html.j2", &context)?).into_response();
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
|
||||
pub async fn overview(
|
||||
State(ref app_state): State<AppState>,
|
||||
State(ref template): State<Arc<tera::Tera>>,
|
||||
State(ref db): State<DatabaseConnection>,
|
||||
session: Session
|
||||
) -> Result<axum::response::Response> {
|
||||
tracing::info!(?session);
|
||||
|
||||
let hosts = get_all_login_hosts(&session, db).await?;
|
||||
|
||||
let mut context = tera::Context::new();
|
||||
let res = {
|
||||
let guard = app_state.cache.read().map_err(|_| ServerError::MutexFailure)?;
|
||||
let time = guard.last_update.format("%Y.%m.%d %H:%M").to_string();
|
||||
context.insert("last_updated", &time);
|
||||
context.insert("instances", &hosts);
|
||||
|
||||
let res = Html(template.render("admin.html.j2", &context)?).into_response();
|
||||
drop(guard);
|
||||
res
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn history_json(
|
||||
State(ref db): State<DatabaseConnection>,
|
||||
Path(host): Path<i32>,
|
||||
session: Session
|
||||
) -> Result<axum::response::Response> {
|
||||
let host = get_specific_login_host(host,&session,db).await?;
|
||||
|
||||
let history: Vec<health_check::Model> = health_check::Entity::find().filter(health_check::Column::Host.eq(host.id))
|
||||
.order_by_asc(health_check::Column::Time).limit(20).all(db).await?;
|
||||
|
||||
Ok(Json(history).into_response())
|
||||
}
|
||||
|
||||
pub async fn history_view(
|
||||
State(ref app_state): State<AppState>,
|
||||
State(ref template): State<Arc<tera::Tera>>,
|
||||
State(ref db): State<DatabaseConnection>,
|
||||
Path(host): Path<i32>,
|
||||
session: Session
|
||||
) -> Result<axum::response::Response> {
|
||||
tracing::info!(?session);
|
||||
|
||||
let host = get_specific_login_host(host,&session,db).await?;
|
||||
|
||||
let mut context = tera::Context::new();
|
||||
let res = {
|
||||
let guard = app_state.cache.read().map_err(|_| ServerError::MutexFailure)?;
|
||||
let time = guard.last_update.format("%Y.%m.%d %H:%M").to_string();
|
||||
context.insert("last_updated", &time);
|
||||
context.insert("HOST_DOMAIN", &host.domain);
|
||||
|
||||
let res = Html(template.render("errors_admin.html.j2", &context)?).into_response();
|
||||
drop(guard);
|
||||
res
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn errors_view(
|
||||
State(ref app_state): State<AppState>,
|
||||
State(ref template): State<Arc<tera::Tera>>,
|
||||
State(ref db): State<DatabaseConnection>,
|
||||
Path(host): Path<i32>,
|
||||
session: Session
|
||||
) -> Result<axum::response::Response> {
|
||||
tracing::info!(?session);
|
||||
|
||||
let host = get_specific_login_host(host,&session,db).await?;
|
||||
|
||||
let errors = check_errors::Entity::find().filter(check_errors::Column::Host.eq(host.id))
|
||||
.order_by_desc(check_errors::Column::Time).limit(20).all(db).await?;
|
||||
|
||||
let mut context = tera::Context::new();
|
||||
let res = {
|
||||
let guard = app_state.cache.read().map_err(|_| ServerError::MutexFailure)?;
|
||||
let time = guard.last_update.format("%Y.%m.%d %H:%M").to_string();
|
||||
context.insert("last_updated", &time);
|
||||
context.insert("ERRORS", &errors);
|
||||
context.insert("HOST_DOMAIN", &host.domain);
|
||||
|
||||
let res = Html(template.render("errors_admin.html.j2", &context)?).into_response();
|
||||
drop(guard);
|
||||
res
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Get all [host::Model] for current [Session]
|
||||
async fn get_all_login_hosts(session: &Session, db: &DatabaseConnection) -> Result<Vec<host::Model>> {
|
||||
let login = get_session_login(&session)?;
|
||||
|
||||
let host_res = host::Entity::find().filter(host::Column::Id.is_in(login.hosts.iter().map(|v|*v)))
|
||||
.all(db).await?;
|
||||
Ok(host_res)
|
||||
}
|
||||
|
||||
/// Get wanted [host::Model] for current [Session] if valid for this user
|
||||
async fn get_specific_login_host(wanted_host_id: i32, session: &Session, db: &DatabaseConnection) -> Result<host::Model> {
|
||||
let login = get_session_login(&session)?;
|
||||
|
||||
if !login.hosts.contains(&wanted_host_id) {
|
||||
return Err(ServerError::MissingPermission);
|
||||
}
|
||||
|
||||
let host_res = host::Entity::find().filter(host::Column::Id.eq(wanted_host_id)).one(db).await?;
|
||||
match host_res {
|
||||
None => {
|
||||
session.delete();
|
||||
return Err(ServerError::HostNotFound(wanted_host_id));
|
||||
},
|
||||
Some(host) => Ok(host),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check for valid session and return the stored [Login] data
|
||||
fn get_session_login(session: &Session) -> Result<ActiveLogin> {
|
||||
if session.active() {
|
||||
if let Some(u) = session.get(LOGIN_KEY).map_err(|_|ServerError::NoLogin)? {
|
||||
return Ok(u);
|
||||
}
|
||||
}
|
||||
Err(ServerError::NoLogin)
|
||||
}
|
||||
+3
-3
@@ -2,16 +2,16 @@
|
||||
use crate::{Result, ServerError};
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{extract::State, Json};
|
||||
use entities::state::Cache;
|
||||
use entities::state::{Cache, AppState};
|
||||
use hyper::http::HeaderValue;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub async fn instances(
|
||||
State(ref cache): State<Cache>,
|
||||
State(ref app_state): State<AppState>,
|
||||
State(ref config): State<Arc<crate::Config>>,
|
||||
) -> Result<axum::response::Response> {
|
||||
let mut res = {
|
||||
let guard = cache.read().map_err(|_| ServerError::MutexFailure)?;
|
||||
let guard = app_state.cache.read().map_err(|_| ServerError::MutexFailure)?;
|
||||
let res = Json(&*guard).into_response();
|
||||
drop(guard);
|
||||
res
|
||||
|
||||
+85
-9
@@ -1,9 +1,10 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
use std::{borrow::Cow, net::SocketAddr, sync::Arc};
|
||||
|
||||
use axum::{extract::DefaultBodyLimit, http::HeaderValue, response::Html, routing::{get, get_service}, Router};
|
||||
use entities::state::{scanner::ScannerConfig, Cache};
|
||||
use axum::{extract::DefaultBodyLimit, http::HeaderValue, response::{Html, Redirect}, routing::{get, get_service}, Router, error_handling::HandleErrorLayer};
|
||||
use entities::state::{scanner::ScannerConfig, AppState};
|
||||
use hyper::{header, StatusCode};
|
||||
use reqwest::Client;
|
||||
use sea_orm::DatabaseConnection;
|
||||
use tera::Tera;
|
||||
use thiserror::Error;
|
||||
@@ -12,23 +13,31 @@ use tower_http::{
|
||||
cors::CorsLayer, limit::RequestBodyLimitLayer, services::{ServeDir, ServeFile},
|
||||
set_header::SetResponseHeaderLayer, trace::TraceLayer,
|
||||
};
|
||||
use tower_sessions::{SessionManagerLayer, cookie::SameSite};
|
||||
|
||||
mod api;
|
||||
mod website;
|
||||
mod admin;
|
||||
|
||||
const LOGIN_URL: &'static str = "/admin/login";
|
||||
const ADMIN_OVERVIEW_URL: &'static str = "/admin";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Config {
|
||||
pub site_url: String,
|
||||
pub max_age: usize,
|
||||
pub session_ttl_seconds: u64,
|
||||
pub login_token_name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, axum::extract::FromRef)]
|
||||
struct AppState {
|
||||
struct WebState {
|
||||
db: DatabaseConnection,
|
||||
config: Arc<Config>,
|
||||
scanner_config: ScannerConfig,
|
||||
cache: Cache,
|
||||
app_state: AppState,
|
||||
templates: Arc<Tera>,
|
||||
login_client: Client,
|
||||
}
|
||||
|
||||
/// Start webserver
|
||||
@@ -37,23 +46,72 @@ pub async fn start(
|
||||
db: DatabaseConnection,
|
||||
config: Config,
|
||||
scanner_config: ScannerConfig,
|
||||
cache: Cache,
|
||||
app_state: AppState,
|
||||
) -> Result<()> {
|
||||
#[cfg(debug_assertions)]
|
||||
let session_secure = false;
|
||||
#[cfg(not(debug_assertions))]
|
||||
let session_secure = true;
|
||||
if !session_secure {
|
||||
tracing::warn!("debug build, sessions are not secure!");
|
||||
}
|
||||
|
||||
let session_store = tower_sessions::MemoryStore::default();
|
||||
let session_service = ServiceBuilder::new()
|
||||
.layer(HandleErrorLayer::new(|e: axum::BoxError| async move {
|
||||
tracing::debug!(session_error=?e);
|
||||
StatusCode::BAD_REQUEST
|
||||
}))
|
||||
.layer(
|
||||
SessionManagerLayer::new(session_store)
|
||||
.with_secure(session_secure)
|
||||
.with_path("/admin".to_string())
|
||||
.with_name("admin_login")
|
||||
.with_same_site(SameSite::Strict)
|
||||
.with_max_age(time::Duration::seconds(config.session_ttl_seconds as _)),
|
||||
);
|
||||
|
||||
let user_agent = format!("nitter-status (+{}/about)",scanner_config.website_url);
|
||||
let login_client = Client::builder()
|
||||
.cookie_store(false)
|
||||
.brotli(true)
|
||||
.deflate(true)
|
||||
.gzip(true)
|
||||
.use_rustls_tls()
|
||||
.user_agent(user_agent)
|
||||
.connect_timeout(std::time::Duration::from_secs(3))
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build().unwrap();
|
||||
|
||||
let config = Arc::new(config);
|
||||
let tera = Tera::new("server/templates/*")?;
|
||||
let state = AppState {
|
||||
let mut tera = Tera::new("server/templates/*")?;
|
||||
tera.autoescape_on(vec![".html.j2"]);
|
||||
let state = WebState {
|
||||
config: config.clone(),
|
||||
db,
|
||||
cache,
|
||||
app_state,
|
||||
scanner_config,
|
||||
templates: Arc::new(tera),
|
||||
login_client,
|
||||
};
|
||||
|
||||
let router = Router::new()
|
||||
.nest_service(
|
||||
"/static",
|
||||
ServeDir::new("server/static").append_index_html_on_directories(false),
|
||||
)
|
||||
.route("/api/v1/instances", get(api::instances))
|
||||
.nest(ADMIN_OVERVIEW_URL, Router::new()
|
||||
.route("/", get(admin::overview))
|
||||
.route("/errors/:host", get(admin::errors_view))
|
||||
// .route("/history/:host", get(admin::history_view))
|
||||
// .route("/api/history", get(admin::history_json))
|
||||
.route("/login", get(admin::login_view).post(admin::login))
|
||||
.route("/logout", get(admin::logout))
|
||||
// .layer(ServiceBuilder::new().layer(SetResponseHeaderLayer::overriding(header::CACHE_CONTROL, "must-revalidate")))
|
||||
.layer(session_service)
|
||||
)
|
||||
// .route("/admin", get(admin::view))
|
||||
.route("/about", get(website::about))
|
||||
.route(
|
||||
"/robots.txt",
|
||||
@@ -98,13 +156,31 @@ pub enum ServerError {
|
||||
MutexFailure,
|
||||
#[error("Failed to perform templating: {0:?}")]
|
||||
Templating(#[from] tera::Error),
|
||||
#[error("Not logged in")]
|
||||
NoLogin,
|
||||
#[error("Internal Error during DB request: {0:?}")]
|
||||
DBError(#[from] sea_orm::DbErr),
|
||||
#[error("Internal Error session handling: {0:?}")]
|
||||
SessionError(#[from] tower_sessions::session::SessionError),
|
||||
#[error("Host '{0}' can't be found, logging out user!")]
|
||||
HostNotFound(i32),
|
||||
#[error("No permission to access this resource")]
|
||||
MissingPermission,
|
||||
}
|
||||
|
||||
impl axum::response::IntoResponse for ServerError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
use ServerError::*;
|
||||
let msg = match &self {
|
||||
MutexFailure | Templating(_) => (
|
||||
NoLogin => {
|
||||
let mut resp = Redirect::temporary(LOGIN_URL).into_response();
|
||||
// *resp.status_mut() = StatusCode::FOUND; // have to use a 301, [Redirect] 307 won't work for referrer
|
||||
return resp;
|
||||
},
|
||||
MissingPermission =>
|
||||
(StatusCode::FORBIDDEN,Cow::Borrowed("Missing permission to access this resource"))
|
||||
,
|
||||
MutexFailure | Templating(_) | DBError(_) | SessionError(_) | HostNotFound(_) => (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Cow::Borrowed("Internal Server Error"),
|
||||
),
|
||||
|
||||
@@ -6,18 +6,19 @@ use crate::Result;
|
||||
use crate::ServerError;
|
||||
use axum::response::IntoResponse;
|
||||
use axum::{extract::State, response::Html};
|
||||
use entities::state::AppState;
|
||||
use entities::state::scanner::ScannerConfig;
|
||||
use entities::state::Cache;
|
||||
use hyper::http::HeaderValue;
|
||||
|
||||
pub async fn instances(
|
||||
State(ref cache): State<Cache>,
|
||||
State(ref app_state): State<AppState>,
|
||||
State(ref template): State<Arc<tera::Tera>>,
|
||||
State(ref config): State<Arc<crate::Config>>,
|
||||
) -> Result<axum::response::Response> {
|
||||
let mut context = tera::Context::new();
|
||||
let mut res = {
|
||||
let guard = cache.read().map_err(|_| ServerError::MutexFailure)?;
|
||||
let guard = app_state.cache.read().map_err(|_| ServerError::MutexFailure)?;
|
||||
context.insert("instances", &guard.hosts);
|
||||
let time = guard.last_update.format("%Y.%m.%d %H:%M").to_string();
|
||||
context.insert("last_updated", &time);
|
||||
@@ -37,7 +38,7 @@ pub async fn instances(
|
||||
}
|
||||
|
||||
pub async fn about(
|
||||
State(ref cache): State<Cache>,
|
||||
State(ref app_state): State<AppState>,
|
||||
State(ref template): State<Arc<tera::Tera>>,
|
||||
State(ref scanner_config): State<ScannerConfig>,
|
||||
) -> Result<axum::response::Response> {
|
||||
@@ -60,7 +61,7 @@ pub async fn about(
|
||||
&scanner_config.ping_range.num_hours(),
|
||||
);
|
||||
{
|
||||
let guard = cache.read().map_err(|_| ServerError::MutexFailure)?;
|
||||
let guard = app_state.cache.read().map_err(|_| ServerError::MutexFailure)?;
|
||||
context.insert("latest_commit", &guard.latest_commit);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user