Working on signed in user

This commit is contained in:
Adrian Woźniak 2023-08-16 16:53:27 +02:00
parent 4d0e589a44
commit 9a05b64eaf
17 changed files with 162 additions and 72 deletions

1
Cargo.lock generated
View File

@ -2638,6 +2638,7 @@ dependencies = [
name = "oswilno-view"
version = "0.1.0"
dependencies = [
"actix-jwt-session",
"actix-web",
"askama",
"askama_actix",

View File

@ -771,6 +771,10 @@ select {
top: 0px;
}
.-z-10 {
z-index: -10;
}
.z-50 {
z-index: 50;
}
@ -853,6 +857,10 @@ select {
height: 2rem;
}
.h-\[28rem\] {
height: 28rem;
}
.h-full {
height: 100%;
}
@ -976,16 +984,6 @@ select {
border-width: 2px;
}
.border-gray-100 {
--tw-border-opacity: 1;
border-color: rgb(243 244 246 / var(--tw-border-opacity));
}
.border-gray-200 {
--tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
}
.border-gray-300 {
--tw-border-opacity: 1;
border-color: rgb(209 213 219 / var(--tw-border-opacity));
@ -1021,6 +1019,10 @@ select {
background-color: rgb(254 226 226 / var(--tw-bg-opacity));
}
.bg-transparent {
background-color: transparent;
}
.bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
@ -1310,16 +1312,6 @@ select {
background-color: rgb(55 65 81 / var(--tw-bg-opacity));
}
.dark\:bg-gray-800 {
--tw-bg-opacity: 1;
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
}
.dark\:bg-gray-900 {
--tw-bg-opacity: 1;
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
}
.dark\:text-gray-200 {
--tw-text-opacity: 1;
color: rgb(229 231 235 / var(--tw-text-opacity));
@ -1438,11 +1430,6 @@ select {
background-color: transparent;
}
.md\:bg-white {
--tw-bg-opacity: 1;
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.md\:p-0 {
padding: 0px;
}
@ -1462,11 +1449,6 @@ select {
}
@media (prefers-color-scheme: dark) {
.md\:dark\:bg-gray-900 {
--tw-bg-opacity: 1;
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
}
.md\:dark\:text-blue-500 {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity));

View File

@ -4,11 +4,13 @@ use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Validati
use serde::{de::DeserializeOwned, Serialize};
use std::sync::Arc;
use uuid::Uuid;
use async_trait::async_trait;
pub static HEADER_NAME: &str = "Authorization";
pub trait Claims: PartialEq + DeserializeOwned + Serialize + Clone + Send + Sync + 'static {
fn jti(&self) -> uuid::Uuid;
fn subject(&self) -> &str;
}
#[derive(Debug, thiserror::Error, PartialEq)]
@ -121,7 +123,7 @@ impl<T: Claims> FromRequest for MaybeAuthenticated<T> {
}
}
#[async_trait::async_trait(?Send)]
#[async_trait(?Send)]
pub trait TokenStorage: Send + Sync {
type ClaimsType: Claims;
@ -182,10 +184,54 @@ impl<ClaimsType: Claims> SessionStorage<ClaimsType> {
}
}
struct Extractor;
#[async_trait(?Send)]
pub trait Extractor {
async fn extract_jwt<ClaimsType: Claims>(
req: &ServiceRequest,
jwt_encoding_key: Arc<EncodingKey>,
jwt_decoding_key: Arc<DecodingKey>,
algorithm: Algorithm,
storage: SessionStorage<ClaimsType>,
) -> Result<(), Error>;
impl Extractor {
async fn extract_bearer_jwt<ClaimsType: Claims>(
fn decode<ClaimsType: Claims>(value: &str,
jwt_decoding_key: Arc<DecodingKey>,
algorithm: Algorithm,
) -> Result<ClaimsType, Error> {
decode::<ClaimsType>(value, &*jwt_decoding_key, &Validation::new(algorithm)).map_err(
|_e| {
// let error_message = e.to_string();
Error::InvalidSession
},
)
}
}
pub struct CookieExtractor;
#[async_trait(?Send)]
impl Extractor for CookieExtractor {
async fn extract_jwt<ClaimsType: Claims>(
req: &ServiceRequest,
jwt_encoding_key: Arc<EncodingKey>,
jwt_decoding_key: Arc<DecodingKey>,
algorithm: Algorithm,
storage: SessionStorage<ClaimsType>,
) -> Result<(), Error> {
let Some(cookie) = req.cookie(HEADER_NAME) else {return Ok(())};
let value = cookie.value();
Ok(())
}
}
pub struct HeaderExtractor;
#[async_trait(?Send)]
impl Extractor for HeaderExtractor {
async fn extract_jwt<ClaimsType: Claims>(
req: &ServiceRequest,
jwt_encoding_key: Arc<EncodingKey>,
jwt_decoding_key: Arc<DecodingKey>,

View File

@ -112,7 +112,7 @@ where
let storage = self.storage.clone();
async move {
Extractor::extract_bearer_jwt(
HeaderExtractor::extract_jwt(
&req,
jwt_encoding_key,
jwt_decoding_key,

View File

@ -7,6 +7,7 @@ mod m20230726_124452_images;
mod m20230726_135630_parking_spaces;
mod m20230805_000001_add_email;
mod m20230809_135630_add_spot;
mod m20230810_105100_create_parking_space_locations;
pub struct Migrator;
@ -19,6 +20,7 @@ impl MigratorTrait for Migrator {
Box::new(m20230726_135630_parking_spaces::Migration),
Box::new(m20230805_000001_add_email::Migration),
Box::new(m20230809_135630_add_spot::Migration),
Box::new(m20230810_105100_create_parking_space_locations::Migration),
]
}
}

View File

@ -137,6 +137,7 @@ pub enum ParkingSpace {
Id,
State,
Location,
LocationId,
Spot,
AccountId,
CreatedAt,

View File

@ -4,6 +4,7 @@ pub mod prelude;
pub mod accounts;
pub mod images;
pub mod parking_space_locations;
pub mod parking_space_rents;
pub mod parking_spaces;
pub mod sea_orm_active_enums;

View File

@ -28,22 +28,22 @@ pub struct Model {
#[actix_admin(primary_key)]
pub id: i32,
pub state: ParkingSpaceState,
pub location: String,
pub account_id: i32,
pub created_at: DateTime,
pub updated_at: DateTime,
pub spot: Option<i32>,
pub location_id: Option<i32>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
Id,
State,
Location,
AccountId,
CreatedAt,
UpdatedAt,
Spot,
LocationId,
}
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
@ -61,6 +61,7 @@ impl PrimaryKeyTrait for PrimaryKey {
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
Accounts,
ParkingSpaceLocations,
ParkingSpaceRents,
}
@ -70,11 +71,11 @@ impl ColumnTrait for Column {
match self {
Self::Id => ColumnType::Integer.def(),
Self::State => ParkingSpaceState::db_type(),
Self::Location => ColumnType::String(None).def(),
Self::AccountId => ColumnType::Integer.def(),
Self::CreatedAt => ColumnType::DateTime.def(),
Self::UpdatedAt => ColumnType::DateTime.def(),
Self::Spot => ColumnType::Integer.def().null(),
Self::LocationId => ColumnType::Integer.def().null(),
}
}
}
@ -86,6 +87,12 @@ impl RelationTrait for Relation {
.from(Column::AccountId)
.to(super::accounts::Column::Id)
.into(),
Self::ParkingSpaceLocations => {
Entity::belongs_to(super::parking_space_locations::Entity)
.from(Column::LocationId)
.to(super::parking_space_locations::Column::Id)
.into()
}
Self::ParkingSpaceRents => Entity::has_many(super::parking_space_rents::Entity).into(),
}
}
@ -97,6 +104,12 @@ impl Related<super::accounts::Entity> for Entity {
}
}
impl Related<super::parking_space_locations::Entity> for Entity {
fn to() -> RelationDef {
Relation::ParkingSpaceLocations.def()
}
}
impl Related<super::parking_space_rents::Entity> for Entity {
fn to() -> RelationDef {
Relation::ParkingSpaceRents.def()

View File

@ -2,5 +2,6 @@
pub use super::accounts::Entity as Accounts;
pub use super::images::Entity as Images;
pub use super::parking_space_locations::Entity as ParkingSpaceLocations;
pub use super::parking_space_rents::Entity as ParkingSpaceRents;
pub use super::parking_spaces::Entity as ParkingSpaces;

View File

@ -1,3 +1,4 @@
use actix_web::http::header::ContentType;
use actix_web::web::{scope, Data, Form, ServiceConfig};
use actix_web::{get, post, HttpRequest, HttpResponse};
use askama_actix::Template;
@ -6,7 +7,7 @@ use oswilno_contract::accounts;
use oswilno_contract::parking_space_rents;
use oswilno_contract::parking_spaces;
use oswilno_session::{Authenticated, MaybeAuthenticated};
use oswilno_view::{is_partial, Layout, Main, MainOpts, SearchOpts, SessionOpts, Blank};
use oswilno_view::{is_partial, Blank, Layout, Main, MainOpts, SearchOpts, SessionOpts};
use sea_orm::prelude::*;
use sea_orm::ActiveValue::{NotSet, Set};
use std::collections::HashMap;
@ -14,10 +15,10 @@ use std::sync::Arc;
pub fn mount(config: &mut ServiceConfig) {
config.service(root).service(
scope("/parking_spaces")
scope("/parking-spaces")
.service(form_show)
.service(all_parking_spaces)
.service(all_parial_parking_spaces)
.service(create),
.service(create)
);
}
@ -25,7 +26,7 @@ pub fn mount(config: &mut ServiceConfig) {
#[get("/")]
async fn root() -> HttpResponse {
HttpResponse::SeeOther()
.append_header(("Location", "/parking_spaces/all"))
.append_header(("Location", "/parking-spaces/all"))
.body("")
}
@ -35,6 +36,7 @@ struct AllPartialParkingSpace {
parking_space_rents: Vec<parking_space_rents::Model>,
parking_space_by_id: HashMap<i32, parking_spaces::Model>,
account_by_id: HashMap<i32, accounts::Model>,
session: Option<SessionOpts>,
}
#[autometrics]
@ -45,15 +47,14 @@ async fn all_parking_spaces(
session: MaybeAuthenticated,
) -> HttpResponse {
let db = db.into_inner();
let parking_spaces = load_parking_spaces(db).await;
let session = session.into_option().map(Into::into);
let mut parking_spaces = load_parking_spaces(db).await;
parking_spaces.session = session.clone();
let main = Main {
body: parking_spaces,
title: Blank,
opts: MainOpts {
session: session.into_option().map(|s| SessionOpts {
login: s.subject.to_owned(),
profile_image_url: None,
}),
session,
search: Some(SearchOpts {
target_url: None,
autocomplete: Vec::with_capacity(0),
@ -76,15 +77,6 @@ async fn search_parking_spaces() -> HttpResponse {
HttpResponse::Ok().body("")
}
#[autometrics]
#[get("/all-partial")]
async fn all_parial_parking_spaces(
db: Data<sea_orm::DatabaseConnection>,
) -> AllPartialParkingSpace {
let db = db.into_inner();
load_parking_spaces(db).await
}
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) = {
@ -126,35 +118,61 @@ async fn load_parking_spaces(db: Arc<DatabaseConnection>) -> AllPartialParkingSp
account_by_id,
parking_space_rents: rents,
parking_space_by_id,
session: None,
}
}
#[derive(Debug, Template)]
#[get("/form")]
async fn form_show(req: HttpRequest, session: Authenticated) -> HttpResponse {
let session = session.into();
let body = ParkingSpaceFormPartial {
..Default::default()
};
let main = Main {
body,
title: Blank,
opts: MainOpts {
show: true,
search: None,
session: Some(session),
},
};
let html = if is_partial(&req) {
main.render()
} else {
Layout { main }.render()
};
HttpResponse::Ok()
.append_header(ContentType::html())
.body(html.unwrap())
}
#[derive(Debug, Default, Template)]
#[template(path = "../templates/parking-spaces/form-partial.html")]
struct ParkingSpaceFormPartial {
form: CreateParkingSpace,
}
#[derive(Debug, serde::Deserialize)]
#[derive(Debug, Default, serde::Deserialize)]
struct CreateParkingSpace {
location: String,
location_id: u32,
spot: Option<u32>,
}
#[autometrics]
#[post("/parking-spaces")]
#[post("/create")]
async fn create(
db: Data<sea_orm::DatabaseConnection>,
p: Form<CreateParkingSpace>,
session: Authenticated,
) -> HttpResponse {
use oswilno_contract::parking_spaces::*;
let CreateParkingSpace { location, spot } = p.into_inner();
let CreateParkingSpace { location_id, spot } = p.into_inner();
let db = db.into_inner();
let model = ActiveModel {
id: NotSet,
location: Set(location.clone()),
location_id: Set(Some(location_id.clone() as i32)),
spot: Set(spot.map(|n| n as i32)),
account_id: Set(session.account_id()),
..Default::default()
@ -162,7 +180,7 @@ async fn create(
if let Err(_e) = model.save(&*db).await {
return HttpResponse::BadRequest().body(
ParkingSpaceFormPartial {
form: CreateParkingSpace { location, spot },
form: CreateParkingSpace { location_id, spot },
}
.render()
.unwrap(),

View File

@ -8,11 +8,20 @@
</div>
<div>
</div>
{% match session %}
{% when Some with (session) %}
<div>
<button class="text-emerald-500 background-transparent font-bold uppercase px-6 py-2 text-sm outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150 border-2 rounded-full">
<a
href="/parking-spaces/form"
x-get="/parking-spaces/form"
x-target="main"
class="text-emerald-500 background-transparent font-bold uppercase px-6 py-2 text-sm outline-none focus:outline-none mr-1 mb-1 ease-linear transition-all duration-150 border-2 rounded-full"
>
<i class="fas fa-plus"></i>
</button>
</a>
</div>
{% when None %}
{% endmatch %}
</section>
<oswilno-parking-space-rents>
{% for parking_space_rent in parking_space_rents -%}

View File

@ -9,7 +9,7 @@ use askama_actix::Template;
use autometrics::autometrics;
use garde::Validate;
use jsonwebtoken::*;
use oswilno_view::{Errors, Lang, Layout, Main, MainOpts, TranslationStorage, Blank};
use oswilno_view::{Blank, Errors, Lang, Layout, Main, MainOpts, TranslationStorage};
use ring::rand::SystemRandom;
use ring::signature::{Ed25519KeyPair, KeyPair};
use sea_orm::DatabaseConnection;
@ -54,6 +54,10 @@ impl actix_jwt_session::Claims for Claims {
fn jti(&self) -> uuid::Uuid {
self.jwt_id
}
fn subject(&self) -> &str {
&self.subject
}
}
impl Claims {
@ -290,11 +294,14 @@ async fn login_inner(
return Err(payload);
}
};
let cookie = actix_web::cookie::Cookie::build(actix_jwt_session::HEADER_NAME, &bearer_token).http_only(true).finish();
Ok(HttpResponse::Ok()
.append_header((
actix_jwt_session::HEADER_NAME,
format!("Bearer {bearer_token}").as_str(),
))
.cookie(cookie)
.body(""))
}

View File

@ -10,3 +10,4 @@ askama_actix = { version = "0.14.0" }
futures-core = "0.3.28"
garde = { version = "0.14.0", features = ["derive"] }
tracing = "0.1.37"
actix-jwt-session = { path = "../actix-jwt-session" }

View File

@ -34,12 +34,20 @@ pub struct SearchOpts {
pub autocomplete: Vec<(String, String)>,
}
#[derive(Debug, Default)]
#[derive(Debug, Clone, Default)]
pub struct SessionOpts {
pub login: String,
pub profile_image_url: Option<String>,
}
impl<C: actix_jwt_session::Claims> From<actix_jwt_session::Authenticated<C>> for SessionOpts {
fn from(session: actix_jwt_session::Authenticated<C>) -> Self {
Self {
login: session.subject().to_owned(),
profile_image_url: None,
}
}
}
#[derive(Debug)]
pub struct MainOpts {
pub show: bool,

View File

@ -1,7 +1,7 @@
<main>
{% include "navbar.html" %}
<section id="main-header" class="relative pt-16 pb-32 flex content-center items-center justify-center" style="min-height:75vh">
<div class="absolute top-0 w-full h-full bg-center bg-cover" style="background-image:url('/banner.jpg')">
<section id="main-header" class="pt-16 pb-32 flex content-center items-center justify-center">
<div class="absolute -z-10 top-0 w-full h-[28rem] bg-center bg-cover" style="background-image:url('/banner.jpg')">
<span id="blackOverlay" class="w-full h-full absolute opacity-75 bg-black"></span>
</div>
{{ title|safe }}

View File

@ -1,5 +1,5 @@
<div class="items-center justify-between hidden w-full md:flex md:w-auto md:order-1" id="navbar-user">
<ul class="flex flex-col font-medium p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:flex-row md:space-x-8 md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700">
<ul class="flex flex-col font-medium p-4 md:p-0 mt-4 border rounded-lg md:flex-row md:space-x-8 md:mt-0 md:border-0">
<li>
<a
href="/"

View File

@ -1,4 +1,4 @@
<nav class="bg-white border-gray-200 dark:bg-gray-900">
<nav class="bg-transparent">
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
<a href="https://flowbite.com/" class="flex items-center">
<img src="https://flowbite.com/docs/images/logo.svg" class="h-8 mr-3" alt="Flowbite Logo" />