Compare commits

...

2 Commits

6 changed files with 57 additions and 15 deletions

View File

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

View File

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

View File

@ -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<Context> {
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<Context> {
pub async fn get_sessions(
State(ctx): State<Context>,
headers: HeaderMap,
Extension(user): Extension<gpodder::User>,
Extension(session): Extension<gpodder::Session>,
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 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<Context>,
Extension(session): Extension<gpodder::Session>,
Path(id): Path<i64>,
) -> 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(())
}

View File

@ -82,7 +82,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(), 0, None).template(),
include_str!("templates/views/sessions.html"),
),
])?;

View File

@ -13,7 +13,15 @@
<tr>
<th>{{ session.user_agent }}</th>
<th>{{ session.last_seen }}</th>
<th>Remove</th>
<th>
{%- if session.id != current_session_id -%}
<a hx-delete="/sessions/{{ session.id }}"
hx-target="closest tr"
>Remove</a>
{%- else -%}
Current session
{%- endif -%}
</th>
</tr>
{% endfor %}
{% if next_page_query %}

View File

@ -6,11 +6,12 @@ use super::{Query, Template};
pub enum View {
Index,
Login,
Sessions(Vec<gpodder::Session>, Option<Query>),
Sessions(Vec<gpodder::Session>, i64, Option<Query>),
}
#[derive(Serialize)]
struct Session {
id: i64,
user_agent: Option<String>,
last_seen: DateTime<Utc>,
}
@ -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::<Vec<_>>(),
);
ctx.insert("current_session_id", &current_session_id);
if let Some(query) = query {
ctx.insert("next_page_query", &query.encode());
@ -49,6 +51,7 @@ impl Template for View {
impl From<gpodder::Session> for Session {
fn from(value: gpodder::Session) -> Self {
Self {
id: value.id,
user_agent: value.user_agent,
last_seen: value.last_seen,
}