From 957387bed7cf34e0d04229979ba19e350c7ed547 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sat, 7 Jun 2025 10:20:49 +0200 Subject: [PATCH 1/2] feat(web): add logout button --- otter/src/server/web/mod.rs | 23 ++++++++++++++++++----- otter/src/web/templates/base.html | 7 +++++++ otter/src/web/templates/views/login.html | 2 +- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/otter/src/server/web/mod.rs b/otter/src/server/web/mod.rs index 98f5d1e..ca6ce6f 100644 --- a/otter/src/server/web/mod.rs +++ b/otter/src/server/web/mod.rs @@ -1,21 +1,21 @@ use axum::{ + Form, RequestExt, Router, extract::{Request, State}, http::HeaderMap, middleware::{self, Next}, response::{IntoResponse, Redirect, Response}, - routing::get, - Form, RequestExt, Router, + routing::{get, post}, }; -use axum_extra::{extract::CookieJar, headers::UserAgent, TypedHeader}; -use cookie::{time::Duration, Cookie}; +use axum_extra::{TypedHeader, extract::CookieJar, headers::UserAgent}; +use cookie::{Cookie, time::Duration}; use gpodder::{AuthErr, Session}; use serde::Deserialize; use crate::web::{Page, TemplateExt, TemplateResponse, View}; use super::{ - error::{AppError, AppResult}, Context, + error::{AppError, AppResult}, }; const SESSION_ID_COOKIE: &str = "sessionid"; @@ -30,6 +30,7 @@ pub fn router(ctx: Context) -> Router { // Login route needs to be handled differently, as the middleware turns it into a redirect // loop .route("/login", get(get_login).post(post_login)) + .route("/logout", post(post_logout)) } async fn get_index(State(ctx): State, headers: HeaderMap) -> TemplateResponse> { @@ -96,6 +97,18 @@ async fn post_login( } } +/// Log out the user by simply removing the session +async fn post_logout( + State(ctx): State, + jar: CookieJar, +) -> AppResult<(CookieJar, Redirect)> { + if let Some(session) = extract_session(ctx.clone(), &jar).await? { + ctx.store.remove_session(session.id)?; + } + + Ok((jar.remove(SESSION_ID_COOKIE), Redirect::to("/"))) +} + async fn extract_session(ctx: Context, jar: &CookieJar) -> AppResult> { if let Some(session_id) = jar .get(SESSION_ID_COOKIE) diff --git a/otter/src/web/templates/base.html b/otter/src/web/templates/base.html index 3afd3e4..68e958f 100644 --- a/otter/src/web/templates/base.html +++ b/otter/src/web/templates/base.html @@ -15,6 +15,13 @@ a:hover {
{{ inner | safe }} diff --git a/otter/src/web/templates/views/login.html b/otter/src/web/templates/views/login.html index ecd0898..b81094c 100644 --- a/otter/src/web/templates/views/login.html +++ b/otter/src/web/templates/views/login.html @@ -1,5 +1,5 @@
-
+ From fc46c4874a394a30504aa100b88a7d3ac6eb8b01 Mon Sep 17 00:00:00 2001 From: Jef Roosens Date: Sun, 8 Jun 2025 12:49:50 +0200 Subject: [PATCH 2/2] fix(web): refresh navbar on login and logout --- CHANGELOG.md | 5 ++++ otter/src/server/web/mod.rs | 38 ++++++++++++++---------- otter/src/web/page.rs | 9 ++++++ otter/src/web/templates/base.html | 9 +++++- otter/src/web/templates/views/index.html | 2 -- 5 files changed, 45 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9129687..d61481f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased](https://git.rustybever.be/Chewing_Bever/otter) +### Added + +* Web UI + * Login/logout button + ## [0.1.0](https://git.rustybever.be/Chewing_Bever/otter/src/tag/0.1.0) ### Added diff --git a/otter/src/server/web/mod.rs b/otter/src/server/web/mod.rs index ca6ce6f..2099ab4 100644 --- a/otter/src/server/web/mod.rs +++ b/otter/src/server/web/mod.rs @@ -1,7 +1,7 @@ use axum::{ Form, RequestExt, Router, extract::{Request, State}, - http::HeaderMap, + http::{HeaderMap, HeaderName, HeaderValue, header}, middleware::{self, Next}, response::{IntoResponse, Redirect, Response}, routing::{get, post}, @@ -23,18 +23,27 @@ const SESSION_ID_COOKIE: &str = "sessionid"; pub fn router(ctx: Context) -> Router { Router::new() .route("/", get(get_index)) - .layer(middleware::from_fn_with_state( - ctx.clone(), - auth_web_middleware, - )) + // .layer(middleware::from_fn_with_state( + // ctx.clone(), + // auth_web_middleware, + // )) // Login route needs to be handled differently, as the middleware turns it into a redirect // loop .route("/login", get(get_login).post(post_login)) .route("/logout", post(post_logout)) } -async fn get_index(State(ctx): State, headers: HeaderMap) -> TemplateResponse> { - View::Index.page(&headers).response(&ctx.tera) +async fn get_index( + State(ctx): State, + headers: HeaderMap, + jar: CookieJar, +) -> AppResult>> { + let authenticated = extract_session(ctx.clone(), &jar).await?.is_some(); + + Ok(View::Index + .page(&headers) + .authenticated(authenticated) + .response(&ctx.tera)) } async fn get_login(State(ctx): State, headers: HeaderMap, jar: CookieJar) -> Response { @@ -79,15 +88,16 @@ async fn post_login( .unwrap() { Ok(session) => Ok(( - jar.add( + // Redirect forces htmx to reload the full page, refreshing the navbar + [("HX-Redirect", "/")], + (jar.add( Cookie::build((SESSION_ID_COOKIE, session.id.to_string())) .secure(true) .same_site(cookie::SameSite::Lax) .http_only(true) .path("/") .max_age(Duration::days(365)), - ), - Redirect::to("/"), + )), ) .into_response()), Err(AuthErr::UnknownUser | AuthErr::InvalidPassword) => { @@ -98,15 +108,13 @@ async fn post_login( } /// Log out the user by simply removing the session -async fn post_logout( - State(ctx): State, - jar: CookieJar, -) -> AppResult<(CookieJar, Redirect)> { +async fn post_logout(State(ctx): State, jar: CookieJar) -> AppResult { if let Some(session) = extract_session(ctx.clone(), &jar).await? { ctx.store.remove_session(session.id)?; } - Ok((jar.remove(SESSION_ID_COOKIE), Redirect::to("/"))) + // Redirect forces htmx to reload the full page, refreshing the navbar + Ok(([("HX-Redirect", "/")], jar.remove(SESSION_ID_COOKIE))) } async fn extract_session(ctx: Context, jar: &CookieJar) -> AppResult> { diff --git a/otter/src/web/page.rs b/otter/src/web/page.rs index 0ba40c5..d7c0b4c 100644 --- a/otter/src/web/page.rs +++ b/otter/src/web/page.rs @@ -10,6 +10,7 @@ const HX_HISTORY_RESTORE_HEADER: &str = "HX-History-Restore-Request"; pub struct Page { template: T, wrap_with_base: bool, + authenticated: bool, } impl Template for Page { @@ -23,6 +24,7 @@ impl Template for Page { if self.wrap_with_base { let mut ctx = tera::Context::new(); ctx.insert("inner", &inner); + ctx.insert("authenticated", &self.authenticated); tera.render(super::BASE_TEMPLATE, &ctx) } else { @@ -36,6 +38,7 @@ impl Page { Self { template, wrap_with_base: false, + authenticated: false, } } @@ -50,4 +53,10 @@ impl Page { self } + + pub fn authenticated(mut self, authenticated: bool) -> Self { + self.authenticated = authenticated; + + self + } } diff --git a/otter/src/web/templates/base.html b/otter/src/web/templates/base.html index 68e958f..bf13d16 100644 --- a/otter/src/web/templates/base.html +++ b/otter/src/web/templates/base.html @@ -16,10 +16,17 @@ a:hover {
diff --git a/otter/src/web/templates/views/index.html b/otter/src/web/templates/views/index.html index 7116f34..6fc5b55 100644 --- a/otter/src/web/templates/views/index.html +++ b/otter/src/web/templates/views/index.html @@ -1,5 +1,3 @@

Otter

Otter is a self-hostable Gpodder implementation. - -If you're seeing this, you're logged in.