feat(server): partial implementation of session page pagination

signup-links
Jef Roosens 2025-06-17 09:52:47 +02:00
parent 32d70daab2
commit e8e0c94937
No known key found for this signature in database
GPG Key ID: 21FD3D77D56BAF49
7 changed files with 110 additions and 35 deletions

View File

@ -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"

View File

@ -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<Pagination> 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<Context> {
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<Context> {
// 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<Context>,
headers: HeaderMap,
) -> TemplateResponse<Page<View>> {
View::Sessions(Vec::new())
.page(&headers)
.headers(&headers)
.authenticated(true)
.response(&ctx.tera)
}

View File

@ -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<Context> {
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<Context>,
headers: HeaderMap,
Extension(user): Extension<gpodder::User>,
Query(page): Query<super::Pagination>,
) -> AppResult<TemplateResponse<Page<View>>> {
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))
}

View File

@ -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<String>;
fn render(self, tera: &tera::Tera) -> tera::Result<String>;
}
/// Useful additional functions on sized Template implementors

View File

@ -18,7 +18,7 @@ impl<T: Template> Template for Page<T> {
self.template.template()
}
fn render(&self, tera: &tera::Tera) -> tera::Result<String> {
fn render(self, tera: &tera::Tera) -> tera::Result<String> {
let inner = self.template.render(tera)?;
if self.wrap_with_base {

View File

@ -9,10 +9,12 @@
</tr>
</thead>
<tbody>
{% for session in sessions %}
<tr>
<th>Firefox</th>
<th>yesterday</th>
<th>{{ session.user_agent }}</th>
<th>{{ session.last_seen }}</th>
<th>Remove</th>
</tr>
{% endfor %}
</tbody>
</table>

View File

@ -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<gpodder::Session>),
}
#[derive(Serialize)]
struct Session {
user_agent: Option<String>,
last_seen: DateTime<Utc>,
}
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<String> {
fn render(self, tera: &tera::Tera) -> tera::Result<String> {
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::<Vec<_>>(),
);
}
_ => {}
};
tera.render(self.template(), &ctx)
tera.render(template, &ctx)
}
}
impl From<gpodder::Session> for Session {
fn from(value: gpodder::Session) -> Self {
Self {
user_agent: value.user_agent,
last_seen: value.last_seen,
}
}
}