Compare commits

..

No commits in common. "a57e301d1683962a23f9b88283d3029465d2ce36" and "32d70daab2c6227d2d2d17f4b57764992e3af39f" have entirely different histories.

22 changed files with 64 additions and 213 deletions

1
Cargo.lock generated
View File

@ -1260,7 +1260,6 @@ dependencies = [
"http-body-util",
"rand",
"serde",
"serde_urlencoded",
"tera",
"tokio",
"tower-http",

View File

@ -46,7 +46,7 @@ run:
--log debug
doc:
cargo doc --workspace --frozen --open
cargo doc --workspace --frozen
publish-release-binaries tag: build-release-static
curl \

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

@ -25,4 +25,3 @@ http-body-util = "0.1.3"
tokio = { version = "1.43.0", features = ["full"] }
tracing-subscriber = "0.3.19"
tera = "1.20.0"
serde_urlencoded = "0.7.1"

View File

@ -6,8 +6,8 @@ use std::path::PathBuf;
use clap::{Args, Parser, Subcommand, ValueEnum};
use figment::{
Figment,
providers::{Env, Format, Serialized, Toml},
Figment,
};
use serde::Serialize;

View File

@ -1,20 +1,20 @@
use axum::{
Router,
extract::{Path, State},
routing::post,
Router,
};
use axum_extra::{
extract::{cookie::Cookie, CookieJar},
headers::{authorization::Basic, Authorization, UserAgent},
TypedHeader,
extract::{CookieJar, cookie::Cookie},
headers::{Authorization, UserAgent, authorization::Basic},
};
use cookie::time::Duration;
use gpodder::AuthErr;
use crate::server::{
Context,
error::{AppError, AppResult},
gpodder::SESSION_ID_COOKIE,
Context,
};
pub fn router() -> Router<Context> {

View File

@ -1,18 +1,18 @@
use axum::{
Extension, Json, Router,
extract::{Path, State},
middleware,
routing::{get, post},
Extension, Json, Router,
};
use crate::server::{
Context,
error::{AppError, AppResult},
gpodder::{
auth_api_middleware,
format::{Format, StringWithFormat},
models,
},
Context,
};
pub fn router(ctx: Context) -> Router<Context> {

View File

@ -1,14 +1,13 @@
use axum::{
Extension, Json, Router,
extract::{Path, Query, State},
middleware,
routing::post,
Extension, Json, Router,
};
use chrono::DateTime;
use serde::{Deserialize, Serialize};
use crate::server::{
Context,
error::{AppError, AppResult},
gpodder::{
auth_api_middleware,
@ -16,6 +15,7 @@ use crate::server::{
models,
models::UpdatedUrlsResponse,
},
Context,
};
pub fn router(ctx: Context) -> Router<Context> {

View File

@ -1,19 +1,19 @@
use axum::{
Extension, Json, Router,
extract::{Path, Query, State},
middleware,
routing::post,
Extension, Json, Router,
};
use serde::Deserialize;
use crate::server::{
Context,
error::{AppError, AppResult},
gpodder::{
auth_api_middleware,
format::{Format, StringWithFormat},
models::{SubscriptionDelta, SubscriptionDeltaResponse, UpdatedUrlsResponse},
},
Context,
};
pub fn router(ctx: Context) -> Router<Context> {

View File

@ -1,18 +1,18 @@
use axum::{
Extension, Json, Router,
extract::{Path, State},
middleware,
routing::get,
Extension, Json, Router,
};
use crate::server::{
Context,
error::{AppError, AppResult},
gpodder::{
auth_api_middleware,
format::{Format, StringWithFormat},
models::{SyncStatus, SyncStatusDelta},
},
Context,
};
pub fn router(ctx: Context) -> Router<Context> {

View File

@ -1,8 +1,8 @@
use std::ops::Deref;
use serde::{
de::{value::StrDeserializer, Visitor},
Deserialize,
de::{Visitor, value::StrDeserializer},
};
#[derive(Deserialize, Debug, PartialEq, Eq)]

View File

@ -4,16 +4,16 @@ mod models;
mod simple;
use axum::{
RequestExt, Router,
extract::{Request, State},
http::{HeaderName, HeaderValue, StatusCode, header::WWW_AUTHENTICATE},
http::{header::WWW_AUTHENTICATE, HeaderName, HeaderValue, StatusCode},
middleware::Next,
response::{IntoResponse, Response},
RequestExt, Router,
};
use axum_extra::{
extract::{cookie::Cookie, CookieJar},
headers::{authorization::Basic, Authorization},
TypedHeader,
extract::{CookieJar, cookie::Cookie},
headers::{Authorization, authorization::Basic},
};
use tower_http::set_header::SetResponseHeaderLayer;

View File

@ -1,14 +1,14 @@
use axum::{
Extension, Json, Router,
extract::{Path, State},
middleware,
routing::get,
Extension, Json, Router,
};
use crate::server::{
Context,
error::{AppError, AppResult},
gpodder::{auth_api_middleware, format::StringWithFormat},
Context,
};
pub fn router(ctx: Context) -> Router<Context> {

View File

@ -6,12 +6,12 @@ mod web;
use std::sync::Arc;
use axum::{
Router,
body::Body,
extract::Request,
http::StatusCode,
middleware::Next,
response::{IntoResponse, Response},
Router,
};
use http_body_util::BodyExt;
use tower_http::trace::TraceLayer;

View File

@ -1,7 +1,7 @@
use std::io::Cursor;
use axum::{Router, routing::get};
use axum_extra::{TypedHeader, headers::Range};
use axum::{routing::get, Router};
use axum_extra::{headers::Range, TypedHeader};
use axum_range::{KnownSize, Ranged};
use super::Context;

View File

@ -1,19 +1,17 @@
mod sessions;
use axum::{
Form, RequestExt, Router,
extract::{Request, State},
http::HeaderMap,
middleware::Next,
http::{HeaderMap, HeaderName, HeaderValue, header},
middleware::{self, 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, Serialize};
use serde::Deserialize;
use crate::web::{Page, Query, TemplateExt, TemplateResponse, ToQuery, View};
use crate::web::{Page, TemplateExt, TemplateResponse, View};
use super::{
Context,
@ -22,50 +20,13 @@ use super::{
const SESSION_ID_COOKIE: &str = "sessionid";
#[derive(Deserialize, Clone)]
#[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,
}
}
}
impl ToQuery for Pagination {
fn to_query(self) -> Query {
Query::default()
.parameter("page", self.page)
.parameter("per_page", self.per_page)
}
}
impl Pagination {
pub fn next_page(&self) -> Self {
Self {
page: self.page + 1,
per_page: self.per_page,
}
}
}
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(),
@ -75,7 +36,6 @@ 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(
@ -206,3 +166,14 @@ 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

@ -1,42 +0,0 @@
use axum::{
Extension, Router,
extract::{Query, State},
http::HeaderMap,
routing::get,
};
use crate::{
server::{Context, error::AppResult},
web::{Page, TemplateExt, TemplateResponse, ToQuery, 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 next_page = page.next_page();
let sessions =
tokio::task::spawn_blocking(move || ctx.store.paginated_sessions(&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)
.page(&headers)
.headers(&headers)
.authenticated(true)
.response(&ctx.tera))
}

View File

@ -1,5 +1,4 @@
mod page;
mod query;
mod view;
use std::sync::Arc;
@ -11,7 +10,6 @@ use axum::{
};
pub use page::Page;
pub use query::{Query, ToQuery};
pub use view::View;
const BASE_TEMPLATE: &str = "base.html";
@ -24,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
@ -82,7 +80,7 @@ pub fn initialize_tera() -> tera::Result<tera::Tera> {
include_str!("templates/views/login.html"),
),
(
View::Sessions(Vec::new(), None).template(),
View::Sessions(Vec::new()).template(),
include_str!("templates/views/sessions.html"),
),
])?;

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

@ -1,37 +0,0 @@
/// Represents a list of query parameters
#[derive(Default)]
pub struct Query(Vec<(String, String)>);
impl Query {
/// Combine two queries into one
pub fn join(mut self, other: impl ToQuery) -> Self {
let mut other = other.to_query();
self.0.append(&mut other.0);
self
}
/// Convert the query into a url-encoded query parameter string
pub fn encode(self) -> String {
// TODO is this unwrap safe?
serde_urlencoded::to_string(&self.0).unwrap()
}
/// Builder-style method that appends a parameter to the query
pub fn parameter(mut self, key: impl ToString, value: impl ToString) -> Self {
self.0.push((key.to_string(), value.to_string()));
self
}
}
/// Allows objects to be converted into queries
pub trait ToQuery {
fn to_query(self) -> Query;
}
impl ToQuery for Query {
fn to_query(self) -> Query {
self
}
}

View File

@ -9,20 +9,10 @@
</tr>
</thead>
<tbody>
{% for session in sessions %}
<tr>
<th>{{ session.user_agent }}</th>
<th>{{ session.last_seen }}</th>
<th>Firefox</th>
<th>yesterday</th>
<th>Remove</th>
</tr>
{% endfor %}
{% if next_page_query %}
<tr
hx-get="/sessions?{{ next_page_query }}"
hx-trigger="revealed"
hx-swap="outerHTML"
hx-select="table > tbody > tr"
></tr>
{% endif %}
</tbody>
</table>

View File

@ -1,18 +1,9 @@
use chrono::{DateTime, Utc};
use serde::Serialize;
use super::{Query, Template};
use super::Template;
pub enum View {
Index,
Login,
Sessions(Vec<gpodder::Session>, Option<Query>),
}
#[derive(Serialize)]
struct Session {
user_agent: Option<String>,
last_seen: DateTime<Utc>,
Sessions(Vec<gpodder::Session>),
}
impl Template for View {
@ -20,37 +11,19 @@ impl Template for View {
match self {
Self::Index => "views/index.html",
Self::Login => "views/login.html",
Self::Sessions(..) => "views/sessions.html",
Self::Sessions(_) => "views/sessions.html",
}
}
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, query) => {
ctx.insert(
"sessions",
&sessions.into_iter().map(Session::from).collect::<Vec<_>>(),
);
// match self {
// Self::Sessions(sessions) => {
// ctx.insert("sessions", sessions);
// }
// };
if let Some(query) = query {
ctx.insert("next_page_query", &query.encode());
}
}
_ => {}
};
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,
}
tera.render(self.template(), &ctx)
}
}