diff --git a/Cargo.lock b/Cargo.lock index 3bc70a7..feb6704 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/Cargo.toml b/Cargo.toml index f24a1c4..bbf4431 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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] diff --git a/assets/styles.css b/assets/styles.css index 44a3ca8..f79f65a 100644 --- a/assets/styles.css +++ b/assets/styles.css @@ -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; } diff --git a/run b/run deleted file mode 100755 index fbe8286..0000000 --- a/run +++ /dev/null @@ -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 diff --git a/gen_types b/scripts/gen_types similarity index 100% rename from gen_types rename to scripts/gen_types diff --git a/render_css b/scripts/render_css similarity index 100% rename from render_css rename to scripts/render_css diff --git a/scripts/run b/scripts/run new file mode 100755 index 0000000..5898a68 --- /dev/null +++ b/scripts/run @@ -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 diff --git a/seed.sh b/scripts/seed.sh similarity index 100% rename from seed.sh rename to scripts/seed.sh diff --git a/watch b/scripts/watch similarity index 59% rename from watch rename to scripts/watch index ba2b4be..8757658 100755 --- a/watch +++ b/scripts/watch @@ -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" diff --git a/src/main.rs b/src/main.rs index 14a84ba..48d2653 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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, + count: u64, + session: Option, + page: Page, +} +#[derive(Debug, Template)] +#[template(path = "top_bar.html")] +struct TopBar<'s> { + session: &'s Option, +} +impl<'s> TopBar<'s> { + pub fn new(session: &'s Option) -> 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, + 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, payload: Form, - redis: Data, + admin: Option, ) -> 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, +} + #[get("/")] -async fn index_html(db: Data) -> IndexTemplate { +async fn index_html( + db: Data, + q: actix_web::web::Query, + admin: Option, +) -> 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, steps: Vec, ingeredients: Vec, + session: Option, + page: Page, } #[get("/recipe/{id}")] -async fn show(id: Path, db: Data) -> impl Responder { +async fn show( + id: Path, + db: Data, + admin: Option, +) -> 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, db: Data) -> 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()) diff --git a/templates/index.html b/templates/index.html index e724202..1802514 100644 --- a/templates/index.html +++ b/templates/index.html @@ -2,9 +2,7 @@ {% block content %}
-
- Login -
+ {{ TopBar::new(session)|safe }}

Recipes

@@ -16,6 +14,15 @@ {{ recipe|safe }} {% endfor %} +
+ {% match count %} + {% when 0 %} + {% when _ %} + {% for page in 0..count %} + {{page}} + {% endfor %} + {% endmatch %} +
Load More
diff --git a/templates/nav.html b/templates/nav.html index 6cb6911..020a6b4 100644 --- a/templates/nav.html +++ b/templates/nav.html @@ -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" /> - + +
  • + + Search + + + + + +
  • diff --git a/templates/recipies/show.html b/templates/recipies/show.html index a1e5ffe..03a1fd7 100644 --- a/templates/recipies/show.html +++ b/templates/recipies/show.html @@ -5,7 +5,7 @@ {% include "../top_bar.html" %}
    - {{recipe.title}} + {{recipe.title}}

    diff --git a/templates/sign_in/form.html b/templates/sign_in/form.html index 4d9b816..a9cc84a 100644 --- a/templates/sign_in/form.html +++ b/templates/sign_in/form.html @@ -34,7 +34,7 @@ - +
    diff --git a/templates/top_bar.html b/templates/top_bar.html index 27f0202..d9bf203 100644 --- a/templates/top_bar.html +++ b/templates/top_bar.html @@ -1,3 +1,8 @@ +{% match session %} +{% when Some with (user) %} +
    Signed in as {{user}}
    +{% when None %} +{% endmatch %}