diff --git a/otter.toml b/otter.toml index 89481b4..185fe94 100644 --- a/otter.toml +++ b/otter.toml @@ -1,9 +1,9 @@ data_dir = "./data" [net] -# type = "tcp" -# domain = "127.0.0.1" -# port = 8080 +type = "tcp" +domain = "127.0.0.1" +port = 8080 -type = "unix" -path = "./otter.socket" +# type = "unix" +# path = "./otter.socket" diff --git a/otter/src/server/web/mod.rs b/otter/src/server/web/mod.rs index b8ec379..f924f9b 100644 --- a/otter/src/server/web/mod.rs +++ b/otter/src/server/web/mod.rs @@ -1,15 +1,17 @@ +mod sessions; + use axum::{ Form, RequestExt, Router, extract::{Request, State}, - http::{HeaderMap, HeaderName, HeaderValue, header}, - middleware::{self, Next}, + http::HeaderMap, + middleware::Next, response::{IntoResponse, Redirect, Response}, routing::{get, post}, }; use axum_extra::{TypedHeader, extract::CookieJar, headers::UserAgent}; use cookie::{Cookie, time::Duration}; use gpodder::{AuthErr, Session}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use crate::web::{Page, TemplateExt, TemplateResponse, View}; @@ -20,13 +22,33 @@ use super::{ const SESSION_ID_COOKIE: &str = "sessionid"; +#[derive(Deserialize, Serialize)] +#[serde(default)] +pub struct Pagination { + page: u32, + per_page: u32, +} + +impl From for gpodder::Page { + fn from(value: Pagination) -> Self { + Self { + page: value.page, + per_page: value.per_page, + } + } +} + +impl Default for Pagination { + fn default() -> Self { + Self { + page: 0, + per_page: 1, + } + } +} + pub fn router(ctx: Context) -> Router { Router::new() - .route("/sessions", get(get_sessions)) - .route_layer(axum::middleware::from_fn_with_state( - ctx.clone(), - auth_web_middleware, - )) .route("/", get(get_index)) // .layer(middleware::from_fn_with_state( // ctx.clone(), @@ -36,6 +58,7 @@ pub fn router(ctx: Context) -> Router { // loop .route("/login", get(get_login).post(post_login)) .route("/logout", post(post_logout)) + .merge(sessions::router(ctx.clone())) } async fn get_index( @@ -166,14 +189,3 @@ pub async fn auth_web_middleware( Err(err) => err.into_response(), } } - -pub async fn get_sessions( - State(ctx): State, - headers: HeaderMap, -) -> TemplateResponse> { - View::Sessions(Vec::new()) - .page(&headers) - .headers(&headers) - .authenticated(true) - .response(&ctx.tera) -} diff --git a/otter/src/server/web/sessions.rs b/otter/src/server/web/sessions.rs new file mode 100644 index 0000000..9020bab --- /dev/null +++ b/otter/src/server/web/sessions.rs @@ -0,0 +1,38 @@ +use axum::{ + Extension, Router, + extract::{Query, State}, + http::HeaderMap, + routing::get, +}; + +use crate::{ + server::{Context, error::AppResult}, + web::{Page, TemplateExt, TemplateResponse, View}, +}; + +pub fn router(ctx: Context) -> Router { + Router::new() + .route("/sessions", get(get_sessions)) + .route_layer(axum::middleware::from_fn_with_state( + ctx.clone(), + super::auth_web_middleware, + )) +} + +pub async fn get_sessions( + State(ctx): State, + headers: HeaderMap, + Extension(user): Extension, + Query(page): Query, +) -> AppResult>> { + let sessions = + tokio::task::spawn_blocking(move || ctx.store.paginated_sessions(&user, page.into())) + .await + .unwrap()?; + + Ok(View::Sessions(sessions) + .page(&headers) + .headers(&headers) + .authenticated(true) + .response(&ctx.tera)) +} diff --git a/otter/src/web/mod.rs b/otter/src/web/mod.rs index 0035779..05d66e2 100644 --- a/otter/src/web/mod.rs +++ b/otter/src/web/mod.rs @@ -22,7 +22,7 @@ pub trait Template { /// Render the template using the given Tera instance. /// /// Templates are expected to manage their own context requirements if needed. - fn render(&self, tera: &tera::Tera) -> tera::Result; + fn render(self, tera: &tera::Tera) -> tera::Result; } /// Useful additional functions on sized Template implementors diff --git a/otter/src/web/page.rs b/otter/src/web/page.rs index d7c0b4c..e7f7f6b 100644 --- a/otter/src/web/page.rs +++ b/otter/src/web/page.rs @@ -18,7 +18,7 @@ impl Template for Page { self.template.template() } - fn render(&self, tera: &tera::Tera) -> tera::Result { + fn render(self, tera: &tera::Tera) -> tera::Result { let inner = self.template.render(tera)?; if self.wrap_with_base { diff --git a/otter/src/web/templates/views/sessions.html b/otter/src/web/templates/views/sessions.html index 679a565..f160d41 100644 --- a/otter/src/web/templates/views/sessions.html +++ b/otter/src/web/templates/views/sessions.html @@ -9,10 +9,12 @@ + {% for session in sessions %} - Firefox - yesterday + {{ session.user_agent }} + {{ session.last_seen }} Remove + {% endfor %} diff --git a/otter/src/web/view.rs b/otter/src/web/view.rs index 4f15834..e1a0f59 100644 --- a/otter/src/web/view.rs +++ b/otter/src/web/view.rs @@ -1,3 +1,6 @@ +use chrono::{DateTime, Utc}; +use serde::Serialize; + use super::Template; pub enum View { @@ -6,6 +9,12 @@ pub enum View { Sessions(Vec), } +#[derive(Serialize)] +struct Session { + user_agent: Option, + last_seen: DateTime, +} + impl Template for View { fn template(&self) -> &'static str { match self { @@ -15,15 +24,29 @@ impl Template for View { } } - fn render(&self, tera: &tera::Tera) -> tera::Result { + fn render(self, tera: &tera::Tera) -> tera::Result { let mut ctx = tera::Context::new(); + let template = self.template(); - // match self { - // Self::Sessions(sessions) => { - // ctx.insert("sessions", sessions); - // } - // }; + match self { + Self::Sessions(sessions) => { + ctx.insert( + "sessions", + &sessions.into_iter().map(Session::from).collect::>(), + ); + } + _ => {} + }; - tera.render(self.template(), &ctx) + tera.render(template, &ctx) + } +} + +impl From for Session { + fn from(value: gpodder::Session) -> Self { + Self { + user_agent: value.user_agent, + last_seen: value.last_seen, + } } }