mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 04:55:42 +09:00
fix(admin): use instance branding on self-hosted (#3401)
This commit is contained in:
1 parent
af7724dbf6
commit
50c28aa8fa
9 files changed
+206
-49
No files matched your search
@@ -49,6 +49,15 @@ impl AdminApiClient {
|
|||||||
Self { generated }
|
Self { generated }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn anonymous(http_client: &reqwest::Client, config: &AdminConfig) -> Self {
|
||||||
|
let generated = GeneratedClient::new_with_client(
|
||||||
|
&config.api_endpoint,
|
||||||
|
http_client.clone(),
|
||||||
|
build_proxy_client_ip_headers(config),
|
||||||
|
);
|
||||||
|
Self { generated }
|
||||||
|
}
|
||||||
|
|
||||||
fn build_url(&self, path: &str, query_params: Option<&[(&str, &str)]>) -> String {
|
fn build_url(&self, path: &str, query_params: Option<&[(&str, &str)]>) -> String {
|
||||||
let mut url = format!("{}{}", self.generated.baseurl(), path);
|
let mut url = format!("{}{}", self.generated.baseurl(), path);
|
||||||
let query = query_params
|
let query = query_params
|
||||||
|
|||||||
@@ -119,6 +119,9 @@ pub struct InstancePremiumDiscoveryAppPublic {
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Default, Deserialize)]
|
#[derive(Clone, Debug, Default, Deserialize)]
|
||||||
pub struct InstancePremiumDiscoveryBranding {
|
pub struct InstancePremiumDiscoveryBranding {
|
||||||
|
pub product_name: Option<String>,
|
||||||
|
pub icon_url: Option<String>,
|
||||||
|
pub favicon_url: Option<String>,
|
||||||
pub premium_product_name: Option<String>,
|
pub premium_product_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,46 +133,67 @@ pub struct InstancePremiumDiscoveryFeatures {
|
|||||||
|
|
||||||
impl InstancePremiumDiscovery {
|
impl InstancePremiumDiscovery {
|
||||||
pub fn premium_product_name(&self) -> Option<&str> {
|
pub fn premium_product_name(&self) -> Option<&str> {
|
||||||
self.app_public
|
non_blank(self.app_public.branding.premium_product_name.as_deref())
|
||||||
.branding
|
|
||||||
.premium_product_name
|
|
||||||
.as_deref()
|
|
||||||
.map(str::trim)
|
|
||||||
.filter(|name| !name.is_empty())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||||
pub struct PremiumBranding {
|
pub struct PremiumBranding {
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub premium_enabled: bool,
|
pub premium_enabled: bool,
|
||||||
|
pub product_name: Option<String>,
|
||||||
|
pub favicon_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PremiumBranding {
|
impl PremiumBranding {
|
||||||
pub fn from_discovery(discovery: &InstancePremiumDiscovery) -> Self {
|
pub fn from_discovery(discovery: &InstancePremiumDiscovery) -> Self {
|
||||||
|
let branding = &discovery.app_public.branding;
|
||||||
Self {
|
Self {
|
||||||
name: discovery.premium_product_name().map(str::to_owned),
|
name: discovery.premium_product_name().map(str::to_owned),
|
||||||
premium_enabled: discovery.features.premium_enabled,
|
premium_enabled: discovery.features.premium_enabled,
|
||||||
|
product_name: non_blank(branding.product_name.as_deref()).map(str::to_owned),
|
||||||
|
favicon_url: favicon_url(
|
||||||
|
branding.favicon_url.as_deref(),
|
||||||
|
branding.icon_url.as_deref(),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_instance_config(config: &InstanceConfigResponse) -> Self {
|
pub fn from_instance_config(config: &InstanceConfigResponse) -> Self {
|
||||||
Self::from_config_parts(
|
let branding = &config.app_public.branding;
|
||||||
config.self_hosted,
|
Self {
|
||||||
&config.app_public.branding.premium_product_name,
|
product_name: non_blank(Some(&branding.product_name)).map(str::to_owned),
|
||||||
config.policy.premium_mode,
|
favicon_url: favicon_url(
|
||||||
)
|
branding.favicon_url.as_deref(),
|
||||||
|
branding.icon_url.as_deref(),
|
||||||
|
),
|
||||||
|
..Self::from_config_parts(
|
||||||
|
config.self_hosted,
|
||||||
|
&branding.premium_product_name,
|
||||||
|
config.policy.premium_mode,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_config_parts(self_hosted: bool, name: &str, premium_mode: PremiumMode) -> Self {
|
fn from_config_parts(self_hosted: bool, name: &str, premium_mode: PremiumMode) -> Self {
|
||||||
let name = name.trim();
|
|
||||||
Self {
|
Self {
|
||||||
name: (!name.is_empty()).then(|| name.to_owned()),
|
name: non_blank(Some(name)).map(str::to_owned),
|
||||||
premium_enabled: !self_hosted || matches!(premium_mode, PremiumMode::Mirror),
|
premium_enabled: !self_hosted || matches!(premium_mode, PremiumMode::Mirror),
|
||||||
|
..Self::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn non_blank(value: Option<&str>) -> Option<&str> {
|
||||||
|
value.map(str::trim).filter(|value| !value.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn favicon_url(favicon_url: Option<&str>, icon_url: Option<&str>) -> Option<String> {
|
||||||
|
non_blank(favicon_url)
|
||||||
|
.or_else(|| non_blank(icon_url))
|
||||||
|
.map(str::to_owned)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -304,18 +328,52 @@ mod tests {
|
|||||||
PremiumBranding::from_discovery(&discovery),
|
PremiumBranding::from_discovery(&discovery),
|
||||||
PremiumBranding {
|
PremiumBranding {
|
||||||
name: Some("Gold".to_owned()),
|
name: Some("Gold".to_owned()),
|
||||||
premium_enabled: true
|
premium_enabled: true,
|
||||||
|
product_name: Some("Example".to_owned()),
|
||||||
|
favicon_url: None,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn instance_branding_prefers_the_favicon_and_falls_back_to_the_icon() {
|
||||||
|
let discovery = |branding| -> InstancePremiumDiscovery {
|
||||||
|
serde_json::from_value(json!({"app_public": {"branding": branding}}))
|
||||||
|
.expect("discovery")
|
||||||
|
};
|
||||||
|
let both = discovery(json!({
|
||||||
|
"favicon_url": "https://media.example/favicon.png",
|
||||||
|
"icon_url": "https://media.example/icon.png"
|
||||||
|
}));
|
||||||
|
assert_eq!(
|
||||||
|
PremiumBranding::from_discovery(&both)
|
||||||
|
.favicon_url
|
||||||
|
.as_deref(),
|
||||||
|
Some("https://media.example/favicon.png")
|
||||||
|
);
|
||||||
|
let icon_only =
|
||||||
|
discovery(json!({"favicon_url": " ", "icon_url": "https://media.example/icon.png"}));
|
||||||
|
assert_eq!(
|
||||||
|
PremiumBranding::from_discovery(&icon_only)
|
||||||
|
.favicon_url
|
||||||
|
.as_deref(),
|
||||||
|
Some("https://media.example/icon.png")
|
||||||
|
);
|
||||||
|
let neither = discovery(json!({"product_name": " "}));
|
||||||
|
assert_eq!(
|
||||||
|
PremiumBranding::from_discovery(&neither),
|
||||||
|
PremiumBranding::default()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn premium_branding_from_instance_config_matches_discovery_rules() {
|
fn premium_branding_from_instance_config_matches_discovery_rules() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
PremiumBranding::from_config_parts(true, " Gold ", PremiumMode::Everyone),
|
PremiumBranding::from_config_parts(true, " Gold ", PremiumMode::Everyone),
|
||||||
PremiumBranding {
|
PremiumBranding {
|
||||||
name: Some("Gold".to_owned()),
|
name: Some("Gold".to_owned()),
|
||||||
premium_enabled: false
|
premium_enabled: false,
|
||||||
|
..PremiumBranding::default()
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
@@ -325,7 +383,8 @@ mod tests {
|
|||||||
PremiumBranding::from_config_parts(false, " ", PremiumMode::Everyone),
|
PremiumBranding::from_config_parts(false, " ", PremiumMode::Everyone),
|
||||||
PremiumBranding {
|
PremiumBranding {
|
||||||
name: None,
|
name: None,
|
||||||
premium_enabled: true
|
premium_enabled: true,
|
||||||
|
..PremiumBranding::default()
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::types::AdminUser,
|
api::types::{AdminUser, PremiumBranding},
|
||||||
middleware::flash,
|
middleware::flash,
|
||||||
session::{self, Session},
|
session::{self, Session},
|
||||||
state::AppState,
|
state::AppState,
|
||||||
@@ -16,6 +16,7 @@ use axum::{
|
|||||||
pub struct AuthContext {
|
pub struct AuthContext {
|
||||||
pub session: Session,
|
pub session: Session,
|
||||||
pub admin_user: Option<AdminUser>,
|
pub admin_user: Option<AdminUser>,
|
||||||
|
pub branding: Option<PremiumBranding>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn require_auth(
|
pub async fn require_auth(
|
||||||
@@ -28,27 +29,19 @@ pub async fn require_auth(
|
|||||||
let Some(session) = session else {
|
let Some(session) = session else {
|
||||||
return Redirect::to(&format!("{}/auth/start", config.base_path)).into_response();
|
return Redirect::to(&format!("{}/auth/start", config.base_path)).into_response();
|
||||||
};
|
};
|
||||||
let admin_result = fetch_admin_user(state.http_client(), config, &session).await;
|
let admin_user = match fetch_admin_user(state.http_client(), config, &session).await {
|
||||||
match admin_result {
|
|
||||||
AdminFetchResult::Unauthorized => {
|
AdminFetchResult::Unauthorized => {
|
||||||
let login_url = format!("{}/login", config.base_path);
|
let login_url = format!("{}/login", config.base_path);
|
||||||
return clear_session_and_redirect(&login_url, config);
|
return clear_session_and_redirect(&login_url, config);
|
||||||
}
|
}
|
||||||
AdminFetchResult::Ok(admin_user) => {
|
AdminFetchResult::Ok(admin_user) => Some(*admin_user),
|
||||||
let auth_context = AuthContext {
|
AdminFetchResult::None => None,
|
||||||
session,
|
};
|
||||||
admin_user: Some(*admin_user),
|
request.extensions_mut().insert(AuthContext {
|
||||||
};
|
session,
|
||||||
request.extensions_mut().insert(auth_context);
|
admin_user,
|
||||||
}
|
branding: state.self_hosted_branding().await,
|
||||||
AdminFetchResult::None => {
|
});
|
||||||
let auth_context = AuthContext {
|
|
||||||
session,
|
|
||||||
admin_user: None,
|
|
||||||
};
|
|
||||||
request.extensions_mut().insert(auth_context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let had_flash = flash::extract_flash(&request);
|
let had_flash = flash::extract_flash(&request);
|
||||||
if let Some(ref fd) = had_flash {
|
if let Some(ref fd) = had_flash {
|
||||||
|
|||||||
@@ -87,7 +87,8 @@ async fn login_page(
|
|||||||
Some(_) => Some("Login error. Please try again."),
|
Some(_) => Some("Login error. Please try again."),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
Html(login::login_page(config, error_msg).into_string()).into_response()
|
let branding = state.self_hosted_branding().await;
|
||||||
|
Html(login::login_page(config, branding.as_ref(), error_msg).into_string()).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn auth_start(State(state): State<AppState>) -> Response {
|
async fn auth_start(State(state): State<AppState>) -> Response {
|
||||||
|
|||||||
@@ -480,11 +480,13 @@ mod tests {
|
|||||||
let gold = PremiumBranding {
|
let gold = PremiumBranding {
|
||||||
name: Some("Gold".to_owned()),
|
name: Some("Gold".to_owned()),
|
||||||
premium_enabled: true,
|
premium_enabled: true,
|
||||||
|
..PremiumBranding::default()
|
||||||
};
|
};
|
||||||
assert_eq!(premium_badge_name(Some(&gold)).as_deref(), Some("Gold"));
|
assert_eq!(premium_badge_name(Some(&gold)).as_deref(), Some("Gold"));
|
||||||
let unnamed = PremiumBranding {
|
let unnamed = PremiumBranding {
|
||||||
name: None,
|
name: None,
|
||||||
premium_enabled: true,
|
premium_enabled: true,
|
||||||
|
..PremiumBranding::default()
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
premium_badge_name(Some(&unnamed)).as_deref(),
|
premium_badge_name(Some(&unnamed)).as_deref(),
|
||||||
@@ -493,6 +495,7 @@ mod tests {
|
|||||||
let everyone = PremiumBranding {
|
let everyone = PremiumBranding {
|
||||||
name: Some("Gold".to_owned()),
|
name: Some("Gold".to_owned()),
|
||||||
premium_enabled: false,
|
premium_enabled: false,
|
||||||
|
..PremiumBranding::default()
|
||||||
};
|
};
|
||||||
assert_eq!(premium_badge_name(Some(&everyone)), None);
|
assert_eq!(premium_badge_name(Some(&everyone)), None);
|
||||||
assert_eq!(premium_badge_name(None).as_deref(), Some("Premium"));
|
assert_eq!(premium_badge_name(None).as_deref(), Some("Premium"));
|
||||||
|
|||||||
@@ -88,6 +88,17 @@ impl AppState {
|
|||||||
Some(branding)
|
Some(branding)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn self_hosted_branding(&self) -> Option<PremiumBranding> {
|
||||||
|
if !self.config().self_hosted {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.premium_branding(&AdminApiClient::anonymous(
|
||||||
|
self.http_client(),
|
||||||
|
self.config(),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn account_identity(&self, client: &AdminApiClient) -> AccountIdentityMode {
|
pub async fn account_identity(&self, client: &AdminApiClient) -> AccountIdentityMode {
|
||||||
self.account_identity_settings(client).await.mode
|
self.account_identity_settings(client).await.mode
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
use crate::{api::types::FlashLevel, config::AdminConfig, middleware::auth::AuthContext};
|
use crate::{
|
||||||
|
api::types::{FlashLevel, PremiumBranding},
|
||||||
|
config::AdminConfig,
|
||||||
|
middleware::auth::AuthContext,
|
||||||
|
};
|
||||||
use maud::{DOCTYPE, Markup, PreEscaped, html};
|
use maud::{DOCTYPE, Markup, PreEscaped, html};
|
||||||
|
|
||||||
use super::components::drawer::drawer_controller_script;
|
use super::components::drawer::drawer_controller_script;
|
||||||
@@ -11,6 +15,28 @@ use super::layout_scripts::{
|
|||||||
};
|
};
|
||||||
use super::layout_sidebar::render_sidebar;
|
use super::layout_sidebar::render_sidebar;
|
||||||
|
|
||||||
|
const DEFAULT_PRODUCT_NAME: &str = "Fluxer";
|
||||||
|
|
||||||
|
pub fn product_name(branding: Option<&PremiumBranding>) -> &str {
|
||||||
|
branding
|
||||||
|
.and_then(|branding| branding.product_name.as_deref())
|
||||||
|
.unwrap_or(DEFAULT_PRODUCT_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn favicon_links(config: &AdminConfig, branding: Option<&PremiumBranding>) -> Markup {
|
||||||
|
let cdn = &config.static_cdn_endpoint;
|
||||||
|
html! {
|
||||||
|
@if let Some(favicon_url) = branding.and_then(|branding| branding.favicon_url.as_deref()) {
|
||||||
|
link rel="icon" href=(favicon_url);
|
||||||
|
} @else {
|
||||||
|
link rel="icon" type="image/x-icon" href={(cdn) "/web/favicon.ico"};
|
||||||
|
link rel="apple-touch-icon" href={(cdn) "/web/apple-touch-icon.png"};
|
||||||
|
link rel="icon" type="image/png" sizes="32x32" href={(cdn) "/web/favicon-32x32.png"};
|
||||||
|
link rel="icon" type="image/png" sizes="16x16" href={(cdn) "/web/favicon-16x16.png"};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn cache_busted_asset(base_path: &str, asset_version: &str, path: &str) -> String {
|
fn cache_busted_asset(base_path: &str, asset_version: &str, path: &str) -> String {
|
||||||
format!("{base_path}{path}?t={asset_version}")
|
format!("{base_path}{path}?t={asset_version}")
|
||||||
}
|
}
|
||||||
@@ -72,10 +98,7 @@ pub fn admin_layout_ext(
|
|||||||
title { (title) " ~ Fluxer Admin" }
|
title { (title) " ~ Fluxer Admin" }
|
||||||
link rel="stylesheet" href={(base) "/static/fonts/" (crate::fonts::STYLESHEET_FILE_NAME)};
|
link rel="stylesheet" href={(base) "/static/fonts/" (crate::fonts::STYLESHEET_FILE_NAME)};
|
||||||
link rel="stylesheet" href=(cache_busted_asset(base, asset_version, "/static/app.css"));
|
link rel="stylesheet" href=(cache_busted_asset(base, asset_version, "/static/app.css"));
|
||||||
link rel="icon" type="image/x-icon" href={(config.static_cdn_endpoint) "/web/favicon.ico"};
|
(favicon_links(config, auth.branding.as_ref()))
|
||||||
link rel="apple-touch-icon" href={(config.static_cdn_endpoint) "/web/apple-touch-icon.png"};
|
|
||||||
link rel="icon" type="image/png" sizes="32x32" href={(config.static_cdn_endpoint) "/web/favicon-32x32.png"};
|
|
||||||
link rel="icon" type="image/png" sizes="16x16" href={(config.static_cdn_endpoint) "/web/favicon-16x16.png"};
|
|
||||||
script src=(cache_busted_asset(base, asset_version, "/static/htmx.min.js")) defer {}
|
script src=(cache_busted_asset(base, asset_version, "/static/htmx.min.js")) defer {}
|
||||||
}
|
}
|
||||||
body class="min-h-screen overflow-hidden bg-neutral-50" hx-boost="true" {
|
body class="min-h-screen overflow-hidden bg-neutral-50" hx-boost="true" {
|
||||||
@@ -126,3 +149,53 @@ fn render_flash(flash: &crate::api::types::FlashMessage) -> Markup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::config::ProxyConfig;
|
||||||
|
|
||||||
|
fn config() -> AdminConfig {
|
||||||
|
AdminConfig {
|
||||||
|
host: String::new(),
|
||||||
|
port: 3020,
|
||||||
|
secret_key_base: "test-secret".to_owned(),
|
||||||
|
base_path: String::new(),
|
||||||
|
api_endpoint: String::new(),
|
||||||
|
media_endpoint: String::new(),
|
||||||
|
static_cdn_endpoint: "https://static.example".to_owned(),
|
||||||
|
reports_bucket_origin: String::new(),
|
||||||
|
admin_endpoint: String::new(),
|
||||||
|
web_app_endpoint: String::new(),
|
||||||
|
oauth_client_id: String::new(),
|
||||||
|
oauth_client_secret: String::new(),
|
||||||
|
oauth_redirect_uri: String::new(),
|
||||||
|
build_version: "test".to_owned(),
|
||||||
|
self_hosted: true,
|
||||||
|
proxy: ProxyConfig {
|
||||||
|
trust_client_ip_header: false,
|
||||||
|
client_ip_header_name: String::new(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn instance_branding_replaces_the_fluxer_name_and_favicons() {
|
||||||
|
let branding = PremiumBranding {
|
||||||
|
product_name: Some("Example".to_owned()),
|
||||||
|
favicon_url: Some("https://media.example/favicon.png".to_owned()),
|
||||||
|
..PremiumBranding::default()
|
||||||
|
};
|
||||||
|
let links = favicon_links(&config(), Some(&branding)).into_string();
|
||||||
|
assert!(links.contains("https://media.example/favicon.png"));
|
||||||
|
assert!(!links.contains("static.example"));
|
||||||
|
assert_eq!(product_name(Some(&branding)), "Example");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_branding_keeps_the_fluxer_name_and_favicons() {
|
||||||
|
let links = favicon_links(&config(), Some(&PremiumBranding::default())).into_string();
|
||||||
|
assert!(links.contains("https://static.example/web/favicon.ico"));
|
||||||
|
assert_eq!(product_name(None), "Fluxer");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
use crate::config::AdminConfig;
|
use crate::{
|
||||||
|
api::types::PremiumBranding,
|
||||||
|
config::AdminConfig,
|
||||||
|
templates::layout::{favicon_links, product_name},
|
||||||
|
};
|
||||||
use maud::{DOCTYPE, Markup, html};
|
use maud::{DOCTYPE, Markup, html};
|
||||||
|
|
||||||
pub fn login_page(config: &AdminConfig, error_message: Option<&str>) -> Markup {
|
pub fn login_page(
|
||||||
|
config: &AdminConfig,
|
||||||
|
branding: Option<&PremiumBranding>,
|
||||||
|
error_message: Option<&str>,
|
||||||
|
) -> Markup {
|
||||||
let base = &config.base_path;
|
let base = &config.base_path;
|
||||||
html! {
|
html! {
|
||||||
(DOCTYPE)
|
(DOCTYPE)
|
||||||
@@ -14,7 +22,7 @@ pub fn login_page(config: &AdminConfig, error_message: Option<&str>) -> Markup {
|
|||||||
title { "Login ~ Fluxer Admin" }
|
title { "Login ~ Fluxer Admin" }
|
||||||
link rel="stylesheet" href={(base) "/static/fonts/" (crate::fonts::STYLESHEET_FILE_NAME)};
|
link rel="stylesheet" href={(base) "/static/fonts/" (crate::fonts::STYLESHEET_FILE_NAME)};
|
||||||
link rel="stylesheet" href={(base) "/static/app.css"};
|
link rel="stylesheet" href={(base) "/static/app.css"};
|
||||||
link rel="icon" type="image/x-icon" href={(config.static_cdn_endpoint) "/web/favicon.ico"};
|
(favicon_links(config, branding))
|
||||||
}
|
}
|
||||||
body class="flex min-h-[100dvh] items-center justify-center bg-neutral-50 p-4" {
|
body class="flex min-h-[100dvh] items-center justify-center bg-neutral-50 p-4" {
|
||||||
main class="w-full max-w-sm" {
|
main class="w-full max-w-sm" {
|
||||||
@@ -38,7 +46,7 @@ pub fn login_page(config: &AdminConfig, error_message: Option<&str>) -> Markup {
|
|||||||
bg-neutral-900 text-white hover:bg-neutral-800 \
|
bg-neutral-900 text-white hover:bg-neutral-800 \
|
||||||
px-4 py-2 text-base w-full sm:w-fit \
|
px-4 py-2 text-base w-full sm:w-fit \
|
||||||
focus:ring-offset-white" {
|
focus:ring-offset-white" {
|
||||||
span { "Sign in with Fluxer" }
|
span { "Sign in with " (product_name(branding)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use crate::{
|
|||||||
},
|
},
|
||||||
page_container::{card, page_header},
|
page_container::{card, page_header},
|
||||||
},
|
},
|
||||||
layout::admin_layout,
|
layout::{admin_layout, product_name},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use maud::{Markup, html};
|
use maud::{Markup, html};
|
||||||
@@ -37,9 +37,9 @@ pub fn system_dm_page(
|
|||||||
"Send a system DM"
|
"Send a system DM"
|
||||||
}
|
}
|
||||||
p class="text-sm text-neutral-500" {
|
p class="text-sm text-neutral-500" {
|
||||||
"Sent from the official Fluxer system account. Each recipient \
|
"Sent from the official " (product_name(auth.branding.as_ref()))
|
||||||
will receive the same content as a DM. Progress is observable \
|
" system account. Each recipient will receive the same content \
|
||||||
on the "
|
as a DM. Progress is observable on the "
|
||||||
a href=(jobs_url) class="font-medium text-neutral-900 hover:underline" {
|
a href=(jobs_url) class="font-medium text-neutral-900 hover:underline" {
|
||||||
"Jobs page"
|
"Jobs page"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user