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"
|
name = "stationslauf"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"argon2",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
"axum-login",
|
"axum-login",
|
||||||
|
@ -22,6 +22,7 @@ tower-sessions-sqlx-store-chrono = { version = "0.14", features = ["sqlite"] }
|
|||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
rand = "0.9"
|
rand = "0.9"
|
||||||
|
argon2 = "0.5"
|
||||||
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
@ -51,7 +51,8 @@ CREATE TABLE IF NOT EXISTS rating (
|
|||||||
CREATE TABLE IF NOT EXISTS user (
|
CREATE TABLE IF NOT EXISTS user (
|
||||||
id INTEGER PRIMARY KEY NOT NULL,
|
id INTEGER PRIMARY KEY NOT NULL,
|
||||||
name TEXT NOT NULL UNIQUE,
|
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" (
|
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::{extract::State, routing::get, Router};
|
||||||
use axum_login::login_required;
|
use axum_login::login_required;
|
||||||
use maud::{html, Markup};
|
use maud::{html, Markup};
|
||||||
|
use rand::{
|
||||||
|
distr::{Distribution, Uniform},
|
||||||
|
rng,
|
||||||
|
};
|
||||||
use route::Route;
|
use route::Route;
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@ -10,6 +14,15 @@ use tower_sessions::Session;
|
|||||||
pub(crate) mod route;
|
pub(crate) mod route;
|
||||||
pub(crate) mod station;
|
pub(crate) mod station;
|
||||||
pub(crate) mod team;
|
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 {
|
async fn highscore(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
|
||||||
let routes = Route::all(&db).await;
|
let routes = Route::all(&db).await;
|
||||||
@ -131,6 +144,11 @@ async fn index(session: Session) -> Markup {
|
|||||||
"Highscore"
|
"Highscore"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
li {
|
||||||
|
a role="button" href="/admin/user" {
|
||||||
|
"Admins"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -144,5 +162,6 @@ pub(super) fn routes() -> Router<AppState> {
|
|||||||
.nest("/station", station::routes())
|
.nest("/station", station::routes())
|
||||||
.nest("/route", route::routes())
|
.nest("/route", route::routes())
|
||||||
.nest("/team", team::routes())
|
.nest("/team", team::routes())
|
||||||
|
.nest("/user", user::routes())
|
||||||
.layer(login_required!(Backend, login_url = "/auth/login"))
|
.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::{
|
use crate::{
|
||||||
admin::route::Route,
|
admin::route::Route,
|
||||||
models::rating::{Rating, TeamsAtStationLocation},
|
models::rating::{Rating, TeamsAtStationLocation},
|
||||||
@ -6,11 +6,6 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use chrono::{DateTime, Local, NaiveDateTime, Utc};
|
use chrono::{DateTime, Local, NaiveDateTime, Utc};
|
||||||
use rand::{
|
|
||||||
distr::{Distribution, Uniform},
|
|
||||||
rng,
|
|
||||||
};
|
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{FromRow, SqlitePool};
|
use sqlx::{FromRow, SqlitePool};
|
||||||
|
|
||||||
@ -83,16 +78,8 @@ impl Station {
|
|||||||
.unwrap();
|
.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> {
|
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)
|
sqlx::query!("INSERT INTO station(name, pw) VALUES (?, ?)", name, code)
|
||||||
.execute(db)
|
.execute(db)
|
||||||
.await
|
.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))
|
||||||
|
}
|
32
src/auth.rs
32
src/auth.rs
@ -17,9 +17,10 @@ pub type UserId<Backend> = <<Backend as AuthnBackend>::User as AuthUser>::Id;
|
|||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, FromRow, Debug)]
|
#[derive(Clone, Serialize, Deserialize, FromRow, Debug)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
id: i64,
|
pub(crate) id: i64,
|
||||||
name: String,
|
pub(crate) name: String,
|
||||||
pw: String,
|
pub(crate) pw: String,
|
||||||
|
pub(crate) require_new_password_code: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
@ -67,22 +68,27 @@ impl AuthnBackend for Backend {
|
|||||||
&self,
|
&self,
|
||||||
creds: Self::Credentials,
|
creds: Self::Credentials,
|
||||||
) -> Result<Option<Self::User>, Self::Error> {
|
) -> Result<Option<Self::User>, Self::Error> {
|
||||||
let user: Option<Self::User> =
|
let user: Option<Self::User> = sqlx::query_as(
|
||||||
sqlx::query_as("SELECT id, name, pw FROM user WHERE name = ? ")
|
"SELECT id, name, pw, require_new_password_code FROM user WHERE name = ? ",
|
||||||
.bind(creds.name)
|
)
|
||||||
.fetch_optional(&self.db)
|
.bind(creds.name)
|
||||||
.await?;
|
.fetch_optional(&self.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// We're using password-based authentication--this works by comparing our form
|
// We're using password-based authentication--this works by comparing our form
|
||||||
// input with an argon2 password hash.
|
// 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> {
|
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 =
|
||||||
.bind(user_id)
|
sqlx::query_as("SELECT id, name, pw, require_new_password_code FROM user WHERE id = ?")
|
||||||
.fetch_optional(&self.db)
|
.bind(user_id)
|
||||||
.await?;
|
.fetch_optional(&self.db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
Ok(user)
|
Ok(user)
|
||||||
}
|
}
|
||||||
|
108
src/lib.rs
108
src/lib.rs
@ -17,20 +17,22 @@ macro_rules! testdb {
|
|||||||
i18n!("locales", fallback = "de-AT");
|
i18n!("locales", fallback = "de-AT");
|
||||||
|
|
||||||
use admin::station::Station;
|
use admin::station::Station;
|
||||||
use auth::Backend;
|
use auth::{AuthSession, Backend, User};
|
||||||
use axum::{
|
use axum::{
|
||||||
body::Body,
|
body::Body,
|
||||||
extract::FromRef,
|
extract::{FromRef, State},
|
||||||
response::{IntoResponse, Redirect, Response},
|
response::{IntoResponse, Redirect, Response},
|
||||||
routing::get,
|
routing::{get, post},
|
||||||
Router,
|
Form, Router,
|
||||||
};
|
};
|
||||||
use axum_login::AuthManagerLayerBuilder;
|
use axum_login::AuthManagerLayerBuilder;
|
||||||
|
use maud::{html, Markup};
|
||||||
use partials::page;
|
use partials::page;
|
||||||
|
use serde::Deserialize;
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::net::TcpListener;
|
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;
|
use tower_sessions_sqlx_store_chrono::SqliteStore;
|
||||||
|
|
||||||
pub(crate) mod admin;
|
pub(crate) mod admin;
|
||||||
@ -168,6 +170,100 @@ impl FromRef<AppState> for Arc<SqlitePool> {
|
|||||||
state.db.clone()
|
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 {
|
fn router(db: SqlitePool) -> Router {
|
||||||
let session_store = SqliteStore::new(db.clone());
|
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("/s/{id}/{code}", station::routes()) // TODO: maybe switch to "/"
|
||||||
.nest("/admin", admin::routes())
|
.nest("/admin", admin::routes())
|
||||||
.nest("/auth", auth::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("/pico.css", get(serve_pico_css))
|
||||||
.route("/style.css", get(serve_my_css))
|
.route("/style.css", get(serve_my_css))
|
||||||
.route("/leaflet.css", get(serve_leaflet_css))
|
.route("/leaflet.css", get(serve_leaflet_css))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user