add: user secondary permission

This commit is contained in:
trisua 2025-06-23 19:42:02 -04:00
parent 339aa59434
commit 9528d71b2a
6 changed files with 128 additions and 95 deletions

View file

@ -393,10 +393,6 @@
(script ("id" "editor_content") ("type" "text/markdown") (text "{{ note.content|remove_script_tags|safe }}")) (script ("id" "editor_content") ("type" "text/markdown") (text "{{ note.content|remove_script_tags|safe }}"))
(script (script
(text "setTimeout(async () => { (text "setTimeout(async () => {
if (!document.getElementById(\"preview_tab\").shadowRoot) {
document.getElementById(\"preview_tab\").attachShadow({ mode: \"open\" });
}
document.getElementById(\"editor_tab\").innerHTML = \"\"; document.getElementById(\"editor_tab\").innerHTML = \"\";
globalThis.editor = CodeMirror(document.getElementById(\"editor_tab\"), { globalThis.editor = CodeMirror(document.getElementById(\"editor_tab\"), {
value: document.getElementById(\"editor_content\").innerHTML, value: document.getElementById(\"editor_content\").innerHTML,
@ -451,8 +447,7 @@
).text(); ).text();
const preview_token = window.crypto.randomUUID(); const preview_token = window.crypto.randomUUID();
document.getElementById(\"preview_tab\").shadowRoot.innerHTML = `${res}<style> document.getElementById(\"preview_tab\").innerHTML = `${res}<style>
@import url(\"/css/style.css\");
@import url(\"/api/v1/journals/{{ journal.id }}/journal.css?v=preview-${preview_token}\"); @import url(\"/api/v1/journals/{{ journal.id }}/journal.css?v=preview-${preview_token}\");
</style>`; </style>`;
trigger(\"atto::hooks::tabs:switch\", [\"preview\"]); trigger(\"atto::hooks::tabs:switch\", [\"preview\"]);

View file

@ -3,6 +3,7 @@ use oiseau::cache::Cache;
use crate::model::auth::UserConnections; use crate::model::auth::UserConnections;
use crate::model::moderation::AuditLogEntry; use crate::model::moderation::AuditLogEntry;
use crate::model::oauth::AuthGrant; use crate::model::oauth::AuthGrant;
use crate::model::permissions::SecondaryPermission;
use crate::model::{ use crate::model::{
Error, Result, Error, Result,
auth::{Token, User, UserSettings}, auth::{Token, User, UserSettings},
@ -46,6 +47,7 @@ impl DataManager {
grants: serde_json::from_str(&get!(x->19(String)).to_string()).unwrap(), grants: serde_json::from_str(&get!(x->19(String)).to_string()).unwrap(),
associated: serde_json::from_str(&get!(x->20(String)).to_string()).unwrap(), associated: serde_json::from_str(&get!(x->20(String)).to_string()).unwrap(),
invite_code: get!(x->21(i64)) as usize, invite_code: get!(x->21(i64)) as usize,
secondary_permissions: SecondaryPermission::from_bits(get!(x->7(i32)) as u32).unwrap(),
} }
} }
@ -224,7 +226,8 @@ impl DataManager {
&"", &"",
&serde_json::to_string(&data.grants).unwrap(), &serde_json::to_string(&data.grants).unwrap(),
&serde_json::to_string(&data.associated).unwrap(), &serde_json::to_string(&data.associated).unwrap(),
&(data.invite_code as i64) &(data.invite_code as i64),
&(SecondaryPermission::DEFAULT.bits() as i32),
] ]
); );

View file

@ -19,5 +19,6 @@ CREATE TABLE IF NOT EXISTS users (
connections TEXT NOT NULL, connections TEXT NOT NULL,
stripe_id TEXT NOT NULL, stripe_id TEXT NOT NULL,
grants TEXT NOT NULL, grants TEXT NOT NULL,
associated TEXT NOT NULL associated TEXT NOT NULL,
secondary_permissions INT NOT NULL
) )

View file

@ -1,6 +1,9 @@
use std::collections::HashMap; use std::collections::HashMap;
use super::{oauth::AuthGrant, permissions::FinePermission}; use super::{
oauth::AuthGrant,
permissions::{FinePermission, SecondaryPermission},
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use totp_rs::TOTP; use totp_rs::TOTP;
use tetratto_shared::{ use tetratto_shared::{
@ -52,6 +55,9 @@ pub struct User {
/// The ID of the [`InviteCode`] this user provided during registration. /// The ID of the [`InviteCode`] this user provided during registration.
#[serde(default)] #[serde(default)]
pub invite_code: usize, pub invite_code: usize,
/// Secondary permissions because the regular permissions struct ran out of possible bits.
#[serde(default)]
pub secondary_permissions: SecondaryPermission,
} }
pub type UserConnections = pub type UserConnections =
@ -287,6 +293,7 @@ impl User {
grants: Vec::new(), grants: Vec::new(),
associated: Vec::new(), associated: Vec::new(),
invite_code: 0, invite_code: 0,
secondary_permissions: SecondaryPermission::DEFAULT,
} }
} }

View file

@ -44,18 +44,20 @@ bitflags! {
} }
} }
impl Serialize for FinePermission { macro_rules! user_permission {
($struct:ident, $visitor:ident, $banned_check:ident) => {
impl Serialize for $struct {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where where
S: serde::Serializer, S: serde::Serializer,
{ {
serializer.serialize_u32(self.bits()) serializer.serialize_u32(self.bits())
} }
} }
struct FinePermissionVisitor; struct $visitor;
impl Visitor<'_> for FinePermissionVisitor { impl Visitor<'_> for $visitor {
type Value = FinePermission; type Value = $struct;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("u32") formatter.write_str("u32")
@ -65,10 +67,10 @@ impl Visitor<'_> for FinePermissionVisitor {
where where
E: DeError, E: DeError,
{ {
if let Some(permission) = FinePermission::from_bits(value) { if let Some(permission) = $struct::from_bits(value) {
Ok(permission) Ok(permission)
} else { } else {
Ok(FinePermission::from_bits_retain(value)) Ok($struct::from_bits_retain(value))
} }
} }
@ -76,10 +78,10 @@ impl Visitor<'_> for FinePermissionVisitor {
where where
E: DeError, E: DeError,
{ {
if let Some(permission) = FinePermission::from_bits(value as u32) { if let Some(permission) = $struct::from_bits(value as u32) {
Ok(permission) Ok(permission)
} else { } else {
Ok(FinePermission::from_bits_retain(value as u32)) Ok($struct::from_bits_retain(value as u32))
} }
} }
@ -87,35 +89,35 @@ impl Visitor<'_> for FinePermissionVisitor {
where where
E: DeError, E: DeError,
{ {
if let Some(permission) = FinePermission::from_bits(value as u32) { if let Some(permission) = $struct::from_bits(value as u32) {
Ok(permission) Ok(permission)
} else { } else {
Ok(FinePermission::from_bits_retain(value as u32)) Ok($struct::from_bits_retain(value as u32))
}
} }
} }
}
impl<'de> Deserialize<'de> for FinePermission { impl<'de> Deserialize<'de> for $struct {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
deserializer.deserialize_any(FinePermissionVisitor) deserializer.deserialize_any($visitor)
}
} }
}
impl FinePermission { impl $struct {
/// Join two [`FinePermission`]s into a single `u32`. /// Join two permissions into a single `u32`.
pub fn join(lhs: FinePermission, rhs: FinePermission) -> FinePermission { pub fn join(lhs: $struct, rhs: $struct) -> Self {
lhs | rhs lhs | rhs
} }
/// Check if the given `input` contains the given [`FinePermission`]. /// Check if the given `input` contains the given permission.
pub fn check(self, permission: FinePermission) -> bool { pub fn check(self, permission: $struct) -> bool {
if (self & FinePermission::ADMINISTRATOR) == FinePermission::ADMINISTRATOR { if (self & $struct::ADMINISTRATOR) == $struct::ADMINISTRATOR {
// has administrator permission, meaning everything else is automatically true // has administrator permission, meaning everything else is automatically true
return true; return true;
} else if self.check_banned() { } else if self.$banned_check() {
// has banned permission, meaning everything else is automatically false // has banned permission, meaning everything else is automatically false
return false; return false;
} }
@ -123,7 +125,29 @@ impl FinePermission {
(self & permission) == permission (self & permission) == permission
} }
/// Check if the given [`FinePermission`] qualifies as "Helper" status. /// Sink for checking if the permission is banned.
pub fn sink(&self) -> bool {
false
}
}
impl Default for $struct {
fn default() -> Self {
Self::DEFAULT
}
}
};
}
user_permission!(FinePermission, FinePermissionVisitor, check_banned);
impl FinePermission {
/// Check if the given permission qualifies as "Banned" status.
pub fn check_banned(self) -> bool {
(self & FinePermission::BANNED) == FinePermission::BANNED
}
/// Check if the given permission qualifies as "Helper" status.
pub fn check_helper(self) -> bool { pub fn check_helper(self) -> bool {
self.check(FinePermission::MANAGE_COMMUNITIES) self.check(FinePermission::MANAGE_COMMUNITIES)
&& self.check(FinePermission::MANAGE_POSTS) && self.check(FinePermission::MANAGE_POSTS)
@ -133,24 +157,26 @@ impl FinePermission {
&& self.check(FinePermission::VIEW_AUDIT_LOG) && self.check(FinePermission::VIEW_AUDIT_LOG)
} }
/// Check if the given [`FinePermission`] qualifies as "Manager" status. /// Check if the given permission qualifies as "Manager" status.
pub fn check_manager(self) -> bool { pub fn check_manager(self) -> bool {
self.check_helper() && self.check(FinePermission::MANAGE_USERS) self.check_helper() && self.check(FinePermission::MANAGE_USERS)
} }
/// Check if the given [`FinePermission`] qualifies as "Administrator" status. /// Check if the given permission qualifies as "Administrator" status.
pub fn check_admin(self) -> bool { pub fn check_admin(self) -> bool {
self.check_manager() && self.check(FinePermission::ADMINISTRATOR) self.check_manager() && self.check(FinePermission::ADMINISTRATOR)
} }
}
/// Check if the given [`FinePermission`] qualifies as "Banned" status. bitflags! {
pub fn check_banned(self) -> bool { /// Fine-grained permissions built using bitwise operations. Second permission value.
(self & FinePermission::BANNED) == FinePermission::BANNED #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SecondaryPermission: u32 {
const DEFAULT = 1 << 0;
const ADMINISTRATOR = 1 << 1;
const _ = !0;
} }
} }
impl Default for FinePermission { user_permission!(SecondaryPermission, SecondaryPermissionVisitor, sink);
fn default() -> Self {
Self::DEFAULT
}
}

View file

@ -14,8 +14,9 @@ pub fn render_markdown(input: &str) -> String {
}, },
parse: ParseOptions { parse: ParseOptions {
constructs: Constructs { constructs: Constructs {
gfm_autolink_literal: true, math_flow: true,
..Default::default() math_text: true,
..Constructs::gfm()
}, },
gfm_strikethrough_single_tilde: false, gfm_strikethrough_single_tilde: false,
math_text_single_dollar: false, math_text_single_dollar: false,