mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 04:55:42 +09:00
refactor: remove dead code (#3337)
This commit is contained in:
1 parent
a19002652b
commit
93d1f8af46
1527 files changed
+21170
-73846
No files matched your search
@@ -11,7 +11,6 @@ anyhow = "1.0.104"
|
||||
axum = { version = "0.8.9", features = ["macros"] }
|
||||
base64 = "0.23.1"
|
||||
chrono = { version = "0.4", default-features = false, features = ["serde"] }
|
||||
cookie = "0.18.2"
|
||||
fluxer_common = { path = "../fluxer_common" }
|
||||
hmac = "0.13.0"
|
||||
maud = { version = "0.27.0", features = ["axum"] }
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use crate::api::generated::snowflake;
|
||||
|
||||
use super::client::{AdminApiClient, ApiResult};
|
||||
use super::types::{ActiveJobsResponse, CancelJobResponse, GetJobResponse, ListJobsResponse};
|
||||
use super::types::{CancelJobResponse, GetJobResponse, ListJobsResponse};
|
||||
|
||||
pub struct ListJobsParams {
|
||||
pub limit: u32,
|
||||
@@ -59,15 +59,6 @@ impl AdminApiClient {
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_active_jobs(&self) -> ApiResult<ActiveJobsResponse> {
|
||||
let response = self
|
||||
.generated()
|
||||
.list_admin_active_jobs()
|
||||
.await
|
||||
.map_err(|e| self.generated_error(e))?;
|
||||
self.generated_value(response.into_inner())
|
||||
}
|
||||
}
|
||||
|
||||
fn cursor_field<'a>(cursor: Option<&'a serde_json::Value>, field: &str) -> &'a str {
|
||||
|
||||
@@ -21,8 +21,3 @@ pub struct GetJobResponse {
|
||||
pub struct CancelJobResponse {
|
||||
pub cancelled: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct ActiveJobsResponse {
|
||||
pub jobs: Vec<serde_json::Value>,
|
||||
}
|
||||
@@ -2,13 +2,12 @@
|
||||
|
||||
use crate::{
|
||||
api::types::AdminUser,
|
||||
middleware::flash::{self, FlashData},
|
||||
middleware::flash,
|
||||
session::{self, Session},
|
||||
state::AppState,
|
||||
};
|
||||
use axum::{
|
||||
extract::{Request, State},
|
||||
http::StatusCode,
|
||||
middleware::Next,
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
@@ -65,10 +64,6 @@ pub async fn require_auth(
|
||||
response
|
||||
}
|
||||
|
||||
pub fn get_flash(request: &Request) -> Option<FlashData> {
|
||||
request.extensions().get::<FlashData>().cloned()
|
||||
}
|
||||
|
||||
fn admin_cookie_path(config: &crate::config::AdminConfig) -> &str {
|
||||
if config.base_path.is_empty() {
|
||||
"/"
|
||||
@@ -159,11 +154,3 @@ async fn fetch_admin_user(
|
||||
Err(_) => AdminFetchResult::None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_auth_context(request: &Request) -> Option<&AuthContext> {
|
||||
request.extensions().get::<AuthContext>()
|
||||
}
|
||||
|
||||
pub fn require_auth_context(request: &Request) -> Result<&AuthContext, Box<Response>> {
|
||||
get_auth_context(request).ok_or_else(|| Box::new(StatusCode::UNAUTHORIZED.into_response()))
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{Html, IntoResponse, Response},
|
||||
};
|
||||
use maud::{DOCTYPE, html};
|
||||
|
||||
pub struct AppError {
|
||||
pub status: StatusCode,
|
||||
pub message: String,
|
||||
pub detail: Option<String>,
|
||||
pub home_url: String,
|
||||
}
|
||||
|
||||
impl AppError {
|
||||
pub fn not_found(message: &str, base_path: &str) -> Self {
|
||||
Self {
|
||||
status: StatusCode::NOT_FOUND,
|
||||
message: message.to_owned(),
|
||||
detail: None,
|
||||
home_url: if base_path.is_empty() {
|
||||
"/".to_owned()
|
||||
} else {
|
||||
base_path.to_owned()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn internal(message: &str) -> Self {
|
||||
Self {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
message: message.to_owned(),
|
||||
detail: None,
|
||||
home_url: "/login".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn forbidden(message: &str) -> Self {
|
||||
Self {
|
||||
status: StatusCode::FORBIDDEN,
|
||||
message: message.to_owned(),
|
||||
detail: None,
|
||||
home_url: "/login".to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_detail(mut self, detail: String) -> Self {
|
||||
self.detail = Some(detail);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_base_path(mut self, base_path: &str) -> Self {
|
||||
self.home_url = if base_path.is_empty() {
|
||||
"/".to_owned()
|
||||
} else {
|
||||
base_path.to_owned()
|
||||
};
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
let status_code = self.status.as_u16();
|
||||
let status_text = self.status.canonical_reason().unwrap_or("Error");
|
||||
let home_url = &self.home_url;
|
||||
let (border, bg, text_color) = if status_code >= 500 {
|
||||
("border-red-300", "bg-red-50", "text-red-800")
|
||||
} else if status_code == 403 {
|
||||
("border-yellow-300", "bg-yellow-50", "text-yellow-800")
|
||||
} else {
|
||||
("border-neutral-300", "bg-neutral-50", "text-neutral-800")
|
||||
};
|
||||
let markup = html! {
|
||||
(DOCTYPE)
|
||||
html lang="en" {
|
||||
head {
|
||||
meta charset="UTF-8";
|
||||
meta name="viewport" content="width=device-width, initial-scale=1.0";
|
||||
title { (status_code) " " (status_text) " ~ Fluxer Admin" }
|
||||
}
|
||||
body class="min-h-screen bg-neutral-50 flex items-center justify-center" {
|
||||
div class="mx-auto max-w-lg px-4 py-16 text-center" {
|
||||
h1 class="text-6xl font-bold text-neutral-300 mb-4" {
|
||||
(status_code)
|
||||
}
|
||||
h2 class="text-xl font-semibold text-neutral-700 mb-2" {
|
||||
(status_text)
|
||||
}
|
||||
div class={"rounded-lg border px-4 py-3 text-sm mb-6 " (border) " " (bg) " " (text_color)} {
|
||||
div { (self.message) }
|
||||
@if let Some(ref detail) = self.detail {
|
||||
div class="mt-2 break-all rounded border border-current/20 \
|
||||
bg-white/60 px-3 py-2 text-xs" {
|
||||
(detail)
|
||||
}
|
||||
}
|
||||
}
|
||||
a href=(home_url)
|
||||
class="text-blue-600 hover:text-blue-800 hover:underline text-sm" {
|
||||
"Go to admin"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
(self.status, Html(markup.into_string())).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for AppError {
|
||||
fn from(err: anyhow::Error) -> Self {
|
||||
tracing::error!(?err, "internal server error");
|
||||
Self {
|
||||
status: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
message: "An internal error occurred.".to_owned(),
|
||||
detail: None,
|
||||
home_url: "/login".to_owned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,13 +18,6 @@ pub fn is_htmx_request(headers: &HeaderMap) -> bool {
|
||||
.is_some_and(|value| value == "true")
|
||||
}
|
||||
|
||||
pub fn htmx_current_url(headers: &HeaderMap) -> Option<String> {
|
||||
headers
|
||||
.get("HX-Current-URL")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.map(|s| s.to_owned())
|
||||
}
|
||||
|
||||
pub fn htmx_target(headers: &HeaderMap) -> Option<String> {
|
||||
headers
|
||||
.get("HX-Target")
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
pub mod account_identity;
|
||||
pub mod auth;
|
||||
pub mod csrf;
|
||||
pub mod error_handler;
|
||||
pub mod flash;
|
||||
pub mod htmx;
|
||||
pub mod self_hosted;
|
||||
@@ -9,17 +9,11 @@ pub const ADMIN_OAUTH_SCOPE: &str = "identify email";
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct TokenResponse {
|
||||
pub access_token: String,
|
||||
pub token_type: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UserInfo {
|
||||
pub id: String,
|
||||
pub username: String,
|
||||
pub discriminator: String,
|
||||
pub avatar: Option<String>,
|
||||
pub email: Option<String>,
|
||||
pub global_name: Option<String>,
|
||||
}
|
||||
|
||||
pub fn authorize_url(config: &AdminConfig, state: &str) -> String {
|
||||
|
||||
@@ -12,7 +12,7 @@ use crate::{
|
||||
templates,
|
||||
};
|
||||
use axum::{
|
||||
Form, Json, Router,
|
||||
Form, Router,
|
||||
extract::{FromRequest, Path, Query, Request, State},
|
||||
http::HeaderMap,
|
||||
response::{Html, IntoResponse, Response},
|
||||
@@ -46,7 +46,6 @@ struct JobActionForm {
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/jobs", get(jobs_list))
|
||||
.route("/jobs/active.json", get(jobs_active_json))
|
||||
.route("/jobs/{job_id}", get(job_detail).post(job_detail_post))
|
||||
}
|
||||
|
||||
@@ -128,21 +127,6 @@ async fn jobs_list(
|
||||
Html(markup.into_string()).into_response()
|
||||
}
|
||||
|
||||
async fn jobs_active_json(
|
||||
State(state): State<AppState>,
|
||||
auth: axum::Extension<AuthContext>,
|
||||
) -> Response {
|
||||
let config = state.config();
|
||||
let client = AdminApiClient::new(state.http_client(), config, &auth.0.session);
|
||||
match client.list_active_jobs().await {
|
||||
Ok(data) => Json(serde_json::json!(data)).into_response(),
|
||||
Err(error) => {
|
||||
tracing::warn!(%error, "admin API request failed: list active jobs");
|
||||
Json(serde_json::json!({ "jobs": [] })).into_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn job_detail(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
|
||||
@@ -64,7 +64,6 @@ pub fn router() -> Router<AppState> {
|
||||
"/limit-config",
|
||||
get(limit_config_page).post(system_actions::limit_config_post),
|
||||
)
|
||||
.route("/strange-place", get(strange_place_page))
|
||||
}
|
||||
|
||||
async fn gateway_page(
|
||||
@@ -263,15 +262,6 @@ async fn limit_config_page(
|
||||
Html(markup.into_string()).into_response()
|
||||
}
|
||||
|
||||
async fn strange_place_page(
|
||||
State(state): State<AppState>,
|
||||
auth: axum::Extension<AuthContext>,
|
||||
) -> Response {
|
||||
let config = state.config();
|
||||
let markup = templates::pages::strange_place::strange_place_page(config, &auth.0);
|
||||
Html(markup.into_string()).into_response()
|
||||
}
|
||||
|
||||
fn nonempty(s: &str) -> Option<String> {
|
||||
if s.is_empty() {
|
||||
None
|
||||
|
||||
@@ -77,33 +77,6 @@ main:focus {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.flash-success {
|
||||
border: 1px solid #86efac;
|
||||
background-color: #f0fdf4;
|
||||
color: #166534;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.flash-error {
|
||||
border: 1px solid #fca5a5;
|
||||
background-color: #fef2f2;
|
||||
color: #991b1b;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.flash-info {
|
||||
border: 1px solid #93c5fd;
|
||||
background-color: #eff6ff;
|
||||
color: #1e40af;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.drawer-panel {
|
||||
inset: unset;
|
||||
margin: 0;
|
||||
@@ -115,8 +88,6 @@ main:focus {
|
||||
}
|
||||
|
||||
.drawer-panel[data-drawer-side="right"] { right: 0; top: 0; }
|
||||
.drawer-panel[data-drawer-side="left"] { left: 0; top: 0; }
|
||||
.drawer-panel[data-drawer-side="bottom"] { bottom: 0; left: 0; right: 0; }
|
||||
|
||||
.drawer-panel[data-open],
|
||||
.drawer-panel:popover-open {
|
||||
|
||||
@@ -5,16 +5,12 @@ use maud::{Markup, html};
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum AlertVariant {
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
Success,
|
||||
}
|
||||
|
||||
pub fn alert(variant: AlertVariant, title: Option<&str>, content: Markup) -> Markup {
|
||||
let variant_classes = match variant {
|
||||
AlertVariant::Info => "bg-blue-50 border-blue-200 text-blue-700",
|
||||
AlertVariant::Warning => "bg-neutral-50 border-neutral-200 text-neutral-700",
|
||||
AlertVariant::Error => "bg-red-50 border-red-200 text-red-700",
|
||||
AlertVariant::Success => "bg-green-50 border-green-200 text-green-700",
|
||||
};
|
||||
html! {
|
||||
@@ -30,15 +26,3 @@ pub fn alert(variant: AlertVariant, title: Option<&str>, content: Markup) -> Mar
|
||||
pub fn alert_info(content: Markup) -> Markup {
|
||||
alert(AlertVariant::Info, None, content)
|
||||
}
|
||||
|
||||
pub fn alert_error(title: &str, content: Markup) -> Markup {
|
||||
alert(AlertVariant::Error, Some(title), content)
|
||||
}
|
||||
|
||||
pub fn alert_success(title: &str, content: Markup) -> Markup {
|
||||
alert(AlertVariant::Success, Some(title), content)
|
||||
}
|
||||
|
||||
pub fn alert_warning(title: &str, content: Markup) -> Markup {
|
||||
alert(AlertVariant::Warning, Some(title), content)
|
||||
}
|
||||
@@ -12,7 +12,3 @@ pub fn auto_refresh(enabled: bool, interval_ms: u32) -> Markup {
|
||||
script defer { (PreEscaped(script)) }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn auto_refresh_default(enabled: bool) -> Markup {
|
||||
auto_refresh(enabled, 3000)
|
||||
}
|
||||
@@ -29,11 +29,3 @@ pub fn badge(label: &str, variant: BadgeVariant) -> Markup {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn flag_badge(label: &str, active: bool) -> Markup {
|
||||
if active {
|
||||
badge(label, BadgeVariant::Success)
|
||||
} else {
|
||||
badge(label, BadgeVariant::Default)
|
||||
}
|
||||
}
|
||||
@@ -44,10 +44,6 @@ pub fn data_field_mono(label: &str, value: &str) -> Markup {
|
||||
data_field(label, data_field_value(value, DATA_FIELD_MONO_CLASS))
|
||||
}
|
||||
|
||||
pub fn data_field_link(label: &str, href: &str, display: &str) -> Markup {
|
||||
data_field(label, data_field_link_value(href, display, "break-words"))
|
||||
}
|
||||
|
||||
pub fn data_field_link_mono(label: &str, href: &str, display: &str) -> Markup {
|
||||
data_field(label, data_field_link_value(href, display, "break-all"))
|
||||
}
|
||||
@@ -66,20 +62,3 @@ pub fn data_grid(cols: u8, content: Markup) -> Markup {
|
||||
div class=(col_class) { (content) }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn metadata_row(label: &str, value: Markup) -> Markup {
|
||||
html! {
|
||||
div class="flex flex-wrap gap-x-2 gap-y-0.5" {
|
||||
span class="flex-shrink-0 text-neutral-500 text-sm" {
|
||||
(label) ":"
|
||||
}
|
||||
span class="min-w-0 break-words text-neutral-900 text-sm" {
|
||||
(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn metadata_row_text(label: &str, value: &str) -> Markup {
|
||||
metadata_row(label, html! { (value) })
|
||||
}
|
||||
@@ -7,22 +7,16 @@ use super::icons::{close_icon, spinner_icon};
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum DrawerSide {
|
||||
Right,
|
||||
Left,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum DrawerWidth {
|
||||
Sm,
|
||||
Md,
|
||||
Lg,
|
||||
Xl,
|
||||
}
|
||||
|
||||
fn width_class(width: DrawerWidth) -> &'static str {
|
||||
match width {
|
||||
DrawerWidth::Sm => "sm:max-w-sm",
|
||||
DrawerWidth::Md => "sm:max-w-md",
|
||||
DrawerWidth::Lg => "sm:max-w-lg",
|
||||
DrawerWidth::Xl => "sm:max-w-xl",
|
||||
}
|
||||
@@ -31,24 +25,18 @@ fn width_class(width: DrawerWidth) -> &'static str {
|
||||
fn side_class(side: DrawerSide) -> &'static str {
|
||||
match side {
|
||||
DrawerSide::Right => "right-0 top-0 h-[100dvh] w-full sm:w-[92vw]",
|
||||
DrawerSide::Left => "left-0 top-0 h-[100dvh] w-full sm:w-[92vw]",
|
||||
DrawerSide::Bottom => "bottom-0 left-0 right-0 max-h-[92dvh] w-full",
|
||||
}
|
||||
}
|
||||
|
||||
fn side_off_class(side: DrawerSide) -> &'static str {
|
||||
match side {
|
||||
DrawerSide::Right => "translate-x-full",
|
||||
DrawerSide::Left => "-translate-x-full",
|
||||
DrawerSide::Bottom => "translate-y-full",
|
||||
}
|
||||
}
|
||||
|
||||
fn side_name(side: DrawerSide) -> &'static str {
|
||||
match side {
|
||||
DrawerSide::Right => "right",
|
||||
DrawerSide::Left => "left",
|
||||
DrawerSide::Bottom => "bottom",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,10 +56,7 @@ pub fn drawer(
|
||||
"drawer-panel pointer-events-auto fixed z-50 flex flex-col \
|
||||
bg-white shadow-2xl {} {} {}",
|
||||
side_class(side),
|
||||
match side {
|
||||
DrawerSide::Bottom => "",
|
||||
_ => width_class(width),
|
||||
},
|
||||
width_class(width),
|
||||
side_off_class(side),
|
||||
);
|
||||
html! {
|
||||
@@ -150,16 +135,6 @@ pub fn drawer_loading_state() -> Markup {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drawer_error_state(message: &str) -> Markup {
|
||||
html! {
|
||||
div role="alert"
|
||||
class="rounded-lg border border-red-200 bg-red-50 p-4 \
|
||||
text-red-800 text-sm" {
|
||||
(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn drawer_controller_script() -> Markup {
|
||||
html! {
|
||||
script defer { (PreEscaped(DRAWER_CONTROLLER_SCRIPT)) }
|
||||
|
||||
@@ -2,25 +2,6 @@
|
||||
|
||||
use maud::{Markup, html};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum EmptyStateVariant {
|
||||
Empty,
|
||||
Loading,
|
||||
Error,
|
||||
}
|
||||
|
||||
pub fn empty_state(variant: EmptyStateVariant, content: Markup) -> Markup {
|
||||
let classes = match variant {
|
||||
EmptyStateVariant::Empty | EmptyStateVariant::Loading => {
|
||||
"text-neutral-500 text-center py-8"
|
||||
}
|
||||
EmptyStateVariant::Error => "text-red-600 text-center py-8",
|
||||
};
|
||||
html! {
|
||||
div class=(classes) { (content) }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty_state_full(icon: Option<Markup>, title: &str, description: Option<&str>) -> Markup {
|
||||
html! {
|
||||
div class="flex flex-col items-center justify-center py-12 text-center" {
|
||||
@@ -62,11 +43,3 @@ pub fn not_found_state(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty_state_text(message: &str) -> Markup {
|
||||
html! {
|
||||
div class="text-neutral-500 text-center py-8" {
|
||||
(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,14 +9,3 @@ pub fn error_alert(error: &str) -> Markup {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn error_card(title: &str, message: &str) -> Markup {
|
||||
html! {
|
||||
div class="rounded-lg border border-neutral-200 bg-white p-4 shadow-sm sm:p-6" {
|
||||
div class="flex flex-col gap-4" {
|
||||
h3 class="text-base font-semibold text-gray-900" { (title) }
|
||||
p class="text-sm text-neutral-500" { (message) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,10 +151,6 @@ pub fn danger_button(label: &str) -> Markup {
|
||||
button_markup(label, "submit", DANGER_BUTTON_CLASS)
|
||||
}
|
||||
|
||||
pub fn secondary_button(label: &str) -> Markup {
|
||||
button_markup(label, "button", SECONDARY_BUTTON_CLASS)
|
||||
}
|
||||
|
||||
pub fn form_field_group(
|
||||
label: &str,
|
||||
name: &str,
|
||||
|
||||
@@ -23,39 +23,6 @@ pub fn paperclip_icon(color: &str) -> Markup {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn checkmark_icon(color: &str) -> Markup {
|
||||
html! {
|
||||
svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"
|
||||
class={"inline-block h-4 w-4 " (color)}
|
||||
{
|
||||
(PreEscaped(concat!(
|
||||
r#"<rect width="256" height="256" fill="none"/>"#,
|
||||
r#"<polyline points="40 144 96 200 224 72" fill="none""#,
|
||||
r#" stroke="currentColor" stroke-linecap="round""#,
|
||||
r#" stroke-linejoin="round" stroke-width="24"/>"#,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn x_icon(color: &str) -> Markup {
|
||||
html! {
|
||||
svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"
|
||||
class={"inline-block h-4 w-4 " (color)}
|
||||
{
|
||||
(PreEscaped(concat!(
|
||||
r#"<rect width="256" height="256" fill="none"/>"#,
|
||||
r#"<line x1="200" y1="56" x2="56" y2="200" fill="none""#,
|
||||
r#" stroke="currentColor" stroke-linecap="round""#,
|
||||
r#" stroke-linejoin="round" stroke-width="24"/>"#,
|
||||
r#"<line x1="200" y1="200" x2="56" y2="56" fill="none""#,
|
||||
r#" stroke="currentColor" stroke-linecap="round""#,
|
||||
r#" stroke-linejoin="round" stroke-width="24"/>"#,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_icon() -> Markup {
|
||||
html! {
|
||||
svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||
@@ -82,63 +49,3 @@ pub fn spinner_icon() -> Markup {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn search_icon() -> Markup {
|
||||
html! {
|
||||
svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||
class="h-4 w-4" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
{
|
||||
(PreEscaped(concat!(
|
||||
r#"<circle cx="11" cy="11" r="8"/>"#,
|
||||
r#"<line x1="21" y1="21" x2="16.65" y2="16.65"/>"#,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn chevron_right_icon() -> Markup {
|
||||
html! {
|
||||
svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||
class="h-4 w-4" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
{
|
||||
(PreEscaped(r#"<polyline points="9 18 15 12 9 6"/>"#))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn external_link_icon() -> Markup {
|
||||
html! {
|
||||
svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||
class="h-3.5 w-3.5" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
{
|
||||
(PreEscaped(concat!(
|
||||
r#"<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8"#,
|
||||
r#"a2 2 0 0 1 2-2h6"/>"#,
|
||||
r#"<polyline points="15 3 21 3 21 9"/>"#,
|
||||
r#"<line x1="10" y1="14" x2="21" y2="3"/>"#,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn copy_icon() -> Markup {
|
||||
html! {
|
||||
svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||
class="h-3.5 w-3.5" fill="none" stroke="currentColor"
|
||||
stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
|
||||
aria-hidden="true"
|
||||
{
|
||||
(PreEscaped(concat!(
|
||||
r#"<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>"#,
|
||||
r#"<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9"#,
|
||||
r#"a2 2 0 0 1 2 2v1"/>"#,
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,10 +21,8 @@ pub mod report_category;
|
||||
pub mod report_webhook;
|
||||
pub mod resource_link;
|
||||
pub mod section_card;
|
||||
pub mod stack;
|
||||
pub mod table;
|
||||
pub mod tooltip;
|
||||
pub mod typography;
|
||||
pub mod user_display;
|
||||
pub mod user_profile_badges;
|
||||
pub mod voice;
|
||||
@@ -2,21 +2,6 @@
|
||||
|
||||
use maud::{Markup, PreEscaped, html};
|
||||
|
||||
pub fn page_container(spacing: &str, content: Markup) -> Markup {
|
||||
let class = match spacing {
|
||||
"sm" => "space-y-4",
|
||||
"lg" => "space-y-8",
|
||||
_ => "space-y-6",
|
||||
};
|
||||
html! {
|
||||
div class=(class) { (content) }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn page_container_md(content: Markup) -> Markup {
|
||||
page_container("md", content)
|
||||
}
|
||||
|
||||
pub fn page_header(title: &str, description: Option<&str>) -> Markup {
|
||||
page_header_full(title, description, None, None, None, maud::html! {})
|
||||
}
|
||||
@@ -122,25 +107,3 @@ pub fn detail_row(label: &str, value: Markup) -> Markup {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tabs(tabs: &[(&str, &str, bool)], base_url: &str) -> Markup {
|
||||
html! {
|
||||
div class="border-b border-neutral-200 mb-6" {
|
||||
nav class="-mb-px flex gap-6" aria-label="Tabs" {
|
||||
@for (id, label, active) in tabs {
|
||||
@let classes = if *active {
|
||||
"border-brand-primary text-brand-primary whitespace-nowrap border-b-2 \
|
||||
pb-3 pt-1 text-sm font-medium"
|
||||
} else {
|
||||
"border-transparent text-neutral-500 hover:border-neutral-300 \
|
||||
hover:text-neutral-700 whitespace-nowrap border-b-2 pb-3 pt-1 \
|
||||
text-sm font-medium"
|
||||
};
|
||||
a href={(base_url) "?tab=" (id)} class=(classes) {
|
||||
(label)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,8 +10,6 @@ pub enum ResourceType {
|
||||
|
||||
const RESOURCE_LINK_CLASS: &str = "text-neutral-900 underline decoration-neutral-300 \
|
||||
hover:text-neutral-600 hover:decoration-neutral-500 text-sm";
|
||||
const NAV_LINK_CLASS: &str = "label rounded-lg border border-neutral-300 bg-white \
|
||||
px-3 py-2 text-neutral-700 transition-colors hover:bg-neutral-50";
|
||||
|
||||
impl ResourceType {
|
||||
fn path_segment(self) -> &'static str {
|
||||
@@ -20,20 +18,6 @@ impl ResourceType {
|
||||
ResourceType::Guild => "guilds",
|
||||
}
|
||||
}
|
||||
|
||||
fn peek_drawer_id(self) -> &'static str {
|
||||
match self {
|
||||
ResourceType::User => "user-peek",
|
||||
ResourceType::Guild => "guild-peek",
|
||||
}
|
||||
}
|
||||
|
||||
fn label(self) -> &'static str {
|
||||
match self {
|
||||
ResourceType::User => "User",
|
||||
ResourceType::Guild => "Guild",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resource_link(
|
||||
@@ -52,40 +36,3 @@ pub fn resource_link(
|
||||
a href=(href) class=(RESOURCE_LINK_CLASS) { (display) }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resource_link_peek(
|
||||
base_path: &str,
|
||||
resource_type: ResourceType,
|
||||
resource_id: &str,
|
||||
display: Markup,
|
||||
peek_title: Option<&str>,
|
||||
) -> Markup {
|
||||
let href = format!(
|
||||
"{}/{}/{}",
|
||||
base_path,
|
||||
resource_type.path_segment(),
|
||||
resource_id,
|
||||
);
|
||||
let fragment_href = format!("{}/fragment", href);
|
||||
let title = peek_title
|
||||
.map(|t| t.to_owned())
|
||||
.unwrap_or_else(|| format!("{} {}", resource_type.label(), resource_id));
|
||||
html! {
|
||||
a href=(href)
|
||||
class=(RESOURCE_LINK_CLASS)
|
||||
data-drawer-open=(resource_type.peek_drawer_id())
|
||||
data-drawer-href=(fragment_href)
|
||||
hx-get=(fragment_href)
|
||||
hx-target={"#" (resource_type.peek_drawer_id()) "-body"}
|
||||
hx-swap="innerHTML"
|
||||
data-drawer-title=(title) {
|
||||
(display)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nav_link(href: &str, content: Markup) -> Markup {
|
||||
html! {
|
||||
a href=(href) class=(NAV_LINK_CLASS) { (content) }
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use maud::{Markup, html};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum StackGap {
|
||||
Sm,
|
||||
Md,
|
||||
Lg,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum StackAlign {
|
||||
Start,
|
||||
Center,
|
||||
End,
|
||||
Stretch,
|
||||
}
|
||||
|
||||
fn gap_class(gap: StackGap) -> &'static str {
|
||||
match gap {
|
||||
StackGap::Sm => "gap-2",
|
||||
StackGap::Md => "gap-4",
|
||||
StackGap::Lg => "gap-6",
|
||||
}
|
||||
}
|
||||
|
||||
fn align_class(align: StackAlign) -> &'static str {
|
||||
match align {
|
||||
StackAlign::Start => "items-start",
|
||||
StackAlign::Center => "items-center",
|
||||
StackAlign::End => "items-end",
|
||||
StackAlign::Stretch => "items-stretch",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vstack(gap: StackGap, align: StackAlign, content: Markup) -> Markup {
|
||||
let classes = format!("flex flex-col {} {}", gap_class(gap), align_class(align),);
|
||||
html! {
|
||||
div class=(classes) { (content) }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hstack(gap: StackGap, align: StackAlign, content: Markup) -> Markup {
|
||||
let classes = format!("flex flex-row {} {}", gap_class(gap), align_class(align),);
|
||||
html! {
|
||||
div class=(classes) { (content) }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stack(content: Markup) -> Markup {
|
||||
vstack(StackGap::Md, StackAlign::Stretch, content)
|
||||
}
|
||||
@@ -45,10 +45,6 @@ pub fn table_cell(muted: bool, content: Markup) -> Markup {
|
||||
html! { td class=(table_cell_class(muted)) { (content) } }
|
||||
}
|
||||
|
||||
pub fn table_cell_span(muted: bool, col_span: u32, content: Markup) -> Markup {
|
||||
html! { td class=(table_cell_class(muted)) colspan=(col_span) { (content) } }
|
||||
}
|
||||
|
||||
fn table_cell_class(muted: bool) -> &'static str {
|
||||
if muted {
|
||||
"px-3 py-3 text-sm sm:px-6 sm:py-4 text-neutral-600"
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use maud::{Markup, html};
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum HeadingLevel {
|
||||
H1,
|
||||
H2,
|
||||
H3,
|
||||
H4,
|
||||
H5,
|
||||
H6,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum HeadingSize {
|
||||
Xs,
|
||||
Sm,
|
||||
Base,
|
||||
Lg,
|
||||
Xl,
|
||||
Xl2,
|
||||
Xl3,
|
||||
Xl4,
|
||||
}
|
||||
|
||||
fn default_heading_classes(level: HeadingLevel) -> &'static str {
|
||||
match level {
|
||||
HeadingLevel::H1 => "text-3xl font-bold",
|
||||
HeadingLevel::H2 => "text-2xl font-semibold",
|
||||
HeadingLevel::H3 => "text-xl font-semibold",
|
||||
HeadingLevel::H4 => "text-lg font-semibold",
|
||||
HeadingLevel::H5 => "text-base font-semibold",
|
||||
HeadingLevel::H6 => "text-sm font-semibold",
|
||||
}
|
||||
}
|
||||
|
||||
fn custom_size_class(size: HeadingSize) -> &'static str {
|
||||
match size {
|
||||
HeadingSize::Xs => "text-xs",
|
||||
HeadingSize::Sm => "text-sm",
|
||||
HeadingSize::Base => "text-base",
|
||||
HeadingSize::Lg => "text-lg",
|
||||
HeadingSize::Xl => "text-xl",
|
||||
HeadingSize::Xl2 => "text-2xl",
|
||||
HeadingSize::Xl3 => "text-3xl",
|
||||
HeadingSize::Xl4 => "text-4xl",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn heading(level: HeadingLevel, size: Option<HeadingSize>, content: Markup) -> Markup {
|
||||
let size_classes = match size {
|
||||
Some(s) => custom_size_class(s),
|
||||
None => default_heading_classes(level),
|
||||
};
|
||||
let classes = format!("text-gray-900 tracking-tight {size_classes}");
|
||||
match level {
|
||||
HeadingLevel::H1 => html! { h1 class=(classes) { (content) } },
|
||||
HeadingLevel::H2 => html! { h2 class=(classes) { (content) } },
|
||||
HeadingLevel::H3 => html! { h3 class=(classes) { (content) } },
|
||||
HeadingLevel::H4 => html! { h4 class=(classes) { (content) } },
|
||||
HeadingLevel::H5 => html! { h5 class=(classes) { (content) } },
|
||||
HeadingLevel::H6 => html! { h6 class=(classes) { (content) } },
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum TextSize {
|
||||
Xs,
|
||||
Sm,
|
||||
Base,
|
||||
Lg,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum TextColor {
|
||||
Default,
|
||||
Muted,
|
||||
Primary,
|
||||
Danger,
|
||||
Success,
|
||||
}
|
||||
|
||||
pub fn text(size: TextSize, color: TextColor, content: Markup) -> Markup {
|
||||
let size_class = match size {
|
||||
TextSize::Xs => "text-xs",
|
||||
TextSize::Sm => "text-sm",
|
||||
TextSize::Base => "text-base",
|
||||
TextSize::Lg => "text-lg",
|
||||
};
|
||||
let color_class = match color {
|
||||
TextColor::Default => "text-gray-900",
|
||||
TextColor::Muted => "text-neutral-500",
|
||||
TextColor::Primary => "text-brand-primary",
|
||||
TextColor::Danger => "text-red-600",
|
||||
TextColor::Success => "text-green-600",
|
||||
};
|
||||
html! {
|
||||
p class={(size_class) " font-normal " (color_class)} { (content) }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn caption(content: Markup) -> Markup {
|
||||
html! {
|
||||
p class="text-xs text-gray-500" { (content) }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum CaptionVariant {
|
||||
Default,
|
||||
Error,
|
||||
Success,
|
||||
}
|
||||
|
||||
pub fn caption_variant(variant: CaptionVariant, content: Markup) -> Markup {
|
||||
let color = match variant {
|
||||
CaptionVariant::Default => "text-gray-500",
|
||||
CaptionVariant::Error => "text-red-600",
|
||||
CaptionVariant::Success => "text-green-600",
|
||||
};
|
||||
html! {
|
||||
p class={"text-xs " (color)} { (content) }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label_text(for_id: Option<&str>, required: bool, content: Markup) -> Markup {
|
||||
let classes = "block text-xs font-semibold uppercase tracking-wide text-neutral-500";
|
||||
html! {
|
||||
label for=[for_id] class=(classes) {
|
||||
(content)
|
||||
@if required {
|
||||
span class="ml-1 text-red-600" { "*" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn section_heading(title: &str, actions: Option<Markup>) -> Markup {
|
||||
match actions {
|
||||
Some(action_content) => html! {
|
||||
div class="mb-4 flex items-center justify-between" {
|
||||
h2 class="font-semibold text-gray-900 text-xl" { (title) }
|
||||
div class="flex items-center gap-2" { (action_content) }
|
||||
}
|
||||
},
|
||||
None => html! {
|
||||
h2 class="mb-4 font-semibold text-gray-900 text-xl" { (title) }
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inline_text(content: &str) -> Markup {
|
||||
html! { span { (content) } }
|
||||
}
|
||||
@@ -126,9 +126,3 @@ fn render_flash(flash: &crate::api::types::FlashMessage) -> Markup {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn flash_container() -> Markup {
|
||||
html! {
|
||||
div id="flash-container" class="mb-4 sm:mb-6 empty:hidden" {}
|
||||
}
|
||||
}
|
||||
@@ -585,25 +585,6 @@ window.__adminCopyToClipboard = function (text, btn, successLabel) {
|
||||
};
|
||||
"#;
|
||||
|
||||
pub const ARCHIVE_POLL_SCRIPT: &str = r#"
|
||||
(function () {
|
||||
var rows = document.querySelectorAll('tr[data-archive-id]');
|
||||
var hasPending = false;
|
||||
for (var i = 0; i < rows.length; i++) {
|
||||
var status = rows[i].querySelector('.archive-status');
|
||||
if (status && status.textContent.indexOf('Completed') === -1 && status.textContent.indexOf('Failed') === -1) {
|
||||
hasPending = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hasPending) {
|
||||
setTimeout(function () {
|
||||
window.location.reload();
|
||||
}, 5000);
|
||||
}
|
||||
})();
|
||||
"#;
|
||||
|
||||
pub const SH_LINK_REWRITE_SCRIPT: &str = r#"
|
||||
(function () {
|
||||
if (window.location.search.indexOf('sh=1') === -1) return;
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{acl, config::AdminConfig};
|
||||
use maud::{Markup, PreEscaped, html};
|
||||
|
||||
use super::layout_scripts::SIDEBAR_ACTIVE_SCROLL_SCRIPT;
|
||||
pub use super::layout_sidebar_nav::{NAV_SECTIONS, NavItem, NavSection};
|
||||
use super::layout_sidebar_nav::NAV_SECTIONS;
|
||||
|
||||
pub fn render_sidebar(
|
||||
config: &AdminConfig,
|
||||
|
||||
@@ -21,7 +21,6 @@ use maud::{Markup, html};
|
||||
|
||||
pub struct GuildsListParams {
|
||||
pub q: String,
|
||||
pub ids: String,
|
||||
pub requested_ids: Vec<String>,
|
||||
pub limit: u32,
|
||||
pub page: u32,
|
||||
@@ -38,7 +37,6 @@ impl GuildsListParams {
|
||||
Self {
|
||||
q: q.unwrap_or_default().trim().to_owned(),
|
||||
requested_ids: parse_comma_separated(&ids),
|
||||
ids: ids.trim().to_owned(),
|
||||
limit: match limit.unwrap_or(50) {
|
||||
25 => 25,
|
||||
100 => 100,
|
||||
|
||||
@@ -29,7 +29,6 @@ pub mod profile_substring_bans;
|
||||
pub mod report_detail;
|
||||
pub mod reports_list;
|
||||
pub mod search_index;
|
||||
pub mod strange_place;
|
||||
pub mod system_dm;
|
||||
pub mod url_domain_bans;
|
||||
pub mod user_detail;
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::{config::AdminConfig, middleware::auth::AuthContext, templates::layout::admin_layout};
|
||||
use maud::{Markup, html};
|
||||
|
||||
pub fn strange_place_page(config: &AdminConfig, auth: &AuthContext) -> Markup {
|
||||
let content = html! {
|
||||
div class="mx-auto max-w-2xl" {
|
||||
div class="rounded-lg border border-neutral-200 bg-white p-8 shadow-sm" {
|
||||
div class="space-y-4 text-center" {
|
||||
div class="mx-auto flex h-16 w-16 items-center justify-center \
|
||||
rounded-full bg-neutral-100" {
|
||||
span class="text-lg text-neutral-400" { "?" }
|
||||
}
|
||||
h2 class="text-lg font-semibold text-neutral-900" {
|
||||
"You've reached a strange place"
|
||||
}
|
||||
p class="text-sm text-neutral-600" {
|
||||
"You don't have access to any admin features. "
|
||||
"Contact an administrator to request access."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
admin_layout(config, auth, "Strange Place", "", None, content)
|
||||
}
|
||||
@@ -33,26 +33,6 @@ pub const USER_TABS: &[(&str, &str)] = &[
|
||||
("audit_logs", "Audit Logs"),
|
||||
];
|
||||
|
||||
pub fn user_detail_page(
|
||||
config: &AdminConfig,
|
||||
auth: &AuthContext,
|
||||
user: Option<&AdminUser>,
|
||||
user_id: &str,
|
||||
premium_badge_name: Option<&str>,
|
||||
is_htmx: bool,
|
||||
) -> Markup {
|
||||
user_detail_with_tab(
|
||||
config,
|
||||
auth,
|
||||
user,
|
||||
user_id,
|
||||
"overview",
|
||||
None,
|
||||
premium_badge_name,
|
||||
is_htmx,
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn user_detail_with_tab(
|
||||
config: &AdminConfig,
|
||||
|
||||
@@ -15,7 +15,6 @@ pub mod moderation;
|
||||
pub mod overview;
|
||||
pub mod relationships;
|
||||
pub mod reports;
|
||||
pub mod settings;
|
||||
|
||||
pub(super) fn resolved_user_display(user: &AdminResolvedUser) -> String {
|
||||
let tag = user_tag(
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use crate::{
|
||||
api::types::AdminUser,
|
||||
templates::components::page_container::{card_with_header, detail_row},
|
||||
utils::{
|
||||
bigint::{format_discriminator, has_flag, list_flags},
|
||||
user_tag::user_tag,
|
||||
},
|
||||
};
|
||||
use maud::{Markup, html};
|
||||
|
||||
pub fn settings_tab(user: &AdminUser) -> Markup {
|
||||
html! {
|
||||
div class="space-y-6" {
|
||||
(card_with_header("Profile Settings", html! {
|
||||
dl class="divide-y divide-neutral-100" {
|
||||
(detail_row("Username", html! {
|
||||
(user_tag(&user.username, &format_discriminator(&user.discriminator), user.bot))
|
||||
}))
|
||||
(detail_row("Display Name", html! {
|
||||
@if let Some(ref name) = user.global_name {
|
||||
(name)
|
||||
} @else {
|
||||
span class="text-neutral-400" { "Not set" }
|
||||
}
|
||||
}))
|
||||
(detail_row("Email", html! {
|
||||
@if let Some(ref email) = user.email {
|
||||
(email)
|
||||
} @else {
|
||||
span class="text-neutral-400" { "Hidden" }
|
||||
}
|
||||
}))
|
||||
(detail_row("Avatar", html! {
|
||||
@if let Some(ref hash) = user.avatar {
|
||||
span class="text-xs" { (hash) }
|
||||
} @else {
|
||||
span class="text-neutral-400" { "Default" }
|
||||
}
|
||||
}))
|
||||
(detail_row("Bot", html! {
|
||||
@if user.bot { "Yes" } @else { "No" }
|
||||
}))
|
||||
(detail_row("System", html! {
|
||||
@if user.system { "Yes" } @else { "No" }
|
||||
}))
|
||||
}
|
||||
}))
|
||||
|
||||
(card_with_header("Flags", html! {
|
||||
dl class="divide-y divide-neutral-100" {
|
||||
(detail_row("User Flags (raw)", html! {
|
||||
span class="text-xs" { (user.flags) }
|
||||
}))
|
||||
(detail_row("Premium Flags (raw)", html! {
|
||||
span class="text-xs" { (user.premium_flags) }
|
||||
}))
|
||||
(detail_row("Active flag bits", html! {
|
||||
@let bits = list_flags(user.flags);
|
||||
@if bits.is_empty() {
|
||||
span class="text-neutral-400" { "None" }
|
||||
} @else {
|
||||
div class="flex flex-wrap gap-2" {
|
||||
@for bit in &bits {
|
||||
span class="inline-flex items-center rounded-full px-2 \
|
||||
py-0.5 text-xs font-medium bg-blue-100 \
|
||||
text-blue-700" {
|
||||
"bit " (bit)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}))
|
||||
|
||||
(card_with_header("Known Flags", html! {
|
||||
div class="grid grid-cols-2 gap-2 md:grid-cols-3" {
|
||||
(flag_row("Staff", user.flags, 0))
|
||||
(flag_row("Partner", user.flags, 1))
|
||||
(flag_row("HypeSquad Events", user.flags, 2))
|
||||
(flag_row("Bug Hunter Lvl 1", user.flags, 3))
|
||||
(flag_row("HypeSquad Bravery", user.flags, 6))
|
||||
(flag_row("HypeSquad Brilliance", user.flags, 7))
|
||||
(flag_row("HypeSquad Balance", user.flags, 8))
|
||||
(flag_row("Early Supporter", user.flags, 9))
|
||||
(flag_row("Bug Hunter Lvl 2", user.flags, 14))
|
||||
(flag_row("Verified Bot", user.flags, 16))
|
||||
(flag_row("Verified Developer", user.flags, 17))
|
||||
(flag_row("Certified Moderator", user.flags, 18))
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn flag_row(label: &str, flags: u64, bit: u32) -> Markup {
|
||||
let active = has_flag(flags, 1u64 << bit);
|
||||
let classes = if active {
|
||||
"rounded-lg border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700"
|
||||
} else {
|
||||
"rounded-lg border border-neutral-200 bg-neutral-50 px-3 py-2 text-sm text-neutral-500"
|
||||
};
|
||||
html! {
|
||||
div class=(classes) {
|
||||
(label)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,6 @@ pub struct UserListParams {
|
||||
pub q: String,
|
||||
pub email: String,
|
||||
pub ip: String,
|
||||
pub ids: String,
|
||||
pub requested_ids: Vec<String>,
|
||||
pub limit: u32,
|
||||
pub page: u32,
|
||||
@@ -54,7 +53,6 @@ impl UserListParams {
|
||||
email: email.unwrap_or_default().trim().to_owned(),
|
||||
ip: ip.unwrap_or_default().trim().to_owned(),
|
||||
requested_ids: parse_ids_query(&ids),
|
||||
ids: ids.trim().to_owned(),
|
||||
limit: match limit.unwrap_or(25) {
|
||||
50 => 50,
|
||||
100 => 100,
|
||||
|
||||
@@ -1,31 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
pub fn has_flag(flags: u64, flag: u64) -> bool {
|
||||
flags & flag != 0
|
||||
}
|
||||
|
||||
pub fn set_flag(flags: u64, flag: u64) -> u64 {
|
||||
flags | flag
|
||||
}
|
||||
|
||||
pub fn clear_flag(flags: u64, flag: u64) -> u64 {
|
||||
flags & !flag
|
||||
}
|
||||
|
||||
pub fn toggle_flag(flags: u64, flag: u64) -> u64 {
|
||||
flags ^ flag
|
||||
}
|
||||
|
||||
pub fn list_flags(flags: u64) -> Vec<u32> {
|
||||
let mut result = Vec::new();
|
||||
for bit in 0..64 {
|
||||
if flags & (1u64 << bit) != 0 {
|
||||
result.push(bit);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn format_discriminator(discriminator: &str) -> String {
|
||||
let num: u16 = discriminator.parse().unwrap_or(0);
|
||||
format!("{num:04}")
|
||||
@@ -53,36 +27,4 @@ mod tests {
|
||||
assert_eq!(format_discriminator(""), "0000");
|
||||
assert_eq!(format_discriminator("abc"), "0000");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn has_flag_checks_bit() {
|
||||
assert!(has_flag(0b1010, 0b0010));
|
||||
assert!(!has_flag(0b1010, 0b0001));
|
||||
assert!(has_flag(0b1111, 0b0100));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_flag_sets_bit() {
|
||||
assert_eq!(set_flag(0b1000, 0b0010), 0b1010);
|
||||
assert_eq!(set_flag(0b1010, 0b0010), 0b1010);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clear_flag_clears_bit() {
|
||||
assert_eq!(clear_flag(0b1010, 0b0010), 0b1000);
|
||||
assert_eq!(clear_flag(0b1000, 0b0010), 0b1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toggle_flag_toggles_bit() {
|
||||
assert_eq!(toggle_flag(0b1000, 0b0010), 0b1010);
|
||||
assert_eq!(toggle_flag(0b1010, 0b0010), 0b1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_flags_returns_set_positions() {
|
||||
assert_eq!(list_flags(0b1010), vec![1, 3]);
|
||||
assert_eq!(list_flags(0b0001), vec![0]);
|
||||
assert!(list_flags(0).is_empty());
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use axum::{body, extract::Request};
|
||||
use serde::Deserialize;
|
||||
use std::collections::BTreeMap;
|
||||
use url::form_urlencoded;
|
||||
|
||||
@@ -130,36 +129,6 @@ impl MultiValueForm {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_page(value: Option<&str>) -> u32 {
|
||||
value
|
||||
.and_then(|s| s.parse::<u32>().ok())
|
||||
.unwrap_or(1)
|
||||
.max(1)
|
||||
}
|
||||
|
||||
pub fn parse_per_page(value: Option<&str>, default: u32) -> u32 {
|
||||
value
|
||||
.and_then(|s| s.parse::<u32>().ok())
|
||||
.unwrap_or(default)
|
||||
.clamp(1, 100)
|
||||
}
|
||||
|
||||
pub fn sanitize_redirect(value: &str) -> String {
|
||||
let trimmed = value.trim();
|
||||
if trimmed.is_empty() || !trimmed.starts_with('/') || trimmed.starts_with("//") {
|
||||
return "/".to_owned();
|
||||
}
|
||||
trimmed.to_owned()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct HtmxFormData {
|
||||
#[serde(default)]
|
||||
pub _csrf: String,
|
||||
#[serde(flatten)]
|
||||
pub fields: std::collections::HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -228,45 +197,4 @@ mod tests {
|
||||
assert!(MultiValueForm::parse(b"enabled=true").bool_value("enabled"));
|
||||
assert!(!MultiValueForm::parse(b"enabled=false").bool_value("enabled"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_page_defaults_to_one() {
|
||||
assert_eq!(parse_page(None), 1);
|
||||
assert_eq!(parse_page(Some("")), 1);
|
||||
assert_eq!(parse_page(Some("abc")), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_page_clamps_to_minimum_one() {
|
||||
assert_eq!(parse_page(Some("0")), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_page_valid_input() {
|
||||
assert_eq!(parse_page(Some("5")), 5);
|
||||
assert_eq!(parse_page(Some("100")), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_redirect_prevents_open_redirect() {
|
||||
assert_eq!(sanitize_redirect(""), "/");
|
||||
assert_eq!(sanitize_redirect("https://evil.com"), "/");
|
||||
assert_eq!(sanitize_redirect("//evil.com"), "/");
|
||||
assert_eq!(sanitize_redirect("javascript:alert(1)"), "/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanitize_redirect_allows_valid_paths() {
|
||||
assert_eq!(sanitize_redirect("/users"), "/users");
|
||||
assert_eq!(sanitize_redirect("/guilds/123"), "/guilds/123");
|
||||
assert_eq!(sanitize_redirect(" /trimmed "), "/trimmed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_per_page_clamps_range() {
|
||||
assert_eq!(parse_per_page(None, 25), 25);
|
||||
assert_eq!(parse_per_page(Some("0"), 25), 1);
|
||||
assert_eq!(parse_per_page(Some("200"), 25), 100);
|
||||
assert_eq!(parse_per_page(Some("50"), 25), 50);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
const FLUXER_EPOCH: u64 = 1_420_070_400_000;
|
||||
|
||||
pub fn snowflake_to_timestamp_ms(snowflake: &str) -> Option<u64> {
|
||||
@@ -10,39 +8,6 @@ pub fn snowflake_to_timestamp_ms(snowflake: &str) -> Option<u64> {
|
||||
Some(timestamp_ms)
|
||||
}
|
||||
|
||||
pub fn relative_time(unix_seconds: u64) -> String {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("system clock before Unix epoch")
|
||||
.as_secs();
|
||||
let diff = now.saturating_sub(unix_seconds);
|
||||
if diff < 60 {
|
||||
return "just now".to_owned();
|
||||
}
|
||||
if diff < 3600 {
|
||||
let minutes = diff / 60;
|
||||
return format!(
|
||||
"{} minute{} ago",
|
||||
minutes,
|
||||
if minutes == 1 { "" } else { "s" }
|
||||
);
|
||||
}
|
||||
if diff < 86400 {
|
||||
let hours = diff / 3600;
|
||||
return format!("{} hour{} ago", hours, if hours == 1 { "" } else { "s" });
|
||||
}
|
||||
let days = diff / 86400;
|
||||
if days < 30 {
|
||||
return format!("{} day{} ago", days, if days == 1 { "" } else { "s" });
|
||||
}
|
||||
let months = days / 30;
|
||||
if months < 12 {
|
||||
return format!("{} month{} ago", months, if months == 1 { "" } else { "s" });
|
||||
}
|
||||
let years = months / 12;
|
||||
format!("{} year{} ago", years, if years == 1 { "" } else { "s" })
|
||||
}
|
||||
|
||||
pub fn format_admin_timestamp(iso: &str) -> String {
|
||||
time::OffsetDateTime::parse(iso, &time::format_description::well_known::Rfc3339)
|
||||
.or_else(|_| {
|
||||
@@ -115,45 +80,6 @@ mod tests {
|
||||
assert_eq!(snowflake_to_timestamp_ms("-1"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_time_just_now() {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
assert_eq!(relative_time(now), "just now");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_time_minutes() {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
assert_eq!(relative_time(now - 120), "2 minutes ago");
|
||||
assert_eq!(relative_time(now - 60), "1 minute ago");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_time_hours() {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
assert_eq!(relative_time(now - 3600), "1 hour ago");
|
||||
assert_eq!(relative_time(now - 7200), "2 hours ago");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_time_days() {
|
||||
let now = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
assert_eq!(relative_time(now - 86400), "1 day ago");
|
||||
assert_eq!(relative_time(now - 86400 * 5), "5 days ago");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snowflake_creation_date_invalid() {
|
||||
assert_eq!(snowflake_creation_date("abc"), "Unknown");
|
||||
|
||||
Reference in new issue
Block a user