Init
This commit is contained in:
commit
39afb4c690
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
4166
Cargo.lock
generated
Normal file
4166
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
Cargo.toml
Normal file
22
Cargo.toml
Normal 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
11
build.rs
Normal 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
160
src/main.rs
Normal 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
20
styles.css
Normal 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
131
templates/index.html
Normal 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>
|
0
templates/recipes/create_form.html
Normal file
0
templates/recipes/create_form.html
Normal file
0
templates/recipes/show.html
Normal file
0
templates/recipes/show.html
Normal file
0
templates/recipes/update_form.html
Normal file
0
templates/recipes/update_form.html
Normal file
0
templates/sign_in/form.htl
Normal file
0
templates/sign_in/form.htl
Normal file
0
templates/sign_in/form.html
Normal file
0
templates/sign_in/form.html
Normal file
0
templates/styles.css
Normal file
0
templates/styles.css
Normal file
Loading…
Reference in New Issue
Block a user