From b3cac5f97ac2f056e4401247bf69464196604bb4 Mon Sep 17 00:00:00 2001 From: trisua Date: Sun, 23 Mar 2025 16:37:43 -0400 Subject: [PATCH] add(ui): ability to log out --- Cargo.lock | 1 + README.md | 31 +--- crates/app/src/assets.rs | 10 +- crates/app/src/langs/en-US.toml | 14 +- crates/app/src/macros.rs | 13 +- crates/app/src/public/css/style.css | 26 ++++ crates/app/src/public/html/auth/login.html | 31 +++- crates/app/src/public/html/macros.html | 20 ++- crates/app/src/public/html/misc/index.html | 18 +-- crates/app/src/public/html/root.html | 137 ++++++++++++++++++ .../app/src/public/images/default-avatar.svg | 2 +- .../app/src/public/images/default-banner.svg | 2 +- crates/app/src/public/js/atto.js | 3 + crates/app/src/public/js/me.js | 30 ++++ crates/app/src/routes/api/v1/auth/mod.rs | 52 ++++++- crates/app/src/routes/api/v1/mod.rs | 2 + crates/app/src/routes/assets.rs | 33 ++--- crates/app/src/routes/mod.rs | 3 +- crates/app/src/routes/pages/auth.rs | 4 +- crates/app/src/routes/pages/misc.rs | 2 +- crates/core/Cargo.toml | 1 + crates/core/src/database/auth.rs | 21 +-- .../src/database/drivers/sql/create_users.sql | 3 +- crates/core/src/model/auth.rs | 4 + crates/core/src/model/mod.rs | 19 ++- crates/core/src/model/permissions.rs | 123 ++++++++++++++++ crates/l10n/LICENSE | 1 + crates/shared/LICENSE | 1 + tetratto.toml | 16 -- 29 files changed, 499 insertions(+), 124 deletions(-) create mode 100644 crates/app/src/public/js/me.js create mode 100644 crates/core/src/model/permissions.rs create mode 120000 crates/l10n/LICENSE create mode 120000 crates/shared/LICENSE delete mode 100644 tetratto.toml diff --git a/Cargo.lock b/Cargo.lock index 43705ca..6ee6475 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2569,6 +2569,7 @@ name = "tetratto-core" version = "0.1.0" dependencies = [ "bb8-postgres", + "bitflags 2.9.0", "pathbufd", "rusqlite", "serde", diff --git a/README.md b/README.md index ee966b4..d7efa63 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,10 @@ # 🐐 tetratto! -This is the year of the personal website. - -Tetratto (`4 * 10^-18`) is a _super_ simple **dynamic** site server which takes in a conglomeration of HTML files (which are actually Jinja templates) and static files like CSS and JS, then serves them! +Tetratto is your personal journal! ## Features -- Templated HTML files (`html/` directory) -- Markdown posts (`posts/` directory, served with `html/post.html` template) -- Super simple SQLite database for authentication (and other stuff) - -## Usage - -Install Tetratto CLI: - -```bash -cargo install tetratto -``` - -Clone the `./example` directory to get started. - -You can run a project by running `tetratto` in the directory. The entry file for CSS is assumed to be `public/css/style.css`. Note that your `index.html` file should _not_ include boilerplate stuff, and should instead just include a `{% block body %}` for beginning your content in the body. `{% block head %}` can be used to place data in the page head element. Templates should all extend the `_atto/root.html` template. - -### Config - -You can configure Tetratto by editing the project's `tetratto.toml` file. - -- `name`: the `{{ name }}` variable in templates (default: `Tetratto`) -- `port`: the port the server is served on (default: `4118`) -- `database`: the name of the file to store the SQLite database in (default: `./atto.db`) +- Create new pages in your journal (essentially just posts) +- Create new pages in your journal where people can post messages (essentially message boards that you control) +- Follow other people and see their (public) journal entries + - Journal entries can either be public, unlisted (only accessible via link), and fully private (only accessible to moderators and the owner) diff --git a/crates/app/src/assets.rs b/crates/app/src/assets.rs index 76603d3..27f8400 100644 --- a/crates/app/src/assets.rs +++ b/crates/app/src/assets.rs @@ -8,6 +8,7 @@ use std::{ use tera::Context; use tetratto_core::{config::Config, model::auth::User}; use tetratto_l10n::LangFile; +use tetratto_shared::hash::salt; use tokio::sync::RwLock; use crate::{create_dir_if_not_exists, write_if_track, write_template}; @@ -21,8 +22,9 @@ pub const FAVICON: &str = include_str!("./public/images/favicon.svg"); pub const STYLE_CSS: &str = include_str!("./public/css/style.css"); // js -pub const ATTO_JS: &str = include_str!("./public/js/atto.js"); pub const LOADER_JS: &str = include_str!("./public/js/loader.js"); +pub const ATTO_JS: &str = include_str!("./public/js/atto.js"); +pub const ME_JS: &str = include_str!("./public/js/me.js"); // html pub const ROOT: &str = include_str!("./public/html/root.html"); @@ -165,11 +167,15 @@ pub(crate) async fn init_dirs(config: &Config) { write_template!(langs_path->"en-US.toml"(LANG_EN_US)); } +/// A random ASCII value inserted into the URL of static assets to "break" the cache. Essentially just for cache busting. +pub(crate) static CACHE_BREAKER: LazyLock = LazyLock::new(|| salt()); + /// Create the initial template context. pub(crate) fn initial_context(config: &Config, lang: &LangFile, user: &Option) -> Context { let mut ctx = Context::new(); ctx.insert("config", &config); ctx.insert("user", &user); - ctx.insert("lang", &lang); + ctx.insert("lang", &lang.data); + ctx.insert("random_cache_breaker", &CACHE_BREAKER.clone()); ctx } diff --git a/crates/app/src/langs/en-US.toml b/crates/app/src/langs/en-US.toml index b9b4f00..cefd9ef 100644 --- a/crates/app/src/langs/en-US.toml +++ b/crates/app/src/langs/en-US.toml @@ -2,5 +2,15 @@ name = "com.tetratto.langs:en-US" version = "1.0.0" [data] -"general:action.login" = "Login" -"general:action.register" = "Register" +"general:link.home" = "Home" + +"dialog:action.okay" = "Ok" +"dialog:action.continue" = "Continue" +"dialog:action.cancel" = "Cancel" +"dialog:action.yes" = "Yes" +"dialog:action.no" = "No" + +"auth:action.login" = "Login" +"auth:action.register" = "Register" +"auth:action.logout" = "Logout" +"auth:link.my_profile" = "My profile" diff --git a/crates/app/src/macros.rs b/crates/app/src/macros.rs index 5d9f90d..2c537c2 100644 --- a/crates/app/src/macros.rs +++ b/crates/app/src/macros.rs @@ -55,7 +55,7 @@ macro_rules! create_dir_if_not_exists { #[macro_export] macro_rules! get_user_from_token { - (($jar:ident, $db:expr) ) => {{ + ($jar:ident, $db:expr) => {{ if let Some(token) = $jar.get("__Secure-atto-token") { match $db .get_user_by_token(&tetratto_shared::hash::hash( @@ -70,17 +70,6 @@ macro_rules! get_user_from_token { None } }}; - - ($jar:ident, $db:ident) => {{ - if let Some(token) = $jar.get("__Secure-Atto-Token") { - match $db.get_user_by_token(token) { - Ok(ua) => ua, - Err(_) => return axum::response::Html(crate::data::assets::REDIRECT_TO_AUTH), - } - } else { - None - } - }}; } #[macro_export] diff --git a/crates/app/src/public/css/style.css b/crates/app/src/public/css/style.css index f69a77b..ef37f2c 100644 --- a/crates/app/src/public/css/style.css +++ b/crates/app/src/public/css/style.css @@ -318,6 +318,27 @@ table ol { background: var(--color-surface); } +.card-nest { + box-shadow: var(--shadow-x-offset) var(--shadow-y-offset) var(--shadow-size) + var(--color-shadow); + border-radius: var(--radius); +} + +.card-nest .card { + box-shadow: 0; +} + +.card-nest > .card:first-child { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; + background: var(--color-super-raised); +} + +.card-nest > .card:last-child { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + /* buttons */ button, .button { @@ -616,6 +637,11 @@ dialog[open] { display: block; } +dialog::backdrop { + background: hsla(0, 0%, 0%, 50%); + backdrop-filter: blur(5px); +} + /* dropdown */ .dropdown { position: relative; diff --git a/crates/app/src/public/html/auth/login.html b/crates/app/src/public/html/auth/login.html index 6115710..ed178a9 100644 --- a/crates/app/src/public/html/auth/login.html +++ b/crates/app/src/public/html/auth/login.html @@ -1,7 +1,7 @@ {% extends "auth/base.html" %} {% block head %} Login {% endblock %} {% block title %}Login{% endblock %} {% block content %} -
+
Submit + + {% endblock %} {% block footer %} Or, register {{ icon "house" }} - Home + {{ text "general:link.home" }} {% endif %}
@@ -30,6 +30,20 @@ {{ macros::avatar(username=user.username, size="24px") }} {{ icon "chevron-down" c(dropdown-arrow) }} + +
+ {{ user.username }} + + {{ icon "book-heart" }} + {{ text "auth:link.my_profile" }} + + +
+ +
{% else %} diff --git a/crates/app/src/public/html/misc/index.html b/crates/app/src/public/html/misc/index.html index dd53adf..e89bf38 100644 --- a/crates/app/src/public/html/misc/index.html +++ b/crates/app/src/public/html/misc/index.html @@ -2,22 +2,12 @@ {{ macros::nav(selected="home") }}
-

Hello, world!

- -
- A - B - C -
- -
-
- - - +
+
+ ✨ Welcome to {{ config.name }}!
- +
We're still working on your feed...
{% endblock %} diff --git a/crates/app/src/public/html/root.html b/crates/app/src/public/html/root.html index 7481cea..c6183d1 100644 --- a/crates/app/src/public/html/root.html +++ b/crates/app/src/public/html/root.html @@ -22,6 +22,7 @@ globalThis.ns_config = { root: "/js/", verbose: globalThis.ns_verbose, + version: "cache-breaker-{{ random_cache_breaker }}", }; globalThis._app_base = { @@ -76,5 +77,141 @@ atto["hooks::partial_embeds"](); }); + + + +
+

Pressing continue will bring you to the following URL:

+
+

Are sure you want to go there?

+ +
+
+ + {{ text "dialog:action.continue" }} + + +
+
+
+ + +
+
+ + + +
+
+ +
+ + + +
+
+
+
+
+ + +
+
+ + + +
+
+ +
+ + + +
+
+
+
+
+ + +
+
+ + +
+
+ +
+ + + +
+
+
+
+
diff --git a/crates/app/src/public/images/default-avatar.svg b/crates/app/src/public/images/default-avatar.svg index a4cf241..00fa7ab 100644 --- a/crates/app/src/public/images/default-avatar.svg +++ b/crates/app/src/public/images/default-avatar.svg @@ -5,5 +5,5 @@ fill="none" xmlns="http://www.w3.org/2000/svg" > - + diff --git a/crates/app/src/public/images/default-banner.svg b/crates/app/src/public/images/default-banner.svg index a8edad2..05ae323 100644 --- a/crates/app/src/public/images/default-banner.svg +++ b/crates/app/src/public/images/default-banner.svg @@ -5,5 +5,5 @@ fill="none" xmlns="http://www.w3.org/2000/svg" > - + diff --git a/crates/app/src/public/js/atto.js b/crates/app/src/public/js/atto.js index 1a89521..4499a88 100644 --- a/crates/app/src/public/js/atto.js +++ b/crates/app/src/public/js/atto.js @@ -34,6 +34,9 @@ media_theme_pref(); (() => { const self = reg_ns("atto"); + // init + use("me", () => {}); + // env self.DEBOUNCE = []; self.OBSERVERS = []; diff --git a/crates/app/src/public/js/me.js b/crates/app/src/public/js/me.js new file mode 100644 index 0000000..b617364 --- /dev/null +++ b/crates/app/src/public/js/me.js @@ -0,0 +1,30 @@ +(() => { + const self = reg_ns("me"); + + self.define("logout", async () => { + if ( + !(await trigger("atto::confirm", [ + "Are you sure you would like to do this?", + ])) + ) { + return; + } + + fetch("/api/v1/auth/logout", { + method: "POST", + }) + .then((res) => res.json()) + .then((res) => { + trigger("atto::toast", [ + res.ok ? "sucesss" : "error", + res.message, + ]); + + if (res.ok) { + setTimeout(() => { + window.location.href = "/"; + }, 150); + } + }); + }); +})(); diff --git a/crates/app/src/routes/api/v1/auth/mod.rs b/crates/app/src/routes/api/v1/auth/mod.rs index 7fee2a1..38d4752 100644 --- a/crates/app/src/routes/api/v1/auth/mod.rs +++ b/crates/app/src/routes/api/v1/auth/mod.rs @@ -11,6 +11,7 @@ use axum::{ response::IntoResponse, }; use axum_extra::extract::CookieJar; +use tetratto_shared::hash::hash; /// `/api/v1/auth/register` pub async fn register_request( @@ -20,7 +21,7 @@ pub async fn register_request( Json(props): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = get_user_from_token!((jar, data) ); + let user = get_user_from_token!(jar, data); if user.is_some() { return ( @@ -75,7 +76,7 @@ pub async fn login_request( Json(props): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = get_user_from_token!((jar, data) ); + let user = get_user_from_token!(jar, data); if user.is_some() { return (None, Json(Error::AlreadyAuthenticated.into())); @@ -125,3 +126,50 @@ pub async fn login_request( }), ) } + +/// `/api/v1/auth/logout` +pub async fn logout_request( + jar: CookieJar, + Extension(data): Extension, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data) { + Some(ua) => ua, + None => return (None, Json(Error::NotAllowed.into())), + }; + + // update tokens + let token = jar + .get("__Secure-atto-token") + .unwrap() + .to_string() + .replace("__Secure-atto-token=", ""); + + let mut new_tokens = user.tokens.clone(); + new_tokens.remove( + new_tokens + .iter() + .position(|t| t.1 == hash(token.to_string())) + .unwrap(), + ); + + if let Err(e) = data.update_user_tokens(user.id, new_tokens).await { + return (None, Json(e.into())); + } + + // ... + ( + Some([( + "Set-Cookie", + format!( + "__Secure-atto-token={}; SameSite=Lax; Secure; Path=/; HostOnly=true; HttpOnly=true; Max-Age=0", + "refresh", + ), + )]), + Json(ApiReturn { + ok: true, + message: "Goodbye!".to_string(), + payload: (), + }), + ) +} diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index c0d0906..b927576 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -7,9 +7,11 @@ use serde::Deserialize; pub fn routes() -> Router { Router::new() + // auth // global .route("/auth/register", post(auth::register_request)) .route("/auth/login", post(auth::login_request)) + .route("/auth/logout", post(auth::logout_request)) // profile .route( "/auth/profile/{id}/avatar", diff --git a/crates/app/src/routes/assets.rs b/crates/app/src/routes/assets.rs index 5e7144c..df251d2 100644 --- a/crates/app/src/routes/assets.rs +++ b/crates/app/src/routes/assets.rs @@ -1,27 +1,16 @@ use axum::response::IntoResponse; -/// `/public/favicon.svg` -pub async fn favicon_request() -> impl IntoResponse { - ([("Content-Type", "image/svg+xml")], crate::assets::FAVICON) +macro_rules! serve_asset { + ($fn_name:ident: $name:ident($type:literal)) => { + pub async fn $fn_name() -> impl IntoResponse { + ([("Content-Type", $type)], crate::assets::$name) + } + }; } -/// `/css/style.css` -pub async fn style_css_request() -> impl IntoResponse { - ([("Content-Type", "text/css")], crate::assets::STYLE_CSS) -} +serve_asset!(favicon_request: FAVICON("image/svg+xml")); +serve_asset!(style_css_request: STYLE_CSS("text/css")); -/// `/js/atto.js` -pub async fn atto_js_request() -> impl IntoResponse { - ( - [("Content-Type", "text/javascript")], - crate::assets::ATTO_JS, - ) -} - -/// `/js/atto.js` -pub async fn loader_js_request() -> impl IntoResponse { - ( - [("Content-Type", "text/javascript")], - crate::assets::LOADER_JS, - ) -} +serve_asset!(loader_js_request: LOADER_JS("text/javascript")); +serve_asset!(atto_js_request: ATTO_JS("text/javascript")); +serve_asset!(me_js_request: ME_JS("text/javascript")); diff --git a/crates/app/src/routes/mod.rs b/crates/app/src/routes/mod.rs index 4ec6673..bf73c9d 100644 --- a/crates/app/src/routes/mod.rs +++ b/crates/app/src/routes/mod.rs @@ -12,8 +12,9 @@ pub fn routes(config: &Config) -> Router { Router::new() // assets .route("/css/style.css", get(assets::style_css_request)) - .route("/js/atto.js", get(assets::atto_js_request)) .route("/js/loader.js", get(assets::loader_js_request)) + .route("/js/atto.js", get(assets::atto_js_request)) + .route("/js/me.js", get(assets::me_js_request)) .nest_service( "/public", get_service(tower_http::services::ServeDir::new(&config.dirs.assets)), diff --git a/crates/app/src/routes/pages/auth.rs b/crates/app/src/routes/pages/auth.rs index d162722..85ab3c6 100644 --- a/crates/app/src/routes/pages/auth.rs +++ b/crates/app/src/routes/pages/auth.rs @@ -8,7 +8,7 @@ use axum_extra::extract::CookieJar; /// `/auth/login` pub async fn login_request(jar: CookieJar, Extension(data): Extension) -> impl IntoResponse { let data = data.read().await; - let user = get_user_from_token!((jar, data.0) ); + let user = get_user_from_token!(jar, data.0); if user.is_some() { return Err(Redirect::to("/")); @@ -28,7 +28,7 @@ pub async fn register_request( Extension(data): Extension, ) -> impl IntoResponse { let data = data.read().await; - let user = get_user_from_token!((jar, data.0) ); + let user = get_user_from_token!(jar, data.0); if user.is_some() { return Err(Redirect::to("/")); diff --git a/crates/app/src/routes/pages/misc.rs b/crates/app/src/routes/pages/misc.rs index 53c71f9..acf8f82 100644 --- a/crates/app/src/routes/pages/misc.rs +++ b/crates/app/src/routes/pages/misc.rs @@ -8,7 +8,7 @@ use axum_extra::extract::CookieJar; /// `/` pub async fn index_request(jar: CookieJar, Extension(data): Extension) -> impl IntoResponse { let data = data.read().await; - let user = get_user_from_token!((jar, data.0) ); + let user = get_user_from_token!(jar, data.0); let lang = get_lang!(jar, data.0); let mut context = initial_context(&data.0.0, lang, &user); diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 22f09b9..795efac 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -20,3 +20,4 @@ rusqlite = { version = "0.34.0", optional = true } tokio-postgres = { version = "0.7.13", optional = true } bb8-postgres = { version = "0.9.0", optional = true } +bitflags = "2.9.0" diff --git a/crates/core/src/database/auth.rs b/crates/core/src/database/auth.rs index 857edf9..445ab2f 100644 --- a/crates/core/src/database/auth.rs +++ b/crates/core/src/database/auth.rs @@ -1,4 +1,5 @@ use super::*; +use crate::model::permissions::FinePermission; use crate::model::{Error, Result}; use crate::{execute, get, query_row}; @@ -18,12 +19,13 @@ impl DataManager { ) -> User { User { id: get!(x->0(u64)) as usize, - created: get!(x->1(u64)) as usize as usize, + created: get!(x->1(u64)) as usize, username: get!(x->2(String)), password: get!(x->3(String)), salt: get!(x->4(String)), settings: serde_json::from_str(&get!(x->5(String)).to_string()).unwrap(), tokens: serde_json::from_str(&get!(x->6(String)).to_string()).unwrap(), + permissions: FinePermission::from_bits(get!(x->7(u32))).unwrap(), } } @@ -124,15 +126,16 @@ impl DataManager { let res = execute!( &conn, - "INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7)", + "INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", &[ - &data.id.to_string(), - &data.created.to_string(), - &data.username, - &data.password, - &data.salt, - &serde_json::to_string(&data.settings).unwrap(), - &serde_json::to_string(&data.tokens).unwrap(), + &data.id.to_string().as_str(), + &data.created.to_string().as_str(), + &data.username.as_str(), + &data.password.as_str(), + &data.salt.as_str(), + &serde_json::to_string(&data.settings).unwrap().as_str(), + &serde_json::to_string(&data.tokens).unwrap().as_str(), + &(FinePermission::DEFAULT.bits()).to_string().as_str() ] ); diff --git a/crates/core/src/database/drivers/sql/create_users.sql b/crates/core/src/database/drivers/sql/create_users.sql index 869c9f6..2b7420e 100644 --- a/crates/core/src/database/drivers/sql/create_users.sql +++ b/crates/core/src/database/drivers/sql/create_users.sql @@ -5,5 +5,6 @@ CREATE TABLE IF NOT EXISTS users ( password TEXT NOT NULL, salt TEXT NOT NULL, settings TEXT NOT NULL, - tokens TEXT NOT NULL + tokens TEXT NOT NULL, + permissions INTEGER NOT NULL ) diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs index 6faaafc..0bf3df8 100644 --- a/crates/core/src/model/auth.rs +++ b/crates/core/src/model/auth.rs @@ -5,6 +5,8 @@ use tetratto_shared::{ unix_epoch_timestamp, }; +use super::permissions::FinePermission; + /// `(ip, token, creation timestamp)` pub type Token = (String, String, usize); @@ -17,6 +19,7 @@ pub struct User { pub salt: String, pub settings: UserSettings, pub tokens: Vec, + pub permissions: FinePermission, } #[derive(Debug, Serialize, Deserialize)] @@ -45,6 +48,7 @@ impl User { salt, settings: UserSettings::default(), tokens: Vec::new(), + permissions: FinePermission::DEFAULT, } } diff --git a/crates/core/src/model/mod.rs b/crates/core/src/model/mod.rs index 44d2392..b3af128 100644 --- a/crates/core/src/model/mod.rs +++ b/crates/core/src/model/mod.rs @@ -1,4 +1,5 @@ pub mod auth; +pub mod permissions; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] @@ -18,6 +19,7 @@ pub enum Error { RegistrationDisabled, DatabaseError(String), IncorrectPassword, + NotAllowed, AlreadyAuthenticated, DataTooLong(String), DataTooShort(String), @@ -27,14 +29,15 @@ pub enum Error { impl ToString for Error { fn to_string(&self) -> String { match self { - Error::DatabaseConnection(msg) => msg.to_owned(), - Error::DatabaseError(msg) => format!("Database error: {msg}"), - Error::UserNotFound => "Unable to find user with given parameters".to_string(), - Error::RegistrationDisabled => "Registration is disabled".to_string(), - Error::IncorrectPassword => "The given password is invalid".to_string(), - Error::AlreadyAuthenticated => "Already authenticated".to_string(), - Error::DataTooLong(name) => format!("Given {name} is too long!"), - Error::DataTooShort(name) => format!("Given {name} is too short!"), + Self::DatabaseConnection(msg) => msg.to_owned(), + Self::DatabaseError(msg) => format!("Database error: {msg}"), + Self::UserNotFound => "Unable to find user with given parameters".to_string(), + Self::RegistrationDisabled => "Registration is disabled".to_string(), + Self::IncorrectPassword => "The given password is invalid".to_string(), + Self::NotAllowed => "You are not allowed to do this".to_string(), + Self::AlreadyAuthenticated => "Already authenticated".to_string(), + Self::DataTooLong(name) => format!("Given {name} is too long!"), + Self::DataTooShort(name) => format!("Given {name} is too short!"), _ => format!("An unknown error as occurred: ({:?})", self), } } diff --git a/crates/core/src/model/permissions.rs b/crates/core/src/model/permissions.rs new file mode 100644 index 0000000..f91153a --- /dev/null +++ b/crates/core/src/model/permissions.rs @@ -0,0 +1,123 @@ +use bitflags::bitflags; +use serde::{ + Deserialize, Deserializer, Serialize, + de::{Error as DeError, Visitor}, +}; + +bitflags! { + /// Fine-grained permissions built using bitwise operations. + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub struct FinePermission: u32 { + const DEFAULT = 1 << 0; + const ADMINISTRATOR = 1 << 1; + const MANAGE_JOURNAL_PAGES = 1 << 2; + const MANAGE_JOURNAL_ENTRIES = 1 << 3; + const MANAGE_JOURNAL_ENTRY_COMMENTS = 1 << 4; + const MANAGE_USERS = 1 << 5; + const MANAGE_BANS = 1 << 6; // includes managing IP bans + const MANAGE_WARNINGS = 1 << 7; + const MANAGE_NOTIFICATIONS = 1 << 8; + const VIEW_REPORTS = 1 << 9; + const VIEW_AUDIT_LOG = 1 << 10; + + const _ = !0; + } +} + +impl Serialize for FinePermission { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_u32(self.bits()) + } +} + +struct FinePermissionVisitor; +impl<'de> Visitor<'de> for FinePermissionVisitor { + type Value = FinePermission; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("u32") + } + + fn visit_u32(self, value: u32) -> Result + where + E: DeError, + { + if let Some(permission) = FinePermission::from_bits(value) { + Ok(permission) + } else { + Ok(FinePermission::from_bits_retain(value)) + } + } + + fn visit_i32(self, value: i32) -> Result + where + E: DeError, + { + if let Some(permission) = FinePermission::from_bits(value as u32) { + Ok(permission) + } else { + Ok(FinePermission::from_bits_retain(value as u32)) + } + } + + fn visit_u64(self, value: u64) -> Result + where + E: DeError, + { + if let Some(permission) = FinePermission::from_bits(value as u32) { + Ok(permission) + } else { + Ok(FinePermission::from_bits_retain(value as u32)) + } + } +} + +impl<'de> Deserialize<'de> for FinePermission { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(FinePermissionVisitor) + } +} + +impl FinePermission { + /// Join two [`FinePermission`]s into a single `u32`. + pub fn join(lhs: FinePermission, rhs: FinePermission) -> FinePermission { + lhs | rhs + } + + /// Check if the given `input` contains the given [`FinePermission`]. + pub fn check(self, permission: FinePermission) -> bool { + if (self & FinePermission::ADMINISTRATOR) == FinePermission::ADMINISTRATOR { + // has administrator permission, meaning everything else is automatically true + return true; + } + + (self & permission) == permission + } + + /// Check if thhe given [`FinePermission`] is qualifies as "Helper" status. + pub fn check_helper(self) -> bool { + self.check(FinePermission::MANAGE_JOURNAL_ENTRIES) + && self.check(FinePermission::MANAGE_JOURNAL_PAGES) + && self.check(FinePermission::MANAGE_JOURNAL_ENTRY_COMMENTS) + && self.check(FinePermission::MANAGE_WARNINGS) + && self.check(FinePermission::VIEW_REPORTS) + && self.check(FinePermission::VIEW_AUDIT_LOG) + } + + /// Check if thhe given [`FinePermission`] is qualifies as "Manager" status. + pub fn check_manager(self) -> bool { + self.check_helper() && self.check(FinePermission::ADMINISTRATOR) + } +} + +impl Default for FinePermission { + fn default() -> Self { + Self::DEFAULT + } +} diff --git a/crates/l10n/LICENSE b/crates/l10n/LICENSE new file mode 120000 index 0000000..30cff74 --- /dev/null +++ b/crates/l10n/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/crates/shared/LICENSE b/crates/shared/LICENSE new file mode 120000 index 0000000..30cff74 --- /dev/null +++ b/crates/shared/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/tetratto.toml b/tetratto.toml deleted file mode 100644 index 0aa07cd..0000000 --- a/tetratto.toml +++ /dev/null @@ -1,16 +0,0 @@ -name = "Tetratto" -description = "🐐 tetratto!" -color = "#c9b1bc" -port = 4118 - -[security] -registration_enabled = true -admin_user = "admin" -real_ip_header = "CF-Connecting-IP" - -[dirs] -templates = "html" -assets = "public" - -[database] -name = "atto.db"