diff --git a/crates/app/src/assets.rs b/crates/app/src/assets.rs index 5eba52a..a6bff19 100644 --- a/crates/app/src/assets.rs +++ b/crates/app/src/assets.rs @@ -60,6 +60,7 @@ pub const MOD_AUDIT_LOG: &str = include_str!("./public/html/mod/audit_log.html") pub const MOD_REPORTS: &str = include_str!("./public/html/mod/reports.html"); pub const MOD_FILE_REPORT: &str = include_str!("./public/html/mod/file_report.html"); pub const MOD_IP_BANS: &str = include_str!("./public/html/mod/ip_bans.html"); +pub const MOD_PROFILE: &str = include_str!("./public/html/mod/profile.html"); // langs pub const LANG_EN_US: &str = include_str!("./langs/en-US.toml"); @@ -186,6 +187,7 @@ pub(crate) async fn write_assets(config: &Config) -> PathBufD { write_template!(html_path->"mod/reports.html"(crate::assets::MOD_REPORTS) --config=config); write_template!(html_path->"mod/file_report.html"(crate::assets::MOD_FILE_REPORT) --config=config); write_template!(html_path->"mod/ip_bans.html"(crate::assets::MOD_IP_BANS) --config=config); + write_template!(html_path->"mod/profile.html"(crate::assets::MOD_PROFILE) --config=config); html_path } diff --git a/crates/app/src/langs/en-US.toml b/crates/app/src/langs/en-US.toml index 6275570..079bcc1 100644 --- a/crates/app/src/langs/en-US.toml +++ b/crates/app/src/langs/en-US.toml @@ -30,6 +30,7 @@ version = "1.0.0" "dialog:action.yes" = "Yes" "dialog:action.no" = "No" "dialog:action.save_and_close" = "Save and close" +"dialog:action.close" = "Close" "auth:action.login" = "Login" "auth:action.register" = "Register" @@ -45,7 +46,6 @@ version = "1.0.0" "auth:label.relationship" = "Relationship" "auth:label.joined_communities" = "Joined communities" "auth:label.recent_posts" = "Recent posts" -"auth:label.moderation" = "Moderation" "communities:action.create" = "Create" "communities:action.select" = "Select" @@ -94,3 +94,5 @@ version = "1.0.0" "settings:label.change_banner" = "Change banner" "mod_panel:label.open_reported_content" = "Open reported content" +"mod_panel:label.manage_profile" = "Manage profile" +"mod_panel:label.permissions_level_builder" = "Permission level builder" diff --git a/crates/app/src/public/css/style.css b/crates/app/src/public/css/style.css index 6ec1761..8e8107d 100644 --- a/crates/app/src/public/css/style.css +++ b/crates/app/src/public/css/style.css @@ -406,7 +406,7 @@ table ol { } .card-nest .card { - box-shadow: 0; + box-shadow: none; } .card-nest > .card:first-child { diff --git a/crates/app/src/public/html/communities/settings.html b/crates/app/src/public/html/communities/settings.html index 7c5f099..36d8c27 100644 --- a/crates/app/src/public/html/communities/settings.html +++ b/crates/app/src/public/html/communities/settings.html @@ -325,94 +325,23 @@ } // permissions manager - const permissions = { - // https://trisuaso.github.io/tetratto/tetratto/model/communities_permissions/struct.CommunityPermission.html - DEFAULT: 1 << 0, - ADMINISTRATOR: 1 << 1, - MEMBER: 1 << 2, - MANAGE_POSTS: 1 << 3, - MANAGE_ROLES: 1 << 4, - BANNED: 1 << 5, - REQUESTED: 1 << 6, - MANAGE_PINS: 1 << 7, - MANAGE_COMMUNITY: 1 << 8, - }; - - function all_matching_permissions(role) { - const matching = []; - const not_matching = []; - - for (permission of Object.entries(permissions)) { - if ((role & permission[1]) === permission[1]) { - matching.push(permission[0]); - } else { - not_matching.push(permission[0]); - } - } - - return [matching, not_matching]; - } - - function rebuild_role(matching) { - let role = 0; - - for (const permission of matching) { - role = role | permissions[permission]; - } - - document.getElementById("role").value = role; - return role; - } - - function get_permissions_html(role, id) { - const [matching, not_matching] = - all_matching_permissions(role); - - globalThis.remove_permission_from_role = ( - permission, - ) => { - matching.splice(matching.indexOf(permission), 1); - not_matching.push(permission); - - document.getElementById(id).innerHTML = - get_permissions_html( - rebuild_role(matching), - id, - ); - }; - - globalThis.add_permission_to_role = (permission) => { - not_matching.splice( - not_matching.indexOf(permission), - 1, - ); - matching.push(permission); - - document.getElementById(id).innerHTML = - get_permissions_html( - rebuild_role(matching), - id, - ); - }; - - let permissions_html = ""; - - for (const match of matching) { - permissions_html += `
- ${match} ${permissions[match]} - -
`; - } - - for (const match of not_matching) { - permissions_html += `
- ${match} ${permissions[match]} - -
`; - } - - return permissions_html; - } + const get_permissions_html = trigger( + "ui::generate_permissions_ui", + [ + { + // https://trisuaso.github.io/tetratto/tetratto/model/communities_permissions/struct.CommunityPermission.html + DEFAULT: 1 << 0, + ADMINISTRATOR: 1 << 1, + MEMBER: 1 << 2, + MANAGE_POSTS: 1 << 3, + MANAGE_ROLES: 1 << 4, + BANNED: 1 << 5, + REQUESTED: 1 << 6, + MANAGE_PINS: 1 << 7, + MANAGE_COMMUNITY: 1 << 8, + }, + ], + ); // ... element.innerHTML = `
diff --git a/crates/app/src/public/html/components.html b/crates/app/src/public/html/components.html index 27ccee1..6d1ee56 100644 --- a/crates/app/src/public/html/components.html +++ b/crates/app/src/public/html/components.html @@ -347,4 +347,72 @@ user.settings.private_last_online or is_helper %}
+{% endif %} {%- endmacro %} {% macro town_square_post_form() -%} {% if +config.town_square and user %} +
+
+
+ {{ icon "pencil" }} + {{ text "communities:label.create_post" }} +
+ + Posts created here go to the + town square + community! +
+ +
+
+ + +
+ + +
+
+ + {% endif %} {%- endmacro %} diff --git a/crates/app/src/public/html/macros.html b/crates/app/src/public/html/macros.html index 18a6f6a..761d30e 100644 --- a/crates/app/src/public/html/macros.html +++ b/crates/app/src/public/html/macros.html @@ -39,6 +39,13 @@ show_lhs=true) -%} - - {% endif %} {% block content %}{% endblock %} + {% block content %}{% endblock %} diff --git a/crates/app/src/public/html/profile/posts.html b/crates/app/src/public/html/profile/posts.html index 813ebe3..7a4f3ed 100644 --- a/crates/app/src/public/html/profile/posts.html +++ b/crates/app/src/public/html/profile/posts.html @@ -1,73 +1,5 @@ {% import "macros.html" as macros %} {% extends "profile/base.html" %} {% block -content %} {% if config.town_square and is_self %} -
-
-
- {{ icon "pencil" }} - {{ text "communities:label.create_post" }} -
- - Posts created here go to the - town square - community! -
- -
-
- - -
- - -
-
- - -{% endif %} - +content %}
{{ icon "clock" }} diff --git a/crates/app/src/public/html/root.html b/crates/app/src/public/html/root.html index 8dc53a6..a1438ab 100644 --- a/crates/app/src/public/html/root.html +++ b/crates/app/src/public/html/root.html @@ -295,6 +295,26 @@
+ + +
+ {{ components::town_square_post_form() }} + +
+
+ +
+ +
+
+
+
{% endif %} diff --git a/crates/app/src/public/js/atto.js b/crates/app/src/public/js/atto.js index 50a96e3..ea4866b 100644 --- a/crates/app/src/public/js/atto.js +++ b/crates/app/src/public/js/atto.js @@ -810,4 +810,76 @@ ${option.input_element_type === "textarea" ? `${option.value}` : ""} }; }, ); + + // permissions ui + self.define( + "generate_permissions_ui", + (_, permissions, field_id = "role") => { + function all_matching_permissions(role) { + const matching = []; + const not_matching = []; + + for (const permission of Object.entries(permissions)) { + if ((role & permission[1]) === permission[1]) { + matching.push(permission[0]); + } else { + not_matching.push(permission[0]); + } + } + + return [matching, not_matching]; + } + + function rebuild_role(matching) { + let role = 0; + + for (const permission of matching) { + role = role | permissions[permission]; + } + + document.getElementById(field_id).value = role; + return role; + } + + function get_permissions_html(role, id) { + const [matching, not_matching] = all_matching_permissions(role); + + globalThis.remove_permission_from_role = (permission) => { + matching.splice(matching.indexOf(permission), 1); + not_matching.push(permission); + + document.getElementById(id).innerHTML = + get_permissions_html(rebuild_role(matching), id); + }; + + globalThis.add_permission_to_role = (permission) => { + not_matching.splice(not_matching.indexOf(permission), 1); + matching.push(permission); + + document.getElementById(id).innerHTML = + get_permissions_html(rebuild_role(matching), id); + }; + + let permissions_html = ""; + + for (const match of matching) { + permissions_html += `
+ ${match} ${permissions[match]} + +
`; + } + + for (const match of not_matching) { + permissions_html += `
+ ${match} ${permissions[match]} + +
`; + } + + return permissions_html; + } + + return get_permissions_html; + }, + ); })(); diff --git a/crates/app/src/routes/pages/mod.rs b/crates/app/src/routes/pages/mod.rs index b1ae182..268092e 100644 --- a/crates/app/src/routes/pages/mod.rs +++ b/crates/app/src/routes/pages/mod.rs @@ -30,6 +30,10 @@ pub fn routes() -> Router { get(mod_panel::file_report_request), ) .route("/mod_panel/ip_bans", get(mod_panel::ip_bans_request)) + .route( + "/mod_panel/profile/{id}", + get(mod_panel::manage_profile_request), + ) // auth .route("/auth/register", get(auth::register_request)) .route("/auth/login", get(auth::login_request)) diff --git a/crates/app/src/routes/pages/mod_panel.rs b/crates/app/src/routes/pages/mod_panel.rs index 7f37da4..a815003 100644 --- a/crates/app/src/routes/pages/mod_panel.rs +++ b/crates/app/src/routes/pages/mod_panel.rs @@ -1,9 +1,9 @@ use super::{PaginatedQuery, render_error}; use crate::{State, assets::initial_context, get_lang, get_user_from_token}; use axum::{ - Extension, - extract::Query, + extract::{Path, Query}, response::{Html, IntoResponse}, + Extension, }; use axum_extra::extract::CookieJar; use serde::Deserialize; @@ -149,3 +149,38 @@ pub async fn ip_bans_request( // return Ok(Html(data.1.render("mod/ip_bans.html", &context).unwrap())) } + +/// `/mod_panel/profile/{id}` +pub async fn manage_profile_request( + jar: CookieJar, + Extension(data): Extension, + Path(id): Path, +) -> impl IntoResponse { + let data = data.read().await; + let user = match get_user_from_token!(jar, data.0) { + Some(ua) => ua, + None => { + return Err(Html( + render_error(Error::NotAllowed, &jar, &data, &None).await, + )); + } + }; + + if !user.permissions.check(FinePermission::MANAGE_USERS) { + return Err(Html( + render_error(Error::NotAllowed, &jar, &data, &None).await, + )); + } + + let profile = match data.0.get_user_by_id(id).await { + Ok(p) => p, + Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)), + }; + + let lang = get_lang!(jar, data.0); + let mut context = initial_context(&data.0.0, lang, &Some(user)).await; + context.insert("profile", &profile); + + // return + Ok(Html(data.1.render("mod/profile.html", &context).unwrap())) +} diff --git a/crates/app/src/routes/pages/profile.rs b/crates/app/src/routes/pages/profile.rs index aff32e9..4a35bf7 100644 --- a/crates/app/src/routes/pages/profile.rs +++ b/crates/app/src/routes/pages/profile.rs @@ -8,9 +8,7 @@ use axum::{ use axum_extra::extract::CookieJar; use serde::Deserialize; use tera::Context; -use tetratto_core::model::{ - Error, auth::User, communities::Community, permissions::FinePermission, -}; +use tetratto_core::model::{Error, auth::User, communities::Community, permissions::FinePermission}; #[derive(Deserialize)] pub struct SettingsProps { @@ -87,6 +85,11 @@ pub fn profile_context( context.insert("is_following", &is_following); context.insert("is_following_you", &is_following_you); context.insert("is_blocking", &is_blocking); + + context.insert( + "is_supporter", + &profile.permissions.check(FinePermission::SUPPORTER), + ); } /// `/@{username}` @@ -121,11 +124,14 @@ pub async fn posts_request( // check for private profile if other_user.settings.private_profile { if let Some(ref ua) = user { - if (ua.id != other_user.id) && !ua.permissions.check(FinePermission::MANAGE_USERS) && data + if (ua.id != other_user.id) + && !ua.permissions.check(FinePermission::MANAGE_USERS) + && data .0 .get_userfollow_by_initiator_receiver(other_user.id, ua.id) .await - .is_err() { + .is_err() + { return Err(Html( render_error(Error::NotAllowed, &jar, &data, &user).await, )); @@ -243,11 +249,13 @@ pub async fn following_request( // check for private profile if other_user.settings.private_profile { if let Some(ref ua) = user { - if ua.id != other_user.id && data + if ua.id != other_user.id + && data .0 .get_userfollow_by_initiator_receiver(other_user.id, ua.id) .await - .is_err() { + .is_err() + { return Err(Html( render_error(Error::NotAllowed, &jar, &data, &user).await, )); @@ -367,11 +375,13 @@ pub async fn followers_request( // check for private profile if other_user.settings.private_profile { if let Some(ref ua) = user { - if ua.id != other_user.id && data + if ua.id != other_user.id + && data .0 .get_userfollow_by_initiator_receiver(other_user.id, ua.id) .await - .is_err() { + .is_err() + { return Err(Html( render_error(Error::NotAllowed, &jar, &data, &user).await, )); diff --git a/crates/core/src/database/communities.rs b/crates/core/src/database/communities.rs index 3b596ab..c1d7627 100644 --- a/crates/core/src/database/communities.rs +++ b/crates/core/src/database/communities.rs @@ -183,7 +183,13 @@ impl DataManager { } } - if admin_count >= 5 { + let maximum_count = if owner.permissions.check(FinePermission::SUPPORTER) { + 10 + } else { + 5 + }; + + if admin_count >= maximum_count { return Err(Error::MiscError( "You are already owner/co-owner of too many communities to create another" .to_string(), diff --git a/crates/core/src/model/permissions.rs b/crates/core/src/model/permissions.rs index db4a1e5..3941314 100644 --- a/crates/core/src/model/permissions.rs +++ b/crates/core/src/model/permissions.rs @@ -27,6 +27,7 @@ bitflags! { const MANAGE_REPORTS = 1 << 16; const BANNED = 1 << 17; const INFINITE_COMMUNITIES = 1 << 18; + const SUPPORTER = 1 << 19; const _ = !0; }