feat: bootstrap templating engine

This commit is contained in:
Aravinth Manivannan 2025-01-10 15:51:27 +05:30
parent 104fc1525c
commit 1c327aaffb
Signed by: realaravinth
GPG key ID: F8F50389936984FF
25 changed files with 1051 additions and 158 deletions

499
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -5,7 +5,7 @@ edition = "2021"
[workspace] [workspace]
exclude = ["utils/db-migrations"] #, "utils/cache-bust"] exclude = ["utils/db-migrations"] #, "utils/cache-bust"]
members = [".", "mailpit_client", "twilio_client"] members = [".", "mailpit_client", "twilio_client", "web_ui"]
[dependencies] [dependencies]
actix-identity = "0.8.0" actix-identity = "0.8.0"
@ -38,6 +38,7 @@ url = { version = "2.5.0", features = ["serde"] }
uuid = { version = "1.10.0", features = ["v4", "serde"] } uuid = { version = "1.10.0", features = ["v4", "serde"] }
validator = { version = "0.19.0", features = ["derive"] } validator = { version = "0.19.0", features = ["derive"] }
twilio_client = { path = "./twilio_client" } twilio_client = { path = "./twilio_client" }
web_ui = { path = "./web_ui" }
[dev-dependencies] [dev-dependencies]
#reqwest = { version = "0.12.4", features = ["json"] } #reqwest = { version = "0.12.4", features = ["json"] }

View file

@ -16,6 +16,7 @@ mod inventory;
mod ordering; mod ordering;
mod settings; mod settings;
#[cfg(test)] #[cfg(test)]
#[macro_use]
mod tests; mod tests;
mod types; mod types;
mod utils; mod utils;
@ -33,6 +34,7 @@ async fn main() {
} }
pretty_env_logger::init(); pretty_env_logger::init();
web_ui::init();
let db = db::sqlx_postgres::Postgres::init(&settings.database.url).await; let db = db::sqlx_postgres::Postgres::init(&settings.database.url).await;
db.migrate().await; db.migrate().await;
@ -58,21 +60,10 @@ async fn main() {
.wrap( .wrap(
middleware::DefaultHeaders::new().add(("Permissions-Policy", "interest-cohort=()")), middleware::DefaultHeaders::new().add(("Permissions-Policy", "interest-cohort=()")),
) )
.configure(billing::adapters::load_adapters( .configure(utils::load_adapters::load_adapters(
db.pool.clone(), db.pool.clone(),
settings.clone(), settings.clone(),
)) ))
.configure(identity::adapters::load_adapters(
db.pool.clone(),
settings.clone(),
))
.configure(billing::adapters::load_adapters(
db.pool.clone(),
settings.clone(),
))
// .configure(auth::adapter::load_adapters(db.pool.clone(), &settings))
.configure(utils::random_string::GenerateRandomString::inject())
.configure(utils::uuid::GenerateUUID::inject())
}) })
.bind(&socket_addr) .bind(&socket_addr)
.unwrap() .unwrap()

1
web_ui/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
tmp/

15
web_ui/Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "web_ui"
version = "0.1.0"
edition = "2024"
[dependencies]
derive_builder = "0.20.0"
derive_more = { version = "0.99.17", features = ["error"]}
lazy_static = "1.5.0"
rust-embed = { version = "8.4.0", features = ["debug-embed"]}
serde = { version = "1.0.201", features = ["derive"] }
serde_json = "1.0.117"
tera = "1.19.0"
tracing = { version = "0.1.40", features = ["log"] }
url = { version = "2.5.0", features = ["serde"] }

2
web_ui/Makefile Normal file
View file

@ -0,0 +1,2 @@
check: ## Check for syntax errors on all workspaces
cargo check --tests --all-features

View file

@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
pub mod owner_update_email;
pub mod owner_verify_email;
pub fn register_templates(t: &mut tera::Tera) {
for template in [
owner_update_email::ONWER_UPDATE_EMAIL_PAGE,
owner_verify_email::ONWER_VERIFY_EMAIL_PAGE,
]
.iter()
{
template.register(t).expect(template.name);
}
}

View file

@ -0,0 +1,67 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::cell::RefCell;
use derive_builder::*;
use derive_more::*;
use serde::*;
use tera::Context;
use url::Url;
pub use super::*;
use crate::utils::*;
pub const ONWER_UPDATE_EMAIL_PAGE: TemplateFile = TemplateFile::new(
"identity.owner_update_email",
"identity/owner_update_email.html",
);
pub fn register_templates(t: &mut tera::Tera) {
ONWER_UPDATE_EMAIL_PAGE
.register(t)
.expect(ONWER_UPDATE_EMAIL_PAGE.name);
}
pub struct OwnerUpdateEmailPage {
ctx: RefCell<Context>,
}
impl OwnerUpdateEmailPage {
pub fn new() -> Self {
let mut ctx = context();
//ctx.insert(PAYLOAD_KEY, p);
let ctx = RefCell::new(ctx);
Self { ctx }
}
pub fn render(&self) -> String {
TEMPLATES
.render(ONWER_UPDATE_EMAIL_PAGE.name, &self.ctx.borrow())
.unwrap()
}
pub fn page() -> String {
let p = Self::new();
p.render()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn render_page_and_write() {
const FILE: &str = "./tmp/identity-owner_update_email.html";
let tw = TestWriterBuilder::default()
.file(FILE.into())
.contents(OwnerUpdateEmailPage::page())
.build()
.unwrap();
tw.write();
}
}

View file

@ -0,0 +1,67 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::cell::RefCell;
use derive_builder::*;
use derive_more::*;
use serde::*;
use tera::Context;
use url::Url;
pub use super::*;
use crate::utils::*;
pub const ONWER_VERIFY_EMAIL_PAGE: TemplateFile = TemplateFile::new(
"identity.owner_verify_email",
"identity/owner_verify_email.html",
);
pub fn register_templates(t: &mut tera::Tera) {
ONWER_VERIFY_EMAIL_PAGE
.register(t)
.expect(ONWER_VERIFY_EMAIL_PAGE.name);
}
pub struct OwnerVerifyEmailPage {
ctx: RefCell<Context>,
}
impl OwnerVerifyEmailPage {
pub fn new() -> Self {
let mut ctx = context();
//ctx.insert(PAYLOAD_KEY, p);
let ctx = RefCell::new(ctx);
Self { ctx }
}
pub fn render(&self) -> String {
TEMPLATES
.render(ONWER_VERIFY_EMAIL_PAGE.name, &self.ctx.borrow())
.unwrap()
}
pub fn page() -> String {
let p = Self::new();
p.render()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn render_page_and_write() {
const FILE: &str = "./tmp/identity-owner_verify_email.html";
let tw = TestWriterBuilder::default()
.file(FILE.into())
.contents(OwnerVerifyEmailPage::page())
.build()
.unwrap();
tw.write();
}
}

11
web_ui/src/lib.rs Normal file
View file

@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
pub mod identity;
mod log;
mod utils;
pub fn init() {
lazy_static::initialize(&utils::TEMPLATES);
}

25
web_ui/src/log.rs Normal file
View file

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
#[allow(unused_imports)]
#[cfg(test)]
pub(crate) use println as info;
#[allow(unused_imports)]
#[cfg(test)]
pub(crate) use println as error;
#[allow(unused_imports)]
#[cfg(test)]
pub(crate) use println as trace;
#[allow(unused_imports)]
#[cfg(test)]
pub(crate) use println as debug;
#[allow(unused_imports)]
#[cfg(test)]
pub(crate) use println as warn;
#[cfg(not(test))]
pub use tracing::{debug, error, info, trace, warn};

149
web_ui/src/utils.rs Normal file
View file

@ -0,0 +1,149 @@
// SPDX-FileCopyrightText: 2024 Aravinth Manivannan <realaravinth@batsense.net>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use lazy_static::lazy_static;
use rust_embed::RustEmbed;
use serde::*;
use tera::*;
#[allow(unused_imports)]
#[cfg(test)]
pub use tests::*;
pub struct TemplateFile {
pub name: &'static str,
pub path: &'static str,
}
impl TemplateFile {
pub const fn new(name: &'static str, path: &'static str) -> Self {
Self { name, path }
}
pub fn register(&self, t: &mut Tera) -> std::result::Result<(), tera::Error> {
t.add_raw_template(self.name, &Templates::get_template(self).expect(self.name))
}
#[cfg(test)]
#[allow(dead_code)]
pub fn register_from_file(&self, t: &mut Tera) -> std::result::Result<(), tera::Error> {
use std::path::Path;
t.add_template_file(Path::new("templates/").join(self.path), Some(self.name))
}
}
pub const PAYLOAD_KEY: &str = "payload";
pub const NAV_SEGMENT: TemplateFile = TemplateFile::new("nav_segment.html", "nav_segment.html");
pub const TAILWIND_CONFIG: TemplateFile =
TemplateFile::new("tailwind_config.html", "tailwind_config.html");
lazy_static! {
pub static ref TEMPLATES: Tera = {
let mut tera = Tera::default();
for t in [NAV_SEGMENT, TAILWIND_CONFIG].iter() {
t.register(&mut tera).unwrap();
}
tera.autoescape_on(vec![".html", ".sql"]);
crate::identity::register_templates(&mut tera);
tera
};
}
#[derive(RustEmbed)]
#[folder = "templates/"]
pub struct Templates;
impl Templates {
pub fn get_template(t: &TemplateFile) -> Option<String> {
match Self::get(t.path) {
Some(file) => Some(String::from_utf8_lossy(&file.data).into_owned()),
None => None,
}
}
}
pub fn context() -> Context {
let ctx = Context::new();
// let footer = Footer::new(s);
// ctx.insert("footer", &footer);
// ctx.insert("page", &PAGES);
// ctx.insert("assets", &*ASSETS);
ctx
}
//pub fn auth_ctx(username: Option<&str>, s: &Settings) -> Context {
// use routes::GistProfilePathComponent;
// let mut profile_link = None;
// if let Some(name) = username {
// profile_link = Some(
// PAGES
// .gist
// .get_profile_route(GistProfilePathComponent { username: name }),
// );
// }
// let mut ctx = Context::new();
// let footer = Footer::new(s);
// ctx.insert("footer", &footer);
// ctx.insert("page", &PAGES);
// ctx.insert("assets", &*ASSETS);
// ctx.insert("loggedin_user", &profile_link);
// ctx
//}
#[derive(Serialize)]
pub struct Footer<'a> {
// version: &'a str,
admin_email: &'a str,
source_code: &'a str,
// git_hash: &'a str,
// settings: &'a Settings,
// demo_user: &'a str,
// demo_password: &'a str,
}
impl<'a> Footer<'a> {
pub fn new(admin_email: &'a str, source_code: &'a str) -> Self {
Self {
// version: VERSION,
source_code,
admin_email,
// git_hash: &GIT_COMMIT_HASH[..8],
// demo_user: crate::demo::DEMO_USER,
// demo_password: crate::demo::DEMO_PASSWORD,
// settings,
}
}
}
#[cfg(test)]
pub mod tests {
use super::*;
use derive_builder::*;
use derive_more::*;
use tracing::info;
use std::fs;
use std::path::Path;
#[derive(Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize, Builder)]
pub struct TestWriter {
file: String,
contents: String,
}
impl TestWriter {
pub fn write(&self) {
let p = Path::new(&self.file);
if p.exists() {
info!("{:?} exists; removing", p.canonicalize().unwrap());
fs::remove_file(&self.file).unwrap();
}
let _ = fs::write(&self.file, self.contents.as_bytes());
info!("contents written to file: {:?}", p.canonicalize().unwrap());
}
}
}

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Exit organization | Vanikam</title>
</head>
<body>
<form action="/owner/delete/user" method="post">
<button type="submit">Exit organization</button>
</form>
</body>
</html>

View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login | Vanikam</title>
</head>
<body>
<form action="/employee/login" method="post">
<label for="country_code">
Country Code
<input type="number" name="country_code" id="country_code" value="91">
</label>
<label for="number">
Phone number
<input type="number" name="number" id="number">
</label>
<button type="submit">Login</button>
</form>
<p>New here? Click <a href="/employee/register">here to register!</a></p>
</body>
</html>

View file

@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register | Vanikam</title>
</head>
<body>
<form action="/employee/register" method="post">
<label for="first_name">
First Name
<input type="text" name="first_name" id="first_name">
</label>
<label for="last_name">
Last Name
<input type="text" name="last_name" id="last_name">
</label>
<label for="country_code">
Country Code
<input type="number" name="country_code" id="country_code" value="91">
</label>
<label for="number">
Phone number
<input type="number" name="number" id="number">
</label>
<button type="submit">Register</button>
</form>
<p>Already have an account? Click <a href="/employee/login">here to log in!</a></p>
</body>
</html>

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Create Store | Vanikam</title>
</head>
<body>
<form action="/owner/store" method="post">
<label for="name">
Store name
<input type="text" name="name" id="name" required>
</label>
<label for="address">
Address
<input type="text" name="address" id="address">
</label>
<button type="submit">Create Store</button>
</form>
</body>
</html>

View file

@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Change Password | Vanikam</title>
</head>
<body>
<form action="/owner/user/password/change" method="post">
<label for="current_password">
Current password
<input type="password" name="current_password" id="current_password">
</label>
<label for="new_password">
New password
<input type="password" name="new_password" id="new_password">
</label>
<label for="confirm_new_password">
Confirm new password
<input type="password" name="confirm_new_password" id="confirm_new_password">
</label>
<button type="submit">Change password</button>
</form>
</body>
</html>

View file

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Delete account | Vanikam</title>
</head>
<body>
<form action="/owner/delete/user" method="post">
<label for="password">
Password
<input type="password" name="password" id="password">
</label>
<button type="submit">Delete account</button>
</form>
</body>
</html>

View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login | Vanikam</title>
</head>
<body>
<form action="/owner/login" method="post">
<label for="email">
Email
<input type="email" name="email" id="email">
</label>
<label for="password">
Password
<input type="password" name="password" id="password">
</label>
<button type="submit">Login</button>
</form>
<p>New here? Click <a href="/owner/register">here to register!</a></p>
</body>
</html>

View file

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Register | Vanikam</title>
</head>
<body>
<form action="/owner/register" method="post">
<label for="first_name">
First Name
<input type="text" name="first_name" id="first_name">
</label>
<label for="last_name">
Last Name
<input type="text" name="last_name" id="last_name">
</label>
<label for="email">
Email
<input type="email" name="email" id="email">
</label>
<label for="password">
Password
<input type="password" name="password" id="password">
</label>
<label for="confirm_password">
Confirm Password
<input type="confirm_password" name="confirm_password" id="confirm_password">
</label>
<button type="submit">Register</button>
</form>
<p>Already have an account? Click <a href="/owner/login">here to log in!</a></p>
</body>
</html>

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Update Email | Vanikam</title>
</head>
<body>
<form action="/owner/user/email/update" method="post">
<label for="email">
Email
<input type="email" name="email" id="email">
</label>
<label for="password">
Password
<input type="password" name="password" id="password">
</label>
<button type="submit">Update</button>
</form>
</body>
</html>

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Update Store | Vanikam</title>
</head>
<body>
<form action="/owner/store/update" method="post">
<label for="name">
Store name
<input type="text" name="name" id="name" required>
</label>
<label for="address">
Address
<input type="text" name="address" id="address">
</label>
<button type="submit">Update Store</button>
</form>
</body>
</html>

View file

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Verify email | Vanikam</title>
</head>
<body>
<form action="/owner/delete/user" method="post">
<button type="submit">Verify email</button>
</form>
</body>
</html>

View file

@ -0,0 +1,25 @@
<nav class="flex bg-darkest w-full flex-row items-center mb-9">
<span class="m-4 flex flex-column items-center">
<a href="/">
<img
class="size-16 rounded-md"
src="https://forgeflux.org/logo-thumbnail.png"
alt="Go to home"
/>
</a>
<p class="text-lightest text-3xl ml-3">ForgeFlux</p>
</span>
<form
class="grow m-4 bg-green-300 flex flex-row rounded-md"
action="/search"
method="get"
>
<input
type="text"
class="w-full rounded-md size-8 text-start p-4"
name="q"
id="q"
placeholder="Search for persons, repositories, etc."
/>
</form>
</nav>

View file

@ -0,0 +1,25 @@
<style>
* {
margin: 0;
padding: 9;
}
</style>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
darkest: "#171717",
dark: "#414141",
medium: "#808080",
light: "#e0e0e0",
lightest: "#f5f5f5",
white: "#fff",
primary: "#cc3229",
backdrop: "#e8e9ea",
},
},
},
};
</script>