Compare commits
No commits in common. "fc46c4874a394a30504aa100b88a7d3ac6eb8b01" and "b04955d70ef2ac61ba0326dcebf80512d1d1296c" have entirely different histories.
fc46c4874a
...
b04955d70e
|
@ -7,11 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased](https://git.rustybever.be/Chewing_Bever/otter)
|
## [Unreleased](https://git.rustybever.be/Chewing_Bever/otter)
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
* Web UI
|
|
||||||
* Login/logout button
|
|
||||||
|
|
||||||
## [0.1.0](https://git.rustybever.be/Chewing_Bever/otter/src/tag/0.1.0)
|
## [0.1.0](https://git.rustybever.be/Chewing_Bever/otter/src/tag/0.1.0)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
@ -1,21 +1,21 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
Form, RequestExt, Router,
|
|
||||||
extract::{Request, State},
|
extract::{Request, State},
|
||||||
http::{HeaderMap, HeaderName, HeaderValue, header},
|
http::HeaderMap,
|
||||||
middleware::{self, Next},
|
middleware::{self, Next},
|
||||||
response::{IntoResponse, Redirect, Response},
|
response::{IntoResponse, Redirect, Response},
|
||||||
routing::{get, post},
|
routing::get,
|
||||||
|
Form, RequestExt, Router,
|
||||||
};
|
};
|
||||||
use axum_extra::{TypedHeader, extract::CookieJar, headers::UserAgent};
|
use axum_extra::{extract::CookieJar, headers::UserAgent, TypedHeader};
|
||||||
use cookie::{Cookie, time::Duration};
|
use cookie::{time::Duration, Cookie};
|
||||||
use gpodder::{AuthErr, Session};
|
use gpodder::{AuthErr, Session};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::web::{Page, TemplateExt, TemplateResponse, View};
|
use crate::web::{Page, TemplateExt, TemplateResponse, View};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
Context,
|
|
||||||
error::{AppError, AppResult},
|
error::{AppError, AppResult},
|
||||||
|
Context,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SESSION_ID_COOKIE: &str = "sessionid";
|
const SESSION_ID_COOKIE: &str = "sessionid";
|
||||||
|
@ -23,27 +23,17 @@ const SESSION_ID_COOKIE: &str = "sessionid";
|
||||||
pub fn router(ctx: Context) -> Router<Context> {
|
pub fn router(ctx: Context) -> Router<Context> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", get(get_index))
|
.route("/", get(get_index))
|
||||||
// .layer(middleware::from_fn_with_state(
|
.layer(middleware::from_fn_with_state(
|
||||||
// ctx.clone(),
|
ctx.clone(),
|
||||||
// auth_web_middleware,
|
auth_web_middleware,
|
||||||
// ))
|
))
|
||||||
// Login route needs to be handled differently, as the middleware turns it into a redirect
|
// Login route needs to be handled differently, as the middleware turns it into a redirect
|
||||||
// loop
|
// loop
|
||||||
.route("/login", get(get_login).post(post_login))
|
.route("/login", get(get_login).post(post_login))
|
||||||
.route("/logout", post(post_logout))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_index(
|
async fn get_index(State(ctx): State<Context>, headers: HeaderMap) -> TemplateResponse<Page<View>> {
|
||||||
State(ctx): State<Context>,
|
View::Index.page(&headers).response(&ctx.tera)
|
||||||
headers: HeaderMap,
|
|
||||||
jar: CookieJar,
|
|
||||||
) -> AppResult<TemplateResponse<Page<View>>> {
|
|
||||||
let authenticated = extract_session(ctx.clone(), &jar).await?.is_some();
|
|
||||||
|
|
||||||
Ok(View::Index
|
|
||||||
.page(&headers)
|
|
||||||
.authenticated(authenticated)
|
|
||||||
.response(&ctx.tera))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_login(State(ctx): State<Context>, headers: HeaderMap, jar: CookieJar) -> Response {
|
async fn get_login(State(ctx): State<Context>, headers: HeaderMap, jar: CookieJar) -> Response {
|
||||||
|
@ -88,16 +78,15 @@ async fn post_login(
|
||||||
.unwrap()
|
.unwrap()
|
||||||
{
|
{
|
||||||
Ok(session) => Ok((
|
Ok(session) => Ok((
|
||||||
// Redirect forces htmx to reload the full page, refreshing the navbar
|
jar.add(
|
||||||
[("HX-Redirect", "/")],
|
|
||||||
(jar.add(
|
|
||||||
Cookie::build((SESSION_ID_COOKIE, session.id.to_string()))
|
Cookie::build((SESSION_ID_COOKIE, session.id.to_string()))
|
||||||
.secure(true)
|
.secure(true)
|
||||||
.same_site(cookie::SameSite::Lax)
|
.same_site(cookie::SameSite::Lax)
|
||||||
.http_only(true)
|
.http_only(true)
|
||||||
.path("/")
|
.path("/")
|
||||||
.max_age(Duration::days(365)),
|
.max_age(Duration::days(365)),
|
||||||
)),
|
),
|
||||||
|
Redirect::to("/"),
|
||||||
)
|
)
|
||||||
.into_response()),
|
.into_response()),
|
||||||
Err(AuthErr::UnknownUser | AuthErr::InvalidPassword) => {
|
Err(AuthErr::UnknownUser | AuthErr::InvalidPassword) => {
|
||||||
|
@ -107,16 +96,6 @@ async fn post_login(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Log out the user by simply removing the session
|
|
||||||
async fn post_logout(State(ctx): State<Context>, jar: CookieJar) -> AppResult<impl IntoResponse> {
|
|
||||||
if let Some(session) = extract_session(ctx.clone(), &jar).await? {
|
|
||||||
ctx.store.remove_session(session.id)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect forces htmx to reload the full page, refreshing the navbar
|
|
||||||
Ok(([("HX-Redirect", "/")], jar.remove(SESSION_ID_COOKIE)))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn extract_session(ctx: Context, jar: &CookieJar) -> AppResult<Option<Session>> {
|
async fn extract_session(ctx: Context, jar: &CookieJar) -> AppResult<Option<Session>> {
|
||||||
if let Some(session_id) = jar
|
if let Some(session_id) = jar
|
||||||
.get(SESSION_ID_COOKIE)
|
.get(SESSION_ID_COOKIE)
|
||||||
|
|
|
@ -10,7 +10,6 @@ const HX_HISTORY_RESTORE_HEADER: &str = "HX-History-Restore-Request";
|
||||||
pub struct Page<T> {
|
pub struct Page<T> {
|
||||||
template: T,
|
template: T,
|
||||||
wrap_with_base: bool,
|
wrap_with_base: bool,
|
||||||
authenticated: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Template> Template for Page<T> {
|
impl<T: Template> Template for Page<T> {
|
||||||
|
@ -24,7 +23,6 @@ impl<T: Template> Template for Page<T> {
|
||||||
if self.wrap_with_base {
|
if self.wrap_with_base {
|
||||||
let mut ctx = tera::Context::new();
|
let mut ctx = tera::Context::new();
|
||||||
ctx.insert("inner", &inner);
|
ctx.insert("inner", &inner);
|
||||||
ctx.insert("authenticated", &self.authenticated);
|
|
||||||
|
|
||||||
tera.render(super::BASE_TEMPLATE, &ctx)
|
tera.render(super::BASE_TEMPLATE, &ctx)
|
||||||
} else {
|
} else {
|
||||||
|
@ -38,7 +36,6 @@ impl<T> Page<T> {
|
||||||
Self {
|
Self {
|
||||||
template,
|
template,
|
||||||
wrap_with_base: false,
|
wrap_with_base: false,
|
||||||
authenticated: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,10 +50,4 @@ impl<T> Page<T> {
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn authenticated(mut self, authenticated: bool) -> Self {
|
|
||||||
self.authenticated = authenticated;
|
|
||||||
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,20 +15,6 @@ a:hover {
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a hx-get="/" hx-target="#inner" hx-push-url="true"><strong>Otter</strong></a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
{% if authenticated %}
|
|
||||||
<a hx-post="/logout" hx-target="#inner">Logout</a>
|
|
||||||
{% else %}
|
|
||||||
<a hx-get="/login" hx-target="#inner" hx-push-url="true">Login</a>
|
|
||||||
{% endif %}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
</nav>
|
||||||
<article id="inner">
|
<article id="inner">
|
||||||
{{ inner | safe }}
|
{{ inner | safe }}
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
<h1>Otter</h1>
|
<h1>Otter</h1>
|
||||||
|
|
||||||
Otter is a self-hostable Gpodder implementation.
|
Otter is a self-hostable Gpodder implementation.
|
||||||
|
|
||||||
|
If you're seeing this, you're logged in.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<article>
|
<article>
|
||||||
<form hx-post="/login" hx-target="#inner" hx-push-url="/">
|
<form hx-post="/login" hx-target="#inner">
|
||||||
<label for="username">Username:</label>
|
<label for="username">Username:</label>
|
||||||
<input type="text" id="username" name="username">
|
<input type="text" id="username" name="username">
|
||||||
<label for="password">Password:</label>
|
<label for="password">Password:</label>
|
||||||
|
|
Loading…
Reference in New Issue