From 7a5233e3859f641f8fca0e73121f0ce9e415aa49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcel=20M=C3=BCller?= Date: Sun, 25 Jan 2026 18:03:39 +0100 Subject: [PATCH] Add working password update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Marcel Müller --- Cargo.lock | 66 ++++++++++++++++++- flake.nix | 3 + nixie-server/.env | 2 +- nixie-server/.gitignore | 3 + nixie-server/Cargo.toml | 5 +- .../20260123170914_create_api_keys.up.sql | 2 +- nixie-server/package-lock.json | 18 +++++ nixie-server/package.json | 5 ++ nixie-server/public/.gitkeep | 0 nixie-server/src/main.rs | 37 +++++++---- nixie-server/src/settings/mod.rs | 54 +++++++++++++-- nixie-server/src/users/login.rs | 33 ++++++---- nixie-server/src/users/register.rs | 2 +- nixie-server/templates/base.css | 1 + .../{base.tera.html => base.html.tera} | 12 ++-- .../{index.tera.html => index.html.tera} | 4 +- .../{inputs.tera.html => inputs.html.tera} | 0 ...ror.tera.html => internal_error.html.tera} | 4 +- ...rotected.tera.html => protected.html.tera} | 2 +- ...{api_keys.tera.html => api_keys.html.tera} | 6 +- ...rd.tera.html => change_password.html.tera} | 6 +- .../{index.tera.html => index.html.tera} | 6 +- .../{sidebar.tera.html => sidebar.html.tera} | 0 .../{login.tera.html => login.html.tera} | 6 +- ...{register.tera.html => register.html.tera} | 4 +- 25 files changed, 221 insertions(+), 60 deletions(-) create mode 100644 nixie-server/package-lock.json create mode 100644 nixie-server/package.json create mode 100644 nixie-server/public/.gitkeep create mode 100644 nixie-server/templates/base.css rename nixie-server/templates/{base.tera.html => base.html.tera} (80%) rename nixie-server/templates/{index.tera.html => index.html.tera} (60%) rename nixie-server/templates/{inputs.tera.html => inputs.html.tera} (100%) rename nixie-server/templates/{internal_error.tera.html => internal_error.html.tera} (88%) rename nixie-server/templates/{protected.tera.html => protected.html.tera} (81%) rename nixie-server/templates/settings/{api_keys.tera.html => api_keys.html.tera} (96%) rename nixie-server/templates/settings/{change_password.tera.html => change_password.html.tera} (88%) rename nixie-server/templates/settings/{index.tera.html => index.html.tera} (76%) rename nixie-server/templates/settings/{sidebar.tera.html => sidebar.html.tera} (100%) rename nixie-server/templates/users/{login.tera.html => login.html.tera} (87%) rename nixie-server/templates/users/{register.tera.html => register.html.tera} (93%) diff --git a/Cargo.lock b/Cargo.lock index 9269a68..db11b3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -943,6 +943,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + [[package]] name = "httparse" version = "1.10.1" @@ -1338,6 +1344,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "mio" version = "1.1.1" @@ -1405,6 +1421,7 @@ dependencies = [ "thiserror 2.0.17", "time", "tokio", + "tower-http", "tower-livereload", "tower-sessions", "tower-sessions-sqlx-store", @@ -1432,9 +1449,9 @@ dependencies = [ [[package]] name = "notify-debouncer-full" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375bd3a138be7bfeff3480e4a623df4cbfb55b79df617c055cd810ba466fa078" +checksum = "c02b49179cfebc9932238d04d6079912d26de0379328872846118a0fa0dbb302" dependencies = [ "file-id", "log", @@ -2559,6 +2576,19 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "tower" version = "0.5.2" @@ -2591,6 +2621,32 @@ dependencies = [ "tower-service", ] +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -2756,6 +2812,12 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + [[package]] name = "unicode-bidi" version = "0.3.18" diff --git a/flake.nix b/flake.nix index 3f4e765..468103e 100644 --- a/flake.nix +++ b/flake.nix @@ -94,6 +94,9 @@ pkgs.cargo-nextest pkgs.sqlite pkgs.sqlx-cli + + pkgs.nodejs_24 + pkgs.tailwindcss_4 ]; }; } diff --git a/nixie-server/.env b/nixie-server/.env index 0b9c34e..34fbb21 100644 --- a/nixie-server/.env +++ b/nixie-server/.env @@ -1 +1 @@ -DATABASE_URL=sqlite://database.db +DATABASE_URL=sqlite:database.db diff --git a/nixie-server/.gitignore b/nixie-server/.gitignore index c0d44a5..4a5ae6b 100644 --- a/nixie-server/.gitignore +++ b/nixie-server/.gitignore @@ -1 +1,4 @@ database.db* + +node_modules +public/ diff --git a/nixie-server/Cargo.toml b/nixie-server/Cargo.toml index f87a6a8..cb79760 100644 --- a/nixie-server/Cargo.toml +++ b/nixie-server/Cargo.toml @@ -10,7 +10,7 @@ readme.workspace = true axum = { workspace = true, features = ["macros"] } tokio = { workspace = true, features = ["full"] } axum-login = { workspace = true } -sqlx = { workspace = true, features = ["runtime-tokio", "sqlite"] } +sqlx = { workspace = true, features = ["runtime-tokio", "sqlite", "time"] } anyhow.workspace = true tower-sessions = { workspace = true } serde = { workspace = true, features = ["derive"] } @@ -21,8 +21,9 @@ password-auth = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } tera = "1.20.1" -notify-debouncer-full = "0.6.0" +notify-debouncer-full = "0.7.0" tower-livereload = "0.10.2" time = "0.3.45" rand = "0.9.2" serde_json.workspace = true +tower-http = { version = "0.6.8", features = ["normalize-path", "fs"] } diff --git a/nixie-server/migrations/20260123170914_create_api_keys.up.sql b/nixie-server/migrations/20260123170914_create_api_keys.up.sql index 369a104..d47f7d3 100644 --- a/nixie-server/migrations/20260123170914_create_api_keys.up.sql +++ b/nixie-server/migrations/20260123170914_create_api_keys.up.sql @@ -6,7 +6,7 @@ CREATE TABLE IF NOT EXISTS api_keys user_id INTEGER NOT NULL REFERENCES users(id), token TEXT NOT NULL, name TEXT NOT NULL, - expiration_date TEXT NOT NULL, + expiration_date DATE NOT NULL, permissions TEXT NOT NULL, revoked BOOLEAN NOT NULL ); diff --git a/nixie-server/package-lock.json b/nixie-server/package-lock.json new file mode 100644 index 0000000..3b44fbc --- /dev/null +++ b/nixie-server/package-lock.json @@ -0,0 +1,18 @@ +{ + "name": "nixie-server", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "tailwindcss": "^4.1.18" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "license": "MIT" + } + } +} diff --git a/nixie-server/package.json b/nixie-server/package.json new file mode 100644 index 0000000..924b89f --- /dev/null +++ b/nixie-server/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "tailwindcss": "^4.1.18" + } +} diff --git a/nixie-server/public/.gitkeep b/nixie-server/public/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/nixie-server/src/main.rs b/nixie-server/src/main.rs index 2effe30..130271b 100644 --- a/nixie-server/src/main.rs +++ b/nixie-server/src/main.rs @@ -20,6 +20,8 @@ use tera::Tera; use thiserror::Error; use tokio::task; use tokio::task::AbortHandle; +use tower_http::normalize_path::NormalizePathLayer; +use tower_http::services::ServeDir; use tower_livereload::LiveReloadLayer; use tower_sessions::ExpiredDeletion; use tower_sessions::SessionManagerLayer; @@ -30,16 +32,22 @@ use tracing_subscriber::EnvFilter; pub mod settings; pub mod users; -pub type WebResult = Result; -pub type TemplatedHtml = Html; +pub(crate) type WebResult = Result; +pub(crate) type TemplatedHtml = Html; #[derive(Debug, Error, Display)] -pub enum AppError { +pub(crate) enum AppError { /// An error occurred while templating Tera(#[from] tera::Error), /// An error occurred while interacting with the database Sqlx(#[from] sqlx::Error), + + /// An error occurred while interacting with the sessions + Session(#[from] tower_sessions::session::Error), + + /// An error ocurred while interacting with user logins + Login(#[from] axum_login::Error), } impl IntoResponse for AppError { @@ -51,7 +59,7 @@ impl IntoResponse for AppError { Html( TERA.read() .unwrap() - .render("internal_error.tera.html", &error_context) + .render("internal_error.html.tera", &error_context) .unwrap_or_else(|_| "ERROR RENDERING ERROR! FATAL".to_string()), ), ) @@ -67,7 +75,7 @@ async fn main() -> anyhow::Result<()> { type AuthSession = axum_login::AuthSession; #[derive(Debug, Clone)] -struct Backend { +pub(crate) struct Backend { db: SqlitePool, } @@ -125,7 +133,7 @@ pub struct AppState { } pub static TERA: LazyLock> = - LazyLock::new(|| Tera::new("templates/**.tera.html").unwrap().into()); + LazyLock::new(|| Tera::new("templates/**.html.tera").unwrap().into()); async fn run() -> anyhow::Result<()> { tracing_subscriber::fmt() @@ -156,8 +164,13 @@ async fn run() -> anyhow::Result<()> { let app = Router::new() .merge(users::routes()) .merge(settings::routes()) + .nest_service( + "/assets", + ServeDir::new("public").append_index_html_on_directories(false), + ) .route("/", get(show_index)) .layer(auth_layer) + .layer(NormalizePathLayer::trim_trailing_slash()) .layer(livereload) .with_state(AppState { db }); @@ -193,16 +206,18 @@ async fn run() -> anyhow::Result<()> { let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, app.into_make_service()) - .with_graceful_shutdown(shutdown_signal(deletion_task.abort_handle())) - .await?; + .with_graceful_shutdown(async move { + shutdown_signal(deletion_task.abort_handle()).await; - debouncer.stop(); + debouncer.stop_nonblocking(); + }) + .await?; Ok(()) } async fn show_index(renderer: Renderer) -> WebResult> { - renderer.render_template("index.tera.html", None) + renderer.render_template("index.html.tera", None) } async fn shutdown_signal(handle: AbortHandle) { @@ -233,7 +248,7 @@ pub struct Renderer { } impl Renderer { - pub fn render_template( + pub(crate) fn render_template( &self, name: &str, context: impl Into>, diff --git a/nixie-server/src/settings/mod.rs b/nixie-server/src/settings/mod.rs index f815848..253f7ea 100644 --- a/nixie-server/src/settings/mod.rs +++ b/nixie-server/src/settings/mod.rs @@ -7,6 +7,7 @@ use axum::routing::get; use axum::routing::post; use axum_login::login_required; use password_auth::generate_hash; +use password_auth::verify_password; use rand::distr::Alphanumeric; use rand::distr::SampleString; use rand::rng; @@ -15,7 +16,6 @@ use serde::Serialize; use sqlx::prelude::FromRow; use tera::Context; use time::Date; -use time::Duration; use time::OffsetDateTime; use time::Time; @@ -37,14 +37,56 @@ pub fn routes() -> Router { } async fn show_settings(renderer: Renderer) -> WebResult { - renderer.render_template("settings/index.tera.html", None) + renderer.render_template("settings/index.html.tera", None) } async fn show_change_password(renderer: Renderer) -> WebResult { - renderer.render_template("settings/change_password.tera.html", None) + renderer.render_template("settings/change_password.html.tera", None) } -async fn do_change_password() {} +#[derive(Deserialize)] +struct ChangePasswordForm { + old_password: String, + password: String, + confirm_password: String, +} + +async fn do_change_password( + app_state: State, + auth: AuthSession, + change_password: Form, +) -> WebResult { + let old_password = change_password.old_password.clone(); + let hash = auth.user.as_ref().unwrap().password().to_string(); + + let wrong_password = + tokio::task::spawn_blocking(move || verify_password(&old_password, &hash).is_err()) + .await + .unwrap(); + + if wrong_password { + panic!("WRONG PASSWORD?"); + } + + if change_password.password != change_password.confirm_password { + panic!("Passwords are not equal..."); + } + + let hashed_password = + tokio::task::spawn_blocking(move || generate_hash(&change_password.password)) + .await + .unwrap(); + + sqlx::query("UPDATE users SET password = ? WHERE id = ?") + .bind(&hashed_password) + .bind(auth.user.unwrap().id()) + .execute(&app_state.db) + .await?; + + auth.session.delete().await?; + + Ok(()) +} #[derive(Debug, FromRow, Serialize)] pub struct ApiKey { @@ -52,7 +94,7 @@ pub struct ApiKey { user_id: i64, token: Vec, name: String, - expiration_date: OffsetDateTime, + expiration_date: Date, permissions: String, revoked: bool, } @@ -87,7 +129,7 @@ async fn show_api_keys( .unwrap(), ); - renderer.render_template("settings/api_keys.tera.html", context) + renderer.render_template("settings/api_keys.html.tera", context) } #[derive(Debug, Deserialize)] diff --git a/nixie-server/src/users/login.rs b/nixie-server/src/users/login.rs index f0e77bf..efb0c48 100644 --- a/nixie-server/src/users/login.rs +++ b/nixie-server/src/users/login.rs @@ -1,8 +1,8 @@ use axum::Form; -use axum::http::StatusCode; use axum::response::Html; use axum::response::IntoResponse; use axum::response::Redirect; +use tera::Context; use crate::AuthSession; use crate::Renderer; @@ -10,22 +10,33 @@ use crate::UserCredentials; use crate::WebResult; pub async fn show_login(renderer: Renderer) -> WebResult> { - renderer.render_template("users/login.tera.html", None) + renderer.render_template("users/login.html.tera", None) } pub async fn do_login( mut auth_session: AuthSession, + renderer: Renderer, Form(creds): Form, -) -> impl IntoResponse { - let user = match auth_session.authenticate(creds.clone()).await { - Ok(Some(user)) => user, - Ok(None) => return StatusCode::UNAUTHORIZED.into_response(), - Err(_) => return StatusCode::INTERNAL_SERVER_ERROR.into_response(), +) -> WebResult { + let user = match auth_session.authenticate(creds.clone()).await? { + Some(user) => user, + None => { + let mut context = Context::new(); + + context.insert( + "form", + &serde_json::json! {{ + "username": creds.username + }}, + ); + + return Ok(renderer + .render_template("users/login.html.tera", context) + .into_response()); + } }; - if auth_session.login(&user).await.is_err() { - return StatusCode::INTERNAL_SERVER_ERROR.into_response(); - } + auth_session.login(&user).await?; - Redirect::to("/protected").into_response() + Ok(Redirect::to("/protected").into_response()) } diff --git a/nixie-server/src/users/register.rs b/nixie-server/src/users/register.rs index 572de1c..60e8d79 100644 --- a/nixie-server/src/users/register.rs +++ b/nixie-server/src/users/register.rs @@ -11,7 +11,7 @@ use crate::UserCredentials; use crate::WebResult; pub async fn show_register(renderer: Renderer) -> WebResult> { - renderer.render_template("users/register.tera.html", None) + renderer.render_template("users/register.html.tera", None) } pub async fn do_register( diff --git a/nixie-server/templates/base.css b/nixie-server/templates/base.css new file mode 100644 index 0000000..f1d8c73 --- /dev/null +++ b/nixie-server/templates/base.css @@ -0,0 +1 @@ +@import "tailwindcss"; diff --git a/nixie-server/templates/base.tera.html b/nixie-server/templates/base.html.tera similarity index 80% rename from nixie-server/templates/base.tera.html rename to nixie-server/templates/base.html.tera index cfe8f8f..4c8cbcb 100644 --- a/nixie-server/templates/base.tera.html +++ b/nixie-server/templates/base.html.tera @@ -4,7 +4,7 @@ - + {% block head %} {% block title %}{% endblock title%} - Nixie CI @@ -12,7 +12,7 @@ -