user crud; Fixes #41
This commit is contained in:
parent
4760e9276a
commit
6b96b047e0
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -2216,6 +2216,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
name = "stationslauf"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"axum-login",
|
||||
|
@ -22,6 +22,7 @@ tower-sessions-sqlx-store-chrono = { version = "0.14", features = ["sqlite"] }
|
||||
tracing-subscriber = "0.3"
|
||||
futures = "0.3"
|
||||
rand = "0.9"
|
||||
argon2 = "0.5"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
|
@ -51,7 +51,8 @@ CREATE TABLE IF NOT EXISTS rating (
|
||||
CREATE TABLE IF NOT EXISTS user (
|
||||
id INTEGER PRIMARY KEY NOT NULL,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
pw TEXT NOT NULL
|
||||
pw TEXT NOT NULL,
|
||||
require_new_password_code TEXT
|
||||
);
|
||||
|
||||
create table if not exists "tower_sessions" (
|
||||
|
@ -2,6 +2,10 @@ use crate::{auth::Backend, models::rating::Rating, page, AppState};
|
||||
use axum::{extract::State, routing::get, Router};
|
||||
use axum_login::login_required;
|
||||
use maud::{html, Markup};
|
||||
use rand::{
|
||||
distr::{Distribution, Uniform},
|
||||
rng,
|
||||
};
|
||||
use route::Route;
|
||||
use sqlx::SqlitePool;
|
||||
use std::sync::Arc;
|
||||
@ -10,6 +14,15 @@ use tower_sessions::Session;
|
||||
pub(crate) mod route;
|
||||
pub(crate) mod station;
|
||||
pub(crate) mod team;
|
||||
pub(crate) mod user;
|
||||
|
||||
fn generate_random_alphanumeric(length: usize) -> String {
|
||||
let mut rng = rng();
|
||||
let chars: Vec<char> = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".chars().collect();
|
||||
let dist = Uniform::new(0, chars.len()).unwrap();
|
||||
|
||||
(0..length).map(|_| chars[dist.sample(&mut rng)]).collect()
|
||||
}
|
||||
|
||||
async fn highscore(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
|
||||
let routes = Route::all(&db).await;
|
||||
@ -131,6 +144,11 @@ async fn index(session: Session) -> Markup {
|
||||
"Highscore"
|
||||
}
|
||||
}
|
||||
li {
|
||||
a role="button" href="/admin/user" {
|
||||
"Admins"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -144,5 +162,6 @@ pub(super) fn routes() -> Router<AppState> {
|
||||
.nest("/station", station::routes())
|
||||
.nest("/route", route::routes())
|
||||
.nest("/team", team::routes())
|
||||
.nest("/user", user::routes())
|
||||
.layer(login_required!(Backend, login_url = "/auth/login"))
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use super::team::Team;
|
||||
use super::{generate_random_alphanumeric, team::Team};
|
||||
use crate::{
|
||||
admin::route::Route,
|
||||
models::rating::{Rating, TeamsAtStationLocation},
|
||||
@ -6,11 +6,6 @@ use crate::{
|
||||
};
|
||||
use axum::Router;
|
||||
use chrono::{DateTime, Local, NaiveDateTime, Utc};
|
||||
use rand::{
|
||||
distr::{Distribution, Uniform},
|
||||
rng,
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
@ -83,16 +78,8 @@ impl Station {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn generate_random_alphanumeric(length: usize) -> String {
|
||||
let mut rng = rng();
|
||||
let chars: Vec<char> = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".chars().collect();
|
||||
let dist = Uniform::new(0, chars.len()).unwrap();
|
||||
|
||||
(0..length).map(|_| chars[dist.sample(&mut rng)]).collect()
|
||||
}
|
||||
|
||||
pub(crate) async fn create(db: &SqlitePool, name: &str) -> Result<(), String> {
|
||||
let code = Self::generate_random_alphanumeric(8);
|
||||
let code = generate_random_alphanumeric(8);
|
||||
sqlx::query!("INSERT INTO station(name, pw) VALUES (?, ?)", name, code)
|
||||
.execute(db)
|
||||
.await
|
||||
|
95
src/admin/user/mod.rs
Normal file
95
src/admin/user/mod.rs
Normal file
@ -0,0 +1,95 @@
|
||||
use super::generate_random_alphanumeric;
|
||||
use crate::{auth::User, AppState};
|
||||
use argon2::password_hash::rand_core::OsRng;
|
||||
use argon2::{password_hash::SaltString, Argon2, PasswordHasher};
|
||||
use axum::Router;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
mod web;
|
||||
|
||||
impl User {
|
||||
pub(crate) async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query_as::<_, Self>(
|
||||
"SELECT id, name, pw, require_new_password_code FROM user ORDER BY name;",
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"SELECT id, name, pw, require_new_password_code FROM user WHERE id = ?",
|
||||
id
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
async fn create(db: &SqlitePool, name: &str) -> Result<i64, String> {
|
||||
let code = generate_random_alphanumeric(8);
|
||||
let result = sqlx::query!(
|
||||
"INSERT INTO user(name, pw, require_new_password_code) VALUES (?, 'pw-to-be-defined', ?) RETURNING id",
|
||||
name,
|
||||
code
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(result.id)
|
||||
}
|
||||
|
||||
async fn update_name(&self, db: &SqlitePool, name: &str) {
|
||||
sqlx::query!("UPDATE user SET name = ? WHERE id = ?", name, self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn new_pw(&self, db: &SqlitePool) {
|
||||
let code = generate_random_alphanumeric(8);
|
||||
sqlx::query!(
|
||||
"UPDATE user SET pw='pw-to-be-redefined', require_new_password_code=? WHERE id = ?",
|
||||
code,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub(crate) async fn update_pw(&self, db: &SqlitePool, pw: &str) -> Result<(), String> {
|
||||
if self.require_new_password_code.is_some() {
|
||||
let argon2 = Argon2::default();
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
let password_hash = argon2
|
||||
.hash_password(pw.as_bytes(), &salt)
|
||||
.unwrap()
|
||||
.to_string();
|
||||
sqlx::query!(
|
||||
"UPDATE user SET pw = ?, require_new_password_code=NULL WHERE id = ?",
|
||||
password_hash,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
return Ok(());
|
||||
}
|
||||
Err("User hat schon ein Passwort...".into())
|
||||
}
|
||||
|
||||
async fn delete(&self, db: &SqlitePool) -> Result<(), String> {
|
||||
sqlx::query!("DELETE FROM user WHERE id = ?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn routes() -> Router<AppState> {
|
||||
web::routes()
|
||||
}
|
238
src/admin/user/web.rs
Normal file
238
src/admin/user/web.rs
Normal file
@ -0,0 +1,238 @@
|
||||
use crate::{auth::User, err, partials::page, succ, AppState};
|
||||
use axum::{
|
||||
extract::State,
|
||||
response::{IntoResponse, Redirect},
|
||||
routing::{get, post},
|
||||
Form, Router,
|
||||
};
|
||||
use maud::{html, Markup};
|
||||
use serde::Deserialize;
|
||||
use sqlx::SqlitePool;
|
||||
use std::sync::Arc;
|
||||
use tower_sessions::Session;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CreateForm {
|
||||
name: String,
|
||||
}
|
||||
|
||||
async fn create(
|
||||
State(db): State<Arc<SqlitePool>>,
|
||||
session: Session,
|
||||
Form(form): Form<CreateForm>,
|
||||
) -> impl IntoResponse {
|
||||
let id = match User::create(&db, &form.name).await {
|
||||
Ok(id) => {
|
||||
succ!(session, "User '{}' erfolgreich erstellt!", form.name);
|
||||
id
|
||||
}
|
||||
Err(e) => {
|
||||
err!(
|
||||
session,
|
||||
"User '{}' konnte _NICHT_ erstellt werden: {e:?}",
|
||||
e
|
||||
);
|
||||
|
||||
return Redirect::to("/admin/user");
|
||||
}
|
||||
};
|
||||
|
||||
Redirect::to(&format!("/admin/user/{}", id))
|
||||
}
|
||||
|
||||
async fn delete(
|
||||
State(db): State<Arc<SqlitePool>>,
|
||||
session: Session,
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(user) = User::find_by_id(&db, id).await else {
|
||||
err!(
|
||||
session,
|
||||
"User mit ID {id} konnte nicht gelöscht werden, da sie nicht existiert"
|
||||
);
|
||||
|
||||
return Redirect::to("/admin/user");
|
||||
};
|
||||
|
||||
match user.delete(&db).await {
|
||||
Ok(()) => succ!(session, "User '{}' erfolgreich gelöscht!", user.name),
|
||||
Err(e) => err!(
|
||||
session,
|
||||
"User '{}' kann nicht gelöscht werden, da er/sie bereits verwendet wird. ({e})",
|
||||
user.name
|
||||
),
|
||||
}
|
||||
|
||||
Redirect::to("/admin/user")
|
||||
}
|
||||
|
||||
async fn view(
|
||||
State(db): State<Arc<SqlitePool>>,
|
||||
session: Session,
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
) -> Result<Markup, impl IntoResponse> {
|
||||
let Some(user) = User::find_by_id(&db, id).await else {
|
||||
err!(
|
||||
session,
|
||||
"User mit ID {id} konnte nicht geöffnet werden, da sie nicht existiert"
|
||||
);
|
||||
|
||||
return Err(Redirect::to("/admin/user"));
|
||||
};
|
||||
|
||||
// maybe switch to maud-display impl of team
|
||||
let content = html! {
|
||||
h1 {
|
||||
a href="/admin/user" { "↩️" }
|
||||
"User " (user.name)
|
||||
}
|
||||
article {
|
||||
details {
|
||||
summary { "Name bearbeiten ✏️" }
|
||||
form action=(format!("/admin/user/{}/name", user.id)) method="post" {
|
||||
input type="text" name="name" value=(user.name) required;
|
||||
input type="submit" value="Speichern";
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
table {
|
||||
tbody {
|
||||
tr {
|
||||
th scope="row" { "Name" };
|
||||
td {
|
||||
(user.name)
|
||||
}
|
||||
}
|
||||
tr {
|
||||
th scope="row" { "Passwort" };
|
||||
td {
|
||||
@if let Some(code) = user.require_new_password_code {
|
||||
a href=(format!("/user/{}/set-pw/{}", user.id, code)){
|
||||
"Login-Link"
|
||||
}
|
||||
} @else {
|
||||
a href=(format!("/admin/user/{}/new-pw", user.id))
|
||||
onclick="return confirm('Bist du sicher, dass du einen neuen Passwort-Link generieren willst? Mit dem alten Passwort kann man sich dann nicht mehr einloggen.');" {
|
||||
"Passwort vergessen: neuen Loginlink generieren"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(page(content, session, true).await)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct UpdateNameForm {
|
||||
name: String,
|
||||
}
|
||||
async fn update_name(
|
||||
State(db): State<Arc<SqlitePool>>,
|
||||
session: Session,
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
Form(form): Form<UpdateNameForm>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(user) = User::find_by_id(&db, id).await else {
|
||||
err!(
|
||||
session,
|
||||
"User mit ID {id} konnte nicht bearbeitet werden, da sie nicht existiert"
|
||||
);
|
||||
|
||||
return Redirect::to("/admin/user");
|
||||
};
|
||||
|
||||
user.update_name(&db, &form.name).await;
|
||||
|
||||
succ!(
|
||||
session,
|
||||
"User '{}' heißt ab sofort '{}'.",
|
||||
user.name,
|
||||
form.name
|
||||
);
|
||||
|
||||
Redirect::to(&format!("/admin/user/{id}"))
|
||||
}
|
||||
|
||||
async fn new_pw(
|
||||
State(db): State<Arc<SqlitePool>>,
|
||||
session: Session,
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(user) = User::find_by_id(&db, id).await else {
|
||||
err!(
|
||||
session,
|
||||
"User mit ID {id} konnte nicht bearbeitet werden, da sie nicht existiert"
|
||||
);
|
||||
|
||||
return Redirect::to("/admin/user");
|
||||
};
|
||||
|
||||
user.new_pw(&db).await;
|
||||
|
||||
succ!(
|
||||
session,
|
||||
"Neuer Loginlink für User '{}' wurde generiert.",
|
||||
user.name
|
||||
);
|
||||
|
||||
Redirect::to(&format!("/admin/user/{id}"))
|
||||
}
|
||||
|
||||
async fn index(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
|
||||
let users = User::all(&db).await;
|
||||
|
||||
let content = html! {
|
||||
h1 {
|
||||
a href="/admin" { "↩️" }
|
||||
"User"
|
||||
}
|
||||
article {
|
||||
em { "Admins " }
|
||||
"sind Menschen, die "
|
||||
a href="/admin/station" { "Stationen" }
|
||||
", "
|
||||
a href="/admin/route" { "Routen" }
|
||||
" und "
|
||||
a href="/admin/team" { "Teams" }
|
||||
" bearbeiten können. Zusätzlich sehen sie das "
|
||||
a href="/admin/highscore" { "Ergebnis" }
|
||||
"."
|
||||
}
|
||||
article {
|
||||
h2 { "Neuer User" }
|
||||
form action="/admin/user" method="post" {
|
||||
fieldset role="group" {
|
||||
input type="text" name="name" placeholder="Name" required;
|
||||
input type="submit" value="Neuer User";
|
||||
}
|
||||
}
|
||||
}
|
||||
ul {
|
||||
@for user in &users {
|
||||
li {
|
||||
a href=(format!("/admin/user/{}", user.id)){
|
||||
(user.name)
|
||||
}
|
||||
a href=(format!("/admin/user/{}/delete", user.id))
|
||||
onclick="return confirm('Bist du sicher, dass der User gelöscht werden soll? Das kann _NICHT_ mehr rückgängig gemacht werden.');" {
|
||||
"🗑️"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
page(content, session, false).await
|
||||
}
|
||||
|
||||
pub(super) fn routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(index))
|
||||
.route("/", post(create))
|
||||
.route("/{id}", get(view))
|
||||
.route("/{id}/delete", get(delete))
|
||||
.route("/{id}/new-pw", get(new_pw))
|
||||
.route("/{id}/name", post(update_name))
|
||||
}
|
20
src/auth.rs
20
src/auth.rs
@ -17,9 +17,10 @@ pub type UserId<Backend> = <<Backend as AuthnBackend>::User as AuthUser>::Id;
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, FromRow, Debug)]
|
||||
pub struct User {
|
||||
id: i64,
|
||||
name: String,
|
||||
pw: String,
|
||||
pub(crate) id: i64,
|
||||
pub(crate) name: String,
|
||||
pub(crate) pw: String,
|
||||
pub(crate) require_new_password_code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
@ -67,19 +68,24 @@ impl AuthnBackend for Backend {
|
||||
&self,
|
||||
creds: Self::Credentials,
|
||||
) -> Result<Option<Self::User>, Self::Error> {
|
||||
let user: Option<Self::User> =
|
||||
sqlx::query_as("SELECT id, name, pw FROM user WHERE name = ? ")
|
||||
let user: Option<Self::User> = sqlx::query_as(
|
||||
"SELECT id, name, pw, require_new_password_code FROM user WHERE name = ? ",
|
||||
)
|
||||
.bind(creds.name)
|
||||
.fetch_optional(&self.db)
|
||||
.await?;
|
||||
|
||||
// We're using password-based authentication--this works by comparing our form
|
||||
// input with an argon2 password hash.
|
||||
Ok(user.filter(|user| verify_password(creds.password, &user.pw).is_ok()))
|
||||
Ok(user.filter(|user| {
|
||||
verify_password(creds.password, &user.pw).is_ok()
|
||||
&& user.require_new_password_code.is_none()
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_user(&self, user_id: &UserId<Self>) -> Result<Option<Self::User>, Self::Error> {
|
||||
let user = sqlx::query_as("SELECT id, name, pw FROM user WHERE id = ?")
|
||||
let user =
|
||||
sqlx::query_as("SELECT id, name, pw, require_new_password_code FROM user WHERE id = ?")
|
||||
.bind(user_id)
|
||||
.fetch_optional(&self.db)
|
||||
.await?;
|
||||
|
108
src/lib.rs
108
src/lib.rs
@ -17,20 +17,22 @@ macro_rules! testdb {
|
||||
i18n!("locales", fallback = "de-AT");
|
||||
|
||||
use admin::station::Station;
|
||||
use auth::Backend;
|
||||
use auth::{AuthSession, Backend, User};
|
||||
use axum::{
|
||||
body::Body,
|
||||
extract::FromRef,
|
||||
extract::{FromRef, State},
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
routing::get,
|
||||
Router,
|
||||
routing::{get, post},
|
||||
Form, Router,
|
||||
};
|
||||
use axum_login::AuthManagerLayerBuilder;
|
||||
use maud::{html, Markup};
|
||||
use partials::page;
|
||||
use serde::Deserialize;
|
||||
use sqlx::SqlitePool;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpListener;
|
||||
use tower_sessions::{cookie::time::Duration, Expiry, SessionManagerLayer};
|
||||
use tower_sessions::{cookie::time::Duration, Expiry, Session, SessionManagerLayer};
|
||||
use tower_sessions_sqlx_store_chrono::SqliteStore;
|
||||
|
||||
pub(crate) mod admin;
|
||||
@ -168,6 +170,100 @@ impl FromRef<AppState> for Arc<SqlitePool> {
|
||||
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 {
|
||||
err!(session, "User mit ID {id} gibt's ned");
|
||||
return Err(Redirect::to("/"));
|
||||
};
|
||||
|
||||
let Some(correct_code) = user.require_new_password_code else {
|
||||
err!(
|
||||
session,
|
||||
"User {} hat bereits ein Passwort. Du kannst kein neues setzen.",
|
||||
user.name
|
||||
);
|
||||
return Err(Redirect::to("/"));
|
||||
};
|
||||
|
||||
if correct_code != code {
|
||||
err!(
|
||||
session,
|
||||
"Falscher Code zum Passwort setzen für User {}.",
|
||||
user.name
|
||||
);
|
||||
return Err(Redirect::to("/"));
|
||||
}
|
||||
|
||||
let content = html! {
|
||||
h1 {
|
||||
"Neues Passwort für "
|
||||
(user.name)
|
||||
" setzen"
|
||||
}
|
||||
form action=(format!("/user/{}/set-pw", user.id)) method="post" {
|
||||
input type="hidden" name="code" value=(code);
|
||||
label {
|
||||
"Passwort"
|
||||
input type="password" name="password";
|
||||
}
|
||||
input type="submit" value="Einloggen";
|
||||
}
|
||||
};
|
||||
|
||||
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 {
|
||||
err!(session, "User mit ID {id} gibt's ned");
|
||||
return Redirect::to("/").into_response();
|
||||
};
|
||||
|
||||
let Some(correct_code) = &user.require_new_password_code else {
|
||||
err!(
|
||||
session,
|
||||
"User {} hat bereits ein Passwort. Du kannst kein neues setzen.",
|
||||
user.name
|
||||
);
|
||||
return Redirect::to("/").into_response();
|
||||
};
|
||||
|
||||
if correct_code != &form.code {
|
||||
err!(
|
||||
session,
|
||||
"Falscher Code zum Passwort setzen für 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();
|
||||
succ!(session, "Passwort erfolgreich gesetzt");
|
||||
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());
|
||||
@ -186,6 +282,8 @@ fn router(db: SqlitePool) -> Router {
|
||||
.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))
|
||||
|
Loading…
x
Reference in New Issue
Block a user