add function to be able to delete names
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2671,6 +2671,7 @@ dependencies = [
|
|||||||
"axum-extra",
|
"axum-extra",
|
||||||
"chrono",
|
"chrono",
|
||||||
"maud",
|
"maud",
|
||||||
|
"rand",
|
||||||
"rust-i18n",
|
"rust-i18n",
|
||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
@@ -8,6 +8,7 @@ axum = "0.8"
|
|||||||
axum-extra = { version = "0.10", features = ["cookie-private", "cookie"] }
|
axum-extra = { version = "0.10", features = ["cookie-private", "cookie"] }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
maud = { version = "0.27", features = ["axum"] }
|
maud = { version = "0.27", features = ["axum"] }
|
||||||
|
rand = "0.8"
|
||||||
rust-i18n = "3.1"
|
rust-i18n = "3.1"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "macros", "chrono"] }
|
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "macros", "chrono"] }
|
||||||
|
@@ -24,3 +24,8 @@ CREATE TABLE sightings (
|
|||||||
-- Create indexes for better performance on foreign key lookups
|
-- Create indexes for better performance on foreign key lookups
|
||||||
CREATE INDEX idx_sightings_client_uuid ON sightings(client_uuid);
|
CREATE INDEX idx_sightings_client_uuid ON sightings(client_uuid);
|
||||||
CREATE INDEX idx_sightings_camera_id ON sightings(camera_id);
|
CREATE INDEX idx_sightings_camera_id ON sightings(camera_id);
|
||||||
|
|
||||||
|
CREATE TABLE banned_names (
|
||||||
|
name TEXT PRIMARY KEY NOT NULL,
|
||||||
|
banned_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
112
src/admin.rs
Normal file
112
src/admin.rs
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
use crate::{language::language, page::Page, AppState};
|
||||||
|
use axum::{
|
||||||
|
extract::State,
|
||||||
|
http::HeaderMap,
|
||||||
|
response::{IntoResponse, Redirect, Response},
|
||||||
|
routing::{get, post},
|
||||||
|
Form, Router,
|
||||||
|
};
|
||||||
|
use axum_extra::extract::{
|
||||||
|
cookie::{Cookie, Expiration},
|
||||||
|
CookieJar, PrivateCookieJar,
|
||||||
|
};
|
||||||
|
use maud::{html, Markup};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct LoginForm {
|
||||||
|
password: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn login_page(cookies: CookieJar, headers: HeaderMap) -> Markup {
|
||||||
|
let lang = language(&cookies, &headers);
|
||||||
|
rust_i18n::set_locale(lang.to_locale());
|
||||||
|
|
||||||
|
Page::new(lang).content(html! {
|
||||||
|
h1 { "Admin Login" }
|
||||||
|
form method="POST" action="/admin/login" {
|
||||||
|
fieldset {
|
||||||
|
label for="password" { "Password:" }
|
||||||
|
input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
id="password"
|
||||||
|
required;
|
||||||
|
input type="submit" value="Login";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn login(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
private_cookies: PrivateCookieJar,
|
||||||
|
Form(form): Form<LoginForm>,
|
||||||
|
) -> Response {
|
||||||
|
if form.password == state.admin_password {
|
||||||
|
// Set secure admin session cookie
|
||||||
|
let expiration_date = OffsetDateTime::now_utc() + time::Duration::days(30);
|
||||||
|
let mut cookie = Cookie::new("admin_session", "authenticated");
|
||||||
|
cookie.set_expires(Expiration::DateTime(expiration_date));
|
||||||
|
cookie.set_http_only(true);
|
||||||
|
cookie.set_secure(true);
|
||||||
|
cookie.set_path("/");
|
||||||
|
|
||||||
|
let updated_cookies = private_cookies.add(cookie);
|
||||||
|
(updated_cookies, Redirect::to("/protected")).into_response()
|
||||||
|
} else {
|
||||||
|
// Invalid password, redirect back to login
|
||||||
|
Redirect::to("/admin/login").into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn logout(private_cookies: PrivateCookieJar) -> Response {
|
||||||
|
// Remove admin session cookie
|
||||||
|
let expired_cookie = Cookie::build(("admin_session", ""))
|
||||||
|
.expires(Expiration::DateTime(
|
||||||
|
OffsetDateTime::now_utc() - time::Duration::days(1),
|
||||||
|
))
|
||||||
|
.http_only(true)
|
||||||
|
.secure(true)
|
||||||
|
.path("/")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
let updated_cookies = private_cookies.add(expired_cookie);
|
||||||
|
(updated_cookies, Redirect::to("/")).into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn protected_page(
|
||||||
|
private_cookies: PrivateCookieJar,
|
||||||
|
cookies: CookieJar,
|
||||||
|
headers: HeaderMap,
|
||||||
|
) -> Response {
|
||||||
|
// Check if admin is authenticated
|
||||||
|
if private_cookies.get("admin_session").is_none() {
|
||||||
|
return Redirect::to("/admin/login").into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
let lang = language(&cookies, &headers);
|
||||||
|
rust_i18n::set_locale(lang.to_locale());
|
||||||
|
|
||||||
|
let markup = Page::new(lang).content(html! {
|
||||||
|
h1 { "Protected Admin Area" }
|
||||||
|
p { "Welcome to the admin area! This is a protected route." }
|
||||||
|
p { "Only authenticated administrators can access this page." }
|
||||||
|
|
||||||
|
|
||||||
|
form method="POST" action="/admin/logout" {
|
||||||
|
input type="submit" value="Logout" class="secondary";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
markup.into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn routes() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/admin/login", get(login_page))
|
||||||
|
.route("/admin/login", post(login))
|
||||||
|
.route("/admin/logout", post(logout))
|
||||||
|
.route("/protected", get(protected_page))
|
||||||
|
}
|
58
src/game.rs
58
src/game.rs
@@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
language::language,
|
language::language,
|
||||||
page::{MyMessage, Page},
|
page::{MyMessage, Page},
|
||||||
AppState, Backend, NameUpdateError,
|
AppState, NameUpdateError,
|
||||||
};
|
};
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
@@ -13,26 +13,29 @@ use axum::{
|
|||||||
use axum_extra::extract::{CookieJar, PrivateCookieJar};
|
use axum_extra::extract::{CookieJar, PrivateCookieJar};
|
||||||
use maud::{html, Markup, PreEscaped};
|
use maud::{html, Markup, PreEscaped};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::sync::Arc;
|
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
async fn index(
|
async fn index(
|
||||||
State(backend): State<Arc<Backend>>,
|
State(state): State<AppState>,
|
||||||
cookies: PrivateCookieJar,
|
cookies: PrivateCookieJar,
|
||||||
lang_cookies: CookieJar,
|
lang_cookies: CookieJar,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
retu(backend, cookies, lang_cookies, headers, None).await
|
retu(state, cookies, lang_cookies, headers, None).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn retu(
|
async fn retu(
|
||||||
backend: Arc<Backend>,
|
state: AppState,
|
||||||
cookies: PrivateCookieJar,
|
cookies: PrivateCookieJar,
|
||||||
lang_cookies: CookieJar,
|
lang_cookies: CookieJar,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
message: Option<MyMessage>,
|
message: Option<MyMessage>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
|
let backend = &state.backend;
|
||||||
let (cookies, req) = backend.client_full(cookies, &lang_cookies, &headers).await;
|
let (cookies, req) = backend.client_full(cookies, &lang_cookies, &headers).await;
|
||||||
|
|
||||||
|
// Check if user is admin
|
||||||
|
let is_admin = cookies.get("admin_session").is_some();
|
||||||
let client = req.client;
|
let client = req.client;
|
||||||
rust_i18n::set_locale(&req.lang.to_string());
|
rust_i18n::set_locale(&req.lang.to_string());
|
||||||
|
|
||||||
@@ -99,6 +102,12 @@ async fn retu(
|
|||||||
@if rank.client == client { (PreEscaped("<mark id='ranking'>")) }
|
@if rank.client == client { (PreEscaped("<mark id='ranking'>")) }
|
||||||
(rank.client.get_display_name())
|
(rank.client.get_display_name())
|
||||||
@if rank.client == client { (PreEscaped("</mark>")) }
|
@if rank.client == client { (PreEscaped("</mark>")) }
|
||||||
|
@if is_admin && rank.client.name.is_some() && rank.client.name.as_ref().unwrap() != "***" {
|
||||||
|
form method="POST" action="/game/ban-name" style="display: inline; margin-left: 0.5rem;" {
|
||||||
|
input type="hidden" name="name" value=(rank.client.name.as_ref().unwrap());
|
||||||
|
input type="submit" value="Block" class="secondary" style="font-size: 0.8rem; padding: 0.25rem 0.5rem;";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
span.font-headline.font-lg {
|
span.font-headline.font-lg {
|
||||||
(rank.amount)
|
(rank.amount)
|
||||||
@@ -118,12 +127,13 @@ async fn retu(
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn game(
|
async fn game(
|
||||||
State(backend): State<Arc<Backend>>,
|
State(state): State<AppState>,
|
||||||
cookies: PrivateCookieJar,
|
cookies: PrivateCookieJar,
|
||||||
lang_cookies: CookieJar,
|
lang_cookies: CookieJar,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Path(uuid): Path<String>,
|
Path(uuid): Path<String>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
|
let backend = &state.backend;
|
||||||
let (cookies, req) = backend.client_full(cookies, &lang_cookies, &headers).await;
|
let (cookies, req) = backend.client_full(cookies, &lang_cookies, &headers).await;
|
||||||
let client = req.client;
|
let client = req.client;
|
||||||
rust_i18n::set_locale(req.lang.to_locale());
|
rust_i18n::set_locale(req.lang.to_locale());
|
||||||
@@ -146,7 +156,7 @@ async fn game(
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
retu(backend, cookies, lang_cookies, headers, Some(message)).await
|
retu(state, cookies, lang_cookies, headers, Some(message)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn not_found(cookies: CookieJar, headers: HeaderMap) -> Markup {
|
async fn not_found(cookies: CookieJar, headers: HeaderMap) -> Markup {
|
||||||
@@ -161,13 +171,19 @@ struct NameForm {
|
|||||||
name: String,
|
name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct BanNameForm {
|
||||||
|
name: String,
|
||||||
|
}
|
||||||
|
|
||||||
async fn set_name(
|
async fn set_name(
|
||||||
State(backend): State<Arc<Backend>>,
|
State(state): State<AppState>,
|
||||||
cookies: PrivateCookieJar,
|
cookies: PrivateCookieJar,
|
||||||
lang_cookies: CookieJar,
|
lang_cookies: CookieJar,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Form(form): Form<NameForm>,
|
Form(form): Form<NameForm>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
|
let backend = &state.backend;
|
||||||
let (cookies, req) = backend.client_full(cookies, &lang_cookies, &headers).await;
|
let (cookies, req) = backend.client_full(cookies, &lang_cookies, &headers).await;
|
||||||
let client = req.client;
|
let client = req.client;
|
||||||
rust_i18n::set_locale(req.lang.to_locale());
|
rust_i18n::set_locale(req.lang.to_locale());
|
||||||
@@ -191,12 +207,36 @@ async fn set_name(
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
retu(backend, cookies, lang_cookies, headers, Some(message)).await
|
retu(state, cookies, lang_cookies, headers, Some(message)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ban_name(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
private_cookies: PrivateCookieJar,
|
||||||
|
_lang_cookies: CookieJar,
|
||||||
|
_headers: HeaderMap,
|
||||||
|
Form(form): Form<BanNameForm>,
|
||||||
|
) -> Response {
|
||||||
|
// Check if user is admin
|
||||||
|
if private_cookies.get("admin_session").is_none() {
|
||||||
|
return axum::response::Redirect::to("/game").into_response();
|
||||||
|
}
|
||||||
|
|
||||||
|
let backend = &state.backend;
|
||||||
|
|
||||||
|
// Ban the name
|
||||||
|
let _ = backend.ban_name(&form.name).await;
|
||||||
|
|
||||||
|
// Replace existing instances with asterisks
|
||||||
|
let _ = backend.replace_banned_names_with_asterisks().await;
|
||||||
|
|
||||||
|
axum::response::Redirect::to("/game").into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn routes() -> Router<AppState> {
|
pub(super) fn routes() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/game", get(index))
|
.route("/game", get(index))
|
||||||
.route("/game", post(set_name))
|
.route("/game", post(set_name))
|
||||||
|
.route("/game/ban-name", post(ban_name))
|
||||||
.route("/{*uuid}", get(game))
|
.route("/{*uuid}", get(game))
|
||||||
}
|
}
|
||||||
|
135
src/main.rs
135
src/main.rs
@@ -1,5 +1,10 @@
|
|||||||
use crate::model::client::Client;
|
use crate::model::client::Client;
|
||||||
use axum::{http::HeaderMap, response::Redirect, routing::{get, post}, Router};
|
use axum::{
|
||||||
|
http::HeaderMap,
|
||||||
|
response::Redirect,
|
||||||
|
routing::{get, post},
|
||||||
|
Router,
|
||||||
|
};
|
||||||
use axum_extra::extract::{
|
use axum_extra::extract::{
|
||||||
cookie::{Cookie, Expiration, Key},
|
cookie::{Cookie, Expiration, Key},
|
||||||
CookieJar, PrivateCookieJar,
|
CookieJar, PrivateCookieJar,
|
||||||
@@ -24,6 +29,7 @@ extern crate rust_i18n;
|
|||||||
|
|
||||||
i18n!("locales", fallback = "en");
|
i18n!("locales", fallback = "en");
|
||||||
|
|
||||||
|
mod admin;
|
||||||
mod game;
|
mod game;
|
||||||
mod index;
|
mod index;
|
||||||
pub(crate) mod language;
|
pub(crate) mod language;
|
||||||
@@ -190,7 +196,7 @@ impl Backend {
|
|||||||
if name.len() < 3 {
|
if name.len() < 3 {
|
||||||
return Err(NameUpdateError::TooShort(3, name.len()));
|
return Err(NameUpdateError::TooShort(3, name.len()));
|
||||||
}
|
}
|
||||||
if contains_bad_word(name) {
|
if contains_bad_word(name) || self.is_name_banned(name).await {
|
||||||
return Err(NameUpdateError::ContainsBadWord);
|
return Err(NameUpdateError::ContainsBadWord);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,12 +215,72 @@ impl Backend {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn is_name_banned(&self, name: &str) -> bool {
|
||||||
|
match self {
|
||||||
|
Backend::Sqlite(db) => {
|
||||||
|
let result = sqlx::query!("SELECT name FROM banned_names WHERE name = ?", name)
|
||||||
|
.fetch_optional(db)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
result.is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ban_name(&self, name: &str) -> Result<(), sqlx::Error> {
|
||||||
|
match self {
|
||||||
|
Backend::Sqlite(db) => {
|
||||||
|
sqlx::query!("INSERT OR IGNORE INTO banned_names (name) VALUES (?)", name)
|
||||||
|
.execute(db)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn unban_name(&self, name: &str) -> Result<(), sqlx::Error> {
|
||||||
|
match self {
|
||||||
|
Backend::Sqlite(db) => {
|
||||||
|
sqlx::query!("DELETE FROM banned_names WHERE name = ?", name)
|
||||||
|
.execute(db)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_banned_names(&self) -> Vec<String> {
|
||||||
|
match self {
|
||||||
|
Backend::Sqlite(db) => {
|
||||||
|
let rows = sqlx::query!("SELECT name FROM banned_names ORDER BY banned_at DESC")
|
||||||
|
.fetch_all(db)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
rows.into_iter().map(|row| row.name).collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn replace_banned_names_with_asterisks(&self) -> Result<(), sqlx::Error> {
|
||||||
|
match self {
|
||||||
|
Backend::Sqlite(db) => {
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE client SET name = '***' WHERE name IN (SELECT name FROM banned_names)"
|
||||||
|
)
|
||||||
|
.execute(db)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub(crate) backend: Arc<Backend>,
|
pub(crate) backend: Arc<Backend>,
|
||||||
pub key: Key,
|
pub key: Key,
|
||||||
|
pub admin_password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl axum::extract::FromRef<AppState> for Key {
|
impl axum::extract::FromRef<AppState> for Key {
|
||||||
@@ -232,32 +298,73 @@ impl axum::extract::FromRef<AppState> for Arc<Backend> {
|
|||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
struct Config {
|
struct Config {
|
||||||
key: Vec<u8>,
|
key: Vec<u8>,
|
||||||
|
admin_password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
fn generate() -> Self {
|
fn generate() -> Self {
|
||||||
|
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||||
|
let admin_password: String = thread_rng()
|
||||||
|
.sample_iter(&Alphanumeric)
|
||||||
|
.take(15)
|
||||||
|
.map(char::from)
|
||||||
|
.collect();
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
key: Key::generate().master().to_vec(),
|
key: Key::generate().master().to_vec(),
|
||||||
|
admin_password,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_or_create_key() -> Result<Key, Box<dyn std::error::Error>> {
|
fn load_or_create_config() -> Result<(Key, Config), Box<dyn std::error::Error>> {
|
||||||
let config_path = "config.toml";
|
let config_path = "config.toml";
|
||||||
|
|
||||||
// Try to read existing config
|
// Try to read existing config
|
||||||
if Path::new(config_path).exists() {
|
if Path::new(config_path).exists() {
|
||||||
let content = fs::read_to_string(config_path)?;
|
let content = fs::read_to_string(config_path)?;
|
||||||
let config: Config = toml::from_str(&content)?;
|
|
||||||
return Ok(Key::from(&config.key));
|
// Try to parse as complete config first
|
||||||
|
if let Ok(config) = toml::from_str::<Config>(&content) {
|
||||||
|
let key = Key::from(&config.key);
|
||||||
|
return Ok((key, config));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create new config if file doesn't exist
|
// If that fails, try to parse just the key and generate new admin password
|
||||||
let config = Config::generate();
|
#[derive(Deserialize)]
|
||||||
|
struct PartialConfig {
|
||||||
|
key: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(partial_config) = toml::from_str::<PartialConfig>(&content) {
|
||||||
|
use rand::{distributions::Alphanumeric, thread_rng, Rng};
|
||||||
|
let admin_password: String = thread_rng()
|
||||||
|
.sample_iter(&Alphanumeric)
|
||||||
|
.take(15)
|
||||||
|
.map(char::from)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let config = Config {
|
||||||
|
key: partial_config.key,
|
||||||
|
admin_password,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Write the updated config back
|
||||||
let toml_string = toml::to_string(&config)?;
|
let toml_string = toml::to_string(&config)?;
|
||||||
fs::write(config_path, toml_string)?;
|
fs::write(config_path, toml_string)?;
|
||||||
|
|
||||||
Ok(Key::from(&config.key))
|
let key = Key::from(&config.key);
|
||||||
|
return Ok((key, config));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new config if file doesn't exist or parsing failed
|
||||||
|
let config = Config::generate();
|
||||||
|
let toml_string = toml::to_string(&config)?;
|
||||||
|
fs::write(config_path, toml_string)?;
|
||||||
|
let key = Key::from(&config.key);
|
||||||
|
|
||||||
|
Ok((key, config))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete_personal_data(
|
async fn delete_personal_data(
|
||||||
@@ -275,7 +382,9 @@ async fn delete_personal_data(
|
|||||||
|
|
||||||
// Remove the client_id cookie by setting an expired cookie
|
// Remove the client_id cookie by setting an expired cookie
|
||||||
let expired_cookie = Cookie::build(("client_id", ""))
|
let expired_cookie = Cookie::build(("client_id", ""))
|
||||||
.expires(Expiration::DateTime(OffsetDateTime::now_utc() - time::Duration::days(1)))
|
.expires(Expiration::DateTime(
|
||||||
|
OffsetDateTime::now_utc() - time::Duration::days(1),
|
||||||
|
))
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
.secure(true)
|
.secure(true)
|
||||||
.build();
|
.build();
|
||||||
@@ -299,10 +408,15 @@ async fn main() {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let key = load_or_create_key().unwrap();
|
let (key, config) = load_or_create_config().unwrap();
|
||||||
|
|
||||||
|
// Print admin password for convenience
|
||||||
|
tracing::info!("Admin password: {}", config.admin_password);
|
||||||
|
|
||||||
let state = AppState {
|
let state = AppState {
|
||||||
backend: Arc::new(Backend::Sqlite(db)),
|
backend: Arc::new(Backend::Sqlite(db)),
|
||||||
key,
|
key,
|
||||||
|
admin_password: config.admin_password,
|
||||||
};
|
};
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
@@ -311,6 +425,7 @@ async fn main() {
|
|||||||
.route("/delete-data", post(delete_personal_data))
|
.route("/delete-data", post(delete_personal_data))
|
||||||
.nest_service("/static", ServeDir::new("./static/serve"))
|
.nest_service("/static", ServeDir::new("./static/serve"))
|
||||||
.merge(game::routes())
|
.merge(game::routes())
|
||||||
|
.merge(admin::routes())
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
// run our app with hyper, listening globally on port 3000
|
// run our app with hyper, listening globally on port 3000
|
||||||
|
@@ -37,11 +37,9 @@ impl Backend {
|
|||||||
)
|
)
|
||||||
SELECT rank, name, uuid, amount
|
SELECT rank, name, uuid, amount
|
||||||
FROM ranked_clients
|
FROM ranked_clients
|
||||||
WHERE rank <= (
|
WHERE rank <= COALESCE(
|
||||||
SELECT rank
|
(SELECT rank FROM ranked_clients ORDER BY rank LIMIT 1 OFFSET 9),
|
||||||
FROM ranked_clients
|
(SELECT MAX(rank) FROM ranked_clients)
|
||||||
ORDER BY rank
|
|
||||||
LIMIT 1 OFFSET 9
|
|
||||||
)
|
)
|
||||||
ORDER BY rank, name"
|
ORDER BY rank, name"
|
||||||
)
|
)
|
||||||
|
@@ -139,21 +139,14 @@ ul.iterated > li {
|
|||||||
border-bottom: 2px solid var(--pico-color);
|
border-bottom: 2px solid var(--pico-color);
|
||||||
border-radius: 2% 6% 5% 4% / 1% 1% 2% 4%;
|
border-radius: 2% 6% 5% 4% / 1% 1% 2% 4%;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
border-bottom: 1px solid var(--pico-color);
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate3d(-50%, -50%, 0) scale(1.015) rotate(0.5deg);
|
|
||||||
border-radius: 1% 1% 2% 4% / 2% 6% 5% 4%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ul.iterated > li > * {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1; /* Bring content forward */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
ul.iterated > li.no-border {
|
ul.iterated > li.no-border {
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user