Working session

This commit is contained in:
eraden 2024-10-27 20:23:37 +01:00
parent 0d0845cc7b
commit 2849d9f339
15 changed files with 298 additions and 56 deletions

159
Cargo.lock generated
View File

@ -106,6 +106,22 @@ dependencies = [
"tracing",
]
[[package]]
name = "actix-identity"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b8ddc6f6a8b19c4016aaa13519968da9969bc3bc1c1c883cdb0f25dd6c8cf7"
dependencies = [
"actix-service",
"actix-session",
"actix-utils",
"actix-web",
"derive_more 1.0.0",
"futures-core",
"serde",
"tracing",
]
[[package]]
name = "actix-macros"
version = "0.2.4"
@ -171,6 +187,24 @@ dependencies = [
"pin-project-lite",
]
[[package]]
name = "actix-session"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efe6976a74f34f1b6d07a6c05aadc0ed0359304a7781c367fa5b4029418db08f"
dependencies = [
"actix-service",
"actix-utils",
"actix-web",
"anyhow",
"derive_more 1.0.0",
"rand",
"redis 0.26.1",
"serde",
"serde_json",
"tracing",
]
[[package]]
name = "actix-tls"
version = "3.4.0"
@ -184,7 +218,7 @@ dependencies = [
"impl-more",
"pin-project-lite",
"tokio",
"tokio-rustls",
"tokio-rustls 0.23.4",
"tokio-util",
"tracing",
"webpki-roots 0.22.6",
@ -1072,6 +1106,8 @@ version = "0.1.0"
dependencies = [
"actix",
"actix-files",
"actix-identity",
"actix-session",
"actix-web",
"askama",
"askama_actix",
@ -1080,7 +1116,7 @@ dependencies = [
"humantime",
"humantime-serde",
"migration",
"redis",
"redis 0.27.5",
"rswind",
"rswind_cli",
"sea-orm",
@ -1109,6 +1145,16 @@ dependencies = [
"version_check",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@ -1357,6 +1403,7 @@ dependencies = [
"proc-macro2",
"quote",
"syn 2.0.82",
"unicode-xid",
]
[[package]]
@ -2537,6 +2584,12 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "ordered-float"
version = "3.9.2"
@ -3052,6 +3105,34 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "redis"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e902a69d09078829137b4a5d9d082e0490393537badd7c91a3d69d14639e115f"
dependencies = [
"arc-swap",
"async-trait",
"bytes",
"combine",
"futures",
"futures-util",
"itoa",
"num-bigint",
"percent-encoding",
"pin-project-lite",
"rustls 0.23.15",
"rustls-native-certs",
"rustls-pemfile",
"rustls-pki-types",
"ryu",
"tokio",
"tokio-retry",
"tokio-rustls 0.26.0",
"tokio-util",
"url",
]
[[package]]
name = "redis"
version = "0.27.5"
@ -3431,6 +3512,19 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5"
dependencies = [
"openssl-probe",
"rustls-pemfile",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pemfile"
version = "2.2.0"
@ -3472,6 +3566,15 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "schemars"
version = "1.0.0-alpha.15"
@ -3683,6 +3786,29 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.6.0",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "semver"
version = "1.0.23"
@ -4321,6 +4447,17 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "tokio-retry"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f57eb36ecbe0fc510036adff84824dd3c24bb781e21bfa67b69d556aa85214f"
dependencies = [
"pin-project",
"rand",
"tokio",
]
[[package]]
name = "tokio-retry2"
version = "0.5.6"
@ -4343,6 +4480,17 @@ dependencies = [
"webpki",
]
[[package]]
name = "tokio-rustls"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
dependencies = [
"rustls 0.23.15",
"rustls-pki-types",
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.16"
@ -4510,6 +4658,12 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unicode_categories"
version = "0.1.1"
@ -4562,6 +4716,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
dependencies = [
"getrandom",
"serde",
]
[[package]]

View File

@ -15,7 +15,7 @@ tracing = "0.1.40"
tracing-subscriber = "0.3.18"
serde = "1.0.210"
serde_json = "1.0.132"
uuid = { version = "1.11.0", features = ["v4", "v8"] }
uuid = { version = "1.11.0", features = ["serde", "v4", "v8"] }
migration = { path = "./migration" }
rswind = "0.0.1-alpha.1"
rswind_cli = "0.0.1-alpha.1"
@ -23,5 +23,7 @@ derive_more = { version = "1.0.0", features = ["deref"] }
chrono = "0.4.38"
humantime = "2.1.0"
humantime-serde = "1.1.1"
actix-session = { version = "0.10.1", features = ["redis-session-rustls"] }
actix-identity = "0.8.0"
[build-dependencies]

View File

@ -2191,8 +2191,8 @@ html{
height: 12rem;
}
.md\:w-72{
width: 18rem;
.md\:w-3\/4{
width: 75%;
}
.md\:w-\[225px\]{
@ -2207,10 +2207,6 @@ html{
justify-content: flex-end;
}
.md\:border-2{
border-width: 2px;
}
.md\:text-left{
text-align: left;
}

1
run
View File

@ -1 +0,0 @@
clear;RUST_LOG=debug BIND=0.0.0.0:7979 ADMINS=admin:admin PSQL=postgres://postgres@localhost/cooked REDIS=redis://localhost cargo run

8
scripts/run Executable file
View File

@ -0,0 +1,8 @@
clear
export RUST_LOG=debug
export BIND=0.0.0.0:7979
export ADMINS=admin@example.com:admin
export PSQL=postgres://postgres@localhost/cooked
export REDIS=redis://localhost
export SECRET='NSvunLCGGNWOn0tZVwGZghTc9uXJhSOok0SR2goue4ENK9wfWzFfz81eJflY+xrX'
cargo run

View File

@ -1,2 +1,2 @@
yarn
cargo watch -w ./templates -w ./src -w ./tailwind.config.js -c --shell "./render_css && ./run"
cargo watch -w ./templates -w ./src -w ./tailwind.config.js -c --shell "./scripts/render_css && ./scripts/run"

View File

@ -1,13 +1,16 @@
use actix_files::Files;
use actix_identity::{Identity, IdentityMiddleware};
use actix_session::{storage::RedisSessionStore, SessionMiddleware};
use actix_web::cookie::Key;
use actix_web::{
cookie::{time::Duration, CookieBuilder},
get, post,
web::{Data, Form, Json, Path},
App, HttpResponse, HttpServer, Responder,
};
use actix_web::{HttpMessage, HttpRequest};
use askama::Template;
use redis::AsyncCommands;
use sea_orm::{prelude::*, Database};
use sea_orm::{prelude::*, Database, QuerySelect};
use serde::Deserialize;
use std::str::FromStr;
mod entities;
@ -20,6 +23,18 @@ mod filters {
}
}
const SESSION_KEY: &'static str = "session";
type User = String;
#[derive(Debug, PartialEq, Clone, Copy)]
enum Page {
Index,
Recipe,
Search,
SignIn,
}
#[derive(Debug, Template, derive_more::Deref)]
#[template(path = "recipe_card.html")]
struct RecipeCard(entities::recipies::Model);
@ -28,6 +43,20 @@ struct RecipeCard(entities::recipies::Model);
#[template(path = "index.html")]
struct IndexTemplate {
recipies: Vec<RecipeCard>,
count: u64,
session: Option<User>,
page: Page,
}
#[derive(Debug, Template)]
#[template(path = "top_bar.html")]
struct TopBar<'s> {
session: &'s Option<User>,
}
impl<'s> TopBar<'s> {
pub fn new(session: &'s Option<User>) -> Self {
tracing::info!("top bar session: {session:?}");
Self { session }
}
}
#[derive(Debug)]
@ -74,23 +103,31 @@ struct SignIn {
password: String,
}
#[derive(Debug, Default, Template)]
#[derive(Debug, Template)]
#[template(path = "sign_in/form.html")]
struct SignInForm {
not_found: bool,
email: String,
session: Option<User>,
page: Page,
}
#[get("/sign-in")]
async fn render_sign_in() -> SignInForm {
SignInForm::default()
SignInForm {
not_found: false,
email: "".into(),
session: None,
page: Page::SignIn,
}
}
#[post("/sign-in")]
async fn sign_in(
req: HttpRequest,
admins: Data<Admins>,
payload: Form<SignIn>,
redis: Data<redis::Client>,
admin: Option<Identity>,
) -> HttpResponse {
let payload = payload.into_inner();
let is_admin = admins
@ -99,33 +136,27 @@ async fn sign_in(
.iter()
.any(|admin| admin.email == payload.email && admin.pass == payload.password);
if is_admin {
let mut con = redis
.get_multiplexed_async_connection()
.await
.expect("Failed to get redis get_multiplexed_async_connection");
tracing::info!("Valid credentials");
// let res = session
// .insert(SESSION_KEY, payload.email.clone())
// .inspect_err(|e| tracing::error!("Failed to save session: {e}"));
// tracing::debug!("Saving session res: {res:?}");
let _s =
Identity::login(&req.extensions(), payload.email).expect("Failed to store session");
let uid = uuid::Uuid::new_v4();
let _: () = con
.set_ex(uid.to_string(), "1", 60 * 60 * 24) // session exists for 1 day
.await
.expect("Failed to store session");
HttpResponse::SeeOther()
.cookie(
CookieBuilder::new("ses", uid.to_string())
.secure(true)
.max_age(Duration::hours(24))
.finish(),
)
.append_header(("location", "/"))
.finish()
} else {
tracing::warn!("Invalid credentials");
HttpResponse::BadRequest()
.append_header(("Content-Type", "text/html"))
.body(
SignInForm {
email: payload.email,
not_found: true,
session: admin.and_then(|s| s.id().ok()),
page: Page::SignIn,
}
.render()
.unwrap_or_default(),
@ -133,14 +164,35 @@ async fn sign_in(
}
}
#[derive(Debug, Deserialize)]
struct Padding {
page: Option<u16>,
}
#[get("/")]
async fn index_html(db: Data<DatabaseConnection>) -> IndexTemplate {
async fn index_html(
db: Data<DatabaseConnection>,
q: actix_web::web::Query<Padding>,
admin: Option<Identity>,
) -> IndexTemplate {
let count = (entities::prelude::Recipies::find()
.count(&**db)
.await
.unwrap_or_default() as f64
/ 20.0)
.ceil() as u64;
let recipies = entities::prelude::Recipies::find()
.limit(20)
.offset(q.page.unwrap_or_default() as u64)
.all(&**db)
.await
.unwrap_or_default();
IndexTemplate {
recipies: recipies.into_iter().map(RecipeCard).collect(),
count,
session: admin.and_then(|s| s.id().ok()),
page: Page::Index,
}
}
@ -151,10 +203,16 @@ struct RecipeDetailTemplate {
tags: Vec<entities::recipe_tags::Model>,
steps: Vec<entities::recipe_steps::Model>,
ingeredients: Vec<entities::recipe_ingeredients::Model>,
session: Option<User>,
page: Page,
}
#[get("/recipe/{id}")]
async fn show(id: Path<i64>, db: Data<DatabaseConnection>) -> impl Responder {
async fn show(
id: Path<i64>,
db: Data<DatabaseConnection>,
admin: Option<Identity>,
) -> impl Responder {
let id = id.into_inner();
let db = &**db;
let Ok(Some(recipe)) = entities::prelude::Recipies::find()
@ -188,6 +246,8 @@ async fn show(id: Path<i64>, db: Data<DatabaseConnection>) -> impl Responder {
tags,
recipe,
ingeredients,
session: admin.and_then(|s| s.id().ok()),
page: Page::Recipe,
}
.render()
.unwrap_or_default(),
@ -209,19 +269,6 @@ async fn main() {
tracing::info!("You can define admins by setting env variable ADMINS");
tracing::info!(" Example: ADMINS=login1:pass1,login2,pass2");
// {
// use glob::GlobMatcher;
// use rswind::*;
// let mut p = create_app();
// p.processor.options.watch = false;
// p.processor.options.parallel = true;
// p.glob =
// GlobMatcher::new(vec!["**/*.html"], "./templates".into()).expect("Glob must be valid");
// let contents = p.generate_contents();
// std::fs::write("./templates/styles.css", &contents).expect("Failed to save styles.css");
// }
std::fs::create_dir_all("./pages").ok();
std::fs::create_dir_all("./tmp").ok();
@ -233,6 +280,8 @@ async fn main() {
std::env::var("PSQL").expect("PSQL is required. Please provide postgresql connection url");
let redis_url =
std::env::var("REDIS").expect("REDIS is required. Pleasde provide redis connection url");
let secret =
std::env::var("SECRET").expect("SECRET is required. Please provider encryption key");
// Build structs
let admins = Admins::from_str(&admins).expect("Parsing admins should be successful here");
@ -244,7 +293,11 @@ async fn main() {
Migrator::up(&db, None).await.unwrap();
}
let redis = redis::Client::open(redis_url).expect("Failed to connect to redis");
let redis = redis::Client::open(redis_url.as_str()).expect("Failed to connect to redis");
let secret_key = Key::from(secret.as_bytes());
drop(secret);
tracing::info!("{:?}", secret_key.master());
let redis_store = RedisSessionStore::new(redis_url.as_str()).await.unwrap();
// Transform to data
let admins = Data::new(admins);
@ -257,6 +310,14 @@ async fn main() {
.wrap(actix_web::middleware::Logger::default())
.wrap(actix_web::middleware::NormalizePath::trim())
.wrap(actix_web::middleware::Compress::default())
.wrap(IdentityMiddleware::default())
.wrap(
SessionMiddleware::builder(redis_store.clone(), secret_key.clone())
.cookie_http_only(true)
.cookie_secure(false)
.cookie_name(SESSION_KEY.to_string())
.build(),
)
.app_data(admins.clone())
.app_data(db.clone())
.app_data(redis.clone())

View File

@ -2,9 +2,7 @@
{% block content %}
<main class="md:col-span-3 flex flex-col gap-4 m-4">
<div class="flex justify-center md:justify-end">
<a href="/sign-in" class="btn md:border-2 hover:shadow-lg transition ease-out duration-300">Login</a>
</div>
{{ TopBar::new(session)|safe }}
<header class="flex flex-col gap-2">
<h2 class="text-gray-600 text-6xl font-semibold text-center">Recipes</h2>
@ -16,6 +14,15 @@
{{ recipe|safe }}
{% endfor %}
</div>
<div class="flex gap-4 flex-wrap">
{% match count %}
{% when 0 %}
{% when _ %}
{% for page in 0..count %}
<a class="btn" href="/?page={{page}}">{{page}}</a>
{% endfor %}
{% endmatch %}
</div>
<div class="flex justify-center">
<div class="btn bg-secondary-100 text-secondary-200 hover:shadow-inner transform hover:scale-110 hover:bg-opacity-60 transition ease-out duration-300">Load More</div>

View File

@ -21,7 +21,16 @@
d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" />
</svg>
</a>
</li>
</li>
<li>
<a href="/search" class="px-4 flex justify-end border-r-4 border-primary hover:shadow-md">
<span>Search</span>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" class="w-5 ml-2">
<rect width="24" height="24" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.25007 2.38782C8.54878 2.0992 10.1243 2 12 2C13.8757 2 15.4512 2.0992 16.7499 2.38782C18.06 2.67897 19.1488 3.176 19.9864 4.01358C20.824 4.85116 21.321 5.94002 21.6122 7.25007C21.9008 8.54878 22 10.1243 22 12C22 13.8757 21.9008 15.4512 21.6122 16.7499C21.321 18.06 20.824 19.1488 19.9864 19.9864C19.1488 20.824 18.06 21.321 16.7499 21.6122C15.4512 21.9008 13.8757 22 12 22C10.1243 22 8.54878 21.9008 7.25007 21.6122C5.94002 21.321 4.85116 20.824 4.01358 19.9864C3.176 19.1488 2.67897 18.06 2.38782 16.7499C2.0992 15.4512 2 13.8757 2 12C2 10.1243 2.0992 8.54878 2.38782 7.25007C2.67897 5.94002 3.176 4.85116 4.01358 4.01358C4.85116 3.176 5.94002 2.67897 7.25007 2.38782ZM9 11.5C9 10.1193 10.1193 9 11.5 9C12.8807 9 14 10.1193 14 11.5C14 12.8807 12.8807 14 11.5 14C10.1193 14 9 12.8807 9 11.5ZM11.5 7C9.01472 7 7 9.01472 7 11.5C7 13.9853 9.01472 16 11.5 16C12.3805 16 13.202 15.7471 13.8957 15.31L15.2929 16.7071C15.6834 17.0976 16.3166 17.0976 16.7071 16.7071C17.0976 16.3166 17.0976 15.6834 16.7071 15.2929L15.31 13.8957C15.7471 13.202 16 12.3805 16 11.5C16 9.01472 13.9853 7 11.5 7Z" fill="#323232"/>
</svg>
</a>
</li>
</ul>
</nav>
</div>

View File

@ -5,7 +5,7 @@
{% include "../top_bar.html" %}
<div class="sm:rounded-3xl w-full flex flex-col gap-4">
<div class="text-center w-full">
<img src={{ recipe.image_url.clone() }} alt="{{recipe.title}}" class="rounded-xl w-40 md:w-72 ml-auto mr-auto">
<img src={{ recipe.image_url.clone() }} alt="{{recipe.title}}" class="rounded-xl w-40 md:w-3/4 ml-auto mr-auto">
</div>
<div class="flex flex-col gap-7">
<h1 class="font-young-serif text-desktop-heading-l text-stone-700 sm:text-stone-800 text-center md:text-left">

View File

@ -34,7 +34,7 @@
<path
d="M15 6.954 8.978 9.86a2.25 2.25 0 0 1-1.956 0L1 6.954V11.5A1.5 1.5 0 0 0 2.5 13h11a1.5 1.5 0 0 0 1.5-1.5V6.954Z" />
</svg>
<input type="email" class="grow" placeholder="Email" id="email" name="email" />
<input type="email" class="grow" placeholder="Email" id="email" name="email" value="{{email}}" />
</label>
<label for="password" class="input input-bordered flex items-center gap-2">
@ -48,7 +48,7 @@
d="M14 6a4 4 0 0 1-4.899 3.899l-1.955 1.955a.5.5 0 0 1-.353.146H5v1.5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1-.5-.5v-2.293a.5.5 0 0 1 .146-.353l3.955-3.955A4 4 0 1 1 14 6Zm-4-2a.75.75 0 0 0 0 1.5.5.5 0 0 1 .5.5.75.75 0 0 0 1.5 0 2 2 0 0 0-2-2Z"
clip-rule="evenodd" />
</svg>
<input type="password" class="grow" value="password" name="password" id="password" />
<input type="password" class="grow" value="" name="password" id="password" />
</label>
<div class="w-full grow">

View File

@ -1,3 +1,8 @@
{% match session %}
{% when Some with (user) %}
<div class="text-base">Signed in as <span class="text-bold">{{user}}</span></div>
{% when None %}
<div class="flex justify-center">
<a href="/sign-in" class="btn sm:border-2 hover:shadow-lg transition ease-out duration-300">Login</a>
</div>
{% endmatch %}