feat: use postgres session
This commit is contained in:
parent
1915baf094
commit
12702ab85d
2
.env
2
.env
@ -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
1245
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||||
|
@ -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);
|
||||||
|
42
src/main.rs
42
src/main.rs
@ -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))?
|
||||||
|
146
src/postgres_session_store.rs
Normal file
146
src/postgres_session_store.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user