feat(server): partial implementation of session page pagination
parent
32d70daab2
commit
e8e0c94937
10
otter.toml
10
otter.toml
|
@ -1,9 +1,9 @@
|
||||||
data_dir = "./data"
|
data_dir = "./data"
|
||||||
|
|
||||||
[net]
|
[net]
|
||||||
# type = "tcp"
|
type = "tcp"
|
||||||
# domain = "127.0.0.1"
|
domain = "127.0.0.1"
|
||||||
# port = 8080
|
port = 8080
|
||||||
|
|
||||||
type = "unix"
|
# type = "unix"
|
||||||
path = "./otter.socket"
|
# path = "./otter.socket"
|
||||||
|
|
|
@ -1,15 +1,17 @@
|
||||||
|
mod sessions;
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Form, RequestExt, Router,
|
Form, RequestExt, Router,
|
||||||
extract::{Request, State},
|
extract::{Request, State},
|
||||||
http::{HeaderMap, HeaderName, HeaderValue, header},
|
http::HeaderMap,
|
||||||
middleware::{self, Next},
|
middleware::Next,
|
||||||
response::{IntoResponse, Redirect, Response},
|
response::{IntoResponse, Redirect, Response},
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
};
|
};
|
||||||
use axum_extra::{TypedHeader, extract::CookieJar, headers::UserAgent};
|
use axum_extra::{TypedHeader, extract::CookieJar, headers::UserAgent};
|
||||||
use cookie::{Cookie, time::Duration};
|
use cookie::{Cookie, time::Duration};
|
||||||
use gpodder::{AuthErr, Session};
|
use gpodder::{AuthErr, Session};
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::web::{Page, TemplateExt, TemplateResponse, View};
|
use crate::web::{Page, TemplateExt, TemplateResponse, View};
|
||||||
|
|
||||||
|
@ -20,13 +22,33 @@ use super::{
|
||||||
|
|
||||||
const SESSION_ID_COOKIE: &str = "sessionid";
|
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> {
|
pub fn router(ctx: Context) -> Router<Context> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/sessions", get(get_sessions))
|
|
||||||
.route_layer(axum::middleware::from_fn_with_state(
|
|
||||||
ctx.clone(),
|
|
||||||
auth_web_middleware,
|
|
||||||
))
|
|
||||||
.route("/", get(get_index))
|
.route("/", get(get_index))
|
||||||
// .layer(middleware::from_fn_with_state(
|
// .layer(middleware::from_fn_with_state(
|
||||||
// ctx.clone(),
|
// ctx.clone(),
|
||||||
|
@ -36,6 +58,7 @@ pub fn router(ctx: Context) -> Router<Context> {
|
||||||
// loop
|
// loop
|
||||||
.route("/login", get(get_login).post(post_login))
|
.route("/login", get(get_login).post(post_login))
|
||||||
.route("/logout", post(post_logout))
|
.route("/logout", post(post_logout))
|
||||||
|
.merge(sessions::router(ctx.clone()))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_index(
|
async fn get_index(
|
||||||
|
@ -166,14 +189,3 @@ pub async fn auth_web_middleware(
|
||||||
Err(err) => err.into_response(),
|
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)
|
|
||||||
}
|
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
|
@ -22,7 +22,7 @@ pub trait Template {
|
||||||
/// Render the template using the given Tera instance.
|
/// Render the template using the given Tera instance.
|
||||||
///
|
///
|
||||||
/// Templates are expected to manage their own context requirements if needed.
|
/// 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
|
/// Useful additional functions on sized Template implementors
|
||||||
|
|
|
@ -18,7 +18,7 @@ impl<T: Template> Template for Page<T> {
|
||||||
self.template.template()
|
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)?;
|
let inner = self.template.render(tera)?;
|
||||||
|
|
||||||
if self.wrap_with_base {
|
if self.wrap_with_base {
|
||||||
|
|
|
@ -9,10 +9,12 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
{% for session in sessions %}
|
||||||
<tr>
|
<tr>
|
||||||
<th>Firefox</th>
|
<th>{{ session.user_agent }}</th>
|
||||||
<th>yesterday</th>
|
<th>{{ session.last_seen }}</th>
|
||||||
<th>Remove</th>
|
<th>Remove</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
use super::Template;
|
use super::Template;
|
||||||
|
|
||||||
pub enum View {
|
pub enum View {
|
||||||
|
@ -6,6 +9,12 @@ pub enum View {
|
||||||
Sessions(Vec<gpodder::Session>),
|
Sessions(Vec<gpodder::Session>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct Session {
|
||||||
|
user_agent: Option<String>,
|
||||||
|
last_seen: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Template for View {
|
impl Template for View {
|
||||||
fn template(&self) -> &'static str {
|
fn template(&self) -> &'static str {
|
||||||
match self {
|
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 mut ctx = tera::Context::new();
|
||||||
|
let template = self.template();
|
||||||
|
|
||||||
// match self {
|
match self {
|
||||||
// Self::Sessions(sessions) => {
|
Self::Sessions(sessions) => {
|
||||||
// ctx.insert("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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue