311 lines
8.6 KiB
Rust
311 lines
8.6 KiB
Rust
#[macro_use]
|
|
extern crate rust_i18n;
|
|
|
|
#[cfg(test)]
|
|
#[macro_export]
|
|
macro_rules! testdb {
|
|
() => {{
|
|
let pool = SqlitePool::connect(":memory:").await.unwrap();
|
|
sqlx::query_file!("./migration.sql")
|
|
.execute(&pool)
|
|
.await
|
|
.unwrap();
|
|
pool
|
|
}};
|
|
}
|
|
|
|
i18n!("locales", fallback = "de-AT");
|
|
|
|
use admin::station::{print::station_pdf, Station};
|
|
use auth::{AuthSession, Backend, User};
|
|
use axum::{
|
|
body::Body,
|
|
extract::{FromRef, State},
|
|
response::{IntoResponse, Redirect, Response},
|
|
routing::{get, post},
|
|
Form, Router,
|
|
};
|
|
use axum_login::AuthManagerLayerBuilder;
|
|
use maud::{html, Markup};
|
|
use partials::page;
|
|
use serde::Deserialize;
|
|
use sqlx::SqlitePool;
|
|
use std::{env, sync::Arc};
|
|
use tokio::net::TcpListener;
|
|
use tower_sessions::{cookie::time::Duration, Expiry, Session, SessionManagerLayer};
|
|
use tower_sessions_sqlx_store_chrono::SqliteStore;
|
|
|
|
pub(crate) mod admin;
|
|
mod auth;
|
|
pub(crate) mod models;
|
|
mod partials;
|
|
pub(crate) mod station;
|
|
|
|
pub(crate) fn test_version() -> bool {
|
|
env::var("TEST_VERSION").is_ok()
|
|
}
|
|
pub fn url() -> String {
|
|
env::var("URL").unwrap()
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! err {
|
|
($session:expr, $fmt:expr $(, $arg:expr)*) => {
|
|
$session
|
|
.insert(
|
|
"err",
|
|
&format!($fmt $(, $arg)*)
|
|
)
|
|
.await
|
|
.unwrap()
|
|
};
|
|
}
|
|
#[macro_export]
|
|
macro_rules! succ {
|
|
($session:expr, $fmt:expr $(, $arg:expr)*) => {
|
|
$session
|
|
.insert(
|
|
"succ",
|
|
&format!($fmt $(, $arg)*)
|
|
)
|
|
.await
|
|
.unwrap()
|
|
};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! suc {
|
|
($session:expr, $message:expr) => {
|
|
$session.insert("succ", &$message).await.unwrap()
|
|
};
|
|
}
|
|
#[macro_export]
|
|
macro_rules! er {
|
|
($session:expr, $message:expr) => {
|
|
$session.insert("err", &$message).await.unwrap()
|
|
};
|
|
}
|
|
|
|
const PICO_CSS: &str = include_str!("../assets/pico.min.css");
|
|
const MY_CSS: &str = include_str!("../assets/style.css");
|
|
const LEAFLET_CSS: &str = include_str!("../assets/leaflet.css");
|
|
const LEAFLET_JS: &str = include_str!("../assets/leaflet.js");
|
|
const MARKER_PNG: &[u8] = include_bytes!("../assets/marker-icon.png");
|
|
const LOGO_HOR: &[u8] = include_bytes!("../assets/logo-horizontal.svg");
|
|
const LOGO_VER: &[u8] = include_bytes!("../assets/logo-vertical.svg");
|
|
const LOGO_HOR_INV: &[u8] = include_bytes!("../assets/logo-horizontal-inverted.svg");
|
|
const LOGO_VER_INV: &[u8] = include_bytes!("../assets/logo-vertical-inverted.svg");
|
|
|
|
async fn serve_pico_css() -> Response<Body> {
|
|
Response::builder()
|
|
.header("Content-Type", "text/css")
|
|
.body(Body::from(PICO_CSS))
|
|
.unwrap()
|
|
}
|
|
|
|
async fn serve_my_css() -> Response<Body> {
|
|
Response::builder()
|
|
.header("Content-Type", "text/css")
|
|
.body(Body::from(MY_CSS))
|
|
.unwrap()
|
|
}
|
|
|
|
async fn serve_leaflet_css() -> Response<Body> {
|
|
Response::builder()
|
|
.header("Content-Type", "text/css")
|
|
.body(Body::from(LEAFLET_CSS))
|
|
.unwrap()
|
|
}
|
|
|
|
async fn serve_leaflet_js() -> Response<Body> {
|
|
Response::builder()
|
|
.header("Content-Type", "application/javascript")
|
|
.body(Body::from(LEAFLET_JS))
|
|
.unwrap()
|
|
}
|
|
|
|
async fn serve_marker_png() -> Response<Body> {
|
|
Response::builder()
|
|
.header("Content-Type", "image/png")
|
|
.body(Body::from(MARKER_PNG))
|
|
.unwrap()
|
|
}
|
|
async fn logo_hor() -> Response<Body> {
|
|
Response::builder()
|
|
.header("Content-Type", "image/svg+xml")
|
|
.body(Body::from(LOGO_HOR))
|
|
.unwrap()
|
|
}
|
|
async fn logo_ver() -> Response<Body> {
|
|
Response::builder()
|
|
.header("Content-Type", "image/svg+xml")
|
|
.body(Body::from(LOGO_VER))
|
|
.unwrap()
|
|
}
|
|
async fn logo_hor_inv() -> Response<Body> {
|
|
Response::builder()
|
|
.header("Content-Type", "image/svg+xml")
|
|
.body(Body::from(LOGO_HOR_INV))
|
|
.unwrap()
|
|
}
|
|
async fn logo_ver_inv() -> Response<Body> {
|
|
Response::builder()
|
|
.header("Content-Type", "image/svg+xml")
|
|
.body(Body::from(LOGO_VER_INV))
|
|
.unwrap()
|
|
}
|
|
|
|
async fn redirect() -> impl IntoResponse {
|
|
Redirect::to("/admin")
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct AppState {
|
|
db: Arc<SqlitePool>,
|
|
}
|
|
|
|
impl FromRef<AppState> for Arc<SqlitePool> {
|
|
fn from_ref(state: &AppState) -> Self {
|
|
state.db.clone()
|
|
}
|
|
}
|
|
async fn set_pw(
|
|
State(db): State<Arc<SqlitePool>>,
|
|
session: Session,
|
|
axum::extract::Path((id, code)): axum::extract::Path<(i64, String)>,
|
|
) -> Result<Markup, impl IntoResponse> {
|
|
let Some(user) = User::find_by_id(&db, id).await else {
|
|
er!(session, t!("user_id_nonexisting", id = id));
|
|
return Err(Redirect::to("/"));
|
|
};
|
|
|
|
let Some(correct_code) = user.require_new_password_code else {
|
|
er!(
|
|
session,
|
|
t!(
|
|
"cant_update_pw_if_already_existing_for_user",
|
|
user = user.name
|
|
)
|
|
);
|
|
return Err(Redirect::to("/"));
|
|
};
|
|
|
|
if correct_code != code {
|
|
er!(
|
|
session,
|
|
t!("cant_update_pw_with_wrong_code", user = user.name)
|
|
);
|
|
return Err(Redirect::to("/"));
|
|
}
|
|
|
|
let content = html! {
|
|
h1 {
|
|
(t!("new_pw_for_user", user=user.name))
|
|
}
|
|
form action=(format!("/user/{}/set-pw", user.id)) method="post" {
|
|
input type="hidden" name="code" value=(code);
|
|
label {
|
|
(t!("pw"))
|
|
input type="password" name="password";
|
|
}
|
|
input type="submit" value=(t!("do_login"));
|
|
}
|
|
};
|
|
|
|
Ok(page(content, session, false).await)
|
|
}
|
|
#[derive(Deserialize)]
|
|
struct NewPwForm {
|
|
code: String,
|
|
password: String,
|
|
}
|
|
async fn set_concrete_pw(
|
|
mut auth_session: AuthSession,
|
|
State(db): State<Arc<SqlitePool>>,
|
|
session: Session,
|
|
axum::extract::Path(id): axum::extract::Path<i64>,
|
|
Form(form): Form<NewPwForm>,
|
|
) -> impl IntoResponse {
|
|
let Some(user) = User::find_by_id(&db, id).await else {
|
|
er!(session, t!("user_id_nonexisting", id = id));
|
|
return Redirect::to("/").into_response();
|
|
};
|
|
|
|
let Some(correct_code) = &user.require_new_password_code else {
|
|
er!(
|
|
session,
|
|
t!(
|
|
"cant_update_pw_if_already_existing_for_user",
|
|
user = user.name
|
|
)
|
|
);
|
|
return Redirect::to("/").into_response();
|
|
};
|
|
|
|
if correct_code != &form.code {
|
|
er!(
|
|
session,
|
|
t!("cant_update_pw_with_wrong_code", user = user.name)
|
|
);
|
|
return Redirect::to("/").into_response();
|
|
}
|
|
|
|
match user.update_pw(&db, &form.password).await {
|
|
Ok(()) => {
|
|
let user = User::find_by_id(&db, id).await.unwrap();
|
|
auth_session.login(&user).await.unwrap();
|
|
suc!(session, t!("pw_set"));
|
|
Redirect::to("/admin").into_response()
|
|
}
|
|
Err(e) => {
|
|
err!(session, "{e}");
|
|
Redirect::to("/admin").into_response()
|
|
}
|
|
}
|
|
}
|
|
|
|
fn router(db: SqlitePool) -> Router {
|
|
let session_store = SqliteStore::new(db.clone());
|
|
|
|
let session_layer = SessionManagerLayer::new(session_store)
|
|
.with_secure(false)
|
|
.with_expiry(Expiry::OnInactivity(Duration::weeks(2)));
|
|
|
|
let backend = Backend::new(db.clone());
|
|
let auth_layer = AuthManagerLayerBuilder::new(backend, session_layer).build();
|
|
|
|
let state = AppState { db: Arc::new(db) };
|
|
|
|
Router::new()
|
|
.route("/", get(redirect))
|
|
.nest("/s/{id}/{code}", station::routes()) // TODO: maybe switch to "/"
|
|
.nest("/admin", admin::routes())
|
|
.nest("/auth", auth::routes())
|
|
.route("/user/{id}/set-pw/{code}", get(set_pw))
|
|
.route("/user/{id}/set-pw", post(set_concrete_pw))
|
|
.route("/pico.css", get(serve_pico_css))
|
|
.route("/style.css", get(serve_my_css))
|
|
.route("/leaflet.css", get(serve_leaflet_css))
|
|
.route("/leaflet.js", get(serve_leaflet_js))
|
|
.route("/marker.png", get(serve_marker_png))
|
|
.route("/logo-hor.svg", get(logo_hor))
|
|
.route("/logo-ver.svg", get(logo_ver))
|
|
.route("/logo-hor-inv.svg", get(logo_hor_inv))
|
|
.route("/logo-ver-inv.svg", get(logo_ver_inv))
|
|
.with_state(state)
|
|
.layer(auth_layer)
|
|
}
|
|
|
|
/// Starts the main application.
|
|
pub async fn start(listener: TcpListener, db: SqlitePool) {
|
|
let app = router(db.clone());
|
|
|
|
tokio::spawn(async move {
|
|
// Kick-off typst compilation, to reduce wait time for 1st load
|
|
let stations = Station::all(&db).await;
|
|
station_pdf(stations).await;
|
|
});
|
|
|
|
axum::serve(listener, app).await.unwrap();
|
|
}
|