feat: use postgres session

This commit is contained in:
Max Hohlfeld 2024-04-02 23:28:31 +02:00
parent 1915baf094
commit 12702ab85d
6 changed files with 963 additions and 484 deletions

2
.env
View File

@ -2,4 +2,6 @@
# DATABASE_URL=postgres://postgres@localhost/my_database # DATABASE_URL=postgres://postgres@localhost/my_database
# SQLite # SQLite
DATABASE_URL=postgresql://max@localhost/brass DATABASE_URL=postgresql://max@localhost/brass
# 64 byte long
SECRET_KEY="changeInProdOrHandAb11111111111111111111111111111111111111111111"

1245
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -21,3 +21,5 @@ askama_actix = "0.14.0"
futures-util = "0.3.30" futures-util = "0.3.30"
serde_json = "1.0.114" serde_json = "1.0.114"
pico-args = "0.5.0" pico-args = "0.5.0"
rand = "0.8.5"
async-trait = "0.1.79"

View File

@ -78,3 +78,13 @@ CREATE TABLE vehicleassignement
vehicleId INTEGER REFERENCES vehicle (id), vehicleId INTEGER REFERENCES vehicle (id),
PRIMARY KEY (eventId, vehicleId) PRIMARY KEY (eventId, vehicleId)
); );
CREATE UNLOGGED TABLE session
(
id SERIAL PRIMARY KEY,
key TEXT UNIQUE NOT NULL,
sessionstate JSONB,
expires TIMESTAMP
);
CREATE INDEX idx_cache_key ON session (key);

View File

@ -1,9 +1,10 @@
use std::env; use std::env;
use std::io::{stdin, stdout, Write}; use std::io::{stdin, stdout, Write};
use std::process::exit; use std::process::exit;
use std::time::Duration;
use actix_identity::IdentityMiddleware; use actix_identity::IdentityMiddleware;
use actix_session::{storage::CookieSessionStore, SessionMiddleware}; use actix_session::SessionMiddleware;
use actix_web::cookie::Key; use actix_web::cookie::Key;
use actix_web::{web, App, HttpServer}; use actix_web::{web, App, HttpServer};
use dotenv::dotenv; use dotenv::dotenv;
@ -12,19 +13,22 @@ use sqlx::postgres::PgPool;
use crate::auth::redirect; use crate::auth::redirect;
use crate::auth::utils::generate_salt_and_hash_plain_password; use crate::auth::utils::generate_salt_and_hash_plain_password;
use crate::models::User; use crate::models::User;
use crate::postgres_session_store::SqlxPostgresqlSessionStore;
mod auth; mod auth;
mod calendar; mod calendar;
mod models;
mod endpoints; mod endpoints;
mod models;
mod postgres_session_store;
pub enum Command { pub enum Command {
Migrate, Migrate,
CreateAdmin CreateAdmin,
} }
pub struct Args { pub struct Args {
command: Option<Command> command: Option<Command>,
} }
fn parse_args() -> Result<Args, pico_args::Error> { fn parse_args() -> Result<Args, pico_args::Error> {
@ -32,15 +36,13 @@ fn parse_args() -> Result<Args, pico_args::Error> {
let command = pargs.free_from_str::<String>(); let command = pargs.free_from_str::<String>();
let mut args = Args { let mut args = Args { command: None };
command: None
};
if let Ok(parsed) = command { if let Ok(parsed) = command {
match parsed.trim() { match parsed.trim() {
"migrate" => args.command = Some(Command::Migrate), "migrate" => args.command = Some(Command::Migrate),
"createadmin" => args.command = Some(Command::CreateAdmin), "createadmin" => args.command = Some(Command::CreateAdmin),
_ => () _ => (),
} }
} }
@ -70,16 +72,15 @@ async fn main() -> anyhow::Result<()> {
}; };
let pool = PgPool::connect(&env::var("DATABASE_URL")?).await?; let pool = PgPool::connect(&env::var("DATABASE_URL")?).await?;
let secret_key = Key::generate(); let secret_key = Key::from(env::var("SECRET_KEY")?.as_bytes());
let store = SqlxPostgresqlSessionStore::from_pool(pool.clone().into());
match args.command { match args.command {
Some(Command::Migrate) => { Some(Command::Migrate) => {
sqlx::migrate!("./migrations") sqlx::migrate!("./migrations").run(&pool).await?;
.run(&pool)
.await?;
exit(0); exit(0);
}, }
Some(Command::CreateAdmin) => { Some(Command::CreateAdmin) => {
let name = prompt("Full name of Admin")?; let name = prompt("Full name of Admin")?;
let email = prompt("E-Mail of Admin (for login)")?; let email = prompt("E-Mail of Admin (for login)")?;
@ -106,8 +107,8 @@ async fn main() -> anyhow::Result<()> {
.await?; .await?;
exit(0); exit(0);
}, }
None => () None => (),
}; };
println!("Starting server on http://localhost:8080."); println!("Starting server on http://localhost:8080.");
@ -119,11 +120,12 @@ async fn main() -> anyhow::Result<()> {
.configure(calendar::init) .configure(calendar::init)
.configure(endpoints::init) .configure(endpoints::init)
.wrap(redirect::CheckLogin) .wrap(redirect::CheckLogin)
.wrap(IdentityMiddleware::default()) .wrap(
.wrap(SessionMiddleware::new( IdentityMiddleware::builder()
CookieSessionStore::default(), .visit_deadline(Some(Duration::from_secs(300)))
secret_key.clone(), .build(),
)) )
.wrap(SessionMiddleware::new(store.clone(), secret_key.clone()))
.service(actix_files::Files::new("/static", "./static").show_files_listing()) .service(actix_files::Files::new("/static", "./static").show_files_listing())
}) })
.bind(("127.0.0.1", 8080))? .bind(("127.0.0.1", 8080))?

View File

@ -0,0 +1,146 @@
// took code from https://github.com/chriswk/actix-session-sqlx-postgres and adapted it to own usecase
use actix_session::storage::{LoadError, SaveError, SessionKey, SessionStore, UpdateError};
use actix_web::cookie::time::Duration;
use chrono::Utc;
use rand::{distributions::Alphanumeric, rngs::OsRng, Rng as _};
use serde_json::{self, Value};
use sqlx::{Pool, Postgres, Row};
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Clone)]
struct CacheConfiguration {
cache_keygen: Arc<dyn Fn(&str) -> String + Send + Sync>,
}
impl Default for CacheConfiguration {
fn default() -> Self {
Self {
cache_keygen: Arc::new(str::to_owned),
}
}
}
#[derive(Clone)]
pub struct SqlxPostgresqlSessionStore {
client_pool: Arc<Pool<Postgres>>,
configuration: CacheConfiguration,
}
fn generate_session_key() -> SessionKey {
let value = std::iter::repeat(())
.map(|()| OsRng.sample(Alphanumeric))
.take(64)
.collect::<Vec<_>>();
// These unwraps will never panic because pre-conditions are always verified
// (i.e. length and character set)
String::from_utf8(value).unwrap().try_into().unwrap()
}
impl SqlxPostgresqlSessionStore {
pub fn from_pool(pool: Arc<Pool<Postgres>>) -> SqlxPostgresqlSessionStore {
SqlxPostgresqlSessionStore {
client_pool: pool,
configuration: CacheConfiguration::default(),
}
}
}
pub(crate) type SessionState = HashMap<String, String>;
#[async_trait::async_trait(?Send)]
impl SessionStore for SqlxPostgresqlSessionStore {
async fn load(&self, session_key: &SessionKey) -> Result<Option<SessionState>, LoadError> {
let key = (self.configuration.cache_keygen)(session_key.as_ref());
let row =
sqlx::query("SELECT sessionstate FROM session WHERE key = $1 AND expires > NOW()")
.bind(key)
.fetch_optional(self.client_pool.as_ref())
.await
.map_err(Into::into)
.map_err(LoadError::Other)?;
match row {
None => Ok(None),
Some(r) => {
let data: Value = r.get("sessionstate");
let state: SessionState = serde_json::from_value(data)
.map_err(Into::into)
.map_err(LoadError::Deserialization)?;
Ok(Some(state))
}
}
}
async fn save(
&self,
session_state: SessionState,
ttl: &Duration,
) -> Result<SessionKey, SaveError> {
let body = serde_json::to_value(&session_state)
.map_err(Into::into)
.map_err(SaveError::Serialization)?;
let key = generate_session_key();
let cache_key = (self.configuration.cache_keygen)(key.as_ref());
let expires = Utc::now() + chrono::Duration::seconds(ttl.whole_seconds());
sqlx::query("INSERT INTO session(key, sessionstate, expires) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING")
.bind(cache_key)
.bind(body)
.bind(expires)
.execute(self.client_pool.as_ref())
.await
.map_err(Into::into)
.map_err(SaveError::Other)?;
Ok(key)
}
async fn update(
&self,
session_key: SessionKey,
session_state: SessionState,
ttl: &Duration,
) -> Result<SessionKey, UpdateError> {
let body = serde_json::to_value(&session_state)
.map_err(Into::into)
.map_err(UpdateError::Serialization)?;
let cache_key = (self.configuration.cache_keygen)(session_key.as_ref());
let new_expires = Utc::now() + chrono::Duration::seconds(ttl.whole_seconds());
sqlx::query("UPDATE session SET sessionstate = $1, expires = $2 WHERE key = $3")
.bind(body)
.bind(new_expires)
.bind(cache_key)
.execute(self.client_pool.as_ref())
.await
.map_err(Into::into)
.map_err(UpdateError::Other)?;
Ok(session_key)
}
async fn update_ttl(
&self,
session_key: &SessionKey,
ttl: &Duration,
) -> Result<(), anyhow::Error> {
let new_expires = Utc::now() + chrono::Duration::seconds(ttl.whole_seconds());
let key = (self.configuration.cache_keygen)(session_key.as_ref());
sqlx::query("UPDATE session SET expires = $1 WHERE key = $2")
.bind(new_expires)
.bind(key)
.execute(self.client_pool.as_ref())
.await
.map_err(Into::into)
.map_err(UpdateError::Other)?;
Ok(())
}
async fn delete(&self, session_key: &SessionKey) -> Result<(), anyhow::Error> {
let key = (self.configuration.cache_keygen)(session_key.as_ref());
sqlx::query("DELETE FROM session WHERE key = $1")
.bind(key)
.execute(self.client_pool.as_ref())
.await
.map_err(Into::into)
.map_err(UpdateError::Other)?;
Ok(())
}
}