chore: bug fix cleanup (#4)

This commit is contained in:
hampus-fluxer
2026-01-03 06:44:40 +01:00
committed by GitHub
parent 275126d61b
commit c9c5dceb47
80 changed files with 4639 additions and 3709 deletions

View File

@@ -19,6 +19,7 @@ import fluxer_admin/api/common.{type UserLookupResult}
import fluxer_admin/avatar
import fluxer_admin/components/flash
import fluxer_admin/components/icons_meta
import fluxer_admin/navigation
import fluxer_admin/user
import fluxer_admin/web.{type Context, type Session, cache_busted_asset, href}
import gleam/list
@@ -96,13 +97,15 @@ pub fn page_with_refresh(
content: element.Element(a),
auto_refresh: Bool,
) {
let admin_acls = admin_acls_from(current_admin)
h.html(
[a.attribute("lang", "en"), a.attribute("data-base-path", ctx.base_path)],
[
build_head_with_refresh(title, ctx, auto_refresh),
h.body([a.class("min-h-screen bg-neutral-50 overflow-hidden")], [
h.div([a.class("flex h-screen")], [
sidebar(ctx, active_page),
sidebar(ctx, active_page, admin_acls),
h.div(
[
a.attribute("data-sidebar-overlay", ""),
@@ -135,7 +138,7 @@ pub fn page_with_refresh(
)
}
fn sidebar(ctx: Context, active_page: String) {
fn sidebar(ctx: Context, active_page: String, admin_acls: List(String)) {
h.div(
[
a.attribute("data-sidebar", ""),
@@ -173,7 +176,7 @@ fn sidebar(ctx: Context, active_page: String) {
[
a.class("flex-1 overflow-y-auto p-4 space-y-1 sidebar-scrollbar"),
],
admin_sidebar(ctx, active_page),
admin_sidebar(ctx, active_page, admin_acls),
),
h.script(
[a.attribute("defer", "defer")],
@@ -183,126 +186,20 @@ fn sidebar(ctx: Context, active_page: String) {
)
}
fn admin_sidebar(ctx: Context, active_page: String) -> List(element.Element(a)) {
[
sidebar_section("Lookup", [
sidebar_item(ctx, "Users", "/users", active_page == "users"),
sidebar_item(ctx, "Guilds", "/guilds", active_page == "guilds"),
]),
sidebar_section("Moderation", [
sidebar_item(ctx, "Reports", "/reports", active_page == "reports"),
sidebar_item(
ctx,
"Pending Verifications",
"/pending-verifications",
active_page == "pending-verifications",
),
sidebar_item(
ctx,
"Bulk Actions",
"/bulk-actions",
active_page == "bulk-actions",
),
]),
sidebar_section("Bans", [
sidebar_item(ctx, "IP Bans", "/ip-bans", active_page == "ip-bans"),
sidebar_item(
ctx,
"Email Bans",
"/email-bans",
active_page == "email-bans",
),
sidebar_item(
ctx,
"Phone Bans",
"/phone-bans",
active_page == "phone-bans",
),
]),
sidebar_section("Content", [
sidebar_item(
ctx,
"Message Tools",
"/messages",
active_page == "message-tools",
),
sidebar_item(ctx, "Archives", "/archives", active_page == "archives"),
sidebar_item(
ctx,
"Asset Purge",
"/asset-purge",
active_page == "asset-purge",
),
]),
sidebar_section("Metrics", [
sidebar_item(ctx, "Overview", "/metrics", active_page == "metrics"),
sidebar_item(
ctx,
"Messaging & API",
"/messages-metrics",
active_page == "messages-metrics",
),
]),
sidebar_section("Observability", [
sidebar_item(ctx, "Gateway", "/gateway", active_page == "gateway"),
sidebar_item(ctx, "Jobs", "/jobs", active_page == "jobs"),
sidebar_item(ctx, "Storage", "/storage", active_page == "storage"),
sidebar_item(
ctx,
"Audit Logs",
"/audit-logs",
active_page == "audit-logs",
),
]),
sidebar_section("Platform", [
sidebar_item(
ctx,
"Search Index",
"/search-index",
active_page == "search-index",
),
sidebar_item(
ctx,
"Voice Regions",
"/voice-regions",
active_page == "voice-regions",
),
sidebar_item(
ctx,
"Voice Servers",
"/voice-servers",
active_page == "voice-servers",
),
]),
sidebar_section("Configuration", [
sidebar_item(
ctx,
"Instance Config",
"/instance-config",
active_page == "instance-config",
),
sidebar_item(
ctx,
"Feature Flags",
"/feature-flags",
active_page == "feature-flags",
),
]),
sidebar_section("Codes", [
sidebar_item(
ctx,
"Beta Codes",
"/beta-codes",
active_page == "beta-codes",
),
sidebar_item(
ctx,
"Gift Codes",
"/gift-codes",
active_page == "gift-codes",
),
]),
]
fn admin_sidebar(
ctx: Context,
active_page: String,
admin_acls: List(String),
) -> List(element.Element(a)) {
navigation.accessible_sections(admin_acls)
|> list.map(fn(section) {
let items =
list.map(section.items, fn(item) {
sidebar_item(ctx, item.title, item.path, active_page == item.active_key)
})
sidebar_section(section.title, items)
})
}
fn sidebar_section(title: String, items: List(element.Element(a))) {
@@ -536,3 +433,12 @@ fn sidebar_interaction_script() {
",
)
}
fn admin_acls_from(
current_admin: Option(UserLookupResult),
) -> List(String) {
case current_admin {
option.Some(admin) -> admin.acls
option.None -> []
}
}

View File

@@ -219,7 +219,11 @@ pub const acl_wildcard = "*"
pub const acl_authenticate = "admin:authenticate"
pub const acl_process_memory_stats = "process:memory_stats"
pub const acl_gateway_memory_stats = "gateway:memory_stats"
pub const acl_process_memory_stats = acl_gateway_memory_stats
pub const acl_gateway_reload_all = "gateway:reload_all"
pub const acl_user_lookup = "user:lookup"
@@ -367,6 +371,10 @@ pub const acl_feature_flag_view = "feature_flag:view"
pub const acl_feature_flag_manage = "feature_flag:manage"
pub const acl_instance_config_view = "instance:config:view"
pub const acl_instance_config_update = "instance:config:update"
pub type FeatureFlag {
FeatureFlag(id: String, name: String, description: String)
}
@@ -490,7 +498,8 @@ pub fn get_all_acls() -> List(String) {
[
acl_wildcard,
acl_authenticate,
acl_process_memory_stats,
acl_gateway_memory_stats,
acl_gateway_reload_all,
acl_user_lookup,
acl_user_list_sessions,
acl_user_list_guilds,
@@ -564,5 +573,7 @@ pub fn get_all_acls() -> List(String) {
acl_metrics_view,
acl_feature_flag_view,
acl_feature_flag_manage,
acl_instance_config_view,
acl_instance_config_update,
]
}

View File

@@ -0,0 +1,171 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_admin/acl
import fluxer_admin/constants
import gleam/list
import gleam/option
pub type NavItem {
NavItem(
title: String,
path: String,
active_key: String,
required_acls: List(String),
)
}
pub type NavSection {
NavSection(title: String, items: List(NavItem))
}
pub fn sections() -> List(NavSection) {
[
NavSection("Lookup", [
NavItem("Users", "/users", "users", [constants.acl_user_lookup]),
NavItem("Guilds", "/guilds", "guilds", [constants.acl_guild_lookup]),
]),
NavSection("Moderation", [
NavItem("Reports", "/reports", "reports", [constants.acl_report_view]),
NavItem(
"Pending Verifications",
"/pending-verifications",
"pending-verifications",
[constants.acl_pending_verification_view],
),
NavItem("Bulk Actions", "/bulk-actions", "bulk-actions", [
constants.acl_bulk_update_user_flags,
constants.acl_bulk_update_guild_features,
constants.acl_bulk_add_guild_members,
constants.acl_bulk_delete_users,
]),
]),
NavSection("Bans", [
NavItem("IP Bans", "/ip-bans", "ip-bans", [
constants.acl_ban_ip_check,
constants.acl_ban_ip_add,
constants.acl_ban_ip_remove,
]),
NavItem("Email Bans", "/email-bans", "email-bans", [
constants.acl_ban_email_check,
constants.acl_ban_email_add,
constants.acl_ban_email_remove,
]),
NavItem("Phone Bans", "/phone-bans", "phone-bans", [
constants.acl_ban_phone_check,
constants.acl_ban_phone_add,
constants.acl_ban_phone_remove,
]),
]),
NavSection("Content", [
NavItem("Message Tools", "/messages", "message-tools", [
constants.acl_message_lookup,
constants.acl_message_delete,
constants.acl_message_shred,
constants.acl_message_delete_all,
]),
NavItem("Archives", "/archives", "archives", [
constants.acl_archive_view_all,
constants.acl_archive_trigger_user,
constants.acl_archive_trigger_guild,
]),
NavItem("Asset Purge", "/asset-purge", "asset-purge", [
constants.acl_asset_purge,
]),
]),
NavSection("Metrics", [
NavItem("Overview", "/metrics", "metrics", [constants.acl_metrics_view]),
NavItem("Messaging & API", "/messages-metrics", "messages-metrics", [
constants.acl_metrics_view,
]),
]),
NavSection("Observability", [
NavItem("Gateway", "/gateway", "gateway", [
constants.acl_gateway_memory_stats,
constants.acl_gateway_reload_all,
]),
NavItem("Jobs", "/jobs", "jobs", [constants.acl_metrics_view]),
NavItem("Storage", "/storage", "storage", [constants.acl_metrics_view]),
NavItem("Audit Logs", "/audit-logs", "audit-logs", [
constants.acl_audit_log_view,
]),
]),
NavSection("Platform", [
NavItem("Search Index", "/search-index", "search-index", [
constants.acl_guild_lookup,
]),
NavItem("Voice Regions", "/voice-regions", "voice-regions", [
constants.acl_voice_region_list,
]),
NavItem("Voice Servers", "/voice-servers", "voice-servers", [
constants.acl_voice_server_list,
]),
]),
NavSection("Configuration", [
NavItem("Instance Config", "/instance-config", "instance-config", [
constants.acl_instance_config_view,
constants.acl_instance_config_update,
]),
NavItem("Feature Flags", "/feature-flags", "feature-flags", [
constants.acl_feature_flag_view,
constants.acl_feature_flag_manage,
]),
]),
NavSection("Codes", [
NavItem("Beta Codes", "/beta-codes", "beta-codes", [
constants.acl_beta_codes_generate,
]),
NavItem("Gift Codes", "/gift-codes", "gift-codes", [
constants.acl_gift_codes_generate,
]),
]),
]
}
pub fn accessible_sections(admin_acls: List(String)) -> List(NavSection) {
sections()
|> list.map(fn(section) {
let visible_items =
list.filter(section.items, fn(item) {
has_access(admin_acls, item.required_acls)
})
NavSection(section.title, visible_items)
})
|> list.filter(fn(section) { !list.is_empty(section.items) })
}
pub fn first_accessible_path(admin_acls: List(String)) -> option.Option(String) {
case accessible_sections(admin_acls) {
[] -> option.None
[section, ..] ->
case section.items {
[] -> option.None
[item, ..] -> option.Some(item.path)
}
}
}
fn has_access(admin_acls: List(String), required_acls: List(String)) -> Bool {
case required_acls {
[] -> True
_ ->
list.any(required_acls, fn(required_acl) {
acl.has_permission(admin_acls, required_acl)
})
}
}

View File

@@ -0,0 +1,72 @@
//// Copyright (C) 2026 Fluxer Contributors
////
//// This file is part of Fluxer.
////
//// Fluxer is free software: you can redistribute it and/or modify
//// it under the terms of the GNU Affero General Public License as published by
//// the Free Software Foundation, either version 3 of the License, or
//// (at your option) any later version.
////
//// Fluxer is distributed in the hope that it will be useful,
//// but WITHOUT ANY WARRANTY; without even the implied warranty of
//// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//// GNU Affero General Public License for more details.
////
//// You should have received a copy of the GNU Affero General Public License
//// along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
import fluxer_admin/api/common
import fluxer_admin/components/flash
import fluxer_admin/components/layout
import fluxer_admin/components/ui
import fluxer_admin/web.{type Context, type Session}
import gleam/option.{type Option}
import lustre/attribute as a
import lustre/element
import lustre/element/html as h
import wisp.{type Response}
pub fn view(
ctx: Context,
session: Session,
current_admin: Option(common.UserLookupResult),
flash_data: Option(flash.Flash),
) -> Response {
let content =
h.div([a.class("max-w-2xl mx-auto")], [
ui.heading_page("You find yourself in a strange place..."),
ui.card(ui.PaddingMedium, [
ui.stack("4", [
h.p(
[a.class("text-neutral-700 leading-relaxed")],
[
element.text(
"Your account is authenticated, but no admin tabs are available for your current permissions.",
),
],
),
h.p(
[a.class("text-neutral-600 leading-relaxed")],
[
element.text(
"If you believe this is a mistake, reach out to an administrator to request the necessary access.",
),
],
),
]),
]),
])
let html =
layout.page(
"Strange Place",
"strange-place",
ctx,
session,
current_admin,
flash_data,
content,
)
wisp.html_response(element.to_document_string(html), 200)
}

View File

@@ -49,11 +49,13 @@ import fluxer_admin/pages/report_detail_page
import fluxer_admin/pages/reports_page
import fluxer_admin/pages/search_index_page
import fluxer_admin/pages/storage_page
import fluxer_admin/pages/strange_place_page
import fluxer_admin/pages/user_detail_page
import fluxer_admin/pages/users_page
import fluxer_admin/pages/voice_regions_page
import fluxer_admin/pages/voice_servers_page
import fluxer_admin/session
import fluxer_admin/navigation
import fluxer_admin/web.{type Context, prepend_base_path}
import gleam/http.{Get, Post}
import gleam/http/request
@@ -184,6 +186,26 @@ fn api_error_message(err: common.ApiError) -> String {
}
}
fn admin_acls_from(
current_admin: option.Option(common.UserLookupResult),
) -> List(String) {
case current_admin {
option.Some(admin) -> admin.acls
option.None -> []
}
}
fn home_path(admin_acls: List(String)) -> String {
case navigation.first_accessible_path(admin_acls) {
option.Some(path) -> path
option.None -> "/strange-place"
}
}
fn redirect_to_home(ctx: Context, admin_acls: List(String)) -> Response {
wisp.redirect(prepend_base_path(ctx, home_path(admin_acls)))
}
pub fn handle_request(req: Request, ctx: Context) -> Response {
use req <- web_middleware(req)
@@ -216,7 +238,7 @@ pub fn handle_request(req: Request, ctx: Context) -> Response {
case wisp.get_cookie(req, "session", wisp.Signed) {
Ok(cookie) ->
case session.get(ctx, cookie) {
Ok(_session) -> wisp.redirect(prepend_base_path(ctx, "/users"))
Ok(_session) -> wisp.redirect(prepend_base_path(ctx, "/dashboard"))
Error(_) -> login_page.view(ctx, error_msg)
}
Error(_) -> login_page.view(ctx, error_msg)
@@ -284,7 +306,11 @@ pub fn handle_request(req: Request, ctx: Context) -> Response {
fn handle_authenticated_request(req: Request, ctx: Context) -> Response {
case wisp.path_segments(req) {
["dashboard"] -> wisp.redirect(prepend_base_path(ctx, "/users"))
["dashboard"] -> {
use _session, current_admin <- with_session_and_admin(req, ctx)
let admin_acls = admin_acls_from(current_admin)
redirect_to_home(ctx, admin_acls)
}
["users"] ->
case req.method {
Get -> {
@@ -1396,7 +1422,20 @@ fn handle_authenticated_request(req: Request, ctx: Context) -> Response {
}
_ -> wisp.method_not_allowed([Get, Post])
}
[] -> wisp.redirect(prepend_base_path(ctx, "/users"))
["strange-place"] ->
case req.method {
Get -> {
use user_session, current_admin <- with_session_and_admin(req, ctx)
let flash_data = flash.from_request(req)
strange_place_page.view(ctx, user_session, current_admin, flash_data)
}
_ -> wisp.method_not_allowed([Get])
}
[] -> {
use _session, current_admin <- with_session_and_admin(req, ctx)
let admin_acls = admin_acls_from(current_admin)
redirect_to_home(ctx, admin_acls)
}
_ -> wisp.not_found()
}
}