Compare commits
3 Commits
330877c8c5
...
65e83ecb1f
| Author | SHA1 | Date |
|---|---|---|
|
|
65e83ecb1f | |
|
|
bc80515474 | |
|
|
f00d842bad |
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use crate::{db, server};
|
use crate::{db, server};
|
||||||
|
|
||||||
pub fn serve(config: &crate::config::Config) -> u8 {
|
pub fn serve(config: &crate::config::Config) -> u8 {
|
||||||
|
|
@ -11,7 +13,7 @@ pub fn serve(config: &crate::config::Config) -> u8 {
|
||||||
let ctx = server::Context {
|
let ctx = server::Context {
|
||||||
store: crate::gpodder::GpodderRepository::new(repo),
|
store: crate::gpodder::GpodderRepository::new(repo),
|
||||||
};
|
};
|
||||||
let app = server::app(ctx);
|
let app = server::app(ctx.clone());
|
||||||
|
|
||||||
let rt = tokio::runtime::Builder::new_multi_thread()
|
let rt = tokio::runtime::Builder::new_multi_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
|
|
@ -21,7 +23,28 @@ pub fn serve(config: &crate::config::Config) -> u8 {
|
||||||
let address = format!("{}:{}", config.domain, config.port);
|
let address = format!("{}:{}", config.domain, config.port);
|
||||||
tracing::info!("Starting server on {address}");
|
tracing::info!("Starting server on {address}");
|
||||||
|
|
||||||
|
let session_removal_duration = Duration::from_secs(config.session_cleanup_interval);
|
||||||
|
|
||||||
rt.block_on(async {
|
rt.block_on(async {
|
||||||
|
tokio::task::spawn(async move {
|
||||||
|
let mut interval = tokio::time::interval(session_removal_duration);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
interval.tick().await;
|
||||||
|
|
||||||
|
tracing::info!("Performing session cleanup");
|
||||||
|
|
||||||
|
match ctx.store.remove_old_sessions() {
|
||||||
|
Ok(n) => {
|
||||||
|
tracing::info!("Removed {} old sessions", n);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!("Error occured during session cleanup: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(address).await.unwrap();
|
let listener = tokio::net::TcpListener::bind(address).await.unwrap();
|
||||||
axum::serve(listener, app.into_make_service())
|
axum::serve(listener, app.into_make_service())
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ pub struct Config {
|
||||||
pub domain: String,
|
pub domain: String,
|
||||||
#[serde(default = "default_port")]
|
#[serde(default = "default_port")]
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
|
#[serde(default = "default_session_cleanup_interval")]
|
||||||
|
pub session_cleanup_interval: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_data_dir() -> PathBuf {
|
fn default_data_dir() -> PathBuf {
|
||||||
|
|
@ -23,3 +25,8 @@ fn default_domain() -> String {
|
||||||
fn default_port() -> u16 {
|
fn default_port() -> u16 {
|
||||||
8080
|
8080
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn default_session_cleanup_interval() -> u64 {
|
||||||
|
// Default is once a day
|
||||||
|
60 * 60 * 24
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -181,4 +181,29 @@ impl gpodder::AuthStore for SqliteRepository {
|
||||||
.execute(&mut self.pool.get()?)
|
.execute(&mut self.pool.get()?)
|
||||||
.map(|_| ())?)
|
.map(|_| ())?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn refresh_session(
|
||||||
|
&self,
|
||||||
|
session: &gpodder::Session,
|
||||||
|
timestamp: DateTime<chrono::Utc>,
|
||||||
|
) -> Result<(), AuthErr> {
|
||||||
|
if diesel::update(sessions::table.filter(sessions::id.eq(session.id)))
|
||||||
|
.set(sessions::last_seen.eq(timestamp.timestamp()))
|
||||||
|
.execute(&mut self.pool.get()?)?
|
||||||
|
== 0
|
||||||
|
{
|
||||||
|
Err(AuthErr::UnknownSession)
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_old_sessions(&self, min_last_seen: DateTime<chrono::Utc>) -> Result<usize, AuthErr> {
|
||||||
|
let min_last_seen = min_last_seen.timestamp();
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
diesel::delete(sessions::table.filter(sessions::last_seen.lt(min_last_seen)))
|
||||||
|
.execute(&mut self.pool.get()?)?,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
pub mod models;
|
pub mod models;
|
||||||
mod repository;
|
mod repository;
|
||||||
|
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
pub use models::*;
|
pub use models::*;
|
||||||
pub use repository::GpodderRepository;
|
pub use repository::GpodderRepository;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub enum AuthErr {
|
pub enum AuthErr {
|
||||||
UnknownSession,
|
UnknownSession,
|
||||||
UnknownUser,
|
UnknownUser,
|
||||||
|
|
@ -12,6 +15,19 @@ pub enum AuthErr {
|
||||||
Other(Box<dyn std::error::Error + Sync + Send>),
|
Other(Box<dyn std::error::Error + Sync + Send>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for AuthErr {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::UnknownUser => write!(f, "unknown user"),
|
||||||
|
Self::UnknownSession => write!(f, "unknown session"),
|
||||||
|
Self::InvalidPassword => write!(f, "invalid password"),
|
||||||
|
Self::Other(err) => err.fmt(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for AuthErr {}
|
||||||
|
|
||||||
pub trait Store:
|
pub trait Store:
|
||||||
AuthStore + DeviceRepository + SubscriptionRepository + EpisodeActionRepository
|
AuthStore + DeviceRepository + SubscriptionRepository + EpisodeActionRepository
|
||||||
{
|
{
|
||||||
|
|
@ -52,6 +68,12 @@ pub trait AuthStore {
|
||||||
|
|
||||||
/// Remove the session with the given session ID
|
/// Remove the session with the given session ID
|
||||||
fn remove_session(&self, session_id: i64) -> Result<(), AuthErr>;
|
fn remove_session(&self, session_id: i64) -> Result<(), AuthErr>;
|
||||||
|
|
||||||
|
/// Update the session's timestamp
|
||||||
|
fn refresh_session(&self, session: &Session, timestamp: DateTime<Utc>) -> Result<(), AuthErr>;
|
||||||
|
|
||||||
|
/// Remove any sessions whose last_seen timestamp is before the given minimum value
|
||||||
|
fn remove_old_sessions(&self, min_last_seen: DateTime<Utc>) -> Result<usize, AuthErr>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait DeviceRepository {
|
pub trait DeviceRepository {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ impl GpodderRepository {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate_session(&self, session_id: i64) -> Result<models::Session, AuthErr> {
|
pub fn get_session(&self, session_id: i64) -> Result<models::Session, AuthErr> {
|
||||||
let session = self
|
let session = self
|
||||||
.store
|
.store
|
||||||
.get_session(session_id)?
|
.get_session(session_id)?
|
||||||
|
|
@ -65,10 +65,22 @@ impl GpodderRepository {
|
||||||
Ok(session)
|
Ok(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn refresh_session(&self, session: &models::Session) -> Result<(), AuthErr> {
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
self.store.refresh_session(session, now)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn remove_session(&self, session_id: i64) -> Result<(), AuthErr> {
|
pub fn remove_session(&self, session_id: i64) -> Result<(), AuthErr> {
|
||||||
self.store.remove_session(session_id)
|
self.store.remove_session(session_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn remove_old_sessions(&self) -> Result<usize, AuthErr> {
|
||||||
|
let min_last_seen = Utc::now() - TimeDelta::seconds(MAX_SESSION_AGE);
|
||||||
|
|
||||||
|
self.store.remove_old_sessions(min_last_seen)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn devices_for_user(&self, user: &models::User) -> Result<Vec<models::Device>, AuthErr> {
|
pub fn devices_for_user(&self, user: &models::User) -> Result<Vec<models::Device>, AuthErr> {
|
||||||
self.store.devices_for_user(user)
|
self.store.devices_for_user(user)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,11 @@ use axum::{
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use axum_extra::{
|
use axum_extra::{
|
||||||
extract::{
|
extract::{cookie::Cookie, CookieJar},
|
||||||
cookie::{Cookie, Expiration},
|
|
||||||
CookieJar,
|
|
||||||
},
|
|
||||||
headers::{authorization::Basic, Authorization},
|
headers::{authorization::Basic, Authorization},
|
||||||
TypedHeader,
|
TypedHeader,
|
||||||
};
|
};
|
||||||
|
use cookie::time::Duration;
|
||||||
|
|
||||||
use crate::server::{
|
use crate::server::{
|
||||||
error::{AppError, AppResult},
|
error::{AppError, AppResult},
|
||||||
|
|
@ -45,7 +43,7 @@ async fn post_login(
|
||||||
.unwrap()?;
|
.unwrap()?;
|
||||||
|
|
||||||
Ok(jar.add(
|
Ok(jar.add(
|
||||||
Cookie::build((SESSION_ID_COOKIE, session.id.to_string())).expires(Expiration::Session),
|
Cookie::build((SESSION_ID_COOKIE, session.id.to_string())).max_age(Duration::days(365)),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,8 +47,13 @@ pub async fn auth_middleware(State(ctx): State<Context>, mut req: Request, next:
|
||||||
.get(SESSION_ID_COOKIE)
|
.get(SESSION_ID_COOKIE)
|
||||||
.and_then(|c| c.value().parse::<i64>().ok())
|
.and_then(|c| c.value().parse::<i64>().ok())
|
||||||
{
|
{
|
||||||
let ctx_clone = ctx.clone();
|
let ctx = ctx.clone();
|
||||||
match tokio::task::spawn_blocking(move || ctx_clone.store.validate_session(session_id))
|
match tokio::task::spawn_blocking(move || {
|
||||||
|
let session = ctx.store.get_session(session_id)?;
|
||||||
|
ctx.store.refresh_session(&session)?;
|
||||||
|
|
||||||
|
Ok(session)
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue