diff --git a/CHANGELOG.md b/CHANGELOG.md index d61481f..1cf7e52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added * Web UI + * Started development based on HTMX and PicoCSS + * Very simple homepage * Login/logout button + * Page for managing logged-in sessions ## [0.1.0](https://git.rustybever.be/Chewing_Bever/otter/src/tag/0.1.0) diff --git a/otter/src/server/web/mod.rs b/otter/src/server/web/mod.rs index dac3229..44e35a5 100644 --- a/otter/src/server/web/mod.rs +++ b/otter/src/server/web/mod.rs @@ -42,7 +42,7 @@ impl Default for Pagination { fn default() -> Self { Self { page: 0, - per_page: 1, + per_page: 25, } } } @@ -198,7 +198,7 @@ pub async fn auth_web_middleware( match extract_session(ctx, &jar).await { Ok(Some(session)) => { - req.extensions_mut().insert(session.user); + req.extensions_mut().insert(session); next.run(req).await } diff --git a/otter/src/server/web/sessions.rs b/otter/src/server/web/sessions.rs index 611372a..d05e3a7 100644 --- a/otter/src/server/web/sessions.rs +++ b/otter/src/server/web/sessions.rs @@ -1,18 +1,22 @@ use axum::{ Extension, Router, - extract::{Query, State}, + extract::{Path, Query, State}, http::HeaderMap, - routing::get, + routing::{delete, get}, }; use crate::{ - server::{Context, error::AppResult}, + server::{ + Context, + error::{AppError, AppResult}, + }, web::{Page, TemplateExt, TemplateResponse, ToQuery, View}, }; pub fn router(ctx: Context) -> Router { Router::new() .route("/sessions", get(get_sessions)) + .route("/sessions/{id}", delete(delete_session)) .route_layer(axum::middleware::from_fn_with_state( ctx.clone(), super::auth_web_middleware, @@ -22,21 +26,45 @@ pub fn router(ctx: Context) -> Router { pub async fn get_sessions( State(ctx): State, headers: HeaderMap, - Extension(user): Extension, + Extension(session): Extension, Query(page): Query, ) -> AppResult>> { let next_page = page.next_page(); - let sessions = - tokio::task::spawn_blocking(move || ctx.store.paginated_sessions(&user, page.into())) - .await - .unwrap()?; + let sessions = tokio::task::spawn_blocking(move || { + ctx.store.paginated_sessions(&session.user, page.into()) + }) + .await + .unwrap()?; let next_page_query = (sessions.len() == next_page.per_page as usize).then_some(next_page.to_query()); - Ok(View::Sessions(sessions, next_page_query) + Ok(View::Sessions(sessions, session.id, next_page_query) .page(&headers) .headers(&headers) .authenticated(true) .response(&ctx.tera)) } + +pub async fn delete_session( + State(ctx): State, + Extension(session): Extension, + Path(id): Path, +) -> AppResult<()> { + tokio::task::spawn_blocking(move || { + let other_session = ctx.store.get_session(id)?; + + // Check to ensure a user can't remove a session that's not theirs + if session.user.id != other_session.user.id { + return Err(AppError::Unauthorized); + } + + ctx.store.remove_session(session.id)?; + + Ok(()) + }) + .await + .unwrap()?; + + Ok(()) +} diff --git a/otter/src/web/mod.rs b/otter/src/web/mod.rs index f99ff53..93a7119 100644 --- a/otter/src/web/mod.rs +++ b/otter/src/web/mod.rs @@ -82,7 +82,7 @@ pub fn initialize_tera() -> tera::Result { include_str!("templates/views/login.html"), ), ( - View::Sessions(Vec::new(), None).template(), + View::Sessions(Vec::new(), 0, None).template(), include_str!("templates/views/sessions.html"), ), ])?; diff --git a/otter/src/web/templates/views/sessions.html b/otter/src/web/templates/views/sessions.html index 1a59e34..bdd1a2e 100644 --- a/otter/src/web/templates/views/sessions.html +++ b/otter/src/web/templates/views/sessions.html @@ -13,7 +13,15 @@ {{ session.user_agent }} {{ session.last_seen }} - Remove + + {%- if session.id != current_session_id -%} + Remove + {%- else -%} + Current session + {%- endif -%} + {% endfor %} {% if next_page_query %} diff --git a/otter/src/web/view.rs b/otter/src/web/view.rs index 5636a16..b7e45ec 100644 --- a/otter/src/web/view.rs +++ b/otter/src/web/view.rs @@ -6,11 +6,12 @@ use super::{Query, Template}; pub enum View { Index, Login, - Sessions(Vec, Option), + Sessions(Vec, i64, Option), } #[derive(Serialize)] struct Session { + id: i64, user_agent: Option, last_seen: DateTime, } @@ -29,11 +30,12 @@ impl Template for View { let template = self.template(); match self { - Self::Sessions(sessions, query) => { + Self::Sessions(sessions, current_session_id, query) => { ctx.insert( "sessions", &sessions.into_iter().map(Session::from).collect::>(), ); + ctx.insert("current_session_id", ¤t_session_id); if let Some(query) = query { ctx.insert("next_page_query", &query.encode()); @@ -49,6 +51,7 @@ impl Template for View { impl From for Session { fn from(value: gpodder::Session) -> Self { Self { + id: value.id, user_agent: value.user_agent, last_seen: value.last_seen, }