Compare commits

...

4 Commits

Author SHA1 Message Date
8526a45e13 Use new authenticator 2023-08-13 15:31:05 +02:00
7ec783651f Add middleware factory 2023-08-12 21:41:41 +02:00
f498846c53 New layout 2023-08-11 22:31:16 +02:00
6d42bb477a New layout 2023-08-11 22:31:16 +02:00
14 changed files with 204 additions and 137 deletions

4
Cargo.lock generated
View File

@ -2517,7 +2517,6 @@ name = "oswilno"
version = "0.1.0"
dependencies = [
"actix",
"actix-jwt-session",
"actix-rt",
"actix-web",
"actix-web-grants",
@ -2594,6 +2593,8 @@ dependencies = [
"askama",
"askama_actix",
"oswilno-contract",
"oswilno-session",
"oswilno-view",
"sea-orm",
"serde",
"serde_json",
@ -2636,6 +2637,7 @@ version = "0.1.0"
dependencies = [
"actix-web",
"askama",
"askama_actix",
"futures-core",
"garde",
"tracing",

View File

@ -1,5 +1,5 @@
use actix_web::{HttpMessage, FromRequest};
use actix_web::{dev::ServiceRequest, HttpResponse};
use actix_web::HttpMessage;
use jsonwebtoken::{decode, DecodingKey, Validation};
use serde::{de::DeserializeOwned, Serialize};
use std::sync::Arc;
@ -20,6 +20,10 @@ pub enum Error {
InvalidSession,
#[error("No http authentication header")]
NoAuthHeader,
#[error("Failed to serialize claims")]
SerializeFailed,
#[error("Unable to write claims to storage")]
WriteFailed,
}
impl actix_web::ResponseError for Error {
@ -35,13 +39,41 @@ impl actix_web::ResponseError for Error {
}
}
#[derive(Clone)]
pub struct Authenticated<T>(Arc<T>);
impl<T> std::ops::Deref for Authenticated<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&*self.0
}
}
impl<T: Claims> FromRequest for Authenticated<T> {
type Error = actix_web::error::Error;
type Future = std::future::Ready<Result<Self, actix_web::Error>>;
fn from_request(
req: &actix_web::HttpRequest,
_payload: &mut actix_web::dev::Payload,
) -> Self::Future {
let value = req.extensions_mut().get::<Authenticated<T>>().map(Clone::clone);
std::future::ready(value.ok_or_else(|| Error::NotFound.into()))
}
}
#[async_trait::async_trait(?Send)]
pub trait TokenStorage {
type ClaimsType: Claims;
async fn get_from_jti(self: Arc<Self>, jti: uuid::Uuid) -> Result<Self::ClaimsType, Error>;
async fn set_by_jti(
self: Arc<Self>,
claims: Self::ClaimsType,
exp: std::time::Duration,
) -> Result<(), Error>;
}
struct Extractor;
@ -53,7 +85,6 @@ impl Extractor {
jwt_validator: Arc<Validation>,
storage: Arc<dyn TokenStorage<ClaimsType = ClaimsType>>,
) -> Result<(), Error> {
let authorisation_header = req
.headers()
.get("Authorization")

View File

@ -1,8 +1,9 @@
use super::*;
use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse};
use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform};
use futures_util::future::LocalBoxFuture;
use jsonwebtoken::{DecodingKey, Validation};
use redis::AsyncCommands;
use std::future::{ready, Ready};
use std::marker::PhantomData;
use std::rc::Rc;
use std::sync::Arc;
@ -38,6 +39,20 @@ where
.map_err(|_| Error::NotFound)?;
bincode::deserialize(&val).map_err(|_| Error::RecordMalformed)
}
async fn set_by_jti(
self: Arc<Self>,
claims: Self::ClaimsType,
exp: std::time::Duration,
) -> Result<(), Error> {
let pool = self.pool.clone();
let mut conn = pool.get().await.map_err(|_| Error::RedisConn)?;
let val = bincode::serialize(&claims).map_err(|_| Error::SerializeFailed)?;
conn.set_ex::<_, _, String>(claims.jti().as_bytes(), val, exp.as_secs() as usize)
.await
.map_err(|_| Error::WriteFailed)?;
Ok(())
}
}
pub struct RedisMiddleware<S, ClaimsType>
@ -79,6 +94,51 @@ where
}
}
#[derive(Clone)]
pub struct RedisMiddlewareFactory<ClaimsType: Claims> {
jwt_decoding_key: Arc<DecodingKey>,
jwt_validator: Arc<Validation>,
storage: Arc<RedisStorage<ClaimsType>>,
_claims_type_marker: PhantomData<ClaimsType>,
}
impl<ClaimsType: Claims> RedisMiddlewareFactory<ClaimsType> {
pub fn new(
jwt_decoding_key: Arc<DecodingKey>,
jwt_validator: Arc<Validation>,
storage: Arc<RedisStorage<ClaimsType>>,
) -> Self {
Self {
jwt_decoding_key,
jwt_validator,
storage,
_claims_type_marker: Default::default(),
}
}
}
impl<S, B, ClaimsType> Transform<S, ServiceRequest> for RedisMiddlewareFactory<ClaimsType>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error> + 'static,
ClaimsType: Claims,
{
type Response = ServiceResponse<B>;
type Error = actix_web::Error;
type Transform = RedisMiddleware<S, ClaimsType>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(RedisMiddleware {
service: Rc::new(service),
storage: self.storage.clone(),
jwt_decoding_key: self.jwt_decoding_key.clone(),
jwt_validator: self.jwt_validator.clone(),
_claims_type_marker: PhantomData,
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -95,6 +155,5 @@ mod tests {
}
#[tokio::test]
async fn extract() {
}
async fn extract() {}
}

View File

@ -32,6 +32,7 @@ pub struct Model {
pub account_id: i32,
pub created_at: DateTime,
pub updated_at: DateTime,
pub spot: Option<i32>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
@ -42,6 +43,7 @@ pub enum Column {
AccountId,
CreatedAt,
UpdatedAt,
Spot,
}
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
@ -72,6 +74,7 @@ impl ColumnTrait for Column {
Self::AccountId => ColumnType::Integer.def(),
Self::CreatedAt => ColumnType::DateTime.def(),
Self::UpdatedAt => ColumnType::DateTime.def(),
Self::Spot => ColumnType::Integer.def().null(),
}
}
}

View File

@ -10,6 +10,8 @@ actix-web = "4.3.1"
askama = { version = "0.12.0", features = ["serde", "with-actix-web", "comrak", "mime"] }
askama_actix = "0.14.0"
oswilno-contract = { path = "../oswilno-contract" }
oswilno-session = { path = "../oswilno-session" }
oswilno-view = { path = "../oswilno-view" }
sea-orm = { version = "0.11.1", features = ["runtime-actix-rustls", "sqlx", "sqlx-postgres", "debug-print"] }
serde = { version = "1.0.176", features = ["derive"] }
serde_json = "1.0.104"

View File

@ -5,25 +5,22 @@ use oswilno_contract::accounts;
use oswilno_contract::parking_space_rents;
use oswilno_contract::parking_spaces;
use sea_orm::prelude::*;
use sea_orm::ActiveValue::{NotSet, Set};
use std::collections::HashMap;
use std::sync::Arc;
use oswilno_session::{Claims, Authenticated};
use oswilno_view::Layout;
pub fn mount(config: &mut ServiceConfig) {
config.service(
scope("/parking_spaces")
.service(all_parking_spaces)
.service(all_parial_parking_spaces),
.service(all_parial_parking_spaces)
.service(create),
);
}
#[derive(Template)]
#[template(path = "../templates/parking-spaces/all.html")]
struct AllParkingSpace {
parking_space_rents: Vec<parking_space_rents::Model>,
parking_space_by_id: HashMap<i32, parking_spaces::Model>,
account_by_id: HashMap<i32, accounts::Model>,
}
#[derive(Template)]
#[template(path = "../templates/parking-spaces/all-partial.html")]
struct AllPartialParkingSpace {
@ -33,10 +30,13 @@ struct AllPartialParkingSpace {
}
#[get("/all")]
async fn all_parking_spaces(db: Data<sea_orm::DatabaseConnection>) -> AllParkingSpace {
async fn all_parking_spaces(
db: Data<sea_orm::DatabaseConnection>,
) -> Layout<AllPartialParkingSpace> {
let db = db.into_inner();
load_parking_spaces(db).await
let main = load_parking_spaces(db).await;
oswilno_view::Layout { main }
}
#[get("/all-partial")]
@ -44,20 +44,10 @@ async fn all_parial_parking_spaces(
db: Data<sea_orm::DatabaseConnection>,
) -> AllPartialParkingSpace {
let db = db.into_inner();
let AllParkingSpace {
parking_space_rents,
parking_space_by_id,
account_by_id,
} = load_parking_spaces(db).await;
AllPartialParkingSpace {
parking_space_rents,
parking_space_by_id,
account_by_id,
}
load_parking_spaces(db).await
}
async fn load_parking_spaces(db: Arc<DatabaseConnection>) -> AllParkingSpace {
async fn load_parking_spaces(db: Arc<DatabaseConnection>) -> AllPartialParkingSpace {
let rents = parking_space_rents::Entity::find().all(&*db).await.unwrap();
let (parking_space_by_id, account_ids) = {
let ids = rents
@ -94,13 +84,19 @@ async fn load_parking_spaces(db: Arc<DatabaseConnection>) -> AllParkingSpace {
})
};
AllParkingSpace {
AllPartialParkingSpace {
account_by_id,
parking_space_rents: rents,
parking_space_by_id,
}
}
#[derive(Debug, Template)]
#[template(path = "../templates/parking-spaces/form-partial.html")]
struct ParkingSpaceFormPartial {
form: CreateParkingSpace,
}
#[derive(Debug, serde::Deserialize)]
struct CreateParkingSpace {
location: String,
@ -111,7 +107,28 @@ struct CreateParkingSpace {
async fn create(
db: Data<sea_orm::DatabaseConnection>,
p: Form<CreateParkingSpace>,
session: Authenticated<Claims>,
) -> HttpResponse {
let p = p.into_inner();
use oswilno_contract::parking_spaces::*;
let CreateParkingSpace { location, spot } = p.into_inner();
let db = db.into_inner();
let model = ActiveModel {
id: NotSet,
location: Set(location.clone()),
spot: Set(spot.map(|n| n as i32)),
account_id: Set(session.account_id()),
..Default::default()
};
if let Err(_e) = model.save(&*db).await {
return HttpResponse::BadRequest().body(
ParkingSpaceFormPartial {
form: CreateParkingSpace { location, spot },
}
.render()
.unwrap(),
);
}
unreachable!()
}

View File

@ -15,7 +15,6 @@ oswilno-config = { path = "../oswilno-config" }
oswilno-parking-space = { path = "../oswilno-parking-space" }
oswilno-session = { path = "../oswilno-session" }
oswilno-view = { path = "../oswilno-view" }
actix-jwt-session = { path = "../actix-jwt-session", features = ["use-redis"] }
redis = { version = "0.17" }
redis-async-pool = "0.2.4"
sea-orm = { version = "0.11", features = ["postgres-array", "runtime-actix-rustls", "sqlx-postgres"] }

View File

@ -50,8 +50,7 @@ async fn main() -> std::io::Result<()> {
let session_config = session_config.clone();
App::new()
.wrap(middleware::Logger::default())
.wrap(actix_jwt_session::RedisMiddleware::new())
// .wrap(session_config.factory())
.wrap(session_config.factory())
.app_data(Data::new(conn.clone()))
.app_data(Data::new(redis.clone()))
.app_data(Data::new(l10n.clone()))

View File

@ -17,7 +17,7 @@ garde = { version = "0.14.0", features = ["derive"] }
jsonwebtoken = "8.3.0"
oswilno-contract = { path = "../oswilno-contract" }
oswilno-view = { path = "../oswilno-view" }
actix-jwt-session = { path = "../actix-jwt-session" }
actix-jwt-session = { path = "../actix-jwt-session", features = ["use-redis"] }
rand = "0.8.5"
redis = { version = "0.17" }
redis-async-pool = "0.2.4"

View File

@ -1,31 +1,27 @@
use std::ops::Add;
use std::sync::Arc;
use actix_jwt_authc::*;
pub use actix_jwt_session::{Error, RedisMiddlewareFactory, Authenticated};
use actix_web::web::{Data, Form, ServiceConfig};
use actix_web::{get, post, HttpResponse};
use askama_actix::Template;
use autometrics::autometrics;
use futures::channel::{mpsc, mpsc::Sender};
use futures::stream::Stream;
use futures::SinkExt;
use garde::Validate;
use jsonwebtoken::*;
use oswilno_view::{Errors, Lang, TranslationStorage};
use oswilno_view::{Errors, Lang, Layout, TranslationStorage};
use ring::rand::SystemRandom;
use ring::signature::{Ed25519KeyPair, KeyPair};
use sea_orm::DatabaseConnection;
use serde::{Deserialize, Serialize};
use time::ext::*;
use time::OffsetDateTime;
use tokio::sync::Mutex;
mod extract_session;
mod hashing;
pub use oswilno_view::filters;
pub type UserSession = Authenticated<Claims>;
pub type UserSession = Claims;
#[derive(Clone, Copy)]
pub struct JWTTtl(time::Duration);
@ -51,6 +47,23 @@ pub struct Claims {
jwt_id: uuid::Uuid,
}
impl actix_jwt_session::Claims for Claims {
fn jti(&self) -> uuid::Uuid {
self.jwt_id
}
}
impl Claims {
pub fn account_id(&self) -> i32 {
self.subject
.split_once('-')
.filter(|(desc, _id)| *desc == "account")
.map(|(_d, id)| id)
.and_then(|id| id.parse().ok())
.unwrap_or_default()
}
}
#[derive(Serialize, Deserialize)]
pub struct EmptyResponse {}
@ -65,15 +78,13 @@ const JWT_SIGNING_ALGO: Algorithm = Algorithm::EdDSA;
#[derive(Clone)]
pub struct SessionConfigurator {
jwt_ttl: Data<JWTTtl>,
invalidated_jwts_store: Data<InvalidatedJWTStore>,
encoding_key: Data<EncodingKey>,
factory: AuthenticateMiddlewareFactory<Claims>,
factory: RedisMiddlewareFactory<Claims>,
}
impl SessionConfigurator {
pub fn app_data(self, config: &mut ServiceConfig) {
config
.app_data(self.invalidated_jwts_store)
.app_data(self.encoding_key)
.app_data(self.jwt_ttl)
.service(login)
@ -86,7 +97,7 @@ impl SessionConfigurator {
.service(register_partial_view);
}
pub fn factory(&self) -> AuthenticateMiddlewareFactory<Claims> {
pub fn factory(&self) -> RedisMiddlewareFactory<Claims> {
self.factory.clone()
}
@ -122,18 +133,13 @@ impl SessionConfigurator {
let jwt_ttl = JWTTtl(31.days());
let jwt_signing_keys = JwtSigningKeys::generate().unwrap();
let validator = Validation::new(JWT_SIGNING_ALGO);
let auth_middleware_settings = AuthenticateMiddlewareSettings {
jwt_decoding_key: jwt_signing_keys.decoding_key,
jwt_authorization_header_prefixes: Some(vec!["Bearer".to_string()]),
jwt_validator: validator,
jwt_session_key: None,
};
let (invalidated_jwts_store, stream) = InvalidatedJWTStore::new_with_stream(redis);
let auth_middleware_factory =
AuthenticateMiddlewareFactory::<Claims>::new(stream, auth_middleware_settings.clone());
let auth_middleware_factory = RedisMiddlewareFactory::<Claims>::new(
Arc::new(jwt_signing_keys.decoding_key),
Arc::new(validator),
Arc::new(actix_jwt_session::RedisStorage::new(redis)),
);
Self {
invalidated_jwts_store: Data::new(invalidated_jwts_store.clone()),
encoding_key: Data::new(jwt_signing_keys.encoding_key.clone()),
jwt_ttl: Data::new(jwt_ttl.clone()),
factory: auth_middleware_factory,
@ -141,15 +147,6 @@ impl SessionConfigurator {
}
}
#[derive(Template)]
#[template(path = "./sign-in/full.html")]
struct SignInTemplate {
form: SignInPayload,
lang: Lang,
t: Arc<TranslationStorage>,
errors: Errors,
}
#[derive(Template)]
#[template(path = "./sign-in/partial.html")]
struct SignInPartialTemplate {
@ -166,12 +163,14 @@ pub struct SignInPayload {
}
#[get("/login")]
async fn login_view(t: Data<TranslationStorage>) -> SignInTemplate {
SignInTemplate {
form: SignInPayload::default(),
lang: Lang::Pl,
t: t.into_inner(),
errors: Errors::default(),
async fn login_view(t: Data<TranslationStorage>) -> Layout<SignInPartialTemplate> {
oswilno_view::Layout {
main: SignInPartialTemplate {
form: SignInPayload::default(),
lang: Lang::Pl,
t: t.into_inner(),
errors: Errors::default(),
},
}
}
#[get("/p/login")]
@ -304,8 +303,8 @@ async fn login_inner(
#[autometrics]
#[get("/session")]
async fn session_info(authenticated: UserSession) -> Result<HttpResponse, Error> {
Ok(HttpResponse::Ok().json(authenticated))
async fn session_info(authenticated: Authenticated<Claims>) -> Result<HttpResponse, Error> {
Ok(HttpResponse::Ok().json(&*authenticated))
}
#[autometrics]
@ -316,7 +315,7 @@ async fn logout(
) -> Result<HttpResponse, Error> {
{
use redis::AsyncCommands;
let jwt_id = authenticated.claims.jwt_id;
let jwt_id = authenticated.jwt_id;
if let Ok(mut conn) = redis.get().await {
if conn.del::<_, String>(jwt_id.as_bytes()).await.is_err() {}
}
@ -336,27 +335,20 @@ struct AccountInfo {
password: String,
}
#[derive(askama_actix::Template)]
#[template(path = "./register/full.html")]
struct RegisterTemplate {
form: AccountInfo,
t: Arc<TranslationStorage>,
lang: Lang,
errors: oswilno_view::Errors,
}
#[get("/register")]
async fn register_view(t: Data<TranslationStorage>) -> RegisterTemplate {
RegisterTemplate {
form: AccountInfo::default(),
t: t.into_inner(),
lang: Lang::Pl,
errors: oswilno_view::Errors::default(),
async fn register_view(t: Data<TranslationStorage>) -> Layout<RegisterPartialTemplate> {
Layout {
main: RegisterPartialTemplate {
form: AccountInfo::default(),
t: t.into_inner(),
lang: Lang::Pl,
errors: oswilno_view::Errors::default(),
},
}
}
#[get("/p/register")]
async fn register_partial_view(t: Data<TranslationStorage>) -> RegisterTemplate {
RegisterTemplate {
async fn register_partial_view(t: Data<TranslationStorage>) -> RegisterPartialTemplate {
RegisterPartialTemplate {
form: AccountInfo::default(),
t: t.into_inner(),
lang: Lang::Pl,
@ -509,48 +501,7 @@ async fn register_internal(
.json(EmptyResponse {}))
}
#[derive(Clone)]
struct InvalidatedJWTStore {
// store: Arc<DashSet<JWT>>,
redis: redis_async_pool::RedisPool,
tx: Arc<Mutex<Sender<InvalidatedTokensEvent>>>,
}
impl InvalidatedJWTStore {
/// Returns a [InvalidatedJWTStore] with a Stream of [InvalidatedTokensEvent]s
fn new_with_stream(
redis: redis_async_pool::RedisPool,
) -> (
InvalidatedJWTStore,
impl Stream<Item = InvalidatedTokensEvent>,
) {
// let invalidated = Arc::new(DashSet::new());
let (tx, rx) = mpsc::channel(100);
let tx_to_hold = Arc::new(Mutex::new(tx));
(
InvalidatedJWTStore {
// store: invalidated,
redis,
tx: tx_to_hold,
},
rx,
)
}
async fn add_to_invalidated(&self, authenticated: Authenticated<Claims>) {
// self.store.insert(authenticated.jwt.clone());
let mut tx = self.tx.lock().await;
if let Err(_e) = tx
.send(InvalidatedTokensEvent::Add(authenticated.jwt))
.await
{
#[cfg(feature = "tracing")]
error!(error = ?_e, "Failed to send update on adding to invalidated")
}
}
}
struct JwtSigningKeys {
pub struct JwtSigningKeys {
encoding_key: EncodingKey,
decoding_key: DecodingKey,
}

View File

@ -6,6 +6,7 @@ edition = "2021"
[dependencies]
actix-web = "4.3.1"
askama = { version = "0.12.0", features = ["serde", "with-actix-web", "comrak", "mime"] }
askama_actix = { version = "0.14.0" }
futures-core = "0.3.28"
garde = { version = "0.14.0", features = ["derive"] }
tracing = "0.1.37"

View File

@ -6,6 +6,12 @@ pub use lang::*;
pub mod filters;
pub mod lang;
#[derive(Debug, askama_actix::Template)]
#[template(path = "../templates/base.html")]
pub struct Layout<BodyTemplate: askama::DynTemplate + std::fmt::Display> {
pub main: BodyTemplate,
}
#[derive(Debug, Clone, Default)]
pub struct Errors {
errors: Vec<String>,

View File

@ -11,9 +11,6 @@
</head>
<body>
<base url="/" />
<main>
{% block body %}
{% endblock %}
</main>
<main>{{ main }}</main>
</body>
</html>