chore: bug fix cleanup (#4)
This commit is contained in:
@@ -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 -> []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
}
|
||||
|
||||
171
fluxer_admin/src/fluxer_admin/navigation.gleam
Normal file
171
fluxer_admin/src/fluxer_admin/navigation.gleam
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
72
fluxer_admin/src/fluxer_admin/pages/strange_place_page.gleam
Normal file
72
fluxer_admin/src/fluxer_admin/pages/strange_place_page.gleam
Normal 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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user