Compare commits
	
		
			9 Commits
		
	
	
		
			jude/new-c
			...
			jude/react
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					1a03c2471b | ||
| a476f43f28 | |||
| 17192b0f89 | |||
| 
						 | 
					0419863afa | ||
| 
						 | 
					827a982a40 | ||
| 
						 | 
					6e435bfc2e | ||
| 8ba0f02b98 | |||
| d36438c6ce | |||
| e0c60e2ce3 | 
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -3,3 +3,5 @@
 | 
			
		||||
/venv
 | 
			
		||||
.cargo
 | 
			
		||||
/.idea
 | 
			
		||||
web/static/index.html
 | 
			
		||||
web/static/assets
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										883
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										883
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,6 +1,6 @@
 | 
			
		||||
[package]
 | 
			
		||||
name = "reminder-rs"
 | 
			
		||||
version = "1.6.48"
 | 
			
		||||
version = "1.6.50"
 | 
			
		||||
authors = ["Jude Southworth <judesouthworth@pm.me>"]
 | 
			
		||||
edition = "2021"
 | 
			
		||||
license = "AGPL-3.0 only"
 | 
			
		||||
@@ -43,6 +43,7 @@ assets = [
 | 
			
		||||
    ["conf/default.env", "etc/reminder-rs/config.env", "600"],
 | 
			
		||||
    ["conf/Rocket.toml", "etc/reminder-rs/Rocket.toml", "600"],
 | 
			
		||||
    ["web/static/**/*", "lib/reminder-rs/static", "644"],
 | 
			
		||||
    ["reminder-dashboard/dist/static/**/*", "lib/reminder-rs/static", "644"],
 | 
			
		||||
    ["web/templates/**/*", "lib/reminder-rs/templates", "644"],
 | 
			
		||||
    ["healthcheck", "lib/reminder-rs/healthcheck", "755"],
 | 
			
		||||
    ["cron.d/reminder_health", "etc/cron.d/reminder_health", "644"],
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
[package]
 | 
			
		||||
name = "reminder_web"
 | 
			
		||||
version = "0.1.2"
 | 
			
		||||
version = "0.1.4"
 | 
			
		||||
authors = ["jellywx <judesouthworth@pm.me>"]
 | 
			
		||||
edition = "2018"
 | 
			
		||||
 | 
			
		||||
@@ -19,3 +19,4 @@ lazy_static = "1.4.0"
 | 
			
		||||
rand = "0.8"
 | 
			
		||||
base64 = "0.13"
 | 
			
		||||
csv = "1.2"
 | 
			
		||||
prometheus = "0.13.3"
 | 
			
		||||
 
 | 
			
		||||
@@ -33,11 +33,9 @@ impl<'r> FromRequest<'r> for Transaction<'r> {
 | 
			
		||||
        match request.guard::<&State<Pool<Database>>>().await {
 | 
			
		||||
            Outcome::Success(pool) => match pool.begin().await {
 | 
			
		||||
                Ok(transaction) => Outcome::Success(Transaction(transaction)),
 | 
			
		||||
                Err(e) => {
 | 
			
		||||
                    Outcome::Failure((Status::InternalServerError, TransactionError::Error(e)))
 | 
			
		||||
                }
 | 
			
		||||
                Err(e) => Outcome::Error((Status::InternalServerError, TransactionError::Error(e))),
 | 
			
		||||
            },
 | 
			
		||||
            Outcome::Failure(e) => Outcome::Failure((e.0, TransactionError::Missing)),
 | 
			
		||||
            Outcome::Error(e) => Outcome::Error((e.0, TransactionError::Missing)),
 | 
			
		||||
            Outcome::Forward(f) => Outcome::Forward(f),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@ mod consts;
 | 
			
		||||
mod macros;
 | 
			
		||||
mod catchers;
 | 
			
		||||
mod guards;
 | 
			
		||||
mod metrics;
 | 
			
		||||
mod routes;
 | 
			
		||||
 | 
			
		||||
use std::{env, path::Path};
 | 
			
		||||
@@ -25,7 +26,10 @@ use serenity::{
 | 
			
		||||
};
 | 
			
		||||
use sqlx::{MySql, Pool};
 | 
			
		||||
 | 
			
		||||
use crate::consts::{CNC_GUILD, DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN, SUBSCRIPTION_ROLES};
 | 
			
		||||
use crate::{
 | 
			
		||||
    consts::{CNC_GUILD, DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN, SUBSCRIPTION_ROLES},
 | 
			
		||||
    metrics::{init_metrics, MetricProducer},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type Database = MySql;
 | 
			
		||||
 | 
			
		||||
@@ -64,7 +68,10 @@ pub async fn initialize(
 | 
			
		||||
    let static_path =
 | 
			
		||||
        if Path::new("web/static").exists() { "web/static" } else { "/lib/reminder-rs/static" };
 | 
			
		||||
 | 
			
		||||
    init_metrics();
 | 
			
		||||
 | 
			
		||||
    rocket::build()
 | 
			
		||||
        .attach(MetricProducer)
 | 
			
		||||
        .attach(Template::fairing())
 | 
			
		||||
        .register(
 | 
			
		||||
            "/",
 | 
			
		||||
@@ -85,12 +92,13 @@ pub async fn initialize(
 | 
			
		||||
        .mount(
 | 
			
		||||
            "/",
 | 
			
		||||
            routes![
 | 
			
		||||
                routes::index,
 | 
			
		||||
                routes::cookies,
 | 
			
		||||
                routes::index,
 | 
			
		||||
                routes::metrics::metrics,
 | 
			
		||||
                routes::privacy,
 | 
			
		||||
                routes::terms,
 | 
			
		||||
                routes::return_to_same_site,
 | 
			
		||||
                routes::report::report_error,
 | 
			
		||||
                routes::return_to_same_site,
 | 
			
		||||
                routes::terms,
 | 
			
		||||
            ],
 | 
			
		||||
        )
 | 
			
		||||
        .mount(
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										43
									
								
								web/src/metrics.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								web/src/metrics.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
			
		||||
use lazy_static::lazy_static;
 | 
			
		||||
use prometheus::{IntCounterVec, Opts, Registry};
 | 
			
		||||
use rocket::{
 | 
			
		||||
    fairing::{Fairing, Info, Kind},
 | 
			
		||||
    Data, Request, Response,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
lazy_static! {
 | 
			
		||||
    pub static ref REGISTRY: Registry = Registry::new();
 | 
			
		||||
    static ref REQUEST_COUNTER: IntCounterVec =
 | 
			
		||||
        IntCounterVec::new(Opts::new("requests", "Requests"), &["method", "route"]).unwrap();
 | 
			
		||||
    static ref RESPONSE_COUNTER: IntCounterVec =
 | 
			
		||||
        IntCounterVec::new(Opts::new("responses", "Responses"), &["status", "route"]).unwrap();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn init_metrics() {
 | 
			
		||||
    REGISTRY.register(Box::new(REQUEST_COUNTER.clone())).unwrap();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct MetricProducer;
 | 
			
		||||
 | 
			
		||||
#[rocket::async_trait]
 | 
			
		||||
impl Fairing for MetricProducer {
 | 
			
		||||
    fn info(&self) -> Info {
 | 
			
		||||
        Info { name: "Metrics fairing", kind: Kind::Request }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn on_request(&self, req: &mut Request<'_>, _data: &mut Data<'_>) {
 | 
			
		||||
        if let Some(route) = req.route() {
 | 
			
		||||
            REQUEST_COUNTER
 | 
			
		||||
                .with_label_values(&[req.method().as_str(), &route.uri.to_string()])
 | 
			
		||||
                .inc();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async fn on_response<'r>(&self, req: &'r Request<'_>, resp: &mut Response<'r>) {
 | 
			
		||||
        if let Some(route) = req.route() {
 | 
			
		||||
            RESPONSE_COUNTER
 | 
			
		||||
                .with_label_values(&[&resp.status().code.to_string(), &route.uri.to_string()])
 | 
			
		||||
                .inc();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -235,6 +235,21 @@ pub async fn edit_reminder(
 | 
			
		||||
                ]);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    } else {
 | 
			
		||||
        sqlx::query!(
 | 
			
		||||
            "
 | 
			
		||||
            UPDATE reminders
 | 
			
		||||
            SET interval_seconds = NULL, interval_days = NULL, interval_months = NULL
 | 
			
		||||
            WHERE uid = ?
 | 
			
		||||
            ",
 | 
			
		||||
            reminder.uid
 | 
			
		||||
        )
 | 
			
		||||
        .execute(transaction.executor())
 | 
			
		||||
        .await
 | 
			
		||||
        .map_err(|e| {
 | 
			
		||||
            warn!("Error updating reminder interval: {:?}", e);
 | 
			
		||||
            json!({ "reminder": Option::<Reminder>::None, "errors": vec!["Unknown error"] })
 | 
			
		||||
        })?;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if reminder.channel > 0 {
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +0,0 @@
 | 
			
		||||
 | 
			
		||||
@@ -1,10 +1,14 @@
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
use std::path::Path;
 | 
			
		||||
 | 
			
		||||
use chrono::{naive::NaiveDateTime, Utc};
 | 
			
		||||
use rand::{rngs::OsRng, seq::IteratorRandom};
 | 
			
		||||
use rocket::{http::CookieJar, response::Redirect, serde::json::json};
 | 
			
		||||
use rocket_dyn_templates::Template;
 | 
			
		||||
use serde::{Deserialize, Deserializer, Serialize};
 | 
			
		||||
use rocket::{
 | 
			
		||||
    fs::{relative, NamedFile},
 | 
			
		||||
    http::CookieJar,
 | 
			
		||||
    response::Redirect,
 | 
			
		||||
    serde::json::json,
 | 
			
		||||
};
 | 
			
		||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
 | 
			
		||||
use serenity::{
 | 
			
		||||
    client::Context,
 | 
			
		||||
    http::Http,
 | 
			
		||||
@@ -27,7 +31,6 @@ use crate::{
 | 
			
		||||
 | 
			
		||||
pub mod api;
 | 
			
		||||
pub mod export;
 | 
			
		||||
pub mod guild;
 | 
			
		||||
 | 
			
		||||
type Unset<T> = Option<T>;
 | 
			
		||||
 | 
			
		||||
@@ -51,12 +54,27 @@ fn interval_default() -> Unset<Option<u32>> {
 | 
			
		||||
    None
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn deserialize_optional_field<'de, T, D>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
 | 
			
		||||
where
 | 
			
		||||
    D: Deserializer<'de>,
 | 
			
		||||
    T: Deserialize<'de>,
 | 
			
		||||
{
 | 
			
		||||
    Ok(Some(Option::deserialize(deserializer)?))
 | 
			
		||||
#[derive(sqlx::Type)]
 | 
			
		||||
#[sqlx(transparent)]
 | 
			
		||||
struct Attachment(Vec<u8>);
 | 
			
		||||
 | 
			
		||||
impl<'de> Deserialize<'de> for Attachment {
 | 
			
		||||
    fn deserialize<D>(deserializer: D) -> Result<Attachment, D::Error>
 | 
			
		||||
    where
 | 
			
		||||
        D: Deserializer<'de>,
 | 
			
		||||
    {
 | 
			
		||||
        let string = String::deserialize(deserializer)?;
 | 
			
		||||
        Ok(Attachment(base64::decode(string).map_err(de::Error::custom)?))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Serialize for Attachment {
 | 
			
		||||
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
 | 
			
		||||
    where
 | 
			
		||||
        S: Serializer,
 | 
			
		||||
    {
 | 
			
		||||
        serializer.collect_str(&base64::encode(&self.0))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize)]
 | 
			
		||||
@@ -67,7 +85,7 @@ pub struct ReminderTemplate {
 | 
			
		||||
    guild_id: u32,
 | 
			
		||||
    #[serde(default = "template_name_default")]
 | 
			
		||||
    name: String,
 | 
			
		||||
    attachment: Option<Vec<u8>>,
 | 
			
		||||
    attachment: Option<Attachment>,
 | 
			
		||||
    attachment_name: Option<String>,
 | 
			
		||||
    avatar: Option<String>,
 | 
			
		||||
    content: String,
 | 
			
		||||
@@ -92,7 +110,7 @@ pub struct ReminderTemplate {
 | 
			
		||||
pub struct ReminderTemplateCsv {
 | 
			
		||||
    #[serde(default = "template_name_default")]
 | 
			
		||||
    name: String,
 | 
			
		||||
    attachment: Option<Vec<u8>>,
 | 
			
		||||
    attachment: Option<Attachment>,
 | 
			
		||||
    attachment_name: Option<String>,
 | 
			
		||||
    avatar: Option<String>,
 | 
			
		||||
    content: String,
 | 
			
		||||
@@ -127,8 +145,7 @@ pub struct EmbedField {
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize)]
 | 
			
		||||
pub struct Reminder {
 | 
			
		||||
    #[serde(with = "base64s")]
 | 
			
		||||
    attachment: Option<Vec<u8>>,
 | 
			
		||||
    attachment: Option<Attachment>,
 | 
			
		||||
    attachment_name: Option<String>,
 | 
			
		||||
    avatar: Option<String>,
 | 
			
		||||
    #[serde(with = "string")]
 | 
			
		||||
@@ -161,8 +178,7 @@ pub struct Reminder {
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize)]
 | 
			
		||||
pub struct ReminderCsv {
 | 
			
		||||
    #[serde(with = "base64s")]
 | 
			
		||||
    attachment: Option<Vec<u8>>,
 | 
			
		||||
    attachment: Option<Attachment>,
 | 
			
		||||
    attachment_name: Option<String>,
 | 
			
		||||
    avatar: Option<String>,
 | 
			
		||||
    channel: String,
 | 
			
		||||
@@ -195,7 +211,7 @@ pub struct PatchReminder {
 | 
			
		||||
    uid: String,
 | 
			
		||||
    #[serde(default)]
 | 
			
		||||
    #[serde(deserialize_with = "deserialize_optional_field")]
 | 
			
		||||
    attachment: Unset<Option<String>>,
 | 
			
		||||
    attachment: Unset<Option<Attachment>>,
 | 
			
		||||
    #[serde(default)]
 | 
			
		||||
    #[serde(deserialize_with = "deserialize_optional_field")]
 | 
			
		||||
    attachment_name: Unset<Option<String>>,
 | 
			
		||||
@@ -291,6 +307,14 @@ pub fn generate_uid() -> String {
 | 
			
		||||
        .join("")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fn deserialize_optional_field<'de, T, D>(deserializer: D) -> Result<Option<Option<T>>, D::Error>
 | 
			
		||||
where
 | 
			
		||||
    D: Deserializer<'de>,
 | 
			
		||||
    T: Deserialize<'de>,
 | 
			
		||||
{
 | 
			
		||||
    Ok(Some(Option::deserialize(deserializer)?))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://github.com/serde-rs/json/issues/329#issuecomment-305608405
 | 
			
		||||
mod string {
 | 
			
		||||
    use std::{fmt::Display, str::FromStr};
 | 
			
		||||
@@ -315,29 +339,6 @@ mod string {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
mod base64s {
 | 
			
		||||
    use serde::{de, Deserialize, Deserializer, Serializer};
 | 
			
		||||
 | 
			
		||||
    pub fn serialize<S>(value: &Option<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
 | 
			
		||||
    where
 | 
			
		||||
        S: Serializer,
 | 
			
		||||
    {
 | 
			
		||||
        if let Some(opt) = value {
 | 
			
		||||
            serializer.collect_str(&base64::encode(opt))
 | 
			
		||||
        } else {
 | 
			
		||||
            serializer.serialize_none()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
 | 
			
		||||
    where
 | 
			
		||||
        D: Deserializer<'de>,
 | 
			
		||||
    {
 | 
			
		||||
        let string = Option::<String>::deserialize(deserializer)?;
 | 
			
		||||
        Some(string.map(|b| base64::decode(b).map_err(de::Error::custom))).flatten().transpose()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize)]
 | 
			
		||||
pub struct DeleteReminder {
 | 
			
		||||
    uid: String,
 | 
			
		||||
@@ -657,22 +658,26 @@ async fn create_database_channel(
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[get("/")]
 | 
			
		||||
pub async fn dashboard_home(cookies: &CookieJar<'_>) -> Result<Template, Redirect> {
 | 
			
		||||
pub async fn dashboard_home(cookies: &CookieJar<'_>) -> Result<NamedFile, Redirect> {
 | 
			
		||||
    if cookies.get_private("userid").is_some() {
 | 
			
		||||
        let mut map = HashMap::new();
 | 
			
		||||
        map.insert("version", env!("CARGO_PKG_VERSION"));
 | 
			
		||||
        Ok(Template::render("dashboard", &map))
 | 
			
		||||
        NamedFile::open(Path::new(relative!("static/index.html"))).await.map_err(|e| {
 | 
			
		||||
            warn!("Couldn't render dashboard: {:?}", e);
 | 
			
		||||
 | 
			
		||||
            Redirect::to("/login/discord")
 | 
			
		||||
        })
 | 
			
		||||
    } else {
 | 
			
		||||
        Err(Redirect::to("/login/discord"))
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[get("/<_..>")]
 | 
			
		||||
pub async fn dashboard(cookies: &CookieJar<'_>) -> Result<Template, Redirect> {
 | 
			
		||||
pub async fn dashboard(cookies: &CookieJar<'_>) -> Result<NamedFile, Redirect> {
 | 
			
		||||
    if cookies.get_private("userid").is_some() {
 | 
			
		||||
        let mut map = HashMap::new();
 | 
			
		||||
        map.insert("version", env!("CARGO_PKG_VERSION"));
 | 
			
		||||
        Ok(Template::render("dashboard", &map))
 | 
			
		||||
        NamedFile::open(Path::new(relative!("static/index.html"))).await.map_err(|e| {
 | 
			
		||||
            warn!("Couldn't render dashboard: {:?}", e);
 | 
			
		||||
 | 
			
		||||
            Redirect::to("/login/discord")
 | 
			
		||||
        })
 | 
			
		||||
    } else {
 | 
			
		||||
        Err(Redirect::to("/login/discord"))
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -31,22 +31,20 @@ pub async fn discord_login(
 | 
			
		||||
 | 
			
		||||
    // store the pkce secret to verify the authorization later
 | 
			
		||||
    cookies.add_private(
 | 
			
		||||
        Cookie::build("verify", pkce_verifier.secret().to_string())
 | 
			
		||||
        Cookie::build(("verify", pkce_verifier.secret().to_string()))
 | 
			
		||||
            .http_only(true)
 | 
			
		||||
            .path("/login")
 | 
			
		||||
            .same_site(SameSite::Lax)
 | 
			
		||||
            .expires(Expiration::Session)
 | 
			
		||||
            .finish(),
 | 
			
		||||
            .expires(Expiration::Session),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // store the csrf token to verify no interference
 | 
			
		||||
    cookies.add_private(
 | 
			
		||||
        Cookie::build("csrf", csrf_token.secret().to_string())
 | 
			
		||||
        Cookie::build(("csrf", csrf_token.secret().to_string()))
 | 
			
		||||
            .http_only(true)
 | 
			
		||||
            .path("/login")
 | 
			
		||||
            .same_site(SameSite::Lax)
 | 
			
		||||
            .expires(Expiration::Session)
 | 
			
		||||
            .finish(),
 | 
			
		||||
            .expires(Expiration::Session),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    Redirect::to(auth_url.to_string())
 | 
			
		||||
@@ -54,9 +52,9 @@ pub async fn discord_login(
 | 
			
		||||
 | 
			
		||||
#[get("/discord/logout")]
 | 
			
		||||
pub async fn discord_logout(cookies: &CookieJar<'_>) -> Redirect {
 | 
			
		||||
    cookies.remove_private(Cookie::named("username"));
 | 
			
		||||
    cookies.remove_private(Cookie::named("userid"));
 | 
			
		||||
    cookies.remove_private(Cookie::named("access_token"));
 | 
			
		||||
    cookies.remove_private(Cookie::from("username"));
 | 
			
		||||
    cookies.remove_private(Cookie::from("userid"));
 | 
			
		||||
    cookies.remove_private(Cookie::from("access_token"));
 | 
			
		||||
 | 
			
		||||
    Redirect::to(uri!(routes::index))
 | 
			
		||||
}
 | 
			
		||||
@@ -80,17 +78,16 @@ pub async fn discord_callback(
 | 
			
		||||
                .request_async(async_http_client)
 | 
			
		||||
                .await;
 | 
			
		||||
 | 
			
		||||
            cookies.remove_private(Cookie::named("verify"));
 | 
			
		||||
            cookies.remove_private(Cookie::named("csrf"));
 | 
			
		||||
            cookies.remove_private(Cookie::from("verify"));
 | 
			
		||||
            cookies.remove_private(Cookie::from("csrf"));
 | 
			
		||||
 | 
			
		||||
            match token_result {
 | 
			
		||||
                Ok(token) => {
 | 
			
		||||
                    cookies.add_private(
 | 
			
		||||
                        Cookie::build("access_token", token.access_token().secret().to_string())
 | 
			
		||||
                        Cookie::build(("access_token", token.access_token().secret().to_string()))
 | 
			
		||||
                            .secure(true)
 | 
			
		||||
                            .http_only(true)
 | 
			
		||||
                            .path("/dashboard")
 | 
			
		||||
                            .finish(),
 | 
			
		||||
                            .path("/dashboard"),
 | 
			
		||||
                    );
 | 
			
		||||
 | 
			
		||||
                    let request_res = reqwest_client
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								web/src/routes/metrics.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								web/src/routes/metrics.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
use prometheus;
 | 
			
		||||
 | 
			
		||||
use crate::metrics::REGISTRY;
 | 
			
		||||
 | 
			
		||||
#[get("/metrics")]
 | 
			
		||||
pub async fn metrics() -> String {
 | 
			
		||||
    let encoder = prometheus::TextEncoder::new();
 | 
			
		||||
    let res_custom = encoder.encode_to_string(®ISTRY.gather());
 | 
			
		||||
 | 
			
		||||
    match res_custom {
 | 
			
		||||
        Ok(s) => s,
 | 
			
		||||
        Err(e) => {
 | 
			
		||||
            warn!("Error encoding metrics: {:?}", e);
 | 
			
		||||
 | 
			
		||||
            String::new()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
pub mod admin;
 | 
			
		||||
pub mod dashboard;
 | 
			
		||||
pub mod login;
 | 
			
		||||
pub mod metrics;
 | 
			
		||||
pub mod report;
 | 
			
		||||
 | 
			
		||||
use std::collections::HashMap;
 | 
			
		||||
 
 | 
			
		||||
@@ -218,17 +218,16 @@ div.inset-content {
 | 
			
		||||
    margin-right: 10%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
div.flash-message {
 | 
			
		||||
div.flash-container {
 | 
			
		||||
    position: fixed;
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
div.flash-message {
 | 
			
		||||
    width: calc(100% - 32px);
 | 
			
		||||
    margin: 16px !important;
 | 
			
		||||
    z-index: 99;
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
div.flash-message.is-active {
 | 
			
		||||
    display: block;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body {
 | 
			
		||||
@@ -633,6 +632,10 @@ li.highlight {
 | 
			
		||||
    display: flex;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.button-row-edit > button {
 | 
			
		||||
    margin-right: 4px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.button-row .button-row-reminder {
 | 
			
		||||
    flex-grow: 0;
 | 
			
		||||
    padding: 2px;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user