This commit is contained in:
eraden 2024-10-22 11:18:39 +02:00
commit 39afb4c690
13 changed files with 4511 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

4166
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

22
Cargo.toml Normal file
View File

@ -0,0 +1,22 @@
[package]
name = "cooked"
version = "0.1.0"
edition = "2021"
[dependencies]
actix = "0.13.5"
actix-files = { version = "0.6.6", features = ["experimental-io-uring"] }
actix-web = { version = "4.9.0", features = ["compress-brotli", "cookies", "experimental-io-uring", "macros", "rustls", "secure-cookies", "unicode"], default-features = false }
askama = { version = "0.12.1", features = ["with-actix-web", "serde_json", "mime_guess", "markdown", "comrak", "mime"] }
askama_actix = "0.14.0"
redis = { version = "0.27.5", features = ["tokio", "json", "uuid", "tokio-comp"] }
sea-orm = { version = "1.1.0", default-features = false, features = ["chrono", "macros", "runtime-actix-rustls", "serde_json", "sqlx-postgres", "with-uuid"] }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
rswind = "0.0.1-alpha.1"
serde = "1.0.210"
serde_json = "1.0.132"
uuid = { version = "1.11.0", features = ["v4", "v8"] }
[build-dependencies]
rswind = "0.0.1-alpha.1"

11
build.rs Normal file
View File

@ -0,0 +1,11 @@
use glob::GlobMatcher;
use rswind::*;
fn main() {
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");
}

160
src/main.rs Normal file
View File

@ -0,0 +1,160 @@
use actix_files::Files;
use actix_web::{
cookie::{time::Duration, CookieBuilder},
delete, get, patch, post,
web::{Data, Json},
App, HttpResponse, HttpServer,
};
use askama::Template;
use redis::{AsyncCommands, Commands};
use sea_orm::{prelude::*, Database, DatabaseConnection};
use std::str::FromStr;
#[derive(Debug)]
pub struct Admin {
pub login: String,
pub pass: String,
}
impl FromStr for Admin {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut it = s.split(':');
Ok(Self {
login: it.next().expect("Admin login is required").into(),
pass: it.next().expect("Admin password is required").into(),
})
}
}
#[derive(Debug)]
pub struct Admins(Vec<Admin>);
impl FromStr for Admins {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self(
s.trim().split(',').filter_map(|s| s.parse().ok()).collect(),
))
}
}
#[derive(Debug, serde::Deserialize)]
struct SignIn {
login: String,
password: String,
}
#[derive(Debug, Default, Template)]
#[template(path = "sign_in/form.html")]
struct SignInForm {
not_found: bool,
login: String,
}
#[get("/sign-in")]
async fn render_sign_in() -> SignInForm {
SignInForm::default()
}
#[post("/sign-in")]
async fn sign_in(
admins: Data<Admins>,
payload: Json<SignIn>,
redis: Data<redis::Client>,
) -> HttpResponse {
let payload = payload.into_inner();
let is_admin = admins
.as_ref()
.0
.iter()
.any(|admin| admin.login == payload.login && admin.pass == payload.password);
if is_admin {
let uid = uuid::Uuid::new_v4();
redis
.set_ex(uid.to_string(), "1", 60 * 60 * 24) // session exists for 1 day
.await
.expect("Failed to store session");
HttpResponse::Ok().cookie(
CookieBuilder::new("ses", uid.to_string())
.secure(true)
.max_age(Duration::hours(24))
.build(),
)
} else {
HttpResponse::Ok().finish()
}
}
#[get("/")]
async fn index_html() -> HttpResponse {
HttpResponse::Ok().body(include_str!("../templates/index.html"))
}
#[actix_web::main]
async fn main() {
let _ = tracing_subscriber::fmt::init();
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();
// Fetch env variables
let bind = std::env::var("BIND").expect("BIND is required. Please provide server address");
let admins = std::env::var("ADMINS")
.expect("ADMINS list is mandatory. Provide list of admins to start applications");
let psql =
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");
// Build structs
let admins = Admins::from_str(&admins).expect("Parsing admins should be successful here");
let db = Database::connect(&psql)
.await
.expect("Failed to connect to postgresql");
// Migrator::up(&conn, None).await.unwrap();
let redis = redis::Client::open(redis_url).expect("Failed to connect to redis");
// Transform to data
let admins = Data::new(admins);
let db = Data::new(db);
let redis = Data::new(redis);
HttpServer::new(move || {
//
App::new()
.wrap(actix_web::middleware::Logger::default())
.wrap(actix_web::middleware::NormalizePath::trim())
.wrap(actix_web::middleware::Compress::default())
.app_data(admins.clone())
.app_data(db.clone())
.app_data(redis.clone())
.service(Files::new("/pages", "./pages"))
.service(render_sign_in)
.service(sign_in)
.service(index_html)
})
.bind(&bind)
.expect("Failed to start http server")
.run()
.await
.expect("Failed to start server");
}

20
styles.css Normal file
View File

@ -0,0 +1,20 @@
@import url('https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200;0,300;0,400;0,600;0,700;0,800;0,900;1,200;1,300;1,400;1,600;1,700;1,800;1,900&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
/* overflow-hidden is to prevent overflow within the card */
.card {
@apply bg-white rounded overflow-hidden shadow-md relative;
}
/* object-cover is to prevent distortion */
.badge {
@apply bg-secondary-100 text-secondary-200 text-xs uppercase font-bold rounded-full p-2 absolute top-0 ml-2 mt-2;
}
/* tracking-wider is for space between letters */
.btn {
@apply rounded-full py-2 px-3 uppercase text-xs font-bold cursor-pointer tracking-wider;
}

131
templates/index.html Normal file
View File

@ -0,0 +1,131 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="styles.css">
<title>Techie Recipes</title>
</head>
<body class="text-gray-500 font-body">
<!-- content-wrapper -->
<div class="grid md:grid-cols-4">
<!-- navigation -->
<div class="md:col-span-1 md:flex md:justify-end">
<nav class="text-right">
<div class="flex justify-between items-center">
<h1 class="font-bold uppercase p-4 border-b border-gray-100">
<a href="/" class="hover:text-gray-600">Food Techie</a>
</h1>
<div class="px-4 cursor-pointer md:hidden" id="burger">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
</svg>
</div>
</div>
<ul class="text-sm mt-6 hidden md:block" id="menu">
<li class="text-gray-700 font-bold py-2">
<a href="#" class="px-4 flex justify-end border-r-4 border-primary hover:shadow-md">
<span>Home</span>
<svg class="w-5 ml-2" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
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 class="py-2">
<a href="#" class="px-4 flex justify-end border-r-4 border-white hover:shadow-md">
<span>About</span>
<svg class="w-5 ml-2" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-8-3a1 1 0 00-.867.5 1 1 0 11-1.731-1A3 3 0 0113 8a3.001 3.001 0 01-2 2.83V11a1 1 0 11-2 0v-1a1 1 0 011-1 1 1 0 100-2zm0 8a1 1 0 100-2 1 1 0 000 2z"
clip-rule="evenodd" />
</svg>
</a>
</li>
<li class="py-2">
<a href="#" class="px-4 flex justify-end border-r-4 border-white hover:shadow-md">
<span>Contact</span>
<svg class="w-5 ml-2" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z" />
<path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z" />
</svg>
</a>
</li>
</ul>
</nav>
</div>
<!-- main content -->
<main class="px-16 py-6 bg-gray-100 md:col-span-3">
<div class="flex justify-center md:justify-end">
<a href="#" class="btn text-primary border-primary md:border-2 hover:shadow-lg transition ease-out duration-300">Login</a>
<a href="#" class="btn text-primary md:text-white md:bg-primary ml-2 border-primary md:border-2 hover:shadow-lg transition ease-out duration-300">Sign Up</a>
</div>
<header>
<h2 class="text-gray-600 text-6xl font-semibold">Recipes</h2>
<h3 class="text-2xl font-semibold pl-1">For Techies</h3>
</header>
<div>
<h4 class="font-bold mt-12 pb-2 border-b border-gray-200">Latest Recipes</h4>
<div class="mt-8 grid md:grid-cols-3 gap-10">
<div class="card hover:shadow-lg transition ease-linear transform hover:scale-105">
<img src="img/stew.jpg" alt="stew" class="w-full h-32 md:h-48 object-cover">
<div class="m-4">
<span class="font-bold">5 Bean Chilli Stew</span>
<span class="block text-gray-500 text-sm">Recipe by McTechie</span>
</div>
<div class="badge">
<svg class="w-5 inline-block" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="mt-2">25 mins</span>
</div>
</div>
<div class="card hover:shadow-lg transition ease-linear transform hover:scale-105">
<img src="img/noodles.jpg" alt="noodles" class="w-full h-32 md:h-48 object-cover">
<div class="m-4">
<span class="font-bold">Lo Mein</span>
<span class="block text-gray-500 text-sm">Recipe by McTechie</span>
</div>
<div class="badge">
<svg class="w-5 inline-block" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="mt-2">25 mins</span>
</div>
</div>
<div class="card hover:shadow-lg transition ease-linear transform hover:scale-105">
<img src="img/curry.jpg" alt="curry" class="w-full h-32 md:h-48 object-cover">
<div class="m-4">
<span class="font-bold">Tofu Curry</span>
<span class="block text-gray-500 text-sm">Recipe by McTechie</span>
</div>
<div class="badge">
<svg class="w-5 inline-block" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span class="mt-2">25 mins</span>
</div>
</div>
</div>
<h4 class="font-bold mt-12 pb-2 border-b border-gray-200">Most Popular</h4>
<div class="mt-8">
<!-- Cards go here -->
</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>
</div>
</div>
</main>
</div>
</body>
</html>

View File

View File

View File

View File

View File

0
templates/styles.css Normal file
View File