check crates UI +imap
EBO <eric@ebii.fr> committé le 2026-10-01 21:58
9f3c757873096e2e101facf168b60dddd24a5797
1 parent(s)
71 fichiers modifiés
+2674
-2917
M
CHANGELOG.md
+7
-0
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
| 7 | 7 | |
| 8 | 8 | ## [Unreleased] |
| 9 | 9 | |
| 10 | +### Security | |
| 11 | + | |
| 12 | +- **RFC 0039 phase 2.** `SecurityHeadersConfig::default` n'émet plus | |
| 13 | + `'unsafe-eval'`. L'UI charge le build Alpine CSP. `'unsafe-inline'` est | |
| 14 | + conservé. La politique 1.0.1 reste disponible via | |
| 15 | + `DEFAULT_CONTENT_SECURITY_POLICY` et `RustwardenBuilder::security_headers`. | |
| 16 | + | |
| 10 | 17 | ### Added |
| 11 | 18 | |
| 12 | 19 | - **Mesure d'audience générique** (`config::analytics`). Support d'un script de suivi |
M
crates/rustwarden-core/src/app.rs
+13
-13
@@ -32,11 +32,11 @@ use tower::ServiceBuilder;
| 32 | 32 | use tower_http::cors::{AllowOrigin, Any, CorsLayer}; |
| 33 | 33 | |
| 34 | 34 | use crate::auth::session::SessionConfig; |
| 35 | -use crate::config::auth::AuthConfig; | |
| 36 | 35 | use crate::config::analytics::{AnalyticsConfig, AnalyticsState}; |
| 36 | +use crate::config::auth::AuthConfig; | |
| 37 | 37 | use crate::config::database; |
| 38 | 38 | use crate::config::email::EmailConfig; |
| 39 | -use crate::config::security_headers::{DEFAULT_CONTENT_SECURITY_POLICY, SecurityHeadersConfig}; | |
| 39 | +use crate::config::security_headers::{CSP_WITHOUT_UNSAFE_EVAL, SecurityHeadersConfig}; | |
| 40 | 40 | use crate::config::server::ServerConfig; |
| 41 | 41 | use crate::dashboard::DashboardCardDTO; |
| 42 | 42 | use crate::hooks::{NoopHooks, RustwardenHooks}; |
@@ -981,13 +981,13 @@ async fn security_headers_middleware(
| 981 | 981 | ); |
| 982 | 982 | // SEC-FIX: CSP header to mitigate XSS attacks. Value comes from the |
| 983 | 983 | // configured policy; an invalid header value (e.g. control characters) |
| 984 | - // falls back to the 1.0.1 default instead of panicking (RFC 0039). | |
| 984 | + // falls back to the default policy (no 'unsafe-eval') instead of panicking. | |
| 985 | 985 | let csp = HeaderValue::from_str(cfg.content_security_policy.as_ref()).unwrap_or_else(|_| { |
| 986 | 986 | tracing::warn!( |
| 987 | 987 | "Invalid Content-Security-Policy value in SecurityHeadersConfig; \ |
| 988 | 988 | falling back to the default policy" |
| 989 | 989 | ); |
| 990 | - HeaderValue::from_static(DEFAULT_CONTENT_SECURITY_POLICY) | |
| 990 | + HeaderValue::from_static(CSP_WITHOUT_UNSAFE_EVAL) | |
| 991 | 991 | }); |
| 992 | 992 | headers.insert(HeaderName::from_static("content-security-policy"), csp); |
| 993 | 993 | // RFC 0039: opt-in cross-origin isolation headers. `None` (the default) |
@@ -1112,7 +1112,7 @@ mod builder_tests {
| 1112 | 1112 | let builder = RustwardenBuilder::default(); |
| 1113 | 1113 | assert_eq!( |
| 1114 | 1114 | builder.security_headers.content_security_policy, |
| 1115 | - DEFAULT_CONTENT_SECURITY_POLICY | |
| 1115 | + CSP_WITHOUT_UNSAFE_EVAL | |
| 1116 | 1116 | ); |
| 1117 | 1117 | assert!( |
| 1118 | 1118 | builder |
@@ -1193,7 +1193,7 @@ mod security_headers_middleware_tests {
| 1193 | 1193 | headers |
| 1194 | 1194 | .get("content-security-policy") |
| 1195 | 1195 | .expect("csp header present"), |
| 1196 | - DEFAULT_CONTENT_SECURITY_POLICY | |
| 1196 | + CSP_WITHOUT_UNSAFE_EVAL | |
| 1197 | 1197 | ); |
| 1198 | 1198 | assert!(headers.get("cross-origin-opener-policy").is_none()); |
| 1199 | 1199 | assert!(headers.get("cross-origin-resource-policy").is_none()); |
@@ -1208,7 +1208,7 @@ mod security_headers_middleware_tests {
| 1208 | 1208 | headers |
| 1209 | 1209 | .get("content-security-policy") |
| 1210 | 1210 | .expect("csp header present"), |
| 1211 | - DEFAULT_CONTENT_SECURITY_POLICY | |
| 1211 | + CSP_WITHOUT_UNSAFE_EVAL | |
| 1212 | 1212 | ); |
| 1213 | 1213 | assert_eq!( |
| 1214 | 1214 | headers |
@@ -1236,8 +1236,8 @@ mod security_headers_middleware_tests {
| 1236 | 1236 | resp.headers() |
| 1237 | 1237 | .get("content-security-policy") |
| 1238 | 1238 | .expect("csp header present"), |
| 1239 | - DEFAULT_CONTENT_SECURITY_POLICY, | |
| 1240 | - "invalid CSP must fall back to the 1.0.1 default" | |
| 1239 | + CSP_WITHOUT_UNSAFE_EVAL, | |
| 1240 | + "invalid CSP must fall back to the default policy" | |
| 1241 | 1241 | ); |
| 1242 | 1242 | } |
| 1243 | 1243 |
@@ -1257,7 +1257,7 @@ mod security_headers_middleware_tests {
| 1257 | 1257 | headers |
| 1258 | 1258 | .get("content-security-policy") |
| 1259 | 1259 | .expect("csp header present"), |
| 1260 | - DEFAULT_CONTENT_SECURITY_POLICY | |
| 1260 | + CSP_WITHOUT_UNSAFE_EVAL | |
| 1261 | 1261 | ); |
| 1262 | 1262 | } |
| 1263 | 1263 |
@@ -1313,8 +1313,8 @@ mod security_headers_middleware_tests {
| 1313 | 1313 | |
| 1314 | 1314 | #[tokio::test] |
| 1315 | 1315 | async fn analytics_disabled_leaves_the_csp_byte_identical() { |
| 1316 | - // The whole point of the default: a consumer that never configures | |
| 1317 | - // analytics must see the historical header, unchanged. | |
| 1316 | + // A consumer that never configures analytics must see the default | |
| 1317 | + // policy (RFC 0039 phase 2: no 'unsafe-eval'), unchanged. | |
| 1318 | 1318 | let csp = headers_for( |
| 1319 | 1319 | &AnalyticsConfig::default(), |
| 1320 | 1320 | SecurityHeadersConfig::default(), |
@@ -1328,7 +1328,7 @@ mod security_headers_middleware_tests {
| 1328 | 1328 | resp.headers() |
| 1329 | 1329 | .get("content-security-policy") |
| 1330 | 1330 | .expect("csp header present"), |
| 1331 | - DEFAULT_CONTENT_SECURITY_POLICY | |
| 1331 | + CSP_WITHOUT_UNSAFE_EVAL | |
| 1332 | 1332 | ); |
| 1333 | 1333 | } |
| 1334 | 1334 |
M
crates/rustwarden-core/src/config/analytics.rs
+39
-10
@@ -151,7 +151,10 @@ impl AnalyticsConfig {
| 151 | 151 | let mut cfg = Self::default(); |
| 152 | 152 | |
| 153 | 153 | if let Ok(raw) = std::env::var("ANALYTICS_ENABLED") { |
| 154 | - cfg.enabled = matches!(raw.trim().to_ascii_lowercase().as_str(), "true" | "1" | "on"); | |
| 154 | + cfg.enabled = matches!( | |
| 155 | + raw.trim().to_ascii_lowercase().as_str(), | |
| 156 | + "true" | "1" | "on" | |
| 157 | + ); | |
| 155 | 158 | } |
| 156 | 159 | if let Ok(url) = std::env::var("ANALYTICS_SCRIPT_URL") { |
| 157 | 160 | let url = url.trim().to_string(); |
@@ -183,7 +186,10 @@ impl AnalyticsConfig {
| 183 | 186 | } |
| 184 | 187 | } |
| 185 | 188 | if let Ok(raw) = std::env::var("ANALYTICS_DEFER") { |
| 186 | - cfg.defer = !matches!(raw.trim().to_ascii_lowercase().as_str(), "false" | "0" | "off"); | |
| 189 | + cfg.defer = !matches!( | |
| 190 | + raw.trim().to_ascii_lowercase().as_str(), | |
| 191 | + "false" | "0" | "off" | |
| 192 | + ); | |
| 187 | 193 | } |
| 188 | 194 | |
| 189 | 195 | cfg.validate()?; |
@@ -583,7 +589,8 @@ impl AnalyticsState {
| 583 | 589 | .await |
| 584 | 590 | .unwrap_or(base.enabled); |
| 585 | 591 | |
| 586 | - if let Some(setting) = AppSettingsService::get_setting(db, SETTING_ANALYTICS_SITE_ID).await? | |
| 592 | + if let Some(setting) = | |
| 593 | + AppSettingsService::get_setting(db, SETTING_ANALYTICS_SITE_ID).await? | |
| 587 | 594 | { |
| 588 | 595 | let value = setting.value.trim().to_string(); |
| 589 | 596 | cfg.site_id = if value.is_empty() { None } else { Some(value) }; |
@@ -687,7 +694,9 @@ where
| 687 | 694 | parts: &mut axum::http::request::Parts, |
| 688 | 695 | _state: &S, |
| 689 | 696 | ) -> Result<Self, Self::Rejection> { |
| 690 | - Ok(Self::from_state(parts.extensions.get::<Arc<AnalyticsState>>())) | |
| 697 | + Ok(Self::from_state( | |
| 698 | + parts.extensions.get::<Arc<AnalyticsState>>(), | |
| 699 | + )) | |
| 691 | 700 | } |
| 692 | 701 | } |
| 693 | 702 |
@@ -897,14 +906,28 @@ mod tests {
| 897 | 906 | #[test] |
| 898 | 907 | fn script_origin_drops_path_and_keeps_port() { |
| 899 | 908 | let cases = [ |
| 900 | - ("https://stats.example.org/api/script.js", "https://stats.example.org"), | |
| 901 | - ("https://stats.example.org:8443/s.js", "https://stats.example.org:8443"), | |
| 902 | - ("https://a.b.c.example.org/s.js?v=1", "https://a.b.c.example.org"), | |
| 909 | + ( | |
| 910 | + "https://stats.example.org/api/script.js", | |
| 911 | + "https://stats.example.org", | |
| 912 | + ), | |
| 913 | + ( | |
| 914 | + "https://stats.example.org:8443/s.js", | |
| 915 | + "https://stats.example.org:8443", | |
| 916 | + ), | |
| 917 | + ( | |
| 918 | + "https://a.b.c.example.org/s.js?v=1", | |
| 919 | + "https://a.b.c.example.org", | |
| 920 | + ), | |
| 903 | 921 | ("http://localhost:3001/s.js", "http://localhost:3001"), |
| 904 | 922 | ]; |
| 905 | 923 | for (url, expected) in cases { |
| 906 | 924 | let cfg = AnalyticsConfig::default().with_script_url(url); |
| 907 | - assert_eq!(cfg.script_origin().as_deref(), Some(expected), "for {}", url); | |
| 925 | + assert_eq!( | |
| 926 | + cfg.script_origin().as_deref(), | |
| 927 | + Some(expected), | |
| 928 | + "for {}", | |
| 929 | + url | |
| 930 | + ); | |
| 908 | 931 | } |
| 909 | 932 | } |
| 910 | 933 |
@@ -938,7 +961,10 @@ mod tests {
| 938 | 961 | #[test] |
| 939 | 962 | fn attributes_json_is_parsed() { |
| 940 | 963 | let parsed = parse_attributes_json(r#"{"data-consent":"opt-in"}"#).unwrap_or_default(); |
| 941 | - assert_eq!(parsed.get("data-consent").map(String::as_str), Some("opt-in")); | |
| 964 | + assert_eq!( | |
| 965 | + parsed.get("data-consent").map(String::as_str), | |
| 966 | + Some("opt-in") | |
| 967 | + ); | |
| 942 | 968 | } |
| 943 | 969 | |
| 944 | 970 | #[test] |
@@ -954,7 +980,10 @@ mod tests {
| 954 | 980 | fn state_pre_renders_the_snippet() { |
| 955 | 981 | let state = AnalyticsState::from_config(enabled_config()); |
| 956 | 982 | assert!(state.snippet().is_some()); |
| 957 | - assert_eq!(state.script_url(), Some("https://stats.example.org/api/script.js")); | |
| 983 | + assert_eq!( | |
| 984 | + state.script_url(), | |
| 985 | + Some("https://stats.example.org/api/script.js") | |
| 986 | + ); | |
| 958 | 987 | } |
| 959 | 988 | |
| 960 | 989 | #[test] |
M
crates/rustwarden-core/src/config/email.rs
+36
-5
@@ -16,7 +16,7 @@
| 16 | 16 | //! |
| 17 | 17 | //! IMAP configuration for bounce processing (Feature 012): |
| 18 | 18 | //! - IMAP_HOST: IMAP server hostname (optional, enables bounce processing if set) |
| 19 | -//! - IMAP_PORT: IMAP server port (default: 993) | |
| 19 | +//! - IMAP_PORT: IMAP server port (default: 993). Ports 25, 465 and 587 are rejected: they are SMTP. | |
| 20 | 20 | //! - IMAP_USER: IMAP username (defaults to SMTP_USER) |
| 21 | 21 | //! - IMAP_PASSWORD: IMAP password (defaults to SMTP_PASSWORD) |
| 22 | 22 | //! - IMAP_USE_TLS: Use TLS for IMAP connection (default: true) |
@@ -265,11 +265,19 @@ impl EmailConfig {
| 265 | 265 | } |
| 266 | 266 | |
| 267 | 267 | if let Some(ref host) = imap_host { |
| 268 | + // 25 (SMTP), 465 (SMTPS) and 587 (submission) speak SMTP. An IMAP | |
| 269 | + // client cannot parse their greeting, which surfaces as a STARTTLS | |
| 270 | + // "Unable to parse status response". | |
| 271 | + Self::validate_imap_port(imap_port)?; | |
| 272 | + let mode = if imap_use_tls { | |
| 273 | + "direct TLS" | |
| 274 | + } else if imap_use_starttls { | |
| 275 | + "STARTTLS" | |
| 276 | + } else { | |
| 277 | + "plain" | |
| 278 | + }; | |
| 268 | 279 | tracing::info!( |
| 269 | - "IMAP bounce processing enabled: host={}, mailbox={}, poll interval={}s", | |
| 270 | - host, | |
| 271 | - imap_mailbox, | |
| 272 | - imap_poll_interval_seconds | |
| 280 | + "IMAP bounce processing enabled: host={host}:{imap_port}, mode={mode}, mailbox={imap_mailbox}, poll interval={imap_poll_interval_seconds}s" | |
| 273 | 281 | ); |
| 274 | 282 | } |
| 275 | 283 |
@@ -366,6 +374,19 @@ impl EmailConfig {
| 366 | 374 | Ok(()) |
| 367 | 375 | } |
| 368 | 376 | |
| 377 | + /// Reject SMTP ports for the IMAP bounce reader. | |
| 378 | + /// | |
| 379 | + /// Ports 25, 465 and 587 speak SMTP. Connecting an IMAP client there fails | |
| 380 | + /// while parsing the greeting. | |
| 381 | + fn validate_imap_port(port: u16) -> Result<(), String> { | |
| 382 | + if matches!(port, 25 | 465 | 587) { | |
| 383 | + return Err(format!( | |
| 384 | + "IMAP_PORT {port} is an SMTP port. Use 143 with IMAP_USE_STARTTLS=true or 993 with IMAP_USE_TLS=true" | |
| 385 | + )); | |
| 386 | + } | |
| 387 | + Ok(()) | |
| 388 | + } | |
| 389 | + | |
| 369 | 390 | /// Basic email format validation |
| 370 | 391 | /// |
| 371 | 392 | /// Validates that the email contains an @ symbol and has a valid format. |
@@ -410,4 +431,14 @@ mod tests {
| 410 | 431 | assert!(!EmailConfig::is_valid_email("user@")); |
| 411 | 432 | assert!(!EmailConfig::is_valid_email("")); |
| 412 | 433 | } |
| 434 | + | |
| 435 | + #[test] | |
| 436 | + fn test_validate_imap_port_rejects_smtp_ports() { | |
| 437 | + for port in [25_u16, 465, 587] { | |
| 438 | + let err = EmailConfig::validate_imap_port(port).unwrap_err(); | |
| 439 | + assert!(err.contains("SMTP port"), "{err}"); | |
| 440 | + } | |
| 441 | + assert!(EmailConfig::validate_imap_port(143).is_ok()); | |
| 442 | + assert!(EmailConfig::validate_imap_port(993).is_ok()); | |
| 443 | + } | |
| 413 | 444 | } |
M
crates/rustwarden-core/src/config/security_headers.rs
+31
-32
@@ -4,10 +4,11 @@
| 4 | 4 | //! `RustwardenApp::builder()`: a configurable `Content-Security-Policy` |
| 5 | 5 | //! plus opt-in `Cross-Origin-Opener-Policy` / `Cross-Origin-Resource-Policy`. |
| 6 | 6 | //! |
| 7 | -//! The [`Default`] implementation is **bit-identical** to the headers | |
| 8 | -//! shipped since patch 1.0.1: the default CSP keeps `'unsafe-inline'` and | |
| 9 | -//! `'unsafe-eval'` (required by the stock Alpine.js build used by the admin | |
| 10 | -//! UI), and COOP/CORP are not emitted at all. | |
| 7 | +//! The [`Default`] implementation ships the phase-2 CSP: `'unsafe-inline'` | |
| 8 | +//! stays (inline boot scripts and HTMX), `'unsafe-eval'` is gone because the | |
| 9 | +//! reference UI loads Alpine's CSP build. The historical 1.0.1 policy remains | |
| 10 | +//! available as [`DEFAULT_CONTENT_SECURITY_POLICY`] for consumers that still | |
| 11 | +//! need `eval`. COOP/CORP are not emitted at all. | |
| 11 | 12 | //! |
| 12 | 13 | //! The other hardcoded headers (`X-Content-Type-Options`, `X-Frame-Options`, |
| 13 | 14 | //! `X-XSS-Protection`, `Referrer-Policy`, `Permissions-Policy`, HSTS) are |
@@ -15,12 +16,15 @@
| 15 | 16 | |
| 16 | 17 | use std::borrow::Cow; |
| 17 | 18 | |
| 18 | -/// Default CSP shipped since SEC-H1 / 1.0.1. Do not tighten in the first | |
| 19 | -/// implementation PR (Alpine.js still needs 'unsafe-eval'). | |
| 19 | +/// Historical CSP shipped in 1.0.1, including `'unsafe-eval'`. | |
| 20 | +/// | |
| 21 | +/// Kept so a consumer that still evaluates Alpine expressions with the stock | |
| 22 | +/// build can opt back in: | |
| 23 | +/// `SecurityHeadersConfig { content_security_policy: Cow::Borrowed(DEFAULT_CONTENT_SECURITY_POLICY), ..SecurityHeadersConfig::default() }`. | |
| 20 | 24 | pub const DEFAULT_CONTENT_SECURITY_POLICY: &str = "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"; |
| 21 | 25 | |
| 22 | -/// Candidate default **after** PLAN_CSP_HTMX.md Phase 1 (Alpine CSP build). | |
| 23 | -/// Not used by [`SecurityHeadersConfig::default`] until Phase 2 of RFC 0039. | |
| 26 | +/// Default CSP since RFC 0039 phase 2 (Alpine CSP build, no `'unsafe-eval'`). | |
| 27 | +/// `'unsafe-inline'` remains until a later CSP phase. | |
| 24 | 28 | pub const CSP_WITHOUT_UNSAFE_EVAL: &str = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"; |
| 25 | 29 | |
| 26 | 30 | /// Configurable security headers applied by the framework's security |
@@ -30,13 +34,12 @@ pub const CSP_WITHOUT_UNSAFE_EVAL: &str = "default-src 'self'; script-src 'self'
| 30 | 34 | /// |
| 31 | 35 | /// ``` |
| 32 | 36 | /// use rustwarden_core::config::security_headers::{ |
| 33 | -/// DEFAULT_CONTENT_SECURITY_POLICY, SecurityHeadersConfig, | |
| 37 | +/// CSP_WITHOUT_UNSAFE_EVAL, SecurityHeadersConfig, | |
| 34 | 38 | /// }; |
| 35 | 39 | /// |
| 36 | -/// // The default configuration matches the 1.0.1 behaviour exactly: | |
| 37 | -/// // legacy CSP, no COOP, no CORP. | |
| 40 | +/// // The default CSP has no 'unsafe-eval'. COOP and CORP stay opt-in. | |
| 38 | 41 | /// let cfg = SecurityHeadersConfig::default(); |
| 39 | -/// assert_eq!(cfg.content_security_policy, DEFAULT_CONTENT_SECURITY_POLICY); | |
| 42 | +/// assert_eq!(cfg.content_security_policy, CSP_WITHOUT_UNSAFE_EVAL); | |
| 40 | 43 | /// assert!(cfg.cross_origin_opener_policy.is_none()); |
| 41 | 44 | /// assert!(cfg.cross_origin_resource_policy.is_none()); |
| 42 | 45 | /// |
@@ -47,7 +50,7 @@ pub const CSP_WITHOUT_UNSAFE_EVAL: &str = "default-src 'self'; script-src 'self'
| 47 | 50 | /// ``` |
| 48 | 51 | #[derive(Clone, Debug)] |
| 49 | 52 | pub struct SecurityHeadersConfig { |
| 50 | - /// `Content-Security-Policy` value. Default keeps 1.0.1 (unsafe-inline/eval). | |
| 53 | + /// `Content-Security-Policy` value. Default is [`CSP_WITHOUT_UNSAFE_EVAL`]. | |
| 51 | 54 | pub content_security_policy: Cow<'static, str>, |
| 52 | 55 | /// `Cross-Origin-Opener-Policy`. `None` = do not set the header (opt-in). |
| 53 | 56 | pub cross_origin_opener_policy: Option<Cow<'static, str>>, |
@@ -58,7 +61,7 @@ pub struct SecurityHeadersConfig {
| 58 | 61 | impl Default for SecurityHeadersConfig { |
| 59 | 62 | fn default() -> Self { |
| 60 | 63 | Self { |
| 61 | - content_security_policy: Cow::Borrowed(DEFAULT_CONTENT_SECURITY_POLICY), | |
| 64 | + content_security_policy: Cow::Borrowed(CSP_WITHOUT_UNSAFE_EVAL), | |
| 62 | 65 | cross_origin_opener_policy: None, |
| 63 | 66 | cross_origin_resource_policy: None, |
| 64 | 67 | } |
@@ -99,7 +102,7 @@ impl SecurityHeadersConfig {
| 99 | 102 | /// .allowing_script_origin("https://stats.example.org"); |
| 100 | 103 | /// |
| 101 | 104 | /// assert!(cfg.content_security_policy.contains( |
| 102 | - /// "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://stats.example.org" | |
| 105 | + /// "script-src 'self' 'unsafe-inline' https://stats.example.org" | |
| 103 | 106 | /// )); |
| 104 | 107 | /// assert!(cfg.content_security_policy.contains( |
| 105 | 108 | /// "connect-src 'self' https://stats.example.org" |
@@ -176,17 +179,16 @@ fn add_source_to_directives(policy: &str, source: &str, directives: &[&str]) ->
| 176 | 179 | mod tests { |
| 177 | 180 | use super::*; |
| 178 | 181 | |
| 179 | - /// Non-drift guard (RFC 0039 testing plan): the default CSP must stay | |
| 180 | - /// strictly equal to the literal string historically hardcoded in | |
| 181 | - /// `security_headers_middleware` (app.rs, patch 1.0.1). If this test | |
| 182 | - /// breaks, the default behaviour changed — that is Phase 2 territory | |
| 183 | - /// and requires its own PR + CHANGELOG `Security` entry. | |
| 182 | + /// Phase 2 (RFC 0039): the default policy is the CSP build, without | |
| 183 | + /// `'unsafe-eval'`. The historical 1.0.1 literal stays on the constant | |
| 184 | + /// so consumers can opt back in. | |
| 184 | 185 | #[test] |
| 185 | - fn default_csp_matches_the_historical_literal() { | |
| 186 | + fn default_csp_omits_unsafe_eval() { | |
| 186 | 187 | let cfg = SecurityHeadersConfig::default(); |
| 187 | - assert_eq!(cfg.content_security_policy, DEFAULT_CONTENT_SECURITY_POLICY); | |
| 188 | + assert_eq!(cfg.content_security_policy, CSP_WITHOUT_UNSAFE_EVAL); | |
| 189 | + assert!(!cfg.content_security_policy.contains("unsafe-eval")); | |
| 188 | 190 | assert_eq!( |
| 189 | - cfg.content_security_policy, | |
| 191 | + DEFAULT_CONTENT_SECURITY_POLICY, | |
| 190 | 192 | "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" |
| 191 | 193 | ); |
| 192 | 194 | } |
@@ -210,7 +212,7 @@ mod tests {
| 210 | 212 | Some("same-origin") |
| 211 | 213 | ); |
| 212 | 214 | // The CSP is untouched by the isolation helper. |
| 213 | - assert_eq!(cfg.content_security_policy, DEFAULT_CONTENT_SECURITY_POLICY); | |
| 215 | + assert_eq!(cfg.content_security_policy, CSP_WITHOUT_UNSAFE_EVAL); | |
| 214 | 216 | } |
| 215 | 217 | |
| 216 | 218 | /// The Phase 2 candidate constant only differs from the default by the |
@@ -258,12 +260,7 @@ mod tests {
| 258 | 260 | // Existing script-src sources are kept, the origin is appended. |
| 259 | 261 | assert_eq!( |
| 260 | 262 | sources_of(policy, "script-src"), |
| 261 | - vec![ | |
| 262 | - "'self'", | |
| 263 | - "'unsafe-inline'", | |
| 264 | - "'unsafe-eval'", | |
| 265 | - "https://stats.example.org" | |
| 266 | - ] | |
| 263 | + vec!["'self'", "'unsafe-inline'", "https://stats.example.org"] | |
| 267 | 264 | ); |
| 268 | 265 | } |
| 269 | 266 |
@@ -271,7 +268,9 @@ mod tests {
| 271 | 268 | fn allowing_script_origin_is_idempotent() { |
| 272 | 269 | let once = |
| 273 | 270 | SecurityHeadersConfig::default().allowing_script_origin("https://stats.example.org"); |
| 274 | - let twice = once.clone().allowing_script_origin("https://stats.example.org"); | |
| 271 | + let twice = once | |
| 272 | + .clone() | |
| 273 | + .allowing_script_origin("https://stats.example.org"); | |
| 275 | 274 | assert_eq!(once.content_security_policy, twice.content_security_policy); |
| 276 | 275 | } |
| 277 | 276 |
@@ -307,6 +306,6 @@ mod tests {
| 307 | 306 | #[test] |
| 308 | 307 | fn allowing_empty_origin_is_a_noop() { |
| 309 | 308 | let cfg = SecurityHeadersConfig::default().allowing_script_origin(" "); |
| 310 | - assert_eq!(cfg.content_security_policy, DEFAULT_CONTENT_SECURITY_POLICY); | |
| 309 | + assert_eq!(cfg.content_security_policy, CSP_WITHOUT_UNSAFE_EVAL); | |
| 311 | 310 | } |
| 312 | 311 | } |
M
crates/rustwarden-core/src/services/imap_bounce_service.rs
+7
-4
@@ -345,11 +345,14 @@ impl ImapBounceService {
| 345 | 345 | .select(mailbox) |
| 346 | 346 | .map_err(|e| AppError::Internal(format!("IMAP select error: {}", e)))?; |
| 347 | 347 | |
| 348 | - // Search for unread DSN messages (Content-Type contains "delivery-status") | |
| 349 | - // We search for messages with specific subjects indicating bounces | |
| 348 | + // RFC 3464: a DSN is multipart/report with report-type=delivery-status. | |
| 349 | + // HEADER matches the top-level Content-Type, so both tokens are needed. | |
| 350 | + // IMAP OR is a prefix operator, not an infix one. | |
| 350 | 351 | let search_result = session |
| 351 | - .search("UNSEEN SUBJECT \"Delivery Status Notification\" OR UNSEEN SUBJECT \"Undelivered\" OR UNSEEN SUBJECT \"Mail delivery failed\"") | |
| 352 | - .unwrap_or_default(); | |
| 352 | + .search( | |
| 353 | + r#"OR (UNSEEN HEADER Content-Type "delivery-status") (UNSEEN HEADER Content-Type "multipart/report")"#, | |
| 354 | + ) | |
| 355 | + .map_err(|e| AppError::Internal(format!("IMAP search error: {e}")))?; | |
| 353 | 356 | |
| 354 | 357 | let mut bounces = Vec::new(); |
| 355 | 358 |
M
crates/rustwarden-fediverse-ui/src/handlers/admin.rs
+73
-0
@@ -156,6 +156,8 @@ pub struct FediverseSettingsTemplate {
| 156 | 156 | pub app_domain: String, |
| 157 | 157 | /// Short app description (header). |
| 158 | 158 | pub app_description: String, |
| 159 | + /// Audience-measurement opt-out. Empty renders nothing. | |
| 160 | + pub analytics_opt_out: rustwarden_ui::analytics::AnalyticsOptOutContext, | |
| 159 | 161 | /// Username for the navbar avatar. |
| 160 | 162 | pub username: String, |
| 161 | 163 | /// Email for the navbar tooltip. |
@@ -245,6 +247,7 @@ pub async fn get_settings(
| 245 | 247 | Extension(claims): Extension<Claims>, |
| 246 | 248 | Extension(i18n_service): Extension<Arc<I18nService>>, |
| 247 | 249 | Extension(locale): Extension<Locale>, |
| 250 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 248 | 251 | State(db): State<DatabaseConnection>, |
| 249 | 252 | session: Session, |
| 250 | 253 | axum::extract::Query(query): axum::extract::Query<SavedQuery>, |
@@ -283,6 +286,12 @@ pub async fn get_settings(
| 283 | 286 | app_version: base_context.app_version, |
| 284 | 287 | app_domain: base_context.app_domain, |
| 285 | 288 | app_description: base_context.app_description, |
| 289 | + analytics_opt_out: rustwarden_ui::analytics::AnalyticsOptOutContext::build( | |
| 290 | + &analytics, | |
| 291 | + &i18n_service, | |
| 292 | + locale.as_str(), | |
| 293 | + ) | |
| 294 | + .await, | |
| 286 | 295 | username: base_context.username, |
| 287 | 296 | email: base_context.email, |
| 288 | 297 | initials: base_context.initials, |
@@ -463,6 +472,8 @@ pub struct FediverseTrustListTemplate {
| 463 | 472 | pub app_version: String, |
| 464 | 473 | pub app_domain: String, |
| 465 | 474 | pub app_description: String, |
| 475 | + /// Audience-measurement opt-out. Empty renders nothing. | |
| 476 | + pub analytics_opt_out: rustwarden_ui::analytics::AnalyticsOptOutContext, | |
| 466 | 477 | pub username: String, |
| 467 | 478 | pub email: String, |
| 468 | 479 | pub initials: String, |
@@ -532,6 +543,7 @@ pub async fn get_trust_list(
| 532 | 543 | Extension(claims): Extension<Claims>, |
| 533 | 544 | Extension(i18n_service): Extension<Arc<I18nService>>, |
| 534 | 545 | Extension(locale): Extension<Locale>, |
| 546 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 535 | 547 | State(db): State<DatabaseConnection>, |
| 536 | 548 | session: Session, |
| 537 | 549 | axum::extract::Query(query): axum::extract::Query<SavedQuery>, |
@@ -598,6 +610,12 @@ pub async fn get_trust_list(
| 598 | 610 | app_version: base_context.app_version, |
| 599 | 611 | app_domain: base_context.app_domain, |
| 600 | 612 | app_description: base_context.app_description, |
| 613 | + analytics_opt_out: rustwarden_ui::analytics::AnalyticsOptOutContext::build( | |
| 614 | + &analytics, | |
| 615 | + &i18n_service, | |
| 616 | + locale.as_str(), | |
| 617 | + ) | |
| 618 | + .await, | |
| 601 | 619 | username: base_context.username, |
| 602 | 620 | email: base_context.email, |
| 603 | 621 | initials: base_context.initials, |
@@ -790,6 +808,8 @@ pub struct FediverseQueueTemplate {
| 790 | 808 | pub app_version: String, |
| 791 | 809 | pub app_domain: String, |
| 792 | 810 | pub app_description: String, |
| 811 | + /// Audience-measurement opt-out. Empty renders nothing. | |
| 812 | + pub analytics_opt_out: rustwarden_ui::analytics::AnalyticsOptOutContext, | |
| 793 | 813 | pub username: String, |
| 794 | 814 | pub email: String, |
| 795 | 815 | pub initials: String, |
@@ -854,6 +874,7 @@ pub async fn get_queue(
| 854 | 874 | Extension(claims): Extension<Claims>, |
| 855 | 875 | Extension(i18n_service): Extension<Arc<I18nService>>, |
| 856 | 876 | Extension(locale): Extension<Locale>, |
| 877 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 857 | 878 | State(db): State<DatabaseConnection>, |
| 858 | 879 | session: Session, |
| 859 | 880 | ) -> Result<axum::response::Response, AppError> { |
@@ -924,6 +945,12 @@ pub async fn get_queue(
| 924 | 945 | app_version: base_context.app_version, |
| 925 | 946 | app_domain: base_context.app_domain, |
| 926 | 947 | app_description: base_context.app_description, |
| 948 | + analytics_opt_out: rustwarden_ui::analytics::AnalyticsOptOutContext::build( | |
| 949 | + &analytics, | |
| 950 | + &i18n_service, | |
| 951 | + locale.as_str(), | |
| 952 | + ) | |
| 953 | + .await, | |
| 927 | 954 | username: base_context.username, |
| 928 | 955 | email: base_context.email, |
| 929 | 956 | initials: base_context.initials, |
@@ -1020,6 +1047,8 @@ pub struct FediverseSessionsRemoteTemplate {
| 1020 | 1047 | pub app_version: String, |
| 1021 | 1048 | pub app_domain: String, |
| 1022 | 1049 | pub app_description: String, |
| 1050 | + /// Audience-measurement opt-out. Empty renders nothing. | |
| 1051 | + pub analytics_opt_out: rustwarden_ui::analytics::AnalyticsOptOutContext, | |
| 1023 | 1052 | pub username: String, |
| 1024 | 1053 | pub email: String, |
| 1025 | 1054 | pub initials: String, |
@@ -1073,6 +1102,7 @@ pub async fn get_sessions_remote(
| 1073 | 1102 | Extension(claims): Extension<Claims>, |
| 1074 | 1103 | Extension(i18n_service): Extension<Arc<I18nService>>, |
| 1075 | 1104 | Extension(locale): Extension<Locale>, |
| 1105 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 1076 | 1106 | State(db): State<DatabaseConnection>, |
| 1077 | 1107 | session: Session, |
| 1078 | 1108 | axum::extract::Query(query): axum::extract::Query<SavedQuery>, |
@@ -1129,6 +1159,12 @@ pub async fn get_sessions_remote(
| 1129 | 1159 | app_version: base_context.app_version, |
| 1130 | 1160 | app_domain: base_context.app_domain, |
| 1131 | 1161 | app_description: base_context.app_description, |
| 1162 | + analytics_opt_out: rustwarden_ui::analytics::AnalyticsOptOutContext::build( | |
| 1163 | + &analytics, | |
| 1164 | + &i18n_service, | |
| 1165 | + locale.as_str(), | |
| 1166 | + ) | |
| 1167 | + .await, | |
| 1132 | 1168 | username: base_context.username, |
| 1133 | 1169 | email: base_context.email, |
| 1134 | 1170 | initials: base_context.initials, |
@@ -1269,6 +1305,8 @@ pub struct FediverseNodeInfoTemplate {
| 1269 | 1305 | pub app_version: String, |
| 1270 | 1306 | pub app_domain: String, |
| 1271 | 1307 | pub app_description: String, |
| 1308 | + /// Audience-measurement opt-out. Empty renders nothing. | |
| 1309 | + pub analytics_opt_out: rustwarden_ui::analytics::AnalyticsOptOutContext, | |
| 1272 | 1310 | pub username: String, |
| 1273 | 1311 | pub email: String, |
| 1274 | 1312 | pub initials: String, |
@@ -1333,6 +1371,7 @@ pub async fn get_nodeinfo(
| 1333 | 1371 | Extension(claims): Extension<Claims>, |
| 1334 | 1372 | Extension(i18n_service): Extension<Arc<I18nService>>, |
| 1335 | 1373 | Extension(locale): Extension<Locale>, |
| 1374 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 1336 | 1375 | State(db): State<DatabaseConnection>, |
| 1337 | 1376 | session: Session, |
| 1338 | 1377 | ) -> Result<axum::response::Response, AppError> { |
@@ -1405,6 +1444,12 @@ pub async fn get_nodeinfo(
| 1405 | 1444 | app_version: base_context.app_version, |
| 1406 | 1445 | app_domain: base_context.app_domain, |
| 1407 | 1446 | app_description: base_context.app_description, |
| 1447 | + analytics_opt_out: rustwarden_ui::analytics::AnalyticsOptOutContext::build( | |
| 1448 | + &analytics, | |
| 1449 | + &i18n_service, | |
| 1450 | + locale.as_str(), | |
| 1451 | + ) | |
| 1452 | + .await, | |
| 1408 | 1453 | username: base_context.username, |
| 1409 | 1454 | email: base_context.email, |
| 1410 | 1455 | initials: base_context.initials, |
@@ -1511,6 +1556,8 @@ pub struct FediverseTeamInvitesTemplate {
| 1511 | 1556 | pub app_version: String, |
| 1512 | 1557 | pub app_domain: String, |
| 1513 | 1558 | pub app_description: String, |
| 1559 | + /// Audience-measurement opt-out. Empty renders nothing. | |
| 1560 | + pub analytics_opt_out: rustwarden_ui::analytics::AnalyticsOptOutContext, | |
| 1514 | 1561 | pub username: String, |
| 1515 | 1562 | pub email: String, |
| 1516 | 1563 | pub initials: String, |
@@ -1566,10 +1613,12 @@ rustwarden_core::impl_template_response!(FediverseTeamInvitesTemplate);
| 1566 | 1613 | const INVITE_TTL: std::time::Duration = std::time::Duration::from_secs(7 * 24 * 60 * 60); |
| 1567 | 1614 | |
| 1568 | 1615 | /// `GET /admin/fediverse/invites/{team_id}` — list pending invites. |
| 1616 | +#[allow(clippy::too_many_arguments)] | |
| 1569 | 1617 | pub async fn get_team_invites( |
| 1570 | 1618 | Extension(claims): Extension<Claims>, |
| 1571 | 1619 | Extension(i18n_service): Extension<Arc<I18nService>>, |
| 1572 | 1620 | Extension(locale): Extension<Locale>, |
| 1621 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 1573 | 1622 | State(db): State<DatabaseConnection>, |
| 1574 | 1623 | session: Session, |
| 1575 | 1624 | axum::extract::Path(team_id): axum::extract::Path<uuid::Uuid>, |
@@ -1624,6 +1673,12 @@ pub async fn get_team_invites(
| 1624 | 1673 | app_version: base_context.app_version, |
| 1625 | 1674 | app_domain: base_context.app_domain, |
| 1626 | 1675 | app_description: base_context.app_description, |
| 1676 | + analytics_opt_out: rustwarden_ui::analytics::AnalyticsOptOutContext::build( | |
| 1677 | + &analytics, | |
| 1678 | + &i18n_service, | |
| 1679 | + locale.as_str(), | |
| 1680 | + ) | |
| 1681 | + .await, | |
| 1627 | 1682 | username: base_context.username, |
| 1628 | 1683 | email: base_context.email, |
| 1629 | 1684 | initials: base_context.initials, |
@@ -1767,6 +1822,8 @@ pub struct FediverseHubTemplate {
| 1767 | 1822 | pub app_version: String, |
| 1768 | 1823 | pub app_domain: String, |
| 1769 | 1824 | pub app_description: String, |
| 1825 | + /// Audience-measurement opt-out. Empty renders nothing. | |
| 1826 | + pub analytics_opt_out: rustwarden_ui::analytics::AnalyticsOptOutContext, | |
| 1770 | 1827 | pub username: String, |
| 1771 | 1828 | pub email: String, |
| 1772 | 1829 | pub initials: String, |
@@ -1799,6 +1856,7 @@ pub async fn get_hub(
| 1799 | 1856 | Extension(claims): Extension<Claims>, |
| 1800 | 1857 | Extension(i18n_service): Extension<Arc<I18nService>>, |
| 1801 | 1858 | Extension(locale): Extension<Locale>, |
| 1859 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 1802 | 1860 | State(db): State<DatabaseConnection>, |
| 1803 | 1861 | session: Session, |
| 1804 | 1862 | ) -> Result<impl IntoResponse, AppError> { |
@@ -1870,6 +1928,12 @@ pub async fn get_hub(
| 1870 | 1928 | app_version: base_context.app_version, |
| 1871 | 1929 | app_domain: base_context.app_domain, |
| 1872 | 1930 | app_description: base_context.app_description, |
| 1931 | + analytics_opt_out: rustwarden_ui::analytics::AnalyticsOptOutContext::build( | |
| 1932 | + &analytics, | |
| 1933 | + &i18n_service, | |
| 1934 | + locale.as_str(), | |
| 1935 | + ) | |
| 1936 | + .await, | |
| 1873 | 1937 | username: base_context.username, |
| 1874 | 1938 | email: base_context.email, |
| 1875 | 1939 | initials: base_context.initials, |
@@ -1918,6 +1982,8 @@ pub struct FediverseTeamsForInvitesTemplate {
| 1918 | 1982 | pub app_version: String, |
| 1919 | 1983 | pub app_domain: String, |
| 1920 | 1984 | pub app_description: String, |
| 1985 | + /// Audience-measurement opt-out. Empty renders nothing. | |
| 1986 | + pub analytics_opt_out: rustwarden_ui::analytics::AnalyticsOptOutContext, | |
| 1921 | 1987 | pub username: String, |
| 1922 | 1988 | pub email: String, |
| 1923 | 1989 | pub initials: String, |
@@ -1952,6 +2018,7 @@ pub async fn get_teams_for_invites(
| 1952 | 2018 | Extension(claims): Extension<Claims>, |
| 1953 | 2019 | Extension(i18n_service): Extension<Arc<I18nService>>, |
| 1954 | 2020 | Extension(locale): Extension<Locale>, |
| 2021 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 1955 | 2022 | State(db): State<DatabaseConnection>, |
| 1956 | 2023 | session: Session, |
| 1957 | 2024 | ) -> Result<axum::response::Response, AppError> { |
@@ -1993,6 +2060,12 @@ pub async fn get_teams_for_invites(
| 1993 | 2060 | app_version: base_context.app_version, |
| 1994 | 2061 | app_domain: base_context.app_domain, |
| 1995 | 2062 | app_description: base_context.app_description, |
| 2063 | + analytics_opt_out: rustwarden_ui::analytics::AnalyticsOptOutContext::build( | |
| 2064 | + &analytics, | |
| 2065 | + &i18n_service, | |
| 2066 | + locale.as_str(), | |
| 2067 | + ) | |
| 2068 | + .await, | |
| 1996 | 2069 | username: base_context.username, |
| 1997 | 2070 | email: base_context.email, |
| 1998 | 2071 | initials: base_context.initials, |
M
crates/rustwarden-fediverse-ui/templates/admin/sessions_remote.html
+4
-5
@@ -7,7 +7,7 @@
| 7 | 7 | {% endblock %} |
| 8 | 8 | |
| 9 | 9 | {% block content %} |
| 10 | -<div class="container mx-auto px-4 py-8" x-data="{ saved: {% if saved_flash %}true{% else %}false{% endif %}, revokeTarget: null, killDomain: null }"> | |
| 10 | +<div class="container mx-auto px-4 py-8" x-data="fediversePage" data-saved="{% if saved_flash %}true{% else %}false{% endif %}"> | |
| 11 | 11 | |
| 12 | 12 | <nav class="text-sm breadcrumbs mb-6" aria-label="breadcrumb"> |
| 13 | 13 | <ul> |
@@ -28,7 +28,6 @@
| 28 | 28 | {# Toast on save #} |
| 29 | 29 | <div |
| 30 | 30 | x-show="saved" |
| 31 | - x-init="$watch('saved', v => { if (v) setTimeout(() => saved = false, 4000) })" | |
| 32 | 31 | x-transition |
| 33 | 32 | role="status" |
| 34 | 33 | aria-live="polite" |
@@ -58,7 +57,7 @@
| 58 | 57 | type="button" |
| 59 | 58 | class="btn btn-error" |
| 60 | 59 | :disabled="!killDomain" |
| 61 | - @click="if (killDomain) { document.getElementById('kill-domain-modal').showModal() }" | |
| 60 | + @click="openKillModal()" | |
| 62 | 61 | > |
| 63 | 62 | {{ kill_switch_button }} |
| 64 | 63 | </button> |
@@ -128,7 +127,7 @@
| 128 | 127 | <h3 id="revoke-title" class="font-bold text-lg">{{ revoke_confirm_title }}</h3> |
| 129 | 128 | <p class="py-4"> |
| 130 | 129 | {{ revoke_confirm_body }} |
| 131 | - <span class="font-mono block mt-2" x-text="revokeTarget ? revokeTarget.split('|')[1] : ''"></span> | |
| 130 | + <span class="font-mono block mt-2" x-text="revokeHandle()"></span> | |
| 132 | 131 | </p> |
| 133 | 132 | <form method="POST" action="/admin/fediverse/sessions_remote/revoke" class="modal-action"> |
| 134 | 133 | <input type="hidden" name="csrf_token" value="{{ csrf_token }}"> |
@@ -153,7 +152,7 @@
| 153 | 152 | <form method="POST" action="/admin/fediverse/sessions_remote/kill_domain" class="modal-action"> |
| 154 | 153 | <input type="hidden" name="csrf_token" value="{{ csrf_token }}"> |
| 155 | 154 | <input type="hidden" name="domain" :value="killDomain"> |
| 156 | - <button type="button" class="btn btn-ghost" @click="document.getElementById('kill-domain-modal').close()">{{ modal_cancel }}</button> | |
| 155 | + <button type="button" class="btn btn-ghost" @click="closeKillModal()">{{ modal_cancel }}</button> | |
| 157 | 156 | <button type="submit" class="btn btn-error">{{ modal_confirm }}</button> |
| 158 | 157 | </form> |
| 159 | 158 | </div> |
M
crates/rustwarden-fediverse-ui/templates/admin/settings.html
+1
-2
@@ -7,7 +7,7 @@
| 7 | 7 | {% endblock %} |
| 8 | 8 | |
| 9 | 9 | {% block content %} |
| 10 | -<div class="container mx-auto px-4 py-8" x-data="{ saved: {% if saved_flash %}true{% else %}false{% endif %} }"> | |
| 10 | +<div class="container mx-auto px-4 py-8" x-data="fediversePage" data-saved="{% if saved_flash %}true{% else %}false{% endif %}"> | |
| 11 | 11 | |
| 12 | 12 | {# Breadcrumb (a11y P7: parent landmarks) #} |
| 13 | 13 | <nav class="text-sm breadcrumbs mb-6" aria-label="breadcrumb"> |
@@ -30,7 +30,6 @@
| 30 | 30 | {# Save toast (Plan UI §3 P3) — auto-dismisses after 4 s #} |
| 31 | 31 | <div |
| 32 | 32 | x-show="saved" |
| 33 | - x-init="$watch('saved', v => { if (v) setTimeout(() => saved = false, 4000) })" | |
| 34 | 33 | x-transition |
| 35 | 34 | role="status" |
| 36 | 35 | aria-live="polite" |
M
crates/rustwarden-fediverse-ui/templates/admin/team_invites.html
+2
-3
@@ -7,7 +7,7 @@
| 7 | 7 | {% endblock %} |
| 8 | 8 | |
| 9 | 9 | {% block content %} |
| 10 | -<div class="container mx-auto px-4 py-8" x-data="{ saved: {% if saved_flash %}true{% else %}false{% endif %}, revokeTarget: null }"> | |
| 10 | +<div class="container mx-auto px-4 py-8" x-data="fediversePage" data-saved="{% if saved_flash %}true{% else %}false{% endif %}"> | |
| 11 | 11 | |
| 12 | 12 | <nav class="text-sm breadcrumbs mb-6" aria-label="breadcrumb"> |
| 13 | 13 | <ul> |
@@ -27,7 +27,6 @@
| 27 | 27 | |
| 28 | 28 | <div |
| 29 | 29 | x-show="saved" |
| 30 | - x-init="$watch('saved', v => { if (v) setTimeout(() => saved = false, 4000) })" | |
| 31 | 30 | x-transition |
| 32 | 31 | role="status" |
| 33 | 32 | aria-live="polite" |
@@ -153,7 +152,7 @@
| 153 | 152 | <h3 id="revoke-title" class="font-bold text-lg">{{ revoke_confirm_title }}</h3> |
| 154 | 153 | <p class="py-4"> |
| 155 | 154 | {{ revoke_confirm_body }} |
| 156 | - <span class="font-mono block mt-2" x-text="revokeTarget ? revokeTarget.split('|')[1] : ''"></span> | |
| 155 | + <span class="font-mono block mt-2" x-text="revokeHandle()"></span> | |
| 157 | 156 | </p> |
| 158 | 157 | <form method="POST" action="/admin/fediverse/invites/{{ team_id }}/revoke" class="modal-action"> |
| 159 | 158 | <input type="hidden" name="csrf_token" value="{{ csrf_token }}"> |
M
crates/rustwarden-fediverse-ui/templates/admin/trust_list.html
+1
-2
@@ -7,7 +7,7 @@
| 7 | 7 | {% endblock %} |
| 8 | 8 | |
| 9 | 9 | {% block content %} |
| 10 | -<div class="container mx-auto px-4 py-8" x-data="{ saved: {% if saved_flash %}true{% else %}false{% endif %}, deleteTarget: null }"> | |
| 10 | +<div class="container mx-auto px-4 py-8" x-data="fediversePage" data-saved="{% if saved_flash %}true{% else %}false{% endif %}"> | |
| 11 | 11 | |
| 12 | 12 | {# Breadcrumb #} |
| 13 | 13 | <nav class="text-sm breadcrumbs mb-6" aria-label="breadcrumb"> |
@@ -37,7 +37,6 @@
| 37 | 37 | {# Toast on save (Plan UI §3 P3) #} |
| 38 | 38 | <div |
| 39 | 39 | x-show="saved" |
| 40 | - x-init="$watch('saved', v => { if (v) setTimeout(() => saved = false, 4000) })" | |
| 41 | 40 | x-transition |
| 42 | 41 | role="status" |
| 43 | 42 | aria-live="polite" |
M
crates/rustwarden-ui/locales/de.yml
+47
-15
@@ -4,9 +4,9 @@
| 4 | 4 | # Anmeldung |
| 5 | 5 | login: |
| 6 | 6 | title: "Anmeldung" |
| 7 | - page_title: "Anmeldung - RustCloud" | |
| 8 | - page_description: "Anmeldung bei RustCloud" | |
| 9 | - subtitle: "Melden Sie sich in Ihrem Cloud-Bereich an" | |
| 7 | + page_title: "Anmeldung" | |
| 8 | + page_description: "Anmeldung bei Ihrem Konto" | |
| 9 | + subtitle: "Melden Sie sich bei Ihrem Konto an" | |
| 10 | 10 | username: "Benutzername oder E-Mail" |
| 11 | 11 | password: "Passwort" |
| 12 | 12 | submit: "Anmelden" |
@@ -36,7 +36,21 @@ login:
| 36 | 36 | register_success: "Konto erfolgreich erstellt! Bitte überprüfen Sie Ihre E-Mail." |
| 37 | 37 | register_error: "Registrierungsfehler" |
| 38 | 38 | email_label: "E-Mail" |
| 39 | - footer_text: "Nextcloud Fork in Rust" | |
| 39 | + footer_text: "Konten und Authentifizierung" | |
| 40 | + | |
| 41 | +# Link zum Zurücksetzen anfordern | |
| 42 | +forgot_password: | |
| 43 | + page_title: "Passwort vergessen" | |
| 44 | + title: "Passwort vergessen" | |
| 45 | + subtitle: "Geben Sie Ihre E-Mail-Adresse ein. Wenn ein Konto existiert, wird ein Link zum Zurücksetzen gesendet." | |
| 46 | + email_label: "E-Mail-Adresse" | |
| 47 | + email_placeholder: "sie@beispiel.de" | |
| 48 | + submit: "Link senden" | |
| 49 | + back_to_login: "Zurück zur Anmeldung" | |
| 50 | + error_default: "Die Anfrage konnte nicht gesendet werden." | |
| 51 | + success_default: "Wenn ein Konto für diese Adresse existiert, wurde eine E-Mail zum Zurücksetzen gesendet." | |
| 52 | + error_missing_email: "Bitte geben Sie Ihre E-Mail ein." | |
| 53 | + error_cannot_send: "Die Anfrage kann derzeit nicht gesendet werden." | |
| 40 | 54 | |
| 41 | 55 | # Bestätigungs-E-Mail erneut senden |
| 42 | 56 | resend_verification: |
@@ -72,7 +86,7 @@ password_reset:
| 72 | 86 | |
| 73 | 87 | # 2FA-Anmeldeseite |
| 74 | 88 | login_2fa: |
| 75 | - page_title: "2FA-Überprüfung - RustCloud" | |
| 89 | + page_title: "2FA-Überprüfung" | |
| 76 | 90 | title: "Zwei-Schritt-Überprüfung" |
| 77 | 91 | subtitle: "Geben Sie den Code aus Ihrer Authenticator-App ein" |
| 78 | 92 | code_label: "Authentifizierungscode" |
@@ -147,7 +161,7 @@ home:
| 147 | 161 | # Präferenzen |
| 148 | 162 | preferences: |
| 149 | 163 | title: "Anzeigeeinstellungen" |
| 150 | - page_title: "Anzeigeeinstellungen - RustCloud" | |
| 164 | + page_title: "Anzeigeeinstellungen" | |
| 151 | 165 | subtitle: "Passen Sie das Erscheinungsbild der Benutzeroberfläche an" |
| 152 | 166 | theme: "Design" |
| 153 | 167 | theme_light: "Hell" |
@@ -181,7 +195,7 @@ preferences:
| 181 | 195 | error: "Fehler: {message}" |
| 182 | 196 | network_error: "Netzwerkfehler. Bitte versuchen Sie es erneut." |
| 183 | 197 | sync_title: "Ihre Einstellungen sind synchronisiert" |
| 184 | - sync_desc: "Ihre Anzeigeeinstellungen werden gespeichert und automatisch auf allen Geräten angewendet, die mit Ihrem RustCloud-Konto verbunden sind." | |
| 198 | + sync_desc: "Ihre Anzeigeeinstellungen werden gespeichert und automatisch auf allen Geräten angewendet, die mit Ihrem Konto verbunden sind." | |
| 185 | 199 | preview_dark: "Vorschau: Dunkles Design wird angewendet" |
| 186 | 200 | preview_light: "Vorschau: Helles Design wird angewendet" |
| 187 | 201 | preview_auto: "Vorschau: Automatisches Design wird angewendet" |
@@ -189,7 +203,7 @@ preferences:
| 189 | 203 | # Profil |
| 190 | 204 | profile: |
| 191 | 205 | title: "Mein Profil" |
| 192 | - page_title: "Mein Profil - RustCloud" | |
| 206 | + page_title: "Mein Profil" | |
| 193 | 207 | subtitle: "Bearbeiten Sie Ihre persönlichen Informationen" |
| 194 | 208 | account_info: "Kontoinformationen" |
| 195 | 209 | username: "Benutzername" |
@@ -204,8 +218,14 @@ profile:
| 204 | 218 | error: "Fehler beim Aktualisieren des Profils" |
| 205 | 219 | email_label: "E-Mail-Adresse" |
| 206 | 220 | email_used_for: "Wird für die Anmeldung verwendet" |
| 207 | - email_warning: "⚠️ Sie werden nach der Änderung abgemeldet" | |
| 208 | - email_change_info: "E-Mail-Änderungen erfordern aus Sicherheitsgründen eine erneute Anmeldung." | |
| 221 | + email_warning: "Die aktuelle Adresse wird hier nicht geändert." | |
| 222 | + email_change_info: "Ein Bestätigungslink geht an die neue Adresse. Die aktuelle Adresse erhält einen Hinweis. Das Konto ändert sich erst nach der Bestätigung." | |
| 223 | + email_change_title: "E-Mail-Adresse ändern" | |
| 224 | + new_email_label: "Neue Adresse" | |
| 225 | + current_password_label: "Aktuelles Passwort" | |
| 226 | + request_button: "Änderung anfordern" | |
| 227 | + email_change_error: "Die Anfrage konnte nicht gesendet werden." | |
| 228 | + network_error: "Netzwerkfehler. Bitte versuchen Sie es erneut." | |
| 209 | 229 | username_help: "3 bis 39 Zeichen (a-z, 0-9, -)" |
| 210 | 230 | additional_info: "Zusätzliche Informationen" |
| 211 | 231 | account_created: "Konto erstellt am" |
@@ -247,6 +267,9 @@ admin:
| 247 | 267 | audit_last_24h: "Letzte 24h" |
| 248 | 268 | audit_by_level: "Nach Stufe" |
| 249 | 269 | chart_label: "Logs nach Stufe" |
| 270 | + view_logs: "Logs ansehen" | |
| 271 | + logs_card_title: "Audit-Logs" | |
| 272 | + logs_card_desc: "Letzte aufgezeichnete Aktionen: E-Mail-Änderungen, blockierte Anmeldungen." | |
| 250 | 273 | roles_card_title: "Rollenverwaltung" |
| 251 | 274 | roles_card_desc: "Rollen und granulare Berechtigungen verwalten" |
| 252 | 275 | roles_stat_title: "Rollen" |
@@ -258,6 +281,14 @@ admin:
| 258 | 281 | orgs_card_desc: "Organisationen und deren Mitglieder erstellen und verwalten" |
| 259 | 282 | orgs_stat_title: "Organisationen" |
| 260 | 283 | orgs_members_stat_title: "Mitglieder gesamt" |
| 284 | + logs: | |
| 285 | + page_title: "Audit-Logs" | |
| 286 | + heading: "Audit-Logs" | |
| 287 | + empty: "Noch keine Einträge." | |
| 288 | + col_date: "Datum" | |
| 289 | + col_level: "Stufe" | |
| 290 | + col_action: "Aktion" | |
| 291 | + col_message: "Nachricht" | |
| 261 | 292 | roles_list: |
| 262 | 293 | page_title: "Rollenverwaltung" |
| 263 | 294 | breadcrumb_roles: "Rollen" |
@@ -367,6 +398,7 @@ admin:
| 367 | 398 | username_readonly_hint: "Der Benutzername kann nach der Erstellung nicht geändert werden" |
| 368 | 399 | email_label: "E-Mail" |
| 369 | 400 | email_placeholder: "benutzer@example.com" |
| 401 | + email_change_hint: "Das Ändern der Adresse schreibt sie sofort, markiert die E-Mail als bestätigt und beendet die Sitzungen dieses Benutzers." | |
| 370 | 402 | password_label: "Passwort" |
| 371 | 403 | password_optional: "Optional" |
| 372 | 404 | password_placeholder: "Leer lassen, um ein zufälliges Passwort zu generieren" |
@@ -380,8 +412,8 @@ admin:
| 380 | 412 | submit_edit: "Änderungen speichern" |
| 381 | 413 | settings: |
| 382 | 414 | title: "Anwendungseinstellungen" |
| 383 | - page_title: "Anwendungseinstellungen - Admin - RustCloud" | |
| 384 | - subtitle: "Globale RustCloud-Konfiguration" | |
| 415 | + page_title: "Anwendungseinstellungen - Admin" | |
| 416 | + subtitle: "Globale Anwendungskonfiguration" | |
| 385 | 417 | general: "Allgemein" |
| 386 | 418 | app_name: "Anwendungsname" |
| 387 | 419 | app_domain: "Domäne" |
@@ -468,7 +500,7 @@ admin:
| 468 | 500 | analytics_script_url_desc: 'Schreibgeschützt: Diese Adresse fließt in die Content Security Policy ein, die beim Start festgelegt wird. Konfiguration über ANALYTICS_SCRIPT_URL.' |
| 469 | 501 | analytics_not_configured: 'Es ist kein Skript zur Reichweitenmessung konfiguriert. Setzen Sie ANALYTICS_SCRIPT_URL und starten Sie die Anwendung neu, um diesen Bereich zu aktivieren.' |
| 470 | 502 | users_list: |
| 471 | - page_title: "Benutzerverwaltung - Admin - RustCloud" | |
| 503 | + page_title: "Benutzerverwaltung - Admin" | |
| 472 | 504 | title: "Benutzerverwaltung" |
| 473 | 505 | breadcrumb_users: "Benutzer" |
| 474 | 506 | create_user: "Benutzer erstellen" |
@@ -523,7 +555,7 @@ admin:
| 523 | 555 | # Passwort ändern |
| 524 | 556 | password: |
| 525 | 557 | title: "Mein Passwort ändern" |
| 526 | - page_title: "Mein Passwort ändern - RustCloud" | |
| 558 | + page_title: "Mein Passwort ändern" | |
| 527 | 559 | subtitle: "Ändern Sie Ihr Passwort, um Ihr Konto zu sichern. Eine Bestätigungs-E-Mail wird gesendet, um die Änderung zu validieren." |
| 528 | 560 | subtitle_no_email: "Ändern Sie Ihr Passwort, um Ihr Konto zu sichern." |
| 529 | 561 | must_change_message: "⚠️ Sie müssen Ihr Passwort ändern, bevor Sie fortfahren können." |
@@ -572,7 +604,7 @@ password:
| 572 | 604 | # Sitzungen |
| 573 | 605 | sessions: |
| 574 | 606 | title: "Aktive Sitzungen" |
| 575 | - page_title: "Aktive Sitzungen - RustCloud" | |
| 607 | + page_title: "Aktive Sitzungen" | |
| 576 | 608 | subtitle: "Verwalten Sie Geräte, die mit Ihrem Konto verbunden sind" |
| 577 | 609 | current_session: "Aktuelle Sitzung" |
| 578 | 610 | current_badge: "Aktuell" |
M
crates/rustwarden-ui/locales/en.yml
+47
-15
@@ -4,9 +4,9 @@
| 4 | 4 | # Login |
| 5 | 5 | login: |
| 6 | 6 | title: "Login" |
| 7 | - page_title: "Login - RustCloud" | |
| 8 | - page_description: "Login to RustCloud" | |
| 9 | - subtitle: "Sign in to your cloud space" | |
| 7 | + page_title: "Login" | |
| 8 | + page_description: "Sign in to your account" | |
| 9 | + subtitle: "Sign in to your account" | |
| 10 | 10 | username: "Username or email" |
| 11 | 11 | password: "Password" |
| 12 | 12 | submit: "Sign in" |
@@ -36,7 +36,21 @@ login:
| 36 | 36 | register_success: "Account created successfully! Please check your email." |
| 37 | 37 | register_error: "Registration error" |
| 38 | 38 | email_label: "Email" |
| 39 | - footer_text: "Nextcloud fork in Rust" | |
| 39 | + footer_text: "Accounts and authentication" | |
| 40 | + | |
| 41 | +# Request a password-reset link | |
| 42 | +forgot_password: | |
| 43 | + page_title: "Forgot password" | |
| 44 | + title: "Forgot password" | |
| 45 | + subtitle: "Enter your email address. If an account exists, a reset link will be sent." | |
| 46 | + email_label: "Email address" | |
| 47 | + email_placeholder: "you@example.com" | |
| 48 | + submit: "Send reset link" | |
| 49 | + back_to_login: "Back to sign in" | |
| 50 | + error_default: "Unable to send the request." | |
| 51 | + success_default: "If an account exists for this address, a reset email has been sent." | |
| 52 | + error_missing_email: "Please enter your email." | |
| 53 | + error_cannot_send: "Unable to send the request right now." | |
| 40 | 54 | |
| 41 | 55 | # Resend verification email |
| 42 | 56 | resend_verification: |
@@ -72,7 +86,7 @@ password_reset:
| 72 | 86 | |
| 73 | 87 | # 2FA Login page |
| 74 | 88 | login_2fa: |
| 75 | - page_title: "2FA Verification - RustCloud" | |
| 89 | + page_title: "2FA Verification" | |
| 76 | 90 | title: "Two-Step Verification" |
| 77 | 91 | subtitle: "Enter the code from your authenticator app" |
| 78 | 92 | code_label: "Authentication code" |
@@ -147,7 +161,7 @@ home:
| 147 | 161 | # Preferences |
| 148 | 162 | preferences: |
| 149 | 163 | title: "Display Preferences" |
| 150 | - page_title: "Display Preferences - RustCloud" | |
| 164 | + page_title: "Display Preferences" | |
| 151 | 165 | subtitle: "Customize the interface appearance" |
| 152 | 166 | theme: "Theme" |
| 153 | 167 | theme_light: "Light" |
@@ -181,7 +195,7 @@ preferences:
| 181 | 195 | error: "Error: {message}" |
| 182 | 196 | network_error: "Network error. Please try again." |
| 183 | 197 | sync_title: "Your preferences are synchronized" |
| 184 | - sync_desc: "Your display preferences are saved and automatically apply to all devices connected to your RustCloud account." | |
| 198 | + sync_desc: "Your display preferences are saved and automatically apply to all devices connected to your account." | |
| 185 | 199 | preview_dark: "Preview: Dark theme will be applied" |
| 186 | 200 | preview_light: "Preview: Light theme will be applied" |
| 187 | 201 | preview_auto: "Preview: Automatic theme will be applied" |
@@ -189,7 +203,7 @@ preferences:
| 189 | 203 | # Profile |
| 190 | 204 | profile: |
| 191 | 205 | title: "My Profile" |
| 192 | - page_title: "My Profile - RustCloud" | |
| 206 | + page_title: "My Profile" | |
| 193 | 207 | subtitle: "Edit your personal information" |
| 194 | 208 | account_info: "Account Information" |
| 195 | 209 | username: "Username" |
@@ -204,8 +218,14 @@ profile:
| 204 | 218 | error: "Error updating profile" |
| 205 | 219 | email_label: "Email address" |
| 206 | 220 | email_used_for: "Used for login" |
| 207 | - email_warning: "⚠️ You will be logged out after modification" | |
| 208 | - email_change_info: "Email changes require reconnection for security reasons." | |
| 221 | + email_warning: "The current address is not changed here." | |
| 222 | + email_change_info: "A confirmation link is sent to the new address. The current address receives a notice. The account changes only after confirmation." | |
| 223 | + email_change_title: "Change email address" | |
| 224 | + new_email_label: "New address" | |
| 225 | + current_password_label: "Current password" | |
| 226 | + request_button: "Request change" | |
| 227 | + email_change_error: "Unable to send the request." | |
| 228 | + network_error: "Network error. Please try again." | |
| 209 | 229 | username_help: "3 to 39 characters (a-z, 0-9, -)" |
| 210 | 230 | additional_info: "Additional Information" |
| 211 | 231 | account_created: "Account created on" |
@@ -247,6 +267,9 @@ admin:
| 247 | 267 | audit_last_24h: "Last 24h" |
| 248 | 268 | audit_by_level: "By level" |
| 249 | 269 | chart_label: "Logs by level" |
| 270 | + view_logs: "View logs" | |
| 271 | + logs_card_title: "Audit logs" | |
| 272 | + logs_card_desc: "Latest recorded actions: email changes, blocked sign-ins." | |
| 250 | 273 | roles_card_title: "Roles Management" |
| 251 | 274 | roles_card_desc: "Manage roles and granular permissions" |
| 252 | 275 | roles_stat_title: "Roles" |
@@ -260,6 +283,14 @@ admin:
| 260 | 283 | orgs_card_desc: "Create and manage organisations and their members" |
| 261 | 284 | orgs_stat_title: "Organisations" |
| 262 | 285 | orgs_members_stat_title: "Total members" |
| 286 | + logs: | |
| 287 | + page_title: "Audit logs" | |
| 288 | + heading: "Audit logs" | |
| 289 | + empty: "No entries yet." | |
| 290 | + col_date: "Date" | |
| 291 | + col_level: "Level" | |
| 292 | + col_action: "Action" | |
| 293 | + col_message: "Message" | |
| 263 | 294 | roles_list: |
| 264 | 295 | page_title: "Roles Management" |
| 265 | 296 | breadcrumb_roles: "Roles" |
@@ -369,6 +400,7 @@ admin:
| 369 | 400 | username_readonly_hint: "Username cannot be changed after creation" |
| 370 | 401 | email_label: "Email" |
| 371 | 402 | email_placeholder: "user@example.com" |
| 403 | + email_change_hint: "Changing the address writes it immediately, marks the email as verified, and closes this user's sessions." | |
| 372 | 404 | password_label: "Password" |
| 373 | 405 | password_optional: "Optional" |
| 374 | 406 | password_placeholder: "Leave empty to generate a random password" |
@@ -382,8 +414,8 @@ admin:
| 382 | 414 | submit_edit: "Save changes" |
| 383 | 415 | settings: |
| 384 | 416 | title: "Application Settings" |
| 385 | - page_title: "Application Settings - Admin - RustCloud" | |
| 386 | - subtitle: "Global RustCloud configuration" | |
| 417 | + page_title: "Application Settings - Admin" | |
| 418 | + subtitle: "Global application configuration" | |
| 387 | 419 | general: "General" |
| 388 | 420 | app_name: "Application name" |
| 389 | 421 | app_domain: "Domain" |
@@ -470,7 +502,7 @@ admin:
| 470 | 502 | analytics_script_url_desc: 'Read-only: this address feeds the Content Security Policy, which is frozen at startup. Configure it through ANALYTICS_SCRIPT_URL.' |
| 471 | 503 | analytics_not_configured: 'No audience-measurement script is configured. Set ANALYTICS_SCRIPT_URL and restart the application to enable this section.' |
| 472 | 504 | users_list: |
| 473 | - page_title: "User Management - Admin - RustCloud" | |
| 505 | + page_title: "User Management - Admin" | |
| 474 | 506 | title: "User Management" |
| 475 | 507 | breadcrumb_users: "Users" |
| 476 | 508 | create_user: "Create user" |
@@ -525,7 +557,7 @@ admin:
| 525 | 557 | # Password Change |
| 526 | 558 | password: |
| 527 | 559 | title: "Change my password" |
| 528 | - page_title: "Change my password - RustCloud" | |
| 560 | + page_title: "Change my password" | |
| 529 | 561 | subtitle: "Modify your password to secure your account. A confirmation email will be sent to validate the change." |
| 530 | 562 | subtitle_no_email: "Modify your password to secure your account." |
| 531 | 563 | must_change_message: "⚠️ You must change your password before continuing." |
@@ -581,7 +613,7 @@ password:
| 581 | 613 | # Sessions |
| 582 | 614 | sessions: |
| 583 | 615 | title: "Active sessions" |
| 584 | - page_title: "Active sessions - RustCloud" | |
| 616 | + page_title: "Active sessions" | |
| 585 | 617 | subtitle: "Manage devices connected to your account" |
| 586 | 618 | current_session: "Current session" |
| 587 | 619 | current_badge: "Current" |
M
crates/rustwarden-ui/locales/es.yml
+47
-15
@@ -4,9 +4,9 @@
| 4 | 4 | # Inicio de sesión |
| 5 | 5 | login: |
| 6 | 6 | title: "Iniciar sesión" |
| 7 | - page_title: "Iniciar sesión - RustCloud" | |
| 8 | - page_description: "Iniciar sesión en RustCloud" | |
| 9 | - subtitle: "Inicie sesión en su espacio en la nube" | |
| 7 | + page_title: "Iniciar sesión" | |
| 8 | + page_description: "Inicie sesión en su cuenta" | |
| 9 | + subtitle: "Inicie sesión en su cuenta" | |
| 10 | 10 | username: "Nombre de usuario o email" |
| 11 | 11 | password: "Contraseña" |
| 12 | 12 | submit: "Iniciar sesión" |
@@ -36,7 +36,21 @@ login:
| 36 | 36 | register_success: "¡Cuenta creada con éxito! Por favor verifique su email." |
| 37 | 37 | register_error: "Error en el registro" |
| 38 | 38 | email_label: "Correo electrónico" |
| 39 | - footer_text: "Fork de Nextcloud en Rust" | |
| 39 | + footer_text: "Cuentas y autenticación" | |
| 40 | + | |
| 41 | +# Solicitar enlace de restablecimiento | |
| 42 | +forgot_password: | |
| 43 | + page_title: "Contraseña olvidada" | |
| 44 | + title: "Contraseña olvidada" | |
| 45 | + subtitle: "Ingrese su correo electrónico. Si existe una cuenta, se enviará un enlace de restablecimiento." | |
| 46 | + email_label: "Dirección de correo electrónico" | |
| 47 | + email_placeholder: "usted@ejemplo.com" | |
| 48 | + submit: "Enviar el enlace" | |
| 49 | + back_to_login: "Volver al inicio de sesión" | |
| 50 | + error_default: "No se pudo enviar la solicitud." | |
| 51 | + success_default: "Si existe una cuenta para esta dirección, se ha enviado un email de restablecimiento." | |
| 52 | + error_missing_email: "Por favor ingrese su correo electrónico." | |
| 53 | + error_cannot_send: "No se puede enviar la solicitud en este momento." | |
| 40 | 54 | |
| 41 | 55 | # Reenviar email de verificación |
| 42 | 56 | resend_verification: |
@@ -72,7 +86,7 @@ password_reset:
| 72 | 86 | |
| 73 | 87 | # Página de inicio de sesión 2FA |
| 74 | 88 | login_2fa: |
| 75 | - page_title: "Verificación 2FA - RustCloud" | |
| 89 | + page_title: "Verificación 2FA" | |
| 76 | 90 | title: "Verificación en dos pasos" |
| 77 | 91 | subtitle: "Ingrese el código de su aplicación de autenticación" |
| 78 | 92 | code_label: "Código de autenticación" |
@@ -147,7 +161,7 @@ home:
| 147 | 161 | # Preferencias |
| 148 | 162 | preferences: |
| 149 | 163 | title: "Preferencias de visualización" |
| 150 | - page_title: "Preferencias de visualización - RustCloud" | |
| 164 | + page_title: "Preferencias de visualización" | |
| 151 | 165 | subtitle: "Personalice la apariencia de la interfaz" |
| 152 | 166 | theme: "Tema" |
| 153 | 167 | theme_light: "Claro" |
@@ -181,7 +195,7 @@ preferences:
| 181 | 195 | error: "Error: {message}" |
| 182 | 196 | network_error: "Error de red. Por favor intente nuevamente." |
| 183 | 197 | sync_title: "Sus preferencias están sincronizadas" |
| 184 | - sync_desc: "Sus preferencias de visualización se guardan y se aplican automáticamente en todos los dispositivos conectados a su cuenta RustCloud." | |
| 198 | + sync_desc: "Sus preferencias de visualización se guardan y se aplican automáticamente en todos los dispositivos conectados a su cuenta." | |
| 185 | 199 | preview_dark: "Vista previa: Se aplicará el tema oscuro" |
| 186 | 200 | preview_light: "Vista previa: Se aplicará el tema claro" |
| 187 | 201 | preview_auto: "Vista previa: Se aplicará el tema automático" |
@@ -189,7 +203,7 @@ preferences:
| 189 | 203 | # Perfil |
| 190 | 204 | profile: |
| 191 | 205 | title: "Mi Perfil" |
| 192 | - page_title: "Mi Perfil - RustCloud" | |
| 206 | + page_title: "Mi Perfil" | |
| 193 | 207 | subtitle: "Edite su información personal" |
| 194 | 208 | account_info: "Información de la cuenta" |
| 195 | 209 | username: "Nombre de usuario" |
@@ -204,8 +218,14 @@ profile:
| 204 | 218 | error: "Error al actualizar el perfil" |
| 205 | 219 | email_label: "Dirección de correo electrónico" |
| 206 | 220 | email_used_for: "Se utiliza para iniciar sesión" |
| 207 | - email_warning: "⚠️ Será desconectado después de la modificación" | |
| 208 | - email_change_info: "Los cambios de correo electrónico requieren reconexión por razones de seguridad." | |
| 221 | + email_warning: "La dirección actual no se cambia aquí." | |
| 222 | + email_change_info: "Se envía un enlace de confirmación a la nueva dirección. La dirección actual recibe un aviso. La cuenta cambia solo después de la confirmación." | |
| 223 | + email_change_title: "Cambiar la dirección de email" | |
| 224 | + new_email_label: "Nueva dirección" | |
| 225 | + current_password_label: "Contraseña actual" | |
| 226 | + request_button: "Solicitar el cambio" | |
| 227 | + email_change_error: "No se pudo enviar la solicitud." | |
| 228 | + network_error: "Error de red. Inténtelo de nuevo." | |
| 209 | 229 | username_help: "De 3 a 39 caracteres (a-z, 0-9, -)" |
| 210 | 230 | additional_info: "Información adicional" |
| 211 | 231 | account_created: "Cuenta creada el" |
@@ -247,6 +267,9 @@ admin:
| 247 | 267 | audit_last_24h: "Últimas 24h" |
| 248 | 268 | audit_by_level: "Por nivel" |
| 249 | 269 | chart_label: "Logs por nivel" |
| 270 | + view_logs: "Ver los logs" | |
| 271 | + logs_card_title: "Logs de auditoría" | |
| 272 | + logs_card_desc: "Últimas acciones registradas: cambios de email, inicios de sesión bloqueados." | |
| 250 | 273 | roles_card_title: "Gestión de Roles" |
| 251 | 274 | roles_card_desc: "Gestionar roles y permisos granulares" |
| 252 | 275 | roles_stat_title: "Roles" |
@@ -258,6 +281,14 @@ admin:
| 258 | 281 | orgs_card_desc: "Crear y gestionar organizaciones y sus miembros" |
| 259 | 282 | orgs_stat_title: "Organizaciones" |
| 260 | 283 | orgs_members_stat_title: "Miembros totales" |
| 284 | + logs: | |
| 285 | + page_title: "Logs de auditoría" | |
| 286 | + heading: "Logs de auditoría" | |
| 287 | + empty: "Todavía no hay entradas." | |
| 288 | + col_date: "Fecha" | |
| 289 | + col_level: "Nivel" | |
| 290 | + col_action: "Acción" | |
| 291 | + col_message: "Mensaje" | |
| 261 | 292 | roles_list: |
| 262 | 293 | page_title: "Gestión de roles" |
| 263 | 294 | breadcrumb_roles: "Roles" |
@@ -367,6 +398,7 @@ admin:
| 367 | 398 | username_readonly_hint: "El nombre de usuario no se puede cambiar después de la creación" |
| 368 | 399 | email_label: "Email" |
| 369 | 400 | email_placeholder: "usuario@example.com" |
| 401 | + email_change_hint: "Cambiar la dirección la escribe de inmediato, marca el email como verificado y cierra las sesiones de este usuario." | |
| 370 | 402 | password_label: "Contraseña" |
| 371 | 403 | password_optional: "Opcional" |
| 372 | 404 | password_placeholder: "Dejar vacío para generar una contraseña aleatoria" |
@@ -380,8 +412,8 @@ admin:
| 380 | 412 | submit_edit: "Guardar cambios" |
| 381 | 413 | settings: |
| 382 | 414 | title: "Configuración de la aplicación" |
| 383 | - page_title: "Configuración de la aplicación - Admin - RustCloud" | |
| 384 | - subtitle: "Configuración global de RustCloud" | |
| 415 | + page_title: "Configuración de la aplicación - Admin" | |
| 416 | + subtitle: "Configuración global de la aplicación" | |
| 385 | 417 | general: "General" |
| 386 | 418 | app_name: "Nombre de la aplicación" |
| 387 | 419 | app_domain: "Dominio" |
@@ -468,7 +500,7 @@ admin:
| 468 | 500 | analytics_script_url_desc: 'Solo lectura: esta dirección alimenta la política de seguridad de contenido, fijada al arrancar. Configúrela mediante ANALYTICS_SCRIPT_URL.' |
| 469 | 501 | analytics_not_configured: 'No hay ningún script de medición de audiencia configurado. Defina ANALYTICS_SCRIPT_URL y reinicie la aplicación para activar esta sección.' |
| 470 | 502 | users_list: |
| 471 | - page_title: "Gestión de Usuarios - Admin - RustCloud" | |
| 503 | + page_title: "Gestión de Usuarios - Admin" | |
| 472 | 504 | title: "Gestión de Usuarios" |
| 473 | 505 | breadcrumb_users: "Usuarios" |
| 474 | 506 | create_user: "Crear usuario" |
@@ -523,7 +555,7 @@ admin:
| 523 | 555 | # Cambio de contraseña |
| 524 | 556 | password: |
| 525 | 557 | title: "Cambiar mi contraseña" |
| 526 | - page_title: "Cambiar mi contraseña - RustCloud" | |
| 558 | + page_title: "Cambiar mi contraseña" | |
| 527 | 559 | subtitle: "Modifique su contraseña para asegurar su cuenta. Se enviará un correo electrónico de confirmación para validar el cambio." |
| 528 | 560 | subtitle_no_email: "Modifique su contraseña para asegurar su cuenta." |
| 529 | 561 | must_change_message: "⚠️ Debe cambiar su contraseña antes de continuar." |
@@ -572,7 +604,7 @@ password:
| 572 | 604 | # Sesiones |
| 573 | 605 | sessions: |
| 574 | 606 | title: "Sesiones activas" |
| 575 | - page_title: "Sesiones activas - RustCloud" | |
| 607 | + page_title: "Sesiones activas" | |
| 576 | 608 | subtitle: "Gestione los dispositivos conectados a su cuenta" |
| 577 | 609 | current_session: "Sesión actual" |
| 578 | 610 | current_badge: "Actual" |
M
crates/rustwarden-ui/locales/fr.yml
+47
-15
@@ -4,9 +4,9 @@
| 4 | 4 | # Connexion |
| 5 | 5 | login: |
| 6 | 6 | title: "Connexion" |
| 7 | - page_title: "Connexion - RustCloud" | |
| 8 | - page_description: "Connexion à RustCloud" | |
| 9 | - subtitle: "Connectez-vous à votre espace cloud" | |
| 7 | + page_title: "Connexion" | |
| 8 | + page_description: "Connexion à votre compte" | |
| 9 | + subtitle: "Connectez-vous à votre compte" | |
| 10 | 10 | username: "Nom d'utilisateur ou email" |
| 11 | 11 | password: "Mot de passe" |
| 12 | 12 | submit: "Se connecter" |
@@ -36,7 +36,21 @@ login:
| 36 | 36 | register_success: "Compte créé avec succès ! Veuillez vérifier votre email." |
| 37 | 37 | register_error: "Erreur lors de l'inscription" |
| 38 | 38 | email_label: "Email" |
| 39 | - footer_text: "Fork de Nextcloud en Rust" | |
| 39 | + footer_text: "Comptes et authentification" | |
| 40 | + | |
| 41 | +# Demande de lien de réinitialisation | |
| 42 | +forgot_password: | |
| 43 | + page_title: "Mot de passe oublié" | |
| 44 | + title: "Mot de passe oublié" | |
| 45 | + subtitle: "Entrez votre adresse email. Si un compte existe, un lien de réinitialisation vous sera envoyé." | |
| 46 | + email_label: "Adresse email" | |
| 47 | + email_placeholder: "vous@example.com" | |
| 48 | + submit: "Envoyer le lien" | |
| 49 | + back_to_login: "Revenir à la connexion" | |
| 50 | + error_default: "Impossible d'envoyer la demande." | |
| 51 | + success_default: "Si un compte existe pour cette adresse, un email de réinitialisation a été envoyé." | |
| 52 | + error_missing_email: "Veuillez renseigner votre email." | |
| 53 | + error_cannot_send: "Impossible d'envoyer la demande pour l'instant." | |
| 40 | 54 | |
| 41 | 55 | # Renvoyer l'email de validation |
| 42 | 56 | resend_verification: |
@@ -72,7 +86,7 @@ password_reset:
| 72 | 86 | |
| 73 | 87 | # Page 2FA login |
| 74 | 88 | login_2fa: |
| 75 | - page_title: "Vérification 2FA - RustCloud" | |
| 89 | + page_title: "Vérification 2FA" | |
| 76 | 90 | title: "Vérification en deux étapes" |
| 77 | 91 | subtitle: "Entrez le code de votre application d'authentification" |
| 78 | 92 | code_label: "Code d'authentification" |
@@ -147,7 +161,7 @@ home:
| 147 | 161 | # Préférences |
| 148 | 162 | preferences: |
| 149 | 163 | title: "Préférences d'affichage" |
| 150 | - page_title: "Préférences d'affichage - RustCloud" | |
| 164 | + page_title: "Préférences d'affichage" | |
| 151 | 165 | subtitle: "Personnalisez l'apparence de l'interface" |
| 152 | 166 | theme: "Thème" |
| 153 | 167 | theme_light: "Clair" |
@@ -181,7 +195,7 @@ preferences:
| 181 | 195 | error: "Erreur : {message}" |
| 182 | 196 | network_error: "Erreur réseau. Veuillez réessayer." |
| 183 | 197 | sync_title: "Vos préférences sont synchronisées" |
| 184 | - sync_desc: "Vos préférences d'affichage sont enregistrées et s'appliquent automatiquement sur tous vos appareils connectés à votre compte RustCloud." | |
| 198 | + sync_desc: "Vos préférences d'affichage sont enregistrées et s'appliquent automatiquement sur tous vos appareils connectés à votre compte." | |
| 185 | 199 | preview_dark: "Aperçu : Thème sombre sera appliqué" |
| 186 | 200 | preview_light: "Aperçu : Thème clair sera appliqué" |
| 187 | 201 | preview_auto: "Aperçu : Thème automatique sera appliqué" |
@@ -189,7 +203,7 @@ preferences:
| 189 | 203 | # Profil |
| 190 | 204 | profile: |
| 191 | 205 | title: "Mon Profil" |
| 192 | - page_title: "Mon Profil - RustCloud" | |
| 206 | + page_title: "Mon Profil" | |
| 193 | 207 | subtitle: "Modifiez vos informations personnelles" |
| 194 | 208 | account_info: "Informations du compte" |
| 195 | 209 | username: "Nom d'utilisateur" |
@@ -204,8 +218,14 @@ profile:
| 204 | 218 | error: "Erreur lors de la mise à jour du profil" |
| 205 | 219 | email_label: "Adresse email" |
| 206 | 220 | email_used_for: "Utilisée pour la connexion" |
| 207 | - email_warning: "⚠️ Vous serez déconnecté après la modification" | |
| 208 | - email_change_info: "Les modifications d'email nécessitent une reconnexion pour des raisons de sécurité." | |
| 221 | + email_warning: "L'adresse actuelle ne se modifie pas ici." | |
| 222 | + email_change_info: "Un lien de confirmation est envoyé à la nouvelle adresse. L'adresse actuelle reçoit un avis. Le compte ne change qu'après confirmation." | |
| 223 | + email_change_title: "Changer l'adresse email" | |
| 224 | + new_email_label: "Nouvelle adresse" | |
| 225 | + current_password_label: "Mot de passe actuel" | |
| 226 | + request_button: "Demander le changement" | |
| 227 | + email_change_error: "Impossible d'envoyer la demande." | |
| 228 | + network_error: "Erreur réseau. Veuillez réessayer." | |
| 209 | 229 | username_help: "3 à 39 caractères (a-z, 0-9, -)" |
| 210 | 230 | additional_info: "Informations supplémentaires" |
| 211 | 231 | account_created: "Compte créé le" |
@@ -248,6 +268,9 @@ admin:
| 248 | 268 | audit_last_24h: "Dernières 24h" |
| 249 | 269 | audit_by_level: "Par niveau" |
| 250 | 270 | chart_label: "Logs par niveau" |
| 271 | + view_logs: "Voir les logs" | |
| 272 | + logs_card_title: "Logs d'audit" | |
| 273 | + logs_card_desc: "Dernières actions enregistrées : changements d'email, connexions bloquées." | |
| 251 | 274 | roles_card_title: "Gestion des Rôles" |
| 252 | 275 | roles_card_desc: "Gérer les rôles et les permissions granulaires" |
| 253 | 276 | roles_stat_title: "Rôles" |
@@ -261,6 +284,14 @@ admin:
| 261 | 284 | orgs_card_desc: "Créer et gérer les organisations et leurs membres" |
| 262 | 285 | orgs_stat_title: "Organisations" |
| 263 | 286 | orgs_members_stat_title: "Membres totaux" |
| 287 | + logs: | |
| 288 | + page_title: "Logs d'audit" | |
| 289 | + heading: "Logs d'audit" | |
| 290 | + empty: "Aucune entrée pour le moment." | |
| 291 | + col_date: "Date" | |
| 292 | + col_level: "Niveau" | |
| 293 | + col_action: "Action" | |
| 294 | + col_message: "Message" | |
| 264 | 295 | roles_list: |
| 265 | 296 | page_title: "Gestion des rôles" |
| 266 | 297 | breadcrumb_roles: "Rôles" |
@@ -370,6 +401,7 @@ admin:
| 370 | 401 | username_readonly_hint: "Le nom d'utilisateur ne peut pas être modifié après création" |
| 371 | 402 | email_label: "Email" |
| 372 | 403 | email_placeholder: "utilisateur@example.com" |
| 404 | + email_change_hint: "Changer l'adresse l'écrit tout de suite, marque l'email comme vérifié et ferme les sessions de cet utilisateur." | |
| 373 | 405 | password_label: "Mot de passe" |
| 374 | 406 | password_optional: "Optionnel" |
| 375 | 407 | password_placeholder: "Laisser vide pour générer un mot de passe aléatoire" |
@@ -383,8 +415,8 @@ admin:
| 383 | 415 | submit_edit: "Enregistrer les modifications" |
| 384 | 416 | settings: |
| 385 | 417 | title: "Paramètres de l'Application" |
| 386 | - page_title: "Paramètres de l'Application - Admin - RustCloud" | |
| 387 | - subtitle: "Configuration globale de RustCloud" | |
| 418 | + page_title: "Paramètres de l'Application - Admin" | |
| 419 | + subtitle: "Configuration globale de l'application" | |
| 388 | 420 | general: "Général" |
| 389 | 421 | app_name: "Nom de l'application" |
| 390 | 422 | app_domain: "Domaine" |
@@ -470,7 +502,7 @@ admin:
| 470 | 502 | analytics_script_url_desc: 'Lecture seule : cette adresse alimente la politique de sécurité du contenu, figée au démarrage. Elle se configure via ANALYTICS_SCRIPT_URL.' |
| 471 | 503 | analytics_not_configured: 'Aucun script de mesure d''audience n''est configuré. Définissez ANALYTICS_SCRIPT_URL puis redémarrez l''application pour activer cette section.' |
| 472 | 504 | users_list: |
| 473 | - page_title: "Gestion des Utilisateurs - Admin - RustCloud" | |
| 505 | + page_title: "Gestion des Utilisateurs - Admin" | |
| 474 | 506 | title: "Gestion des Utilisateurs" |
| 475 | 507 | breadcrumb_users: "Utilisateurs" |
| 476 | 508 | create_user: "Créer un utilisateur" |
@@ -526,7 +558,7 @@ admin:
| 526 | 558 | # Password Change |
| 527 | 559 | password: |
| 528 | 560 | title: "Changer mon mot de passe" |
| 529 | - page_title: "Changer mon mot de passe - RustCloud" | |
| 561 | + page_title: "Changer mon mot de passe" | |
| 530 | 562 | subtitle: "Modifiez votre mot de passe pour sécuriser votre compte. Un email de confirmation vous sera envoyé pour valider le changement." |
| 531 | 563 | subtitle_no_email: "Modifiez votre mot de passe pour sécuriser votre compte." |
| 532 | 564 | must_change_message: "⚠️ Vous devez changer votre mot de passe avant de continuer." |
@@ -582,7 +614,7 @@ password:
| 582 | 614 | # Sessions |
| 583 | 615 | sessions: |
| 584 | 616 | title: "Sessions actives" |
| 585 | - page_title: "Sessions actives - RustCloud" | |
| 617 | + page_title: "Sessions actives" | |
| 586 | 618 | subtitle: "Gérez les appareils connectés à votre compte" |
| 587 | 619 | current_session: "Session actuelle" |
| 588 | 620 | current_badge: "Actuelle" |
M
crates/rustwarden-ui/locales/it.yml
+47
-15
@@ -4,9 +4,9 @@
| 4 | 4 | # Accesso |
| 5 | 5 | login: |
| 6 | 6 | title: "Accesso" |
| 7 | - page_title: "Accesso - RustCloud" | |
| 8 | - page_description: "Accesso a RustCloud" | |
| 9 | - subtitle: "Accedi al tuo spazio cloud" | |
| 7 | + page_title: "Accesso" | |
| 8 | + page_description: "Accesso al tuo account" | |
| 9 | + subtitle: "Accedi al tuo account" | |
| 10 | 10 | username: "Nome utente o email" |
| 11 | 11 | password: "Password" |
| 12 | 12 | submit: "Accedi" |
@@ -36,7 +36,21 @@ login:
| 36 | 36 | register_success: "Account creato con successo! Controlla la tua email." |
| 37 | 37 | register_error: "Errore durante la registrazione" |
| 38 | 38 | email_label: "Email" |
| 39 | - footer_text: "Fork di Nextcloud in Rust" | |
| 39 | + footer_text: "Account e autenticazione" | |
| 40 | + | |
| 41 | +# Richiesta del link di reimpostazione | |
| 42 | +forgot_password: | |
| 43 | + page_title: "Password dimenticata" | |
| 44 | + title: "Password dimenticata" | |
| 45 | + subtitle: "Inserisci il tuo indirizzo email. Se esiste un account, verrà inviato un link di reimpostazione." | |
| 46 | + email_label: "Indirizzo email" | |
| 47 | + email_placeholder: "tu@example.com" | |
| 48 | + submit: "Invia il link" | |
| 49 | + back_to_login: "Torna all'accesso" | |
| 50 | + error_default: "Impossibile inviare la richiesta." | |
| 51 | + success_default: "Se esiste un account per questo indirizzo, è stata inviata un'email di reimpostazione." | |
| 52 | + error_missing_email: "Inserisci la tua email." | |
| 53 | + error_cannot_send: "Impossibile inviare la richiesta al momento." | |
| 40 | 54 | |
| 41 | 55 | # Reinvio email di verifica |
| 42 | 56 | resend_verification: |
@@ -72,7 +86,7 @@ password_reset:
| 72 | 86 | |
| 73 | 87 | # Pagina di accesso 2FA |
| 74 | 88 | login_2fa: |
| 75 | - page_title: "Verifica 2FA - RustCloud" | |
| 89 | + page_title: "Verifica 2FA" | |
| 76 | 90 | title: "Verifica in due passaggi" |
| 77 | 91 | subtitle: "Inserisci il codice dalla tua app di autenticazione" |
| 78 | 92 | code_label: "Codice di autenticazione" |
@@ -147,7 +161,7 @@ home:
| 147 | 161 | # Preferenze |
| 148 | 162 | preferences: |
| 149 | 163 | title: "Preferenze di visualizzazione" |
| 150 | - page_title: "Preferenze di visualizzazione - RustCloud" | |
| 164 | + page_title: "Preferenze di visualizzazione" | |
| 151 | 165 | subtitle: "Personalizza l'aspetto dell'interfaccia" |
| 152 | 166 | theme: "Tema" |
| 153 | 167 | theme_light: "Chiaro" |
@@ -181,7 +195,7 @@ preferences:
| 181 | 195 | error: "Errore: {message}" |
| 182 | 196 | network_error: "Errore di rete. Riprova." |
| 183 | 197 | sync_title: "Le tue preferenze sono sincronizzate" |
| 184 | - sync_desc: "Le tue preferenze di visualizzazione sono salvate e si applicano automaticamente su tutti i dispositivi connessi al tuo account RustCloud." | |
| 198 | + sync_desc: "Le tue preferenze di visualizzazione sono salvate e si applicano automaticamente su tutti i dispositivi connessi al tuo account." | |
| 185 | 199 | preview_dark: "Anteprima: Tema scuro verrà applicato" |
| 186 | 200 | preview_light: "Anteprima: Tema chiaro verrà applicato" |
| 187 | 201 | preview_auto: "Anteprima: Tema automatico verrà applicato" |
@@ -189,7 +203,7 @@ preferences:
| 189 | 203 | # Profilo |
| 190 | 204 | profile: |
| 191 | 205 | title: "Il mio Profilo" |
| 192 | - page_title: "Il mio Profilo - RustCloud" | |
| 206 | + page_title: "Il mio Profilo" | |
| 193 | 207 | subtitle: "Modifica le tue informazioni personali" |
| 194 | 208 | account_info: "Informazioni dell'account" |
| 195 | 209 | username: "Nome utente" |
@@ -204,8 +218,14 @@ profile:
| 204 | 218 | error: "Errore durante l'aggiornamento del profilo" |
| 205 | 219 | email_label: "Indirizzo email" |
| 206 | 220 | email_used_for: "Utilizzato per l'accesso" |
| 207 | - email_warning: "⚠️ Verrai disconnesso dopo la modifica" | |
| 208 | - email_change_info: "Le modifiche all'email richiedono una nuova connessione per motivi di sicurezza." | |
| 221 | + email_warning: "L'indirizzo attuale non si modifica qui." | |
| 222 | + email_change_info: "Un link di conferma viene inviato al nuovo indirizzo. L'indirizzo attuale riceve un avviso. L'account cambia solo dopo la conferma." | |
| 223 | + email_change_title: "Cambiare l'indirizzo email" | |
| 224 | + new_email_label: "Nuovo indirizzo" | |
| 225 | + current_password_label: "Password attuale" | |
| 226 | + request_button: "Richiedere la modifica" | |
| 227 | + email_change_error: "Impossibile inviare la richiesta." | |
| 228 | + network_error: "Errore di rete. Riprova." | |
| 209 | 229 | username_help: "Da 3 a 39 caratteri (a-z, 0-9, -)" |
| 210 | 230 | additional_info: "Informazioni aggiuntive" |
| 211 | 231 | account_created: "Account creato il" |
@@ -248,6 +268,9 @@ admin:
| 248 | 268 | audit_last_24h: "Ultime 24h" |
| 249 | 269 | audit_by_level: "Per livello" |
| 250 | 270 | chart_label: "Log per livello" |
| 271 | + view_logs: "Vedi i log" | |
| 272 | + logs_card_title: "Log di audit" | |
| 273 | + logs_card_desc: "Ultime azioni registrate: cambi di email, accessi bloccati." | |
| 251 | 274 | roles_card_title: "Gestione Ruoli" |
| 252 | 275 | roles_card_desc: "Gestisci i ruoli e i permessi granulari" |
| 253 | 276 | roles_stat_title: "Ruoli" |
@@ -259,6 +282,14 @@ admin:
| 259 | 282 | orgs_card_desc: "Crea e gestisci le organizzazioni e i loro membri" |
| 260 | 283 | orgs_stat_title: "Organizzazioni" |
| 261 | 284 | orgs_members_stat_title: "Membri totali" |
| 285 | + logs: | |
| 286 | + page_title: "Log di audit" | |
| 287 | + heading: "Log di audit" | |
| 288 | + empty: "Nessuna voce per ora." | |
| 289 | + col_date: "Data" | |
| 290 | + col_level: "Livello" | |
| 291 | + col_action: "Azione" | |
| 292 | + col_message: "Messaggio" | |
| 262 | 293 | roles_list: |
| 263 | 294 | page_title: "Gestione dei ruoli" |
| 264 | 295 | breadcrumb_roles: "Ruoli" |
@@ -368,6 +399,7 @@ admin:
| 368 | 399 | username_readonly_hint: "Il nome utente non può essere modificato dopo la creazione" |
| 369 | 400 | email_label: "Email" |
| 370 | 401 | email_placeholder: "utente@example.com" |
| 402 | + email_change_hint: "Cambiare l'indirizzo lo scrive subito, segna l'email come verificata e chiude le sessioni di questo utente." | |
| 371 | 403 | password_label: "Password" |
| 372 | 404 | password_optional: "Opzionale" |
| 373 | 405 | password_placeholder: "Lascia vuoto per generare una password casuale" |
@@ -381,8 +413,8 @@ admin:
| 381 | 413 | submit_edit: "Salva modifiche" |
| 382 | 414 | settings: |
| 383 | 415 | title: "Impostazioni dell'Applicazione" |
| 384 | - page_title: "Impostazioni dell'Applicazione - Admin - RustCloud" | |
| 385 | - subtitle: "Configurazione globale di RustCloud" | |
| 416 | + page_title: "Impostazioni dell'Applicazione - Admin" | |
| 417 | + subtitle: "Configurazione globale dell'applicazione" | |
| 386 | 418 | general: "Generale" |
| 387 | 419 | app_name: "Nome dell'applicazione" |
| 388 | 420 | app_domain: "Dominio" |
@@ -468,7 +500,7 @@ admin:
| 468 | 500 | analytics_script_url_desc: 'Sola lettura: questo indirizzo alimenta la Content Security Policy, fissata all''avvio. Si configura tramite ANALYTICS_SCRIPT_URL.' |
| 469 | 501 | analytics_not_configured: 'Nessuno script di misurazione del pubblico è configurato. Imposta ANALYTICS_SCRIPT_URL e riavvia l''applicazione per attivare questa sezione.' |
| 470 | 502 | users_list: |
| 471 | - page_title: "Gestione Utenti - Admin - RustCloud" | |
| 503 | + page_title: "Gestione Utenti - Admin" | |
| 472 | 504 | title: "Gestione Utenti" |
| 473 | 505 | breadcrumb_users: "Utenti" |
| 474 | 506 | create_user: "Crea un utente" |
@@ -524,7 +556,7 @@ admin:
| 524 | 556 | # Cambio password |
| 525 | 557 | password: |
| 526 | 558 | title: "Cambia la mia password" |
| 527 | - page_title: "Cambia la mia password - RustCloud" | |
| 559 | + page_title: "Cambia la mia password" | |
| 528 | 560 | subtitle: "Modifica la tua password per proteggere il tuo account. Un'email di conferma ti verrà inviata per validare la modifica." |
| 529 | 561 | subtitle_no_email: "Modifica la tua password per proteggere il tuo account." |
| 530 | 562 | must_change_message: "⚠️ Devi cambiare la tua password prima di continuare." |
@@ -580,7 +612,7 @@ password:
| 580 | 612 | # Sessioni |
| 581 | 613 | sessions: |
| 582 | 614 | title: "Sessioni attive" |
| 583 | - page_title: "Sessioni attive - RustCloud" | |
| 615 | + page_title: "Sessioni attive" | |
| 584 | 616 | subtitle: "Gestisci i dispositivi connessi al tuo account" |
| 585 | 617 | current_session: "Sessione attuale" |
| 586 | 618 | current_badge: "Attuale" |
M
crates/rustwarden-ui/locales/pt.yml
+47
-15
@@ -4,9 +4,9 @@
| 4 | 4 | # Acesso |
| 5 | 5 | login: |
| 6 | 6 | title: "Entrar" |
| 7 | - page_title: "Entrar - RustCloud" | |
| 8 | - page_description: "Acesso ao RustCloud" | |
| 9 | - subtitle: "Acesse seu espaço na nuvem" | |
| 7 | + page_title: "Entrar" | |
| 8 | + page_description: "Acesso à sua conta" | |
| 9 | + subtitle: "Acesse sua conta" | |
| 10 | 10 | username: "Nome de usuário ou email" |
| 11 | 11 | password: "Senha" |
| 12 | 12 | submit: "Entrar" |
@@ -36,7 +36,21 @@ login:
| 36 | 36 | register_success: "Conta criada com sucesso! Verifique seu email." |
| 37 | 37 | register_error: "Erro durante o registro" |
| 38 | 38 | email_label: "Email" |
| 39 | - footer_text: "Fork do Nextcloud em Rust" | |
| 39 | + footer_text: "Contas e autenticação" | |
| 40 | + | |
| 41 | +# Pedido de link de redefinição | |
| 42 | +forgot_password: | |
| 43 | + page_title: "Senha esquecida" | |
| 44 | + title: "Senha esquecida" | |
| 45 | + subtitle: "Insira seu endereço de email. Se existir uma conta, um link de redefinição será enviado." | |
| 46 | + email_label: "Endereço de email" | |
| 47 | + email_placeholder: "voce@exemplo.com" | |
| 48 | + submit: "Enviar o link" | |
| 49 | + back_to_login: "Voltar ao login" | |
| 50 | + error_default: "Não foi possível enviar a solicitação." | |
| 51 | + success_default: "Se existir uma conta para este endereço, um email de redefinição foi enviado." | |
| 52 | + error_missing_email: "Insira seu email." | |
| 53 | + error_cannot_send: "Não foi possível enviar a solicitação no momento." | |
| 40 | 54 | |
| 41 | 55 | # Reenvio de email de verificação |
| 42 | 56 | resend_verification: |
@@ -72,7 +86,7 @@ password_reset:
| 72 | 86 | |
| 73 | 87 | # Página de login 2FA |
| 74 | 88 | login_2fa: |
| 75 | - page_title: "Verificação 2FA - RustCloud" | |
| 89 | + page_title: "Verificação 2FA" | |
| 76 | 90 | title: "Verificação em duas etapas" |
| 77 | 91 | subtitle: "Insira o código do seu aplicativo de autenticação" |
| 78 | 92 | code_label: "Código de autenticação" |
@@ -147,7 +161,7 @@ home:
| 147 | 161 | # Preferências |
| 148 | 162 | preferences: |
| 149 | 163 | title: "Preferências de exibição" |
| 150 | - page_title: "Preferências de exibição - RustCloud" | |
| 164 | + page_title: "Preferências de exibição" | |
| 151 | 165 | subtitle: "Personalize a aparência da interface" |
| 152 | 166 | theme: "Tema" |
| 153 | 167 | theme_light: "Claro" |
@@ -181,7 +195,7 @@ preferences:
| 181 | 195 | error: "Erro: {message}" |
| 182 | 196 | network_error: "Erro de rede. Tente novamente." |
| 183 | 197 | sync_title: "Suas preferências estão sincronizadas" |
| 184 | - sync_desc: "Suas preferências de exibição estão salvas e se aplicam automaticamente em todos os dispositivos conectados à sua conta RustCloud." | |
| 198 | + sync_desc: "Suas preferências de exibição estão salvas e se aplicam automaticamente em todos os dispositivos conectados à sua conta." | |
| 185 | 199 | preview_dark: "Prévia: Tema escuro será aplicado" |
| 186 | 200 | preview_light: "Prévia: Tema claro será aplicado" |
| 187 | 201 | preview_auto: "Prévia: Tema automático será aplicado" |
@@ -189,7 +203,7 @@ preferences:
| 189 | 203 | # Perfil |
| 190 | 204 | profile: |
| 191 | 205 | title: "Meu Perfil" |
| 192 | - page_title: "Meu Perfil - RustCloud" | |
| 206 | + page_title: "Meu Perfil" | |
| 193 | 207 | subtitle: "Edite suas informações pessoais" |
| 194 | 208 | account_info: "Informações da conta" |
| 195 | 209 | username: "Nome de usuário" |
@@ -204,8 +218,14 @@ profile:
| 204 | 218 | error: "Erro ao atualizar o perfil" |
| 205 | 219 | email_label: "Endereço de email" |
| 206 | 220 | email_used_for: "Usado para login" |
| 207 | - email_warning: "⚠️ Você será desconectado após a alteração" | |
| 208 | - email_change_info: "Alterações no email requerem uma nova conexão por motivos de segurança." | |
| 221 | + email_warning: "O endereço atual não é alterado aqui." | |
| 222 | + email_change_info: "Um link de confirmação é enviado ao novo endereço. O endereço atual recebe um aviso. A conta só muda após a confirmação." | |
| 223 | + email_change_title: "Alterar o endereço de email" | |
| 224 | + new_email_label: "Novo endereço" | |
| 225 | + current_password_label: "Senha atual" | |
| 226 | + request_button: "Pedir a alteração" | |
| 227 | + email_change_error: "Não foi possível enviar a solicitação." | |
| 228 | + network_error: "Erro de rede. Tente novamente." | |
| 209 | 229 | username_help: "De 3 a 39 caracteres (a-z, 0-9, -)" |
| 210 | 230 | additional_info: "Informações adicionais" |
| 211 | 231 | account_created: "Conta criada em" |
@@ -248,6 +268,9 @@ admin:
| 248 | 268 | audit_last_24h: "Últimas 24h" |
| 249 | 269 | audit_by_level: "Por nível" |
| 250 | 270 | chart_label: "Logs por nível" |
| 271 | + view_logs: "Ver os logs" | |
| 272 | + logs_card_title: "Logs de auditoria" | |
| 273 | + logs_card_desc: "Últimas ações registradas: alterações de email, logins bloqueados." | |
| 251 | 274 | roles_card_title: "Gerenciamento de Funções" |
| 252 | 275 | roles_card_desc: "Gerencie as funções e permissões granulares" |
| 253 | 276 | roles_stat_title: "Funções" |
@@ -259,6 +282,14 @@ admin:
| 259 | 282 | orgs_card_desc: "Crie e gerencie organizações e seus membros" |
| 260 | 283 | orgs_stat_title: "Organizações" |
| 261 | 284 | orgs_members_stat_title: "Membros totais" |
| 285 | + logs: | |
| 286 | + page_title: "Logs de auditoria" | |
| 287 | + heading: "Logs de auditoria" | |
| 288 | + empty: "Nenhuma entrada por enquanto." | |
| 289 | + col_date: "Data" | |
| 290 | + col_level: "Nível" | |
| 291 | + col_action: "Ação" | |
| 292 | + col_message: "Mensagem" | |
| 262 | 293 | roles_list: |
| 263 | 294 | page_title: "Gerenciamento de funções" |
| 264 | 295 | breadcrumb_roles: "Funções" |
@@ -368,6 +399,7 @@ admin:
| 368 | 399 | username_readonly_hint: "O nome de usuário não pode ser alterado após a criação" |
| 369 | 400 | email_label: "Email" |
| 370 | 401 | email_placeholder: "usuario@exemplo.com" |
| 402 | + email_change_hint: "Alterar o endereço grava-o de imediato, marca o email como verificado e encerra as sessões deste usuário." | |
| 371 | 403 | password_label: "Senha" |
| 372 | 404 | password_optional: "Opcional" |
| 373 | 405 | password_placeholder: "Deixe em branco para gerar uma senha aleatória" |
@@ -381,8 +413,8 @@ admin:
| 381 | 413 | submit_edit: "Salvar alterações" |
| 382 | 414 | settings: |
| 383 | 415 | title: "Configurações da Aplicação" |
| 384 | - page_title: "Configurações da Aplicação - Admin - RustCloud" | |
| 385 | - subtitle: "Configuração global do RustCloud" | |
| 416 | + page_title: "Configurações da Aplicação - Admin" | |
| 417 | + subtitle: "Configuração global da aplicação" | |
| 386 | 418 | general: "Geral" |
| 387 | 419 | app_name: "Nome da aplicação" |
| 388 | 420 | app_domain: "Domínio" |
@@ -468,7 +500,7 @@ admin:
| 468 | 500 | analytics_script_url_desc: 'Apenas leitura: este endereço alimenta a Content Security Policy, fixada no arranque. Configure-o através de ANALYTICS_SCRIPT_URL.' |
| 469 | 501 | analytics_not_configured: 'Nenhum script de medição de audiência está configurado. Defina ANALYTICS_SCRIPT_URL e reinicie a aplicação para ativar esta secção.' |
| 470 | 502 | users_list: |
| 471 | - page_title: "Gerenciamento de Usuários - Admin - RustCloud" | |
| 503 | + page_title: "Gerenciamento de Usuários - Admin" | |
| 472 | 504 | title: "Gerenciamento de Usuários" |
| 473 | 505 | breadcrumb_users: "Usuários" |
| 474 | 506 | create_user: "Criar um usuário" |
@@ -524,7 +556,7 @@ admin:
| 524 | 556 | # Alteração de senha |
| 525 | 557 | password: |
| 526 | 558 | title: "Alterar minha senha" |
| 527 | - page_title: "Alterar minha senha - RustCloud" | |
| 559 | + page_title: "Alterar minha senha" | |
| 528 | 560 | subtitle: "Altere sua senha para proteger sua conta. Um email de confirmação será enviado para validar a alteração." |
| 529 | 561 | subtitle_no_email: "Altere sua senha para proteger sua conta." |
| 530 | 562 | must_change_message: "⚠️ Você deve alterar sua senha antes de continuar." |
@@ -580,7 +612,7 @@ password:
| 580 | 612 | # Sessões |
| 581 | 613 | sessions: |
| 582 | 614 | title: "Sessões ativas" |
| 583 | - page_title: "Sessões ativas - RustCloud" | |
| 615 | + page_title: "Sessões ativas" | |
| 584 | 616 | subtitle: "Gerencie os dispositivos conectados à sua conta" |
| 585 | 617 | current_session: "Sessão atual" |
| 586 | 618 | current_badge: "Atual" |
M
crates/rustwarden-ui/src/handlers/admin_handler.rs
+316
-12
@@ -23,7 +23,7 @@ use rustwarden_core::impl_template_response;
| 23 | 23 | use rustwarden_core::middleware::csrf; |
| 24 | 24 | use rustwarden_core::models::{role, user_role}; |
| 25 | 25 | use rustwarden_core::services::app_settings_service::AppSettingsService; |
| 26 | -use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; | |
| 26 | +use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect}; | |
| 27 | 27 | |
| 28 | 28 | #[derive(Debug, Clone)] |
| 29 | 29 | struct NavItem { |
@@ -75,7 +75,7 @@ async fn load_app_domain_and_description(
| 75 | 75 | let app_description = AppSettingsService::get_setting(db, "app_description") |
| 76 | 76 | .await? |
| 77 | 77 | .map(|s| s.value) |
| 78 | - .unwrap_or_else(|| "Secure password manager".to_string()); | |
| 78 | + .unwrap_or_else(|| "User accounts and authentication".to_string()); | |
| 79 | 79 | Ok((app_domain, app_description)) |
| 80 | 80 | } |
| 81 | 81 |
@@ -162,6 +162,7 @@ struct AdminDashboardTemplate {
| 162 | 162 | app_version: String, |
| 163 | 163 | app_domain: String, |
| 164 | 164 | app_description: String, |
| 165 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext, | |
| 165 | 166 | // User fields |
| 166 | 167 | username: String, |
| 167 | 168 | email: String, |
@@ -205,6 +206,9 @@ struct AdminDashboardTemplate {
| 205 | 206 | audit_last_24h: String, |
| 206 | 207 | audit_by_level: String, |
| 207 | 208 | chart_label: String, |
| 209 | + view_logs: String, | |
| 210 | + logs_card_title: String, | |
| 211 | + logs_card_desc: String, | |
| 208 | 212 | roles_card_title: String, |
| 209 | 213 | roles_card_desc: String, |
| 210 | 214 | roles_stat_title: String, |
@@ -241,6 +245,7 @@ pub async fn admin_dashboard(
| 241 | 245 | Extension(claims): Extension<Claims>, |
| 242 | 246 | Extension(i18n_service): Extension<Arc<I18nService>>, |
| 243 | 247 | Extension(locale): Extension<Locale>, |
| 248 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 244 | 249 | State(db): State<DatabaseConnection>, |
| 245 | 250 | extra_cards: Option<Extension<Vec<DashboardCardDTO>>>, |
| 246 | 251 | session: Session, |
@@ -279,6 +284,7 @@ pub async fn admin_dashboard(
| 279 | 284 | let total_roles = all_roles_list.len() as u64; |
| 280 | 285 | |
| 281 | 286 | let nav_items = build_admin_nav_items(&base_context.roles, &i18n_service, &locale, &db).await; |
| 287 | + let audit_stats = load_audit_stats(&db).await; | |
| 282 | 288 | |
| 283 | 289 | let template = AdminDashboardTemplate { |
| 284 | 290 | page_title: i18n_service |
@@ -289,6 +295,12 @@ pub async fn admin_dashboard(
| 289 | 295 | app_version: base_context.app_version, |
| 290 | 296 | app_domain, |
| 291 | 297 | app_description, |
| 298 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext::build( | |
| 299 | + &analytics, | |
| 300 | + &i18n_service, | |
| 301 | + locale.as_str(), | |
| 302 | + ) | |
| 303 | + .await, | |
| 292 | 304 | username: base_context.username, |
| 293 | 305 | email: base_context.email, |
| 294 | 306 | initials: base_context.initials, |
@@ -300,13 +312,13 @@ pub async fn admin_dashboard(
| 300 | 312 | active_sessions, |
| 301 | 313 | total_admins, |
| 302 | 314 | last_activity: "N/A".to_string(), |
| 303 | - has_audit_logs: false, | |
| 304 | - audit_total_logs: 0, | |
| 305 | - audit_logs_last_24h: 0, | |
| 306 | - audit_debug_count: 0, | |
| 307 | - audit_info_count: 0, | |
| 308 | - audit_warn_count: 0, | |
| 309 | - audit_error_count: 0, | |
| 315 | + has_audit_logs: audit_stats.has_rows, | |
| 316 | + audit_total_logs: audit_stats.total, | |
| 317 | + audit_logs_last_24h: audit_stats.last_24h, | |
| 318 | + audit_debug_count: audit_stats.debug, | |
| 319 | + audit_info_count: audit_stats.info, | |
| 320 | + audit_warn_count: audit_stats.warn, | |
| 321 | + audit_error_count: audit_stats.error, | |
| 310 | 322 | total_roles, |
| 311 | 323 | user_menu_profile: i18n_service |
| 312 | 324 | .translate("user_menu.profile", locale_str) |
@@ -360,6 +372,15 @@ pub async fn admin_dashboard(
| 360 | 372 | chart_label: i18n_service |
| 361 | 373 | .translate("admin.dashboard.chart_label", locale_str) |
| 362 | 374 | .await, |
| 375 | + view_logs: i18n_service | |
| 376 | + .translate("admin.dashboard.view_logs", locale_str) | |
| 377 | + .await, | |
| 378 | + logs_card_title: i18n_service | |
| 379 | + .translate("admin.dashboard.logs_card_title", locale_str) | |
| 380 | + .await, | |
| 381 | + logs_card_desc: i18n_service | |
| 382 | + .translate("admin.dashboard.logs_card_desc", locale_str) | |
| 383 | + .await, | |
| 363 | 384 | roles_card_title: i18n_service |
| 364 | 385 | .translate("admin.dashboard.roles_card_title", locale_str) |
| 365 | 386 | .await, |
@@ -399,6 +420,182 @@ pub async fn admin_dashboard(
| 399 | 420 | Ok(template) |
| 400 | 421 | } |
| 401 | 422 | |
| 423 | +struct AuditStats { | |
| 424 | + has_rows: bool, | |
| 425 | + total: u64, | |
| 426 | + last_24h: u64, | |
| 427 | + debug: u64, | |
| 428 | + info: u64, | |
| 429 | + warn: u64, | |
| 430 | + error: u64, | |
| 431 | +} | |
| 432 | + | |
| 433 | +/// Counts rows in `audit_logs`. A missing table leaves the dashboard section hidden. | |
| 434 | +async fn load_audit_stats(db: &DatabaseConnection) -> AuditStats { | |
| 435 | + use rustwarden_core::models::audit_log; | |
| 436 | + | |
| 437 | + let total = match audit_log::Entity::find().count(db).await { | |
| 438 | + Ok(n) => n, | |
| 439 | + Err(e) => { | |
| 440 | + tracing::warn!("audit_logs count failed: {e}"); | |
| 441 | + return AuditStats { | |
| 442 | + has_rows: false, | |
| 443 | + total: 0, | |
| 444 | + last_24h: 0, | |
| 445 | + debug: 0, | |
| 446 | + info: 0, | |
| 447 | + warn: 0, | |
| 448 | + error: 0, | |
| 449 | + }; | |
| 450 | + } | |
| 451 | + }; | |
| 452 | + let since = chrono::Utc::now() - chrono::Duration::hours(24); | |
| 453 | + let last_24h = audit_log::Entity::find() | |
| 454 | + .filter(audit_log::Column::CreatedAt.gte(since)) | |
| 455 | + .count(db) | |
| 456 | + .await | |
| 457 | + .unwrap_or(0); | |
| 458 | + let count_level = |level: &'static str| async move { | |
| 459 | + audit_log::Entity::find() | |
| 460 | + .filter(audit_log::Column::Level.eq(level)) | |
| 461 | + .count(db) | |
| 462 | + .await | |
| 463 | + .unwrap_or(0) | |
| 464 | + }; | |
| 465 | + AuditStats { | |
| 466 | + has_rows: total > 0, | |
| 467 | + total, | |
| 468 | + last_24h, | |
| 469 | + debug: count_level("DEBUG").await, | |
| 470 | + info: count_level("INFO").await, | |
| 471 | + warn: count_level("WARN").await, | |
| 472 | + error: count_level("ERROR").await, | |
| 473 | + } | |
| 474 | +} | |
| 475 | + | |
| 476 | +struct AuditLogRow { | |
| 477 | + created_at: String, | |
| 478 | + level: String, | |
| 479 | + action: String, | |
| 480 | + message: String, | |
| 481 | +} | |
| 482 | + | |
| 483 | +#[derive(Template)] | |
| 484 | +#[template(path = "admin/logs.html")] | |
| 485 | +struct AuditLogsTemplate { | |
| 486 | + page_title: String, | |
| 487 | + csrf_token: String, | |
| 488 | + app_name: String, | |
| 489 | + app_version: String, | |
| 490 | + app_domain: String, | |
| 491 | + app_description: String, | |
| 492 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext, | |
| 493 | + username: String, | |
| 494 | + email: String, | |
| 495 | + initials: String, | |
| 496 | + avatar_color: String, | |
| 497 | + roles_text: String, | |
| 498 | + nav_items: Vec<NavItem>, | |
| 499 | + has_pending_password_change: bool, | |
| 500 | + user_menu_profile: String, | |
| 501 | + user_menu_logout: String, | |
| 502 | + breadcrumb_home: String, | |
| 503 | + breadcrumb_admin: String, | |
| 504 | + heading: String, | |
| 505 | + empty: String, | |
| 506 | + col_date: String, | |
| 507 | + col_level: String, | |
| 508 | + col_action: String, | |
| 509 | + col_message: String, | |
| 510 | + rows: Vec<AuditLogRow>, | |
| 511 | + has_rows: bool, | |
| 512 | +} | |
| 513 | + | |
| 514 | +rustwarden_core::impl_template_response!(AuditLogsTemplate); | |
| 515 | + | |
| 516 | +/// GET /admin/logs — latest audit_logs rows (PROTECTED - Admin only). | |
| 517 | +pub async fn admin_logs( | |
| 518 | + Extension(claims): Extension<Claims>, | |
| 519 | + Extension(i18n_service): Extension<Arc<I18nService>>, | |
| 520 | + Extension(locale): Extension<Locale>, | |
| 521 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 522 | + State(db): State<DatabaseConnection>, | |
| 523 | + session: Session, | |
| 524 | +) -> Result<impl IntoResponse, AppError> { | |
| 525 | + use rustwarden_core::models::audit_log; | |
| 526 | + | |
| 527 | + let locale_str = locale.as_str(); | |
| 528 | + let base_context = build_base_context_with_claims(&session, &db, &claims).await?; | |
| 529 | + let csrf_token = csrf::get_or_create_csrf_token(&session).await?; | |
| 530 | + let (app_domain, app_description) = load_app_domain_and_description(&db).await?; | |
| 531 | + let nav_items = build_admin_nav_items(&base_context.roles, &i18n_service, &locale, &db).await; | |
| 532 | + | |
| 533 | + let logs = audit_log::Entity::find() | |
| 534 | + .order_by_desc(audit_log::Column::CreatedAt) | |
| 535 | + .limit(100) | |
| 536 | + .all(&db) | |
| 537 | + .await | |
| 538 | + .unwrap_or_default(); | |
| 539 | + let rows: Vec<AuditLogRow> = logs | |
| 540 | + .into_iter() | |
| 541 | + .map(|log| AuditLogRow { | |
| 542 | + created_at: log.created_at.format("%Y-%m-%d %H:%M:%S UTC").to_string(), | |
| 543 | + level: log.level, | |
| 544 | + action: log.action, | |
| 545 | + message: log.message, | |
| 546 | + }) | |
| 547 | + .collect(); | |
| 548 | + let has_rows = !rows.is_empty(); | |
| 549 | + | |
| 550 | + Ok(AuditLogsTemplate { | |
| 551 | + page_title: i18n_service | |
| 552 | + .translate("admin.logs.page_title", locale_str) | |
| 553 | + .await, | |
| 554 | + csrf_token, | |
| 555 | + app_name: base_context.app_name, | |
| 556 | + app_version: base_context.app_version, | |
| 557 | + app_domain, | |
| 558 | + app_description, | |
| 559 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext::build( | |
| 560 | + &analytics, | |
| 561 | + &i18n_service, | |
| 562 | + locale.as_str(), | |
| 563 | + ) | |
| 564 | + .await, | |
| 565 | + username: base_context.username, | |
| 566 | + email: base_context.email, | |
| 567 | + initials: base_context.initials, | |
| 568 | + avatar_color: base_context.avatar_color, | |
| 569 | + roles_text: base_context.roles.join(", "), | |
| 570 | + nav_items, | |
| 571 | + has_pending_password_change: false, | |
| 572 | + user_menu_profile: i18n_service | |
| 573 | + .translate("user_menu.profile", locale_str) | |
| 574 | + .await, | |
| 575 | + user_menu_logout: i18n_service.translate("user_menu.logout", locale_str).await, | |
| 576 | + breadcrumb_home: i18n_service.translate("breadcrumb.home", locale_str).await, | |
| 577 | + breadcrumb_admin: i18n_service.translate("breadcrumb.admin", locale_str).await, | |
| 578 | + heading: i18n_service | |
| 579 | + .translate("admin.logs.heading", locale_str) | |
| 580 | + .await, | |
| 581 | + empty: i18n_service.translate("admin.logs.empty", locale_str).await, | |
| 582 | + col_date: i18n_service | |
| 583 | + .translate("admin.logs.col_date", locale_str) | |
| 584 | + .await, | |
| 585 | + col_level: i18n_service | |
| 586 | + .translate("admin.logs.col_level", locale_str) | |
| 587 | + .await, | |
| 588 | + col_action: i18n_service | |
| 589 | + .translate("admin.logs.col_action", locale_str) | |
| 590 | + .await, | |
| 591 | + col_message: i18n_service | |
| 592 | + .translate("admin.logs.col_message", locale_str) | |
| 593 | + .await, | |
| 594 | + rows, | |
| 595 | + has_rows, | |
| 596 | + }) | |
| 597 | +} | |
| 598 | + | |
| 402 | 599 | /// Query parameters for user list endpoint |
| 403 | 600 | #[derive(Debug, Deserialize)] |
| 404 | 601 | pub struct UserListQuery { |
@@ -453,6 +650,7 @@ struct UserListTemplate {
| 453 | 650 | app_version: String, |
| 454 | 651 | app_domain: String, |
| 455 | 652 | app_description: String, |
| 653 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext, | |
| 456 | 654 | username: String, |
| 457 | 655 | email: String, |
| 458 | 656 | initials: String, |
@@ -547,6 +745,7 @@ struct UserFormTemplate {
| 547 | 745 | app_version: String, |
| 548 | 746 | app_domain: String, |
| 549 | 747 | app_description: String, |
| 748 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext, | |
| 550 | 749 | username: String, |
| 551 | 750 | email: String, |
| 552 | 751 | initials: String, |
@@ -577,6 +776,7 @@ struct UserFormTemplate {
| 577 | 776 | username_readonly_hint: String, |
| 578 | 777 | email_label: String, |
| 579 | 778 | email_placeholder: String, |
| 779 | + email_change_hint: String, | |
| 580 | 780 | password_label: String, |
| 581 | 781 | password_optional: String, |
| 582 | 782 | password_placeholder: String, |
@@ -598,6 +798,7 @@ pub async fn list_users(
| 598 | 798 | Extension(claims): Extension<Claims>, |
| 599 | 799 | Extension(i18n_service): Extension<Arc<I18nService>>, |
| 600 | 800 | Extension(locale): Extension<Locale>, |
| 801 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 601 | 802 | State(db): State<DatabaseConnection>, |
| 602 | 803 | session: Session, |
| 603 | 804 | Query(params): Query<UserListQuery>, |
@@ -738,6 +939,12 @@ pub async fn list_users(
| 738 | 939 | app_version: base_context.app_version, |
| 739 | 940 | app_domain, |
| 740 | 941 | app_description, |
| 942 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext::build( | |
| 943 | + &analytics, | |
| 944 | + &i18n_service, | |
| 945 | + locale.as_str(), | |
| 946 | + ) | |
| 947 | + .await, | |
| 741 | 948 | username: base_context.username, |
| 742 | 949 | email: base_context.email, |
| 743 | 950 | initials: base_context.initials, |
@@ -929,6 +1136,7 @@ pub async fn create_user_form(
| 929 | 1136 | Extension(claims): Extension<Claims>, |
| 930 | 1137 | Extension(i18n_service): Extension<Arc<I18nService>>, |
| 931 | 1138 | Extension(locale): Extension<Locale>, |
| 1139 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 932 | 1140 | State(db): State<DatabaseConnection>, |
| 933 | 1141 | session: Session, |
| 934 | 1142 | ) -> Result<impl IntoResponse, AppError> { |
@@ -960,6 +1168,12 @@ pub async fn create_user_form(
| 960 | 1168 | app_version: base_context.app_version, |
| 961 | 1169 | app_domain, |
| 962 | 1170 | app_description, |
| 1171 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext::build( | |
| 1172 | + &analytics, | |
| 1173 | + &i18n_service, | |
| 1174 | + locale.as_str(), | |
| 1175 | + ) | |
| 1176 | + .await, | |
| 963 | 1177 | username: base_context.username, |
| 964 | 1178 | email: base_context.email, |
| 965 | 1179 | initials: base_context.initials, |
@@ -1009,6 +1223,9 @@ pub async fn create_user_form(
| 1009 | 1223 | email_placeholder: i18n_service |
| 1010 | 1224 | .translate("admin.user_form.email_placeholder", locale_str) |
| 1011 | 1225 | .await, |
| 1226 | + email_change_hint: i18n_service | |
| 1227 | + .translate("admin.user_form.email_change_hint", locale_str) | |
| 1228 | + .await, | |
| 1012 | 1229 | password_label: i18n_service |
| 1013 | 1230 | .translate("admin.user_form.password_label", locale_str) |
| 1014 | 1231 | .await, |
@@ -1122,6 +1339,7 @@ pub async fn edit_user_form(
| 1122 | 1339 | Extension(claims): Extension<Claims>, |
| 1123 | 1340 | Extension(i18n_service): Extension<Arc<I18nService>>, |
| 1124 | 1341 | Extension(locale): Extension<Locale>, |
| 1342 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 1125 | 1343 | State(db): State<DatabaseConnection>, |
| 1126 | 1344 | session: Session, |
| 1127 | 1345 | axum::extract::Path(user_id): axum::extract::Path<uuid::Uuid>, |
@@ -1173,6 +1391,12 @@ pub async fn edit_user_form(
| 1173 | 1391 | app_version: base_context.app_version, |
| 1174 | 1392 | app_domain, |
| 1175 | 1393 | app_description, |
| 1394 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext::build( | |
| 1395 | + &analytics, | |
| 1396 | + &i18n_service, | |
| 1397 | + locale.as_str(), | |
| 1398 | + ) | |
| 1399 | + .await, | |
| 1176 | 1400 | username: base_context.username, |
| 1177 | 1401 | email: base_context.email, |
| 1178 | 1402 | initials: base_context.initials, |
@@ -1222,6 +1446,9 @@ pub async fn edit_user_form(
| 1222 | 1446 | email_placeholder: i18n_service |
| 1223 | 1447 | .translate("admin.user_form.email_placeholder", locale_str) |
| 1224 | 1448 | .await, |
| 1449 | + email_change_hint: i18n_service | |
| 1450 | + .translate("admin.user_form.email_change_hint", locale_str) | |
| 1451 | + .await, | |
| 1225 | 1452 | password_label: i18n_service |
| 1226 | 1453 | .translate("admin.user_form.password_label", locale_str) |
| 1227 | 1454 | .await, |
@@ -1288,9 +1515,26 @@ pub async fn update_user(
| 1288 | 1515 | Some(form_data.email) |
| 1289 | 1516 | }; |
| 1290 | 1517 | |
| 1291 | - // Update email if provided | |
| 1518 | + // RFC 0041: an admin address write goes through admin_force_change. | |
| 1519 | + // update_profile refuses any real change. The same address is a no-op, | |
| 1520 | + // so an unchanged field must not require SMTP. | |
| 1521 | + // Changing one's own address revokes that account's sessions. | |
| 1522 | + let mut self_email_change = false; | |
| 1292 | 1523 | if let Some(ref new_email) = email { |
| 1293 | - UserService::update_profile(&db, user_id, None, Some(new_email.clone())).await?; | |
| 1524 | + let current = UserService::get_user_by_id(&db, user_id) | |
| 1525 | + .await? | |
| 1526 | + .ok_or_else(|| AppError::NotFound("User not found".to_string()))?; | |
| 1527 | + if new_email.trim() != current.email.as_str() { | |
| 1528 | + use rustwarden_core::config::email::EmailConfig; | |
| 1529 | + use rustwarden_core::services::email_change_service::EmailChangeService; | |
| 1530 | + | |
| 1531 | + let email_config = EmailConfig::from_env() | |
| 1532 | + .map_err(|e| AppError::Config(format!("Email configuration error: {e}")))?; | |
| 1533 | + EmailChangeService::new(email_config) | |
| 1534 | + .admin_force_change(&db, claims.sub, user_id, new_email, None) | |
| 1535 | + .await?; | |
| 1536 | + self_email_change = claims.sub == user_id; | |
| 1537 | + } | |
| 1294 | 1538 | } |
| 1295 | 1539 | |
| 1296 | 1540 | // Update must_change_password if provided |
@@ -1322,7 +1566,29 @@ pub async fn update_user(
| 1322 | 1566 | } |
| 1323 | 1567 | |
| 1324 | 1568 | tracing::info!("Admin {} updated user {}", claims.sub, user_id); |
| 1325 | - Ok(axum::response::Redirect::to("/admin/users")) | |
| 1569 | + | |
| 1570 | + // The edit form is an HTMX request targeting #form-messages. A 303 would | |
| 1571 | + // be followed and the next page (the user list, or the login page once | |
| 1572 | + // this account's sessions are revoked) would be inserted into that div. | |
| 1573 | + // HX-Redirect navigates the whole window instead. | |
| 1574 | + let (message, redirect) = if self_email_change { | |
| 1575 | + ( | |
| 1576 | + "Adresse mise à jour. Reconnectez-vous : les sessions de ce compte ont été fermées.", | |
| 1577 | + "/login", | |
| 1578 | + ) | |
| 1579 | + } else { | |
| 1580 | + ("Utilisateur mis à jour.", "/admin/users") | |
| 1581 | + }; | |
| 1582 | + let success_template = FormMessageTemplate { | |
| 1583 | + message_type: "success".to_string(), | |
| 1584 | + message: message.to_string(), | |
| 1585 | + }; | |
| 1586 | + let mut response = success_template.into_response(); | |
| 1587 | + response.headers_mut().insert( | |
| 1588 | + axum::http::HeaderName::from_static("hx-redirect"), | |
| 1589 | + axum::http::HeaderValue::from_static(redirect), | |
| 1590 | + ); | |
| 1591 | + Ok(response) | |
| 1326 | 1592 | } |
| 1327 | 1593 | |
| 1328 | 1594 | /// Handler for DELETE /admin/users/:id - Delete user (PROTECTED - Admin only) |
@@ -1479,6 +1745,7 @@ struct AdminSettingsTemplate {
| 1479 | 1745 | app_version: String, |
| 1480 | 1746 | app_domain: String, |
| 1481 | 1747 | app_description: String, |
| 1748 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext, | |
| 1482 | 1749 | username: String, |
| 1483 | 1750 | email: String, |
| 1484 | 1751 | initials: String, |
@@ -1722,6 +1989,12 @@ pub async fn get_app_settings(
| 1722 | 1989 | app_version: base_context.app_version, |
| 1723 | 1990 | app_domain, |
| 1724 | 1991 | app_description, |
| 1992 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext::build( | |
| 1993 | + &analytics, | |
| 1994 | + &i18n_service, | |
| 1995 | + locale.as_str(), | |
| 1996 | + ) | |
| 1997 | + .await, | |
| 1725 | 1998 | username: base_context.username, |
| 1726 | 1999 | email: base_context.email, |
| 1727 | 2000 | initials: base_context.initials, |
@@ -2357,6 +2630,7 @@ struct RolesListTemplate {
| 2357 | 2630 | app_version: String, |
| 2358 | 2631 | app_domain: String, |
| 2359 | 2632 | app_description: String, |
| 2633 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext, | |
| 2360 | 2634 | username: String, |
| 2361 | 2635 | email: String, |
| 2362 | 2636 | initials: String, |
@@ -2401,6 +2675,7 @@ struct RoleFormTemplate {
| 2401 | 2675 | app_version: String, |
| 2402 | 2676 | app_domain: String, |
| 2403 | 2677 | app_description: String, |
| 2678 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext, | |
| 2404 | 2679 | username: String, |
| 2405 | 2680 | email: String, |
| 2406 | 2681 | initials: String, |
@@ -2440,6 +2715,7 @@ pub async fn list_roles(
| 2440 | 2715 | Extension(claims): Extension<Claims>, |
| 2441 | 2716 | Extension(i18n_service): Extension<Arc<I18nService>>, |
| 2442 | 2717 | Extension(locale): Extension<Locale>, |
| 2718 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 2443 | 2719 | State(db): State<DatabaseConnection>, |
| 2444 | 2720 | session: Session, |
| 2445 | 2721 | ) -> Result<impl IntoResponse, AppError> { |
@@ -2479,6 +2755,12 @@ pub async fn list_roles(
| 2479 | 2755 | app_version: base_context.app_version, |
| 2480 | 2756 | app_domain, |
| 2481 | 2757 | app_description, |
| 2758 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext::build( | |
| 2759 | + &analytics, | |
| 2760 | + &i18n_service, | |
| 2761 | + locale.as_str(), | |
| 2762 | + ) | |
| 2763 | + .await, | |
| 2482 | 2764 | username: base_context.username, |
| 2483 | 2765 | email: base_context.email, |
| 2484 | 2766 | initials: base_context.initials, |
@@ -2580,6 +2862,7 @@ pub async fn create_role_form(
| 2580 | 2862 | Extension(claims): Extension<Claims>, |
| 2581 | 2863 | Extension(i18n_service): Extension<Arc<I18nService>>, |
| 2582 | 2864 | Extension(locale): Extension<Locale>, |
| 2865 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 2583 | 2866 | State(db): State<DatabaseConnection>, |
| 2584 | 2867 | session: Session, |
| 2585 | 2868 | ) -> Result<impl IntoResponse, AppError> { |
@@ -2600,6 +2883,12 @@ pub async fn create_role_form(
| 2600 | 2883 | app_version: base_context.app_version, |
| 2601 | 2884 | app_domain, |
| 2602 | 2885 | app_description, |
| 2886 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext::build( | |
| 2887 | + &analytics, | |
| 2888 | + &i18n_service, | |
| 2889 | + locale.as_str(), | |
| 2890 | + ) | |
| 2891 | + .await, | |
| 2603 | 2892 | username: base_context.username, |
| 2604 | 2893 | email: base_context.email, |
| 2605 | 2894 | initials: base_context.initials, |
@@ -2730,6 +3019,7 @@ pub async fn edit_role_form(
| 2730 | 3019 | Extension(claims): Extension<Claims>, |
| 2731 | 3020 | Extension(i18n_service): Extension<Arc<I18nService>>, |
| 2732 | 3021 | Extension(locale): Extension<Locale>, |
| 3022 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 2733 | 3023 | State(db): State<DatabaseConnection>, |
| 2734 | 3024 | session: Session, |
| 2735 | 3025 | axum::extract::Path(role_id): axum::extract::Path<uuid::Uuid>, |
@@ -2765,6 +3055,12 @@ pub async fn edit_role_form(
| 2765 | 3055 | app_version: base_context.app_version, |
| 2766 | 3056 | app_domain, |
| 2767 | 3057 | app_description, |
| 3058 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext::build( | |
| 3059 | + &analytics, | |
| 3060 | + &i18n_service, | |
| 3061 | + locale.as_str(), | |
| 3062 | + ) | |
| 3063 | + .await, | |
| 2768 | 3064 | username: base_context.username, |
| 2769 | 3065 | email: base_context.email, |
| 2770 | 3066 | initials: base_context.initials, |
@@ -2903,6 +3199,7 @@ struct AdminErrorsTemplate {
| 2903 | 3199 | app_version: String, |
| 2904 | 3200 | app_domain: String, |
| 2905 | 3201 | app_description: String, |
| 3202 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext, | |
| 2906 | 3203 | username: String, |
| 2907 | 3204 | email: String, |
| 2908 | 3205 | initials: String, |
@@ -2943,6 +3240,7 @@ pub async fn admin_errors(
| 2943 | 3240 | Extension(claims): Extension<Claims>, |
| 2944 | 3241 | Extension(i18n_service): Extension<Arc<I18nService>>, |
| 2945 | 3242 | Extension(locale): Extension<Locale>, |
| 3243 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 2946 | 3244 | session: Session, |
| 2947 | 3245 | State(db): State<DatabaseConnection>, |
| 2948 | 3246 | ) -> Result<impl IntoResponse, AppError> { |
@@ -3084,6 +3382,12 @@ pub async fn admin_errors(
| 3084 | 3382 | app_version: base_context.app_version, |
| 3085 | 3383 | app_domain, |
| 3086 | 3384 | app_description, |
| 3385 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext::build( | |
| 3386 | + &analytics, | |
| 3387 | + &i18n_service, | |
| 3388 | + locale.as_str(), | |
| 3389 | + ) | |
| 3390 | + .await, | |
| 3087 | 3391 | username: base_context.username, |
| 3088 | 3392 | email: base_context.email, |
| 3089 | 3393 | initials: base_context.initials, |
D
crates/rustwarden-ui/src/handlers/dashboard_handler.rs
+0
-34
@@ -1,34 +0,0 @@
| 1 | -//! Dashboard Handler | |
| 2 | -//! | |
| 3 | -//! Handles the dashboard widgets API endpoint. | |
| 4 | -//! Simplified for Rustwarden - no modular app system. | |
| 5 | - | |
| 6 | -use axum::{Json, response::IntoResponse}; | |
| 7 | -use serde::Serialize; | |
| 8 | - | |
| 9 | -/// DTO for a dashboard widget (API response) | |
| 10 | -#[derive(Debug, Clone, Serialize)] | |
| 11 | -pub struct DashboardWidgetDto { | |
| 12 | - /// Unique widget identifier. | |
| 13 | - pub id: String, | |
| 14 | - /// Identifier of the application that owns this widget. | |
| 15 | - pub app_id: String, | |
| 16 | - /// Display title shown in the widget header. | |
| 17 | - pub title: String, | |
| 18 | - /// Front-end component name used to render the widget. | |
| 19 | - pub component: String, | |
| 20 | - /// Display order relative to other widgets. | |
| 21 | - pub order: i32, | |
| 22 | - /// Size class of the widget (e.g. `"small"`, `"large"`). | |
| 23 | - pub size: String, | |
| 24 | -} | |
| 25 | - | |
| 26 | -/// List dashboard widgets for active apps. | |
| 27 | -/// | |
| 28 | -/// GET /api/v1/dashboard/widgets | |
| 29 | -/// | |
| 30 | -/// Returns empty list (no modular app system in Rustwarden). | |
| 31 | -pub async fn get_dashboard_widgets() -> impl IntoResponse { | |
| 32 | - let widgets: Vec<DashboardWidgetDto> = vec![]; | |
| 33 | - Json(widgets) | |
| 34 | -} |
D
crates/rustwarden-ui/src/handlers/email_handler.rs
+0
-275
@@ -1,275 +0,0 @@
| 1 | -//! Email handler for admin email queue management | |
| 2 | -//! | |
| 3 | -//! This module provides admin endpoints for managing the email queue: | |
| 4 | -//! - List emails (with pagination and filtering) | |
| 5 | -//! - Get email details | |
| 6 | -//! - Retry failed emails | |
| 7 | - | |
| 8 | -use crate::dto::{ | |
| 9 | - EmailDeliveryStatusListResponse, EmailDeliveryStatusResponse, EmailQueueDetailResponse, | |
| 10 | - EmailQueueEntry, EmailQueueListResponse, RetryEmailResponse, | |
| 11 | -}; | |
| 12 | -use axum::{ | |
| 13 | - Json, | |
| 14 | - extract::{Path, Query, State}, | |
| 15 | - http::StatusCode, | |
| 16 | - response::IntoResponse, | |
| 17 | -}; | |
| 18 | -use chrono::{DateTime, Utc}; | |
| 19 | -use rustwarden_core::error::AppError; | |
| 20 | -use rustwarden_core::services::email_queue_service::EmailQueueService; | |
| 21 | -use sea_orm::DatabaseConnection; | |
| 22 | -use serde::Deserialize; | |
| 23 | -use uuid::Uuid; | |
| 24 | - | |
| 25 | -/// Query parameters for listing emails | |
| 26 | -#[derive(Debug, Deserialize)] | |
| 27 | -pub struct ListEmailsQuery { | |
| 28 | - /// Status filter (pending, sent, failed) | |
| 29 | - pub status: Option<String>, | |
| 30 | - /// Email type filter | |
| 31 | - pub email_type: Option<String>, | |
| 32 | - /// Limit (default: 50) | |
| 33 | - pub limit: Option<usize>, | |
| 34 | - /// Offset (default: 0) | |
| 35 | - pub offset: Option<usize>, | |
| 36 | -} | |
| 37 | - | |
| 38 | -/// Query parameters for listing delivery statuses | |
| 39 | -#[derive(Debug, Deserialize)] | |
| 40 | -pub struct ListEmailStatusQuery { | |
| 41 | - /// Recipient filter | |
| 42 | - pub recipient: Option<String>, | |
| 43 | - /// Email type filter | |
| 44 | - pub email_type: Option<String>, | |
| 45 | - /// State filter | |
| 46 | - pub state: Option<String>, | |
| 47 | - /// Since filter (ISO8601) | |
| 48 | - pub since: Option<DateTime<Utc>>, | |
| 49 | - /// Limit (default: 50) | |
| 50 | - pub limit: Option<usize>, | |
| 51 | -} | |
| 52 | - | |
| 53 | -/// List emails from the queue (admin only) | |
| 54 | -/// | |
| 55 | -/// GET /api/v1/admin/emails | |
| 56 | -/// | |
| 57 | -/// # Query Parameters | |
| 58 | -/// | |
| 59 | -/// - `status`: Filter by status (pending, sent, failed) | |
| 60 | -/// - `email_type`: Filter by email type | |
| 61 | -/// - `limit`: Maximum number of results (default: 50) | |
| 62 | -/// - `offset`: Offset for pagination (default: 0) | |
| 63 | -/// | |
| 64 | -/// # Returns | |
| 65 | -/// | |
| 66 | -/// List of email queue entries with pagination info | |
| 67 | -pub async fn list_emails( | |
| 68 | - State(db): State<DatabaseConnection>, | |
| 69 | - Query(params): Query<ListEmailsQuery>, | |
| 70 | -) -> Result<impl IntoResponse, AppError> { | |
| 71 | - let limit = params.limit.unwrap_or(50).min(100); // Max 100 per page | |
| 72 | - let offset = params.offset.unwrap_or(0); | |
| 73 | - | |
| 74 | - // Get emails with filters applied at DB level | |
| 75 | - let emails = EmailQueueService::get_all( | |
| 76 | - &db, | |
| 77 | - params.status.as_deref(), | |
| 78 | - params.email_type.as_deref(), | |
| 79 | - limit, | |
| 80 | - offset, | |
| 81 | - ) | |
| 82 | - .await?; | |
| 83 | - | |
| 84 | - // Convert to DTOs | |
| 85 | - let email_entries: Vec<EmailQueueEntry> = emails | |
| 86 | - .into_iter() | |
| 87 | - .map(|e| EmailQueueEntry { | |
| 88 | - id: e.id, | |
| 89 | - to_email: e.to_email, | |
| 90 | - to_name: e.to_name, | |
| 91 | - subject: e.subject, | |
| 92 | - email_type: e.email_type, | |
| 93 | - status: e.status, | |
| 94 | - attempts: e.attempts, | |
| 95 | - smtp_code: e.smtp_code, | |
| 96 | - smtp_response: e.smtp_response, | |
| 97 | - created_at: e.created_at, | |
| 98 | - sent_at: e.sent_at, | |
| 99 | - }) | |
| 100 | - .collect(); | |
| 101 | - | |
| 102 | - // TODO: Get total count for pagination (requires separate query) | |
| 103 | - let total = email_entries.len() as u64; // Approximate - should query DB for exact count | |
| 104 | - | |
| 105 | - Ok(( | |
| 106 | - StatusCode::OK, | |
| 107 | - Json(EmailQueueListResponse { | |
| 108 | - emails: email_entries, | |
| 109 | - total, | |
| 110 | - limit, | |
| 111 | - offset, | |
| 112 | - }), | |
| 113 | - )) | |
| 114 | -} | |
| 115 | - | |
| 116 | -/// List email delivery statuses (admin) | |
| 117 | -/// | |
| 118 | -/// GET /api/v1/admin/emails/status | |
| 119 | -pub async fn list_email_status( | |
| 120 | - State(db): State<DatabaseConnection>, | |
| 121 | - Query(params): Query<ListEmailStatusQuery>, | |
| 122 | -) -> Result<impl IntoResponse, AppError> { | |
| 123 | - let limit = params.limit.unwrap_or(50).min(100); | |
| 124 | - | |
| 125 | - let items = EmailQueueService::list_delivery_statuses( | |
| 126 | - &db, | |
| 127 | - params.recipient.as_deref(), | |
| 128 | - params.email_type.as_deref(), | |
| 129 | - params.state.as_deref(), | |
| 130 | - params.since, | |
| 131 | - limit, | |
| 132 | - ) | |
| 133 | - .await?; | |
| 134 | - | |
| 135 | - let dto_items: Vec<EmailDeliveryStatusResponse> = items | |
| 136 | - .into_iter() | |
| 137 | - .map(|s| EmailDeliveryStatusResponse { | |
| 138 | - id: s.id, | |
| 139 | - email_type: s.email_type, | |
| 140 | - recipient: s.recipient, | |
| 141 | - template: s.template, | |
| 142 | - state: crate::dto::email_dto::EmailDeliveryState::parse(&s.state), | |
| 143 | - smtp_code: s.smtp_code, | |
| 144 | - smtp_response: s.smtp_response, | |
| 145 | - attempt_count: s.attempt_count, | |
| 146 | - last_attempt_at: s.last_attempt_at, | |
| 147 | - last_state_at: s.last_state_at, | |
| 148 | - correlation_id: s.correlation_id, | |
| 149 | - created_at: s.created_at, | |
| 150 | - updated_at: s.updated_at, | |
| 151 | - }) | |
| 152 | - .collect(); | |
| 153 | - | |
| 154 | - Ok(( | |
| 155 | - StatusCode::OK, | |
| 156 | - Json(EmailDeliveryStatusListResponse { | |
| 157 | - items: dto_items, | |
| 158 | - next_cursor: None, | |
| 159 | - }), | |
| 160 | - )) | |
| 161 | -} | |
| 162 | - | |
| 163 | -/// Get email delivery status by ID (admin) | |
| 164 | -/// | |
| 165 | -/// GET /api/v1/admin/emails/{id}/status | |
| 166 | -pub async fn get_email_status( | |
| 167 | - State(db): State<DatabaseConnection>, | |
| 168 | - Path(id): Path<Uuid>, | |
| 169 | -) -> Result<impl IntoResponse, AppError> { | |
| 170 | - let status = EmailQueueService::get_delivery_status_by_id(&db, id) | |
| 171 | - .await? | |
| 172 | - .ok_or_else(|| AppError::NotFound(format!("Email delivery status {} not found", id)))?; | |
| 173 | - | |
| 174 | - let dto = EmailDeliveryStatusResponse { | |
| 175 | - id: status.id, | |
| 176 | - email_type: status.email_type, | |
| 177 | - recipient: status.recipient, | |
| 178 | - template: status.template, | |
| 179 | - state: crate::dto::email_dto::EmailDeliveryState::parse(&status.state), | |
| 180 | - smtp_code: status.smtp_code, | |
| 181 | - smtp_response: status.smtp_response, | |
| 182 | - attempt_count: status.attempt_count, | |
| 183 | - last_attempt_at: status.last_attempt_at, | |
| 184 | - last_state_at: status.last_state_at, | |
| 185 | - correlation_id: status.correlation_id, | |
| 186 | - created_at: status.created_at, | |
| 187 | - updated_at: status.updated_at, | |
| 188 | - }; | |
| 189 | - | |
| 190 | - Ok((StatusCode::OK, Json(dto))) | |
| 191 | -} | |
| 192 | - | |
| 193 | -/// Get email details by ID (admin only) | |
| 194 | -/// | |
| 195 | -/// GET /api/v1/admin/emails/:id | |
| 196 | -/// | |
| 197 | -/// # Returns | |
| 198 | -/// | |
| 199 | -/// Email queue entry details | |
| 200 | -pub async fn get_email( | |
| 201 | - State(db): State<DatabaseConnection>, | |
| 202 | - Path(id): Path<Uuid>, | |
| 203 | -) -> Result<impl IntoResponse, AppError> { | |
| 204 | - let email = EmailQueueService::get_by_id(&db, id) | |
| 205 | - .await? | |
| 206 | - .ok_or_else(|| AppError::NotFound(format!("Email queue entry {} not found", id)))?; | |
| 207 | - | |
| 208 | - Ok(( | |
| 209 | - StatusCode::OK, | |
| 210 | - Json(EmailQueueDetailResponse { | |
| 211 | - id: email.id, | |
| 212 | - to_email: email.to_email, | |
| 213 | - to_name: email.to_name, | |
| 214 | - from_email: email.from_email, | |
| 215 | - from_name: email.from_name, | |
| 216 | - subject: email.subject, | |
| 217 | - body_html: email.body_html, | |
| 218 | - body_text: email.body_text, | |
| 219 | - email_type: email.email_type, | |
| 220 | - status: email.status, | |
| 221 | - attempts: email.attempts, | |
| 222 | - last_attempt_at: email.last_attempt_at, | |
| 223 | - sent_at: email.sent_at, | |
| 224 | - error_message: email.error_message, | |
| 225 | - smtp_code: email.smtp_code, | |
| 226 | - smtp_response: email.smtp_response, | |
| 227 | - created_at: email.created_at, | |
| 228 | - updated_at: email.updated_at, | |
| 229 | - }), | |
| 230 | - )) | |
| 231 | -} | |
| 232 | - | |
| 233 | -/// Retry a failed email (admin only) | |
| 234 | -/// | |
| 235 | -/// POST /api/v1/admin/emails/:id/retry | |
| 236 | -/// | |
| 237 | -/// # Returns | |
| 238 | -/// | |
| 239 | -/// Success message with email status | |
| 240 | -pub async fn retry_email( | |
| 241 | - State(db): State<DatabaseConnection>, | |
| 242 | - Path(id): Path<Uuid>, | |
| 243 | -) -> Result<impl IntoResponse, AppError> { | |
| 244 | - let email = EmailQueueService::get_by_id(&db, id) | |
| 245 | - .await? | |
| 246 | - .ok_or_else(|| AppError::NotFound(format!("Email queue entry {} not found", id)))?; | |
| 247 | - | |
| 248 | - // Only allow retrying failed emails | |
| 249 | - if email.status != "failed" { | |
| 250 | - return Err(AppError::Validation(format!( | |
| 251 | - "Cannot retry email with status '{}'. Only 'failed' emails can be retried.", | |
| 252 | - email.status | |
| 253 | - ))); | |
| 254 | - } | |
| 255 | - | |
| 256 | - // Reset status to pending and clear error message | |
| 257 | - use rustwarden_core::models::email_queue; | |
| 258 | - use sea_orm::{ActiveModelTrait, Set}; | |
| 259 | - | |
| 260 | - let mut email_active: email_queue::ActiveModel = email.into(); | |
| 261 | - email_active.status = Set("pending".to_string()); | |
| 262 | - email_active.error_message = Set(None); | |
| 263 | - email_active.updated_at = Set(chrono::Utc::now()); | |
| 264 | - | |
| 265 | - let updated_email = email_active.update(&db).await?; | |
| 266 | - | |
| 267 | - Ok(( | |
| 268 | - StatusCode::OK, | |
| 269 | - Json(RetryEmailResponse { | |
| 270 | - message: "Email queued for retry".to_string(), | |
| 271 | - email_id: updated_email.id, | |
| 272 | - status: updated_email.status, | |
| 273 | - }), | |
| 274 | - )) | |
| 275 | -} |
M
crates/rustwarden-ui/src/handlers/home_handler.rs
+9
-1
@@ -31,6 +31,7 @@ struct HomeTemplate {
| 31 | 31 | app_version: String, |
| 32 | 32 | app_domain: String, |
| 33 | 33 | app_description: String, |
| 34 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext, | |
| 34 | 35 | username: String, |
| 35 | 36 | email: String, |
| 36 | 37 | initials: String, |
@@ -70,6 +71,7 @@ pub async fn home(
| 70 | 71 | Extension(claims): Extension<Claims>, |
| 71 | 72 | Extension(i18n_service): Extension<Arc<I18nService>>, |
| 72 | 73 | Extension(locale): Extension<Locale>, |
| 74 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 73 | 75 | State(db): State<DatabaseConnection>, |
| 74 | 76 | ) -> Result<impl IntoResponse, AppError> { |
| 75 | 77 | let username = claims.username.clone(); |
@@ -148,7 +150,7 @@ pub async fn home(
| 148 | 150 | let app_description = AppSettingsService::get_setting(&db, "app_description") |
| 149 | 151 | .await? |
| 150 | 152 | .map(|s| s.value) |
| 151 | - .unwrap_or_else(|| "Secure password manager".to_string()); | |
| 153 | + .unwrap_or_else(|| "User accounts and authentication".to_string()); | |
| 152 | 154 | |
| 153 | 155 | let layout = rustwarden_core::templates::LayoutContext::new("", ""); |
| 154 | 156 |
@@ -159,6 +161,12 @@ pub async fn home(
| 159 | 161 | app_version: layout.app_version.clone(), |
| 160 | 162 | app_domain, |
| 161 | 163 | app_description, |
| 164 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext::build( | |
| 165 | + &analytics, | |
| 166 | + &i18n_service, | |
| 167 | + locale.as_str(), | |
| 168 | + ) | |
| 169 | + .await, | |
| 162 | 170 | username: user.username.clone(), |
| 163 | 171 | email: user.email.clone(), |
| 164 | 172 | initials: user.initials.clone(), |
M
crates/rustwarden-ui/src/handlers/login_handler.rs
+133
-2
@@ -68,6 +68,7 @@ struct ResendVerificationTemplate {
| 68 | 68 | app_version: String, |
| 69 | 69 | app_domain: String, |
| 70 | 70 | app_description: String, |
| 71 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext, | |
| 71 | 72 | page_title: String, |
| 72 | 73 | #[allow(dead_code)] // utilise par base.html (template parent) |
| 73 | 74 | csrf_token: String, |
@@ -91,6 +92,7 @@ struct ResetPasswordTemplate {
| 91 | 92 | app_version: String, |
| 92 | 93 | app_domain: String, |
| 93 | 94 | app_description: String, |
| 95 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext, | |
| 94 | 96 | page_title: String, |
| 95 | 97 | #[allow(dead_code)] // utilise par base.html (template parent) |
| 96 | 98 | csrf_token: String, |
@@ -116,6 +118,7 @@ struct ResetPasswordTemplate {
| 116 | 118 | |
| 117 | 119 | rustwarden_core::impl_template_response!(LoginTemplate); |
| 118 | 120 | rustwarden_core::impl_template_response!(ResendVerificationTemplate); |
| 121 | +rustwarden_core::impl_template_response!(ForgotPasswordTemplate); | |
| 119 | 122 | rustwarden_core::impl_template_response!(ResetPasswordTemplate); |
| 120 | 123 | |
| 121 | 124 | /// Handler for GET /login - Login page (PUBLIC) |
@@ -313,11 +316,126 @@ pub async fn login_page(
| 313 | 316 | Ok(template) |
| 314 | 317 | } |
| 315 | 318 | |
| 319 | +/// Page asking for an email so the API can send a password-reset link. | |
| 320 | +#[derive(Template)] | |
| 321 | +#[template(path = "forgot_password.html")] | |
| 322 | +struct ForgotPasswordTemplate { | |
| 323 | + app_name: String, | |
| 324 | + app_version: String, | |
| 325 | + app_domain: String, | |
| 326 | + app_description: String, | |
| 327 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext, | |
| 328 | + page_title: String, | |
| 329 | + #[allow(dead_code)] // used by the parent base.html | |
| 330 | + csrf_token: String, | |
| 331 | + title: String, | |
| 332 | + subtitle: String, | |
| 333 | + email_label: String, | |
| 334 | + email_placeholder: String, | |
| 335 | + submit: String, | |
| 336 | + back_to_login: String, | |
| 337 | + error_default: String, | |
| 338 | + success_default: String, | |
| 339 | + error_missing_email: String, | |
| 340 | + error_cannot_send: String, | |
| 341 | +} | |
| 342 | + | |
| 343 | +/// Handler for GET /forgot-password (PUBLIC). | |
| 344 | +/// | |
| 345 | +/// The form posts to `POST /api/v1/auth/forgot-password`, which always | |
| 346 | +/// answers 200 so the page does not reveal whether the address exists. | |
| 347 | +pub async fn forgot_password_page( | |
| 348 | + headers: HeaderMap, | |
| 349 | + State(db): State<DatabaseConnection>, | |
| 350 | + Extension(i18n_service): Extension<Arc<I18nService>>, | |
| 351 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 352 | +) -> Result<impl IntoResponse, rustwarden_core::error::AppError> { | |
| 353 | + let jar = CookieJar::from_headers(&headers); | |
| 354 | + let current_locale = jar | |
| 355 | + .get("locale") | |
| 356 | + .and_then(|c| { | |
| 357 | + let value = c.value(); | |
| 358 | + if matches!(value, "fr" | "en" | "de" | "es" | "it" | "pt") { | |
| 359 | + Some(value.to_string()) | |
| 360 | + } else { | |
| 361 | + None | |
| 362 | + } | |
| 363 | + }) | |
| 364 | + .unwrap_or_else(|| "fr".to_string()); | |
| 365 | + let locale_str = current_locale.as_str(); | |
| 366 | + | |
| 367 | + let layout = LayoutContext::new("", ""); | |
| 368 | + let app_domain = | |
| 369 | + rustwarden_core::services::app_settings_service::AppSettingsService::get_setting( | |
| 370 | + &db, | |
| 371 | + "app_domain", | |
| 372 | + ) | |
| 373 | + .await? | |
| 374 | + .map(|s| s.value) | |
| 375 | + .unwrap_or_else(|| "localhost".to_string()); | |
| 376 | + let app_description = | |
| 377 | + rustwarden_core::services::app_settings_service::AppSettingsService::get_setting( | |
| 378 | + &db, | |
| 379 | + "app_description", | |
| 380 | + ) | |
| 381 | + .await? | |
| 382 | + .map(|s| s.value) | |
| 383 | + .unwrap_or_else(|| "User accounts and authentication".to_string()); | |
| 384 | + | |
| 385 | + Ok(ForgotPasswordTemplate { | |
| 386 | + app_name: layout.app_name.clone(), | |
| 387 | + app_version: layout.app_version.clone(), | |
| 388 | + app_domain, | |
| 389 | + app_description, | |
| 390 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext::build( | |
| 391 | + &analytics, | |
| 392 | + &i18n_service, | |
| 393 | + locale_str, | |
| 394 | + ) | |
| 395 | + .await, | |
| 396 | + page_title: i18n_service | |
| 397 | + .translate("forgot_password.page_title", locale_str) | |
| 398 | + .await, | |
| 399 | + csrf_token: String::new(), | |
| 400 | + title: i18n_service | |
| 401 | + .translate("forgot_password.title", locale_str) | |
| 402 | + .await, | |
| 403 | + subtitle: i18n_service | |
| 404 | + .translate("forgot_password.subtitle", locale_str) | |
| 405 | + .await, | |
| 406 | + email_label: i18n_service | |
| 407 | + .translate("forgot_password.email_label", locale_str) | |
| 408 | + .await, | |
| 409 | + email_placeholder: i18n_service | |
| 410 | + .translate("forgot_password.email_placeholder", locale_str) | |
| 411 | + .await, | |
| 412 | + submit: i18n_service | |
| 413 | + .translate("forgot_password.submit", locale_str) | |
| 414 | + .await, | |
| 415 | + back_to_login: i18n_service | |
| 416 | + .translate("forgot_password.back_to_login", locale_str) | |
| 417 | + .await, | |
| 418 | + error_default: i18n_service | |
| 419 | + .translate("forgot_password.error_default", locale_str) | |
| 420 | + .await, | |
| 421 | + success_default: i18n_service | |
| 422 | + .translate("forgot_password.success_default", locale_str) | |
| 423 | + .await, | |
| 424 | + error_missing_email: i18n_service | |
| 425 | + .translate("forgot_password.error_missing_email", locale_str) | |
| 426 | + .await, | |
| 427 | + error_cannot_send: i18n_service | |
| 428 | + .translate("forgot_password.error_cannot_send", locale_str) | |
| 429 | + .await, | |
| 430 | + }) | |
| 431 | +} | |
| 432 | + | |
| 316 | 433 | /// Handler for GET /resend-verification - Resend email verification page (PUBLIC) |
| 317 | 434 | pub async fn resend_verification_page( |
| 318 | 435 | headers: HeaderMap, |
| 319 | 436 | State(db): State<DatabaseConnection>, |
| 320 | 437 | Extension(i18n_service): Extension<Arc<I18nService>>, |
| 438 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 321 | 439 | ) -> Result<impl IntoResponse, rustwarden_core::error::AppError> { |
| 322 | 440 | tracing::debug!("Rendering resend verification page"); |
| 323 | 441 |
@@ -379,13 +497,19 @@ pub async fn resend_verification_page(
| 379 | 497 | let app_description = AppSettingsService::get_setting(&db, "app_description") |
| 380 | 498 | .await? |
| 381 | 499 | .map(|s| s.value) |
| 382 | - .unwrap_or_else(|| "Secure password manager".to_string()); | |
| 500 | + .unwrap_or_else(|| "User accounts and authentication".to_string()); | |
| 383 | 501 | |
| 384 | 502 | let template = ResendVerificationTemplate { |
| 385 | 503 | app_name: layout.app_name.clone(), |
| 386 | 504 | app_version: layout.app_version.clone(), |
| 387 | 505 | app_domain, |
| 388 | 506 | app_description, |
| 507 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext::build( | |
| 508 | + &analytics, | |
| 509 | + &i18n_service, | |
| 510 | + current_locale.as_str(), | |
| 511 | + ) | |
| 512 | + .await, | |
| 389 | 513 | page_title, |
| 390 | 514 | csrf_token: String::new(), |
| 391 | 515 | title, |
@@ -415,6 +539,7 @@ pub async fn reset_password_page(
| 415 | 539 | headers: HeaderMap, |
| 416 | 540 | State(db): State<DatabaseConnection>, |
| 417 | 541 | Extension(i18n_service): Extension<Arc<I18nService>>, |
| 542 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 418 | 543 | axum::extract::Query(_params): axum::extract::Query<ResetPasswordQuery>, |
| 419 | 544 | ) -> Result<impl IntoResponse, rustwarden_core::error::AppError> { |
| 420 | 545 | tracing::debug!("Rendering reset password page"); |
@@ -499,13 +624,19 @@ pub async fn reset_password_page(
| 499 | 624 | let app_description = AppSettingsService::get_setting(&db, "app_description") |
| 500 | 625 | .await? |
| 501 | 626 | .map(|s| s.value) |
| 502 | - .unwrap_or_else(|| "Secure password manager".to_string()); | |
| 627 | + .unwrap_or_else(|| "User accounts and authentication".to_string()); | |
| 503 | 628 | |
| 504 | 629 | let template = ResetPasswordTemplate { |
| 505 | 630 | app_name: layout.app_name.clone(), |
| 506 | 631 | app_version: layout.app_version.clone(), |
| 507 | 632 | app_domain, |
| 508 | 633 | app_description, |
| 634 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext::build( | |
| 635 | + &analytics, | |
| 636 | + &i18n_service, | |
| 637 | + current_locale.as_str(), | |
| 638 | + ) | |
| 639 | + .await, | |
| 509 | 640 | page_title, |
| 510 | 641 | csrf_token: String::new(), |
| 511 | 642 | reset_password_title, |
M
crates/rustwarden-ui/src/handlers/mod.rs
+0
-6
@@ -4,10 +4,6 @@
| 4 | 4 | |
| 5 | 5 | /// Admin panel handlers (users, roles, settings). |
| 6 | 6 | pub mod admin_handler; |
| 7 | -/// Dashboard widget handlers. | |
| 8 | -pub mod dashboard_handler; | |
| 9 | -/// Email delivery status handlers. | |
| 10 | -pub mod email_handler; | |
| 11 | 7 | /// HTML 404 page for unmatched routes. |
| 12 | 8 | pub mod error_page; |
| 13 | 9 | /// Health-check endpoint handler. |
@@ -16,8 +12,6 @@ pub mod health;
| 16 | 12 | pub mod home_handler; |
| 17 | 13 | /// Login and logout handlers. |
| 18 | 14 | pub mod login_handler; |
| 19 | -/// Navigation menu handler. | |
| 20 | -pub mod navigation_handler; | |
| 21 | 15 | /// Root redirect handler. |
| 22 | 16 | pub mod root_handler; |
| 23 | 17 | /// Settings sub-handlers (profile, security, preferences). |
D
crates/rustwarden-ui/src/handlers/navigation_handler.rs
+0
-53
@@ -1,53 +0,0 @@
| 1 | -//! Navigation Handler | |
| 2 | -//! | |
| 3 | -//! Handles the navigation items API endpoint. | |
| 4 | -//! Simplified for Rustwarden - no modular app system. | |
| 5 | - | |
| 6 | -use axum::{Json, response::IntoResponse}; | |
| 7 | -use serde::Serialize; | |
| 8 | - | |
| 9 | -/// DTO for a navigation item (API response) | |
| 10 | -#[derive(Debug, Clone, Serialize)] | |
| 11 | -pub struct NavigationItemDto { | |
| 12 | - /// Unique identifier for this navigation item. | |
| 13 | - pub id: String, | |
| 14 | - /// Display label shown in the navigation menu. | |
| 15 | - pub name: String, | |
| 16 | - /// Icon class or name used by the front-end. | |
| 17 | - pub icon: String, | |
| 18 | - /// Target route path for this navigation item. | |
| 19 | - pub route: String, | |
| 20 | - /// Display order relative to sibling items. | |
| 21 | - pub order: i32, | |
| 22 | - /// Parent item identifier for nested menus. | |
| 23 | - #[serde(skip_serializing_if = "Option::is_none")] | |
| 24 | - pub parent_id: Option<String>, | |
| 25 | -} | |
| 26 | - | |
| 27 | -/// List navigation items for the current user. | |
| 28 | -/// | |
| 29 | -/// GET /api/v1/navigation | |
| 30 | -/// | |
| 31 | -/// Returns static navigation items (no modular app system in Rustwarden). | |
| 32 | -pub async fn get_navigation() -> impl IntoResponse { | |
| 33 | - let items = vec![ | |
| 34 | - NavigationItemDto { | |
| 35 | - id: "home".to_string(), | |
| 36 | - name: "Accueil".to_string(), | |
| 37 | - icon: "home".to_string(), | |
| 38 | - route: "/home".to_string(), | |
| 39 | - order: 0, | |
| 40 | - parent_id: None, | |
| 41 | - }, | |
| 42 | - NavigationItemDto { | |
| 43 | - id: "admin".to_string(), | |
| 44 | - name: "Administration".to_string(), | |
| 45 | - icon: "shield".to_string(), | |
| 46 | - route: "/admin".to_string(), | |
| 47 | - order: 100, | |
| 48 | - parent_id: None, | |
| 49 | - }, | |
| 50 | - ]; | |
| 51 | - | |
| 52 | - Json(items) | |
| 53 | -} |
M
crates/rustwarden-ui/src/handlers/settings/preferences.rs
+9
-1
@@ -124,6 +124,7 @@ struct PreferencesTemplate {
| 124 | 124 | app_version: String, |
| 125 | 125 | app_domain: String, |
| 126 | 126 | app_description: String, |
| 127 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext, | |
| 127 | 128 | username: String, |
| 128 | 129 | email: String, |
| 129 | 130 | roles_text: String, |
@@ -203,6 +204,7 @@ pub async fn get_preferences_page(
| 203 | 204 | Extension(claims): Extension<Claims>, |
| 204 | 205 | Extension(i18n_service): Extension<Arc<I18nService>>, |
| 205 | 206 | Extension(locale): Extension<Locale>, |
| 207 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 206 | 208 | State(db): State<DatabaseConnection>, |
| 207 | 209 | session: Session, |
| 208 | 210 | ) -> Result<impl IntoResponse, AppError> { |
@@ -269,7 +271,7 @@ pub async fn get_preferences_page(
| 269 | 271 | let app_description = AppSettingsService::get_setting(&db, "app_description") |
| 270 | 272 | .await? |
| 271 | 273 | .map(|s| s.value) |
| 272 | - .unwrap_or_else(|| "Secure password manager".to_string()); | |
| 274 | + .unwrap_or_else(|| "User accounts and authentication".to_string()); | |
| 273 | 275 | |
| 274 | 276 | let save_button = i18n_service.translate("preferences.save", locale_str).await; |
| 275 | 277 | let saved_success = i18n_service |
@@ -303,6 +305,12 @@ pub async fn get_preferences_page(
| 303 | 305 | app_version: layout.app_version.clone(), |
| 304 | 306 | app_domain, |
| 305 | 307 | app_description, |
| 308 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext::build( | |
| 309 | + &analytics, | |
| 310 | + &i18n_service, | |
| 311 | + locale.as_str(), | |
| 312 | + ) | |
| 313 | + .await, | |
| 306 | 314 | username: user.username.clone(), |
| 307 | 315 | email: user.email.clone(), |
| 308 | 316 | roles_text: user.roles.join(", "), |
M
crates/rustwarden-ui/src/handlers/settings/profile.rs
+42
-2
@@ -24,7 +24,9 @@ use rustwarden_core::services::user_service::UserService;
| 24 | 24 | pub struct UpdateProfileRequest { |
| 25 | 25 | /// New username, if the user wants to change it. |
| 26 | 26 | pub username: Option<String>, |
| 27 | - /// New email address, if the user wants to change it. | |
| 27 | + /// Ignored for writes. A different address is refused by `update_profile` | |
| 28 | + /// (RFC 0041). The profile page starts a change with | |
| 29 | + /// `POST /api/v1/auth/email-change` instead. | |
| 28 | 30 | pub email: Option<String>, |
| 29 | 31 | } |
| 30 | 32 | /// PUT /settings/profile — Update the authenticated user's profile. |
@@ -115,6 +117,7 @@ struct ProfileTemplate {
| 115 | 117 | app_version: String, |
| 116 | 118 | app_domain: String, |
| 117 | 119 | app_description: String, |
| 120 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext, | |
| 118 | 121 | username: String, |
| 119 | 122 | email: String, |
| 120 | 123 | user_id: String, |
@@ -142,6 +145,15 @@ struct ProfileTemplate {
| 142 | 145 | email_warning: String, |
| 143 | 146 | email_placeholder: String, |
| 144 | 147 | email_change_info: String, |
| 148 | + email_change_title: String, | |
| 149 | + new_email_label: String, | |
| 150 | + current_password_label: String, | |
| 151 | + request_button: String, | |
| 152 | + email_change_error: String, | |
| 153 | + profile_saved: String, | |
| 154 | + profile_error: String, | |
| 155 | + network_error: String, | |
| 156 | + saving: String, | |
| 145 | 157 | save_button: String, |
| 146 | 158 | cancel: String, |
| 147 | 159 | additional_info: String, |
@@ -159,6 +171,7 @@ pub async fn get_profile_page(
| 159 | 171 | Extension(claims): Extension<Claims>, |
| 160 | 172 | Extension(i18n_service): Extension<Arc<I18nService>>, |
| 161 | 173 | Extension(locale): Extension<Locale>, |
| 174 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 162 | 175 | State(db): State<DatabaseConnection>, |
| 163 | 176 | session: Session, |
| 164 | 177 | ) -> Result<impl IntoResponse, AppError> { |
@@ -186,7 +199,7 @@ pub async fn get_profile_page(
| 186 | 199 | let app_description = AppSettingsService::get_setting(&db, "app_description") |
| 187 | 200 | .await? |
| 188 | 201 | .map(|s| s.value) |
| 189 | - .unwrap_or_else(|| "Secure password manager".to_string()); | |
| 202 | + .unwrap_or_else(|| "User accounts and authentication".to_string()); | |
| 190 | 203 | |
| 191 | 204 | let template = ProfileTemplate { |
| 192 | 205 | page_title: layout.page_title.clone(), |
@@ -195,6 +208,12 @@ pub async fn get_profile_page(
| 195 | 208 | app_version: layout.app_version.clone(), |
| 196 | 209 | app_domain, |
| 197 | 210 | app_description, |
| 211 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext::build( | |
| 212 | + &analytics, | |
| 213 | + &i18n_service, | |
| 214 | + locale.as_str(), | |
| 215 | + ) | |
| 216 | + .await, | |
| 198 | 217 | username: user.username.clone(), |
| 199 | 218 | email: user.email.clone(), |
| 200 | 219 | user_id: user.id.to_string(), |
@@ -243,6 +262,27 @@ pub async fn get_profile_page(
| 243 | 262 | email_change_info: i18n_service |
| 244 | 263 | .translate("profile.email_change_info", locale_str) |
| 245 | 264 | .await, |
| 265 | + email_change_title: i18n_service | |
| 266 | + .translate("profile.email_change_title", locale_str) | |
| 267 | + .await, | |
| 268 | + new_email_label: i18n_service | |
| 269 | + .translate("profile.new_email_label", locale_str) | |
| 270 | + .await, | |
| 271 | + current_password_label: i18n_service | |
| 272 | + .translate("profile.current_password_label", locale_str) | |
| 273 | + .await, | |
| 274 | + request_button: i18n_service | |
| 275 | + .translate("profile.request_button", locale_str) | |
| 276 | + .await, | |
| 277 | + email_change_error: i18n_service | |
| 278 | + .translate("profile.email_change_error", locale_str) | |
| 279 | + .await, | |
| 280 | + profile_saved: i18n_service.translate("profile.saved", locale_str).await, | |
| 281 | + profile_error: i18n_service.translate("profile.error", locale_str).await, | |
| 282 | + network_error: i18n_service | |
| 283 | + .translate("profile.network_error", locale_str) | |
| 284 | + .await, | |
| 285 | + saving: i18n_service.translate("profile.saving", locale_str).await, | |
| 246 | 286 | save_button: i18n_service.translate("profile.save", locale_str).await, |
| 247 | 287 | cancel: i18n_service.translate("common.cancel", locale_str).await, |
| 248 | 288 | additional_info: i18n_service |
M
crates/rustwarden-ui/src/handlers/settings/security.rs
+18
-2
@@ -315,6 +315,7 @@ struct PasswordTemplate {
| 315 | 315 | app_version: String, |
| 316 | 316 | app_domain: String, |
| 317 | 317 | app_description: String, |
| 318 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext, | |
| 318 | 319 | username: String, |
| 319 | 320 | email: String, |
| 320 | 321 | roles_text: String, |
@@ -412,6 +413,7 @@ struct SessionsTemplate {
| 412 | 413 | app_version: String, |
| 413 | 414 | app_domain: String, |
| 414 | 415 | app_description: String, |
| 416 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext, | |
| 415 | 417 | username: String, |
| 416 | 418 | email: String, |
| 417 | 419 | roles_text: String, |
@@ -460,6 +462,7 @@ pub async fn get_password_page(
| 460 | 462 | Extension(claims): Extension<Claims>, |
| 461 | 463 | Extension(i18n_service): Extension<Arc<I18nService>>, |
| 462 | 464 | Extension(locale): Extension<Locale>, |
| 465 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 463 | 466 | State(db): State<DatabaseConnection>, |
| 464 | 467 | session: Session, |
| 465 | 468 | Query(query): Query<PasswordPageQuery>, |
@@ -526,7 +529,7 @@ pub async fn get_password_page(
| 526 | 529 | let app_description = AppSettingsService::get_setting(&db, "app_description") |
| 527 | 530 | .await? |
| 528 | 531 | .map(|s| s.value) |
| 529 | - .unwrap_or_else(|| "Secure password manager".to_string()); | |
| 532 | + .unwrap_or_else(|| "User accounts and authentication".to_string()); | |
| 530 | 533 | |
| 531 | 534 | let page_title = i18n_service |
| 532 | 535 | .translate("password.page_title", locale_str) |
@@ -539,6 +542,12 @@ pub async fn get_password_page(
| 539 | 542 | app_version: layout.app_version.clone(), |
| 540 | 543 | app_domain, |
| 541 | 544 | app_description, |
| 545 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext::build( | |
| 546 | + &analytics, | |
| 547 | + &i18n_service, | |
| 548 | + locale.as_str(), | |
| 549 | + ) | |
| 550 | + .await, | |
| 542 | 551 | username: user.username.clone(), |
| 543 | 552 | email: user.email.clone(), |
| 544 | 553 | roles_text: user.roles.join(", "), |
@@ -707,6 +716,7 @@ pub async fn get_sessions_page(
| 707 | 716 | Extension(claims): Extension<Claims>, |
| 708 | 717 | Extension(i18n_service): Extension<Arc<I18nService>>, |
| 709 | 718 | Extension(locale): Extension<Locale>, |
| 719 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 710 | 720 | State(db): State<DatabaseConnection>, |
| 711 | 721 | session: Session, |
| 712 | 722 | ) -> Result<impl IntoResponse, AppError> { |
@@ -764,7 +774,7 @@ pub async fn get_sessions_page(
| 764 | 774 | let app_description = AppSettingsService::get_setting(&db, "app_description") |
| 765 | 775 | .await? |
| 766 | 776 | .map(|s| s.value) |
| 767 | - .unwrap_or_else(|| "Secure password manager".to_string()); | |
| 777 | + .unwrap_or_else(|| "User accounts and authentication".to_string()); | |
| 768 | 778 | |
| 769 | 779 | let page_title = i18n_service |
| 770 | 780 | .translate("sessions.page_title", locale_str) |
@@ -777,6 +787,12 @@ pub async fn get_sessions_page(
| 777 | 787 | app_version: layout.app_version.clone(), |
| 778 | 788 | app_domain, |
| 779 | 789 | app_description, |
| 790 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext::build( | |
| 791 | + &analytics, | |
| 792 | + &i18n_service, | |
| 793 | + locale.as_str(), | |
| 794 | + ) | |
| 795 | + .await, | |
| 780 | 796 | username: user.username.clone(), |
| 781 | 797 | email: user.email.clone(), |
| 782 | 798 | roles_text: user.roles.join(", "), |
M
crates/rustwarden-ui/src/handlers/totp_handler.rs
+9
-1
@@ -72,6 +72,7 @@ struct SecurityTemplate {
| 72 | 72 | app_version: String, |
| 73 | 73 | app_domain: String, |
| 74 | 74 | app_description: String, |
| 75 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext, | |
| 75 | 76 | csrf_token: String, |
| 76 | 77 | username: String, |
| 77 | 78 | email: String, |
@@ -127,6 +128,7 @@ pub async fn security_page(
| 127 | 128 | State(db): State<DatabaseConnection>, |
| 128 | 129 | Extension(i18n_service): Extension<Arc<I18nService>>, |
| 129 | 130 | locale: Locale, |
| 131 | + analytics: rustwarden_core::config::analytics::AnalyticsContext, | |
| 130 | 132 | session: Session, |
| 131 | 133 | ) -> Result<impl IntoResponse, AppError> { |
| 132 | 134 | let user_model = UserService::get_user_by_username(&db, &claims.username) |
@@ -148,7 +150,7 @@ pub async fn security_page(
| 148 | 150 | let app_description = AppSettingsService::get_setting(&db, "app_description") |
| 149 | 151 | .await? |
| 150 | 152 | .map(|s| s.value) |
| 151 | - .unwrap_or_else(|| "Secure password manager".to_string()); | |
| 153 | + .unwrap_or_else(|| "User accounts and authentication".to_string()); | |
| 152 | 154 | |
| 153 | 155 | // Check TOTP status |
| 154 | 156 | let totp_info = TotpService::get_totp_info(&db, claims.sub).await?; |
@@ -175,6 +177,12 @@ pub async fn security_page(
| 175 | 177 | app_version: layout.app_version.clone(), |
| 176 | 178 | app_domain, |
| 177 | 179 | app_description, |
| 180 | + analytics_opt_out: crate::analytics::AnalyticsOptOutContext::build( | |
| 181 | + &analytics, | |
| 182 | + &i18n_service, | |
| 183 | + locale.as_str(), | |
| 184 | + ) | |
| 185 | + .await, | |
| 178 | 186 | csrf_token, |
| 179 | 187 | username: user.username.clone(), |
| 180 | 188 | email: user.email.clone(), |
M
crates/rustwarden-ui/src/routes.rs
+2
-0
@@ -27,6 +27,7 @@ pub fn browser_routes() -> Router<DatabaseConnection> {
| 27 | 27 | "/resend-verification", |
| 28 | 28 | get(login_handler::resend_verification_page), |
| 29 | 29 | ) |
| 30 | + .route("/forgot-password", get(login_handler::forgot_password_page)) | |
| 30 | 31 | .route("/reset-password", get(login_handler::reset_password_page)) |
| 31 | 32 | .route( |
| 32 | 33 | "/confirm-password-change", |
@@ -98,6 +99,7 @@ pub fn browser_routes() -> Router<DatabaseConnection> {
| 98 | 99 | .route("/admin/settings", get(admin_handler::get_app_settings)) |
| 99 | 100 | .route("/admin/settings", put(admin_handler::update_app_settings)) |
| 100 | 101 | .route("/admin/errors", get(admin_handler::admin_errors)) |
| 102 | + .route("/admin/logs", get(admin_handler::admin_logs)) | |
| 101 | 103 | .route( |
| 102 | 104 | "/admin/users/{id}/reset-totp", |
| 103 | 105 | post(admin_handler::reset_user_totp), |
M
crates/rustwarden-ui/src/template_helpers.rs
+0
-1
@@ -427,7 +427,6 @@ fn translate_path_segment(segment: &str) -> String {
| 427 | 427 | "settings" => "Paramètres".to_string(), |
| 428 | 428 | "account" => "Compte".to_string(), |
| 429 | 429 | "profile" => "Profil".to_string(), |
| 430 | - "files" => "Fichiers".to_string(), | |
| 431 | 430 | "admin" => "Administration".to_string(), |
| 432 | 431 | _ => { |
| 433 | 432 | // Capitalize first letter |
M
crates/rustwarden-ui/templates/admin/dashboard.html
+13
-0
@@ -93,12 +93,25 @@
| 93 | 93 | </div> |
| 94 | 94 | </div> |
| 95 | 95 | </div> |
| 96 | + <div class="card-actions justify-end mt-3"> | |
| 97 | + <a href="/admin/logs" class="btn btn-sm btn-outline">{{ view_logs }}</a> | |
| 98 | + </div> | |
| 96 | 99 | </div> |
| 97 | 100 | </div> |
| 98 | 101 | {% endif %} |
| 99 | 102 | |
| 100 | 103 | <!-- Navigation Links --> |
| 101 | 104 | <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> |
| 105 | + <a href="/admin/logs" class="card bg-base-100 shadow-md hover:shadow-lg transition-shadow"> | |
| 106 | + <div class="card-body p-4"> | |
| 107 | + <h2 class="card-title text-info text-base">{{ logs_card_title }}</h2> | |
| 108 | + <p class="text-sm">{{ logs_card_desc }}</p> | |
| 109 | + <div class="card-actions justify-end mt-2"> | |
| 110 | + <span class="btn btn-info btn-sm">{{ view_logs }}</span> | |
| 111 | + </div> | |
| 112 | + </div> | |
| 113 | + </a> | |
| 114 | + | |
| 102 | 115 | <a href="/admin/roles" class="card bg-base-100 shadow-md hover:shadow-lg transition-shadow"> |
| 103 | 116 | <div class="card-body p-4"> |
| 104 | 117 | <h2 class="card-title text-warning text-base">🛡️ {{ roles_card_title }}</h2> |
A
crates/rustwarden-ui/templates/admin/logs.html
+50
-0
@@ -0,0 +1,50 @@
| 1 | +{% extends "base.html" %} | |
| 2 | + | |
| 3 | +{% block title %}{{ page_title }}{% endblock %} | |
| 4 | + | |
| 5 | +{% block navbar %} | |
| 6 | +{% include "components/navbar.html" %} | |
| 7 | +{% endblock %} | |
| 8 | + | |
| 9 | +{% block content %} | |
| 10 | +<div class="container mx-auto px-4 py-4"> | |
| 11 | + <div class="text-sm breadcrumbs mb-3"> | |
| 12 | + <ul> | |
| 13 | + <li><a href="/">{{ breadcrumb_home }}</a></li> | |
| 14 | + <li><a href="/admin">{{ breadcrumb_admin }}</a></li> | |
| 15 | + <li>{{ heading }}</li> | |
| 16 | + </ul> | |
| 17 | + </div> | |
| 18 | + | |
| 19 | + <h1 class="text-2xl font-bold mb-4">{{ heading }}</h1> | |
| 20 | + | |
| 21 | + {% if has_rows %} | |
| 22 | + <div class="overflow-x-auto bg-base-100 shadow-md rounded-box"> | |
| 23 | + <table class="table table-sm"> | |
| 24 | + <thead> | |
| 25 | + <tr> | |
| 26 | + <th>{{ col_date }}</th> | |
| 27 | + <th>{{ col_level }}</th> | |
| 28 | + <th>{{ col_action }}</th> | |
| 29 | + <th>{{ col_message }}</th> | |
| 30 | + </tr> | |
| 31 | + </thead> | |
| 32 | + <tbody> | |
| 33 | + {% for row in rows %} | |
| 34 | + <tr> | |
| 35 | + <td class="whitespace-nowrap">{{ row.created_at }}</td> | |
| 36 | + <td>{{ row.level }}</td> | |
| 37 | + <td>{{ row.action }}</td> | |
| 38 | + <td>{{ row.message }}</td> | |
| 39 | + </tr> | |
| 40 | + {% endfor %} | |
| 41 | + </tbody> | |
| 42 | + </table> | |
| 43 | + </div> | |
| 44 | + {% else %} | |
| 45 | + <div class="alert"> | |
| 46 | + <span>{{ empty }}</span> | |
| 47 | + </div> | |
| 48 | + {% endif %} | |
| 49 | +</div> | |
| 50 | +{% endblock %} |
M
crates/rustwarden-ui/templates/admin/user_form.html
+5
-0
@@ -104,6 +104,11 @@
| 104 | 104 | class="input input-bordered w-full" |
| 105 | 105 | required |
| 106 | 106 | > |
| 107 | + {% if mode == "edit" %} | |
| 108 | + <label class="label"> | |
| 109 | + <span class="label-text-alt">{{ email_change_hint }}</span> | |
| 110 | + </label> | |
| 111 | + {% endif %} | |
| 107 | 112 | </div> |
| 108 | 113 | |
| 109 | 114 | <!-- Password Field (only for create mode or if resetting) --> |
M
crates/rustwarden-ui/templates/base.html
+4
-2
@@ -38,8 +38,9 @@
| 38 | 38 | <!-- HTMX (local) --> |
| 39 | 39 | <script src="/static/js/htmx.min.js"></script> |
| 40 | 40 | |
| 41 | - <!-- Alpine.js (local) --> | |
| 42 | - <script src="/static/js/alpine.min.js" defer></script> | |
| 41 | + <!-- Alpine.js CSP build (no unsafe-eval). Components register on alpine:init. --> | |
| 42 | + <script src="/static/js/alpine-components.js" defer></script> | |
| 43 | + <script src="/static/js/alpine-csp.min.js" defer></script> | |
| 43 | 44 | |
| 44 | 45 | {% block head_extra %}{% endblock %} |
| 45 | 46 | </head> |
@@ -75,6 +76,7 @@
| 75 | 76 | {% if app_description != "" %} |
| 76 | 77 | <p class="text-base-content/70 mt-1">{{ app_description }}</p> |
| 77 | 78 | {% endif %} |
| 79 | + {% include "components/analytics_opt_out.html" %} | |
| 78 | 80 | </div> |
| 79 | 81 | </footer> |
| 80 | 82 | {% endblock %} |
D
crates/rustwarden-ui/templates/base_with_theme.html
+0
-10
@@ -1,10 +0,0 @@
| 1 | -<!-- T155: Theme application in base.html --> | |
| 2 | -<!-- Note: This requires modifying base.html to apply data-theme attribute --> | |
| 3 | -<!-- Add to <html> tag: data-theme="{{ user_theme | default(value='light') }}" --> | |
| 4 | -<!-- DaisyUI will automatically apply the theme based on this attribute --> | |
| 5 | - | |
| 6 | -<!-- Example integration: | |
| 7 | -<html lang="fr" data-theme="{% if user_theme %}{{ user_theme }}{% else %}light{% endif %}"> | |
| 8 | - | |
| 9 | -This allows dynamic theme switching based on user preferences. | |
| 10 | ---> |
M
crates/rustwarden-ui/templates/components/analytics_opt_out.html
+2
-11
@@ -20,17 +20,8 @@
| 20 | 20 | #} |
| 21 | 21 | {% if analytics_opt_out.has_key() %} |
| 22 | 22 | <div class="mt-4 pt-4 border-t border-base-300" |
| 23 | - x-data="{ | |
| 24 | - storageKey: '{{ analytics_opt_out.storage_key }}', | |
| 25 | - optedOut: false, | |
| 26 | - init() { try { this.optedOut = localStorage.getItem(this.storageKey) !== null; } catch (e) {} }, | |
| 27 | - toggle() { | |
| 28 | - try { | |
| 29 | - if (this.optedOut) { localStorage.removeItem(this.storageKey); this.optedOut = false; } | |
| 30 | - else { localStorage.setItem(this.storageKey, '1'); this.optedOut = true; } | |
| 31 | - } catch (e) {} | |
| 32 | - } | |
| 33 | - }"> | |
| 23 | + x-data="analyticsOptOut" | |
| 24 | + data-storage-key="{{ analytics_opt_out.storage_key }}"> | |
| 34 | 25 | <p class="text-sm text-base-content/70 mb-2">{{ analytics_opt_out.hint }}</p> |
| 35 | 26 | <div class="flex items-center gap-3 flex-wrap"> |
| 36 | 27 | <button type="button" class="btn btn-sm btn-outline" @click="toggle()"> |
D
crates/rustwarden-ui/templates/components/breadcrumb.html
+0
-37
@@ -1,37 +0,0 @@
| 1 | -<!-- Breadcrumb Component for Feature 007 --> | |
| 2 | -<!-- Uses DaisyUI breadcrumbs classes --> | |
| 3 | - | |
| 4 | -<div class="container mx-auto px-4 py-2 bg-base-200 border-b border-base-300"> | |
| 5 | - <ul class="breadcrumbs text-sm"> | |
| 6 | - {% for item in breadcrumb_items %} | |
| 7 | - <li> | |
| 8 | - {% if item.is_current %} | |
| 9 | - <span class="text-base-content font-semibold">{{ item.label }}</span> | |
| 10 | - {% else %} | |
| 11 | - <a href="{{ item.url }}" class="text-primary hover:text-primary-focus"> | |
| 12 | - {{ item.label }} | |
| 13 | - </a> | |
| 14 | - {% endif %} | |
| 15 | - </li> | |
| 16 | - {% endfor %} | |
| 17 | - </ul> | |
| 18 | -</div> | |
| 19 | - | |
| 20 | - | |
| 21 | - | |
| 22 | - | |
| 23 | - | |
| 24 | - | |
| 25 | - | |
| 26 | - | |
| 27 | - | |
| 28 | - | |
| 29 | - | |
| 30 | - | |
| 31 | - | |
| 32 | - | |
| 33 | - | |
| 34 | - | |
| 35 | - | |
| 36 | - | |
| 37 | - |
D
crates/rustwarden-ui/templates/components/header.html
+0
-54
@@ -1,54 +0,0 @@
| 1 | -<header class="navbar bg-base-100 border-b border-base-300 shadow-sm"> | |
| 2 | - <div class="container mx-auto px-4"> | |
| 3 | - <div class="flex items-center justify-between h-16"> | |
| 4 | - <!-- Logo --> | |
| 5 | - <div class="flex items-center"> | |
| 6 | - <a href="/home" class="flex items-center space-x-2 hover:opacity-80 transition"> | |
| 7 | - <img src="/static/images/logo.svg" alt="{{ app_name }}" class="h-8 w-8"> | |
| 8 | - <span class="text-xl font-bold text-primary">{{ app_name }}</span> | |
| 9 | - </a> | |
| 10 | - </div> | |
| 11 | - | |
| 12 | - <!-- Desktop Navigation --> | |
| 13 | - <div class="hidden md:flex items-center flex-1 justify-center"> | |
| 14 | - {% include "components/navigation.html" %} | |
| 15 | - </div> | |
| 16 | - | |
| 17 | - <!-- User Menu (Desktop) --> | |
| 18 | - <div class="hidden md:block"> | |
| 19 | - {% include "components/user_menu.html" %} | |
| 20 | - </div> | |
| 21 | - | |
| 22 | - <!-- Mobile Menu Button --> | |
| 23 | - <div class="md:hidden" x-data="{ mobileMenuOpen: false }"> | |
| 24 | - <button @click="mobileMenuOpen = !mobileMenuOpen" | |
| 25 | - class="btn btn-ghost btn-sm"> | |
| 26 | - <svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| 27 | - <path x-show="!mobileMenuOpen" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/> | |
| 28 | - <path x-show="mobileMenuOpen" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/> | |
| 29 | - </svg> | |
| 30 | - </button> | |
| 31 | - | |
| 32 | - <!-- Mobile Menu --> | |
| 33 | - <div x-show="mobileMenuOpen" | |
| 34 | - @click.away="mobileMenuOpen = false" | |
| 35 | - x-transition:enter="transition ease-out duration-200" | |
| 36 | - x-transition:enter-start="opacity-0 scale-95" | |
| 37 | - x-transition:enter-end="opacity-100 scale-100" | |
| 38 | - x-transition:leave="transition ease-in duration-150" | |
| 39 | - x-transition:leave-start="opacity-100 scale-100" | |
| 40 | - x-transition:leave-end="opacity-0 scale-95" | |
| 41 | - class="absolute top-16 right-0 mt-2 w-64 card bg-base-100 shadow-xl border border-base-300 py-2 z-50"> | |
| 42 | - <!-- Mobile Navigation --> | |
| 43 | - <div class="px-4 py-2"> | |
| 44 | - {% include "components/navigation.html" %} | |
| 45 | - </div> | |
| 46 | - <!-- Mobile User Info --> | |
| 47 | - <div class="border-t border-base-300 mt-2 pt-2 px-4 py-2"> | |
| 48 | - {% include "fragments/user_menu_dropdown.html" %} | |
| 49 | - </div> | |
| 50 | - </div> | |
| 51 | - </div> | |
| 52 | - </div> | |
| 53 | - </div> | |
| 54 | -</header> |
D
crates/rustwarden-ui/templates/components/password_change_badge.html
+0
-6
@@ -1,6 +0,0 @@
| 1 | -{% if has_pending_password_change %} | |
| 2 | -<span class="badge badge-sm badge-warning" title="Changement de mot de passe en attente de confirmation"> | |
| 3 | - ⚠️ | |
| 4 | -</span> | |
| 5 | -{% endif %} | |
| 6 | - |
D
crates/rustwarden-ui/templates/components/sidebar.html
+0
-155
@@ -1,155 +0,0 @@
| 1 | -<!-- Sidebar Component for Feature 007 --> | |
| 2 | -<!-- Uses DaisyUI sidebar and menu classes --> | |
| 3 | - | |
| 4 | -<aside class="w-64 bg-base-200 border-r border-base-300 hidden lg:block overflow-y-auto" | |
| 5 | - x-data="{ sidebarOpen: false }"> | |
| 6 | - <div class="p-4"> | |
| 7 | - {% for section in sidebar_sections %} | |
| 8 | - <div class="mb-6"> | |
| 9 | - <h3 class="text-sm font-semibold text-base-content uppercase tracking-wider mb-2"> | |
| 10 | - {{ section.title }} | |
| 11 | - </h3> | |
| 12 | - <ul class="menu menu-vertical w-full"> | |
| 13 | - {% for item in section.items %} | |
| 14 | - <li> | |
| 15 | - {% if item.enabled %} | |
| 16 | - {% if item.has_url %} | |
| 17 | - <a href="{{ item.url }}" | |
| 18 | - class="flex items-center gap-2 hover:bg-base-300"> | |
| 19 | - <span>{{ item.icon }}</span> | |
| 20 | - <span>{{ item.label }}</span> | |
| 21 | - {% if !item.badge.is_empty() %} | |
| 22 | - <span class="badge badge-primary badge-sm ml-auto">{{ item.badge }}</span> | |
| 23 | - {% endif %} | |
| 24 | - </a> | |
| 25 | - {% else %} | |
| 26 | - <div class="flex items-center gap-2 text-base-content/60 cursor-default"> | |
| 27 | - <span>{{ item.icon }}</span> | |
| 28 | - <span>{{ item.label }}</span> | |
| 29 | - {% if !item.badge.is_empty() %} | |
| 30 | - <span class="badge badge-ghost badge-sm ml-auto">{{ item.badge }}</span> | |
| 31 | - {% endif %} | |
| 32 | - </div> | |
| 33 | - {% endif %} | |
| 34 | - {% else %} | |
| 35 | - <div class="flex items-center gap-2 text-base-content/60 cursor-default"> | |
| 36 | - <span>{{ item.icon }}</span> | |
| 37 | - <span>{{ item.label }}</span> | |
| 38 | - {% if !item.badge.is_empty() %} | |
| 39 | - <span class="badge badge-ghost badge-sm ml-auto">{{ item.badge }}</span> | |
| 40 | - {% endif %} | |
| 41 | - </div> | |
| 42 | - {% endif %} | |
| 43 | - </li> | |
| 44 | - {% endfor %} | |
| 45 | - </ul> | |
| 46 | - </div> | |
| 47 | - {% endfor %} | |
| 48 | - </div> | |
| 49 | -</aside> | |
| 50 | - | |
| 51 | -<!-- Mobile Sidebar Toggle Button (visible only on mobile) --> | |
| 52 | -<div class="lg:hidden fixed bottom-4 right-4 z-50" x-data="{ sidebarOpen: false }"> | |
| 53 | - <button @click="sidebarOpen = !sidebarOpen" | |
| 54 | - class="btn btn-primary btn-circle shadow-lg"> | |
| 55 | - <svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| 56 | - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/> | |
| 57 | - </svg> | |
| 58 | - </button> | |
| 59 | - | |
| 60 | - <!-- Mobile Sidebar Overlay --> | |
| 61 | - <div x-show="sidebarOpen" | |
| 62 | - @click.away="sidebarOpen = false" | |
| 63 | - x-transition:enter="transition ease-out duration-300" | |
| 64 | - x-transition:enter-start="opacity-0" | |
| 65 | - x-transition:enter-end="opacity-100" | |
| 66 | - x-transition:leave="transition ease-in duration-200" | |
| 67 | - x-transition:leave-start="opacity-100" | |
| 68 | - x-transition:leave-end="opacity-0" | |
| 69 | - class="fixed inset-0 bg-black bg-opacity-50 z-40" | |
| 70 | - @click="sidebarOpen = false"> | |
| 71 | - </div> | |
| 72 | - | |
| 73 | - <!-- Mobile Sidebar Panel --> | |
| 74 | - <div x-show="sidebarOpen" | |
| 75 | - x-transition:enter="transition ease-out duration-300 transform" | |
| 76 | - x-transition:enter-start="translate-x-full" | |
| 77 | - x-transition:enter-end="translate-x-0" | |
| 78 | - x-transition:leave="transition ease-in duration-200 transform" | |
| 79 | - x-transition:leave-start="translate-x-0" | |
| 80 | - x-transition:leave-end="translate-x-full" | |
| 81 | - class="fixed top-0 right-0 h-full w-64 bg-base-200 border-l border-base-300 shadow-xl z-50 overflow-y-auto"> | |
| 82 | - <div class="p-4"> | |
| 83 | - <div class="flex justify-between items-center mb-4"> | |
| 84 | - <h2 class="text-lg font-bold text-base-content">Menu</h2> | |
| 85 | - <button @click="sidebarOpen = false" class="btn btn-ghost btn-sm btn-circle"> | |
| 86 | - <svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| 87 | - <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/> | |
| 88 | - </svg> | |
| 89 | - </button> | |
| 90 | - </div> | |
| 91 | - | |
| 92 | - {% for section in sidebar_sections %} | |
| 93 | - <div class="mb-6"> | |
| 94 | - <h3 class="text-sm font-semibold text-base-content uppercase tracking-wider mb-2"> | |
| 95 | - {{ section.title }} | |
| 96 | - </h3> | |
| 97 | - <ul class="menu menu-vertical w-full"> | |
| 98 | - {% for item in section.items %} | |
| 99 | - <li> | |
| 100 | - {% if item.enabled %} | |
| 101 | - {% if item.has_url %} | |
| 102 | - <a href="{{ item.url }}" | |
| 103 | - class="flex items-center gap-2 hover:bg-base-300" | |
| 104 | - @click="sidebarOpen = false"> | |
| 105 | - <span>{{ item.icon }}</span> | |
| 106 | - <span>{{ item.label }}</span> | |
| 107 | - {% if !item.badge.is_empty() %} | |
| 108 | - <span class="badge badge-primary badge-sm ml-auto">{{ item.badge }}</span> | |
| 109 | - {% endif %} | |
| 110 | - </a> | |
| 111 | - {% else %} | |
| 112 | - <div class="flex items-center gap-2 text-base-content/60 cursor-default"> | |
| 113 | - <span>{{ item.icon }}</span> | |
| 114 | - <span>{{ item.label }}</span> | |
| 115 | - {% if !item.badge.is_empty() %} | |
| 116 | - <span class="badge badge-ghost badge-sm ml-auto">{{ item.badge }}</span> | |
| 117 | - {% endif %} | |
| 118 | - </div> | |
| 119 | - {% endif %} | |
| 120 | - {% else %} | |
| 121 | - <div class="flex items-center gap-2 text-base-content/60 cursor-default"> | |
| 122 | - <span>{{ item.icon }}</span> | |
| 123 | - <span>{{ item.label }}</span> | |
| 124 | - {% if !item.badge.is_empty() %} | |
| 125 | - <span class="badge badge-ghost badge-sm ml-auto">{{ item.badge }}</span> | |
| 126 | - {% endif %} | |
| 127 | - </div> | |
| 128 | - {% endif %} | |
| 129 | - </li> | |
| 130 | - {% endfor %} | |
| 131 | - </ul> | |
| 132 | - </div> | |
| 133 | - {% endfor %} | |
| 134 | - </div> | |
| 135 | - </div> | |
| 136 | -</div> | |
| 137 | - | |
| 138 | - | |
| 139 | - | |
| 140 | - | |
| 141 | - | |
| 142 | - | |
| 143 | - | |
| 144 | - | |
| 145 | - | |
| 146 | - | |
| 147 | - | |
| 148 | - | |
| 149 | - | |
| 150 | - | |
| 151 | - | |
| 152 | - | |
| 153 | - | |
| 154 | - | |
| 155 | - |
D
crates/rustwarden-ui/templates/emails/account_modified.html
+0
-165
@@ -1,165 +0,0 @@
| 1 | -<!DOCTYPE html> | |
| 2 | -<html lang="{{ locale }}"> | |
| 3 | -<head> | |
| 4 | - <meta charset="UTF-8"> | |
| 5 | - <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| 6 | - <title>{% if locale == "fr" %}Compte modifié{% else %}Account Modified{% endif %}</title> | |
| 7 | - <style> | |
| 8 | - body { | |
| 9 | - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | |
| 10 | - line-height: 1.6; | |
| 11 | - color: #333; | |
| 12 | - max-width: 600px; | |
| 13 | - margin: 0 auto; | |
| 14 | - padding: 20px; | |
| 15 | - background-color: #f4f4f4; | |
| 16 | - } | |
| 17 | - .email-container { | |
| 18 | - background-color: #ffffff; | |
| 19 | - border-radius: 8px; | |
| 20 | - padding: 30px; | |
| 21 | - box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| 22 | - } | |
| 23 | - .header { | |
| 24 | - text-align: center; | |
| 25 | - margin-bottom: 30px; | |
| 26 | - } | |
| 27 | - .logo { | |
| 28 | - font-size: 24px; | |
| 29 | - font-weight: bold; | |
| 30 | - color: #2563eb; | |
| 31 | - margin-bottom: 10px; | |
| 32 | - } | |
| 33 | - h1 { | |
| 34 | - color: #1f2937; | |
| 35 | - font-size: 24px; | |
| 36 | - margin-bottom: 20px; | |
| 37 | - } | |
| 38 | - .content { | |
| 39 | - margin-bottom: 30px; | |
| 40 | - } | |
| 41 | - .changes-list { | |
| 42 | - margin: 20px 0; | |
| 43 | - padding: 15px; | |
| 44 | - background-color: #f3f4f6; | |
| 45 | - border-radius: 6px; | |
| 46 | - } | |
| 47 | - .change-item { | |
| 48 | - margin: 10px 0; | |
| 49 | - padding: 10px; | |
| 50 | - background-color: #ffffff; | |
| 51 | - border-left: 3px solid #3b82f6; | |
| 52 | - border-radius: 4px; | |
| 53 | - } | |
| 54 | - .change-label { | |
| 55 | - font-weight: 600; | |
| 56 | - color: #1f2937; | |
| 57 | - } | |
| 58 | - .change-value { | |
| 59 | - color: #6b7280; | |
| 60 | - margin-top: 5px; | |
| 61 | - } | |
| 62 | - .info-box { | |
| 63 | - margin-top: 20px; | |
| 64 | - padding: 15px; | |
| 65 | - background-color: #fee2e2; | |
| 66 | - border-left: 4px solid #ef4444; | |
| 67 | - border-radius: 4px; | |
| 68 | - font-size: 14px; | |
| 69 | - } | |
| 70 | - .footer { | |
| 71 | - margin-top: 30px; | |
| 72 | - padding-top: 20px; | |
| 73 | - border-top: 1px solid #e5e7eb; | |
| 74 | - font-size: 14px; | |
| 75 | - color: #6b7280; | |
| 76 | - text-align: center; | |
| 77 | - } | |
| 78 | - </style> | |
| 79 | -</head> | |
| 80 | -<body> | |
| 81 | - <div class="email-container"> | |
| 82 | - <div class="header"> | |
| 83 | - <div class="logo">{{ app_name }}</div> | |
| 84 | - </div> | |
| 85 | - | |
| 86 | - <h1> | |
| 87 | - {% if locale == "fr" %} | |
| 88 | - Compte modifié | |
| 89 | - {% else %} | |
| 90 | - Account Modified | |
| 91 | - {% endif %} | |
| 92 | - </h1> | |
| 93 | - | |
| 94 | - <div class="content"> | |
| 95 | - {% if locale == "fr" %} | |
| 96 | - <p>Bonjour {{ username }},</p> | |
| 97 | - <p>Nous vous informons que les informations de votre compte <strong>{{ app_name }}</strong> ont été modifiées.</p> | |
| 98 | - <p><strong>Date et heure de modification :</strong> {{ modified_at }}</p> | |
| 99 | - {% else %} | |
| 100 | - <p>Hello {{ username }},</p> | |
| 101 | - <p>We are informing you that your <strong>{{ app_name }}</strong> account information has been modified.</p> | |
| 102 | - <p><strong>Date and time of modification:</strong> {{ modified_at }}</p> | |
| 103 | - {% endif %} | |
| 104 | - </div> | |
| 105 | - | |
| 106 | - {% if !changes.is_empty() %} | |
| 107 | - <div class="changes-list"> | |
| 108 | - {% if locale == "fr" %} | |
| 109 | - <p style="font-weight: 600; margin-bottom: 15px;">Modifications effectuées :</p> | |
| 110 | - {% else %} | |
| 111 | - <p style="font-weight: 600; margin-bottom: 15px;">Changes made:</p> | |
| 112 | - {% endif %} | |
| 113 | - {% for change in changes %} | |
| 114 | - <div class="change-item"> | |
| 115 | - <div class="change-label">{{ change.field }}</div> | |
| 116 | - {% if let Some(old_val) = change.old_value %} | |
| 117 | - <div class="change-value"> | |
| 118 | - {% if locale == "fr" %} | |
| 119 | - Ancien : {{ old_val }} → Nouveau : {{ change.new_value }} | |
| 120 | - {% else %} | |
| 121 | - Old: {{ old_val }} → New: {{ change.new_value }} | |
| 122 | - {% endif %} | |
| 123 | - </div> | |
| 124 | - {% else %} | |
| 125 | - <div class="change-value">{{ change.new_value }}</div> | |
| 126 | - {% endif %} | |
| 127 | - </div> | |
| 128 | - {% endfor %} | |
| 129 | - </div> | |
| 130 | - {% endif %} | |
| 131 | - | |
| 132 | - <div class="info-box"> | |
| 133 | - {% if locale == "fr" %} | |
| 134 | - <p><strong>⚠️ Sécurité :</strong></p> | |
| 135 | - <ul style="margin: 10px 0; padding-left: 20px;"> | |
| 136 | - <li>Si vous n'avez pas effectué ces modifications, contactez immédiatement le support technique</li> | |
| 137 | - <li>Vérifiez que vous pouvez toujours accéder à votre compte</li> | |
| 138 | - <li>Si votre email a été modifié, vérifiez que vous recevez bien les emails à la nouvelle adresse</li> | |
| 139 | - </ul> | |
| 140 | - {% else %} | |
| 141 | - <p><strong>⚠️ Security:</strong></p> | |
| 142 | - <ul style="margin: 10px 0; padding-left: 20px;"> | |
| 143 | - <li>If you did not make these changes, contact technical support immediately</li> | |
| 144 | - <li>Verify that you can still access your account</li> | |
| 145 | - <li>If your email was changed, verify that you receive emails at the new address</li> | |
| 146 | - </ul> | |
| 147 | - {% endif %} | |
| 148 | - </div> | |
| 149 | - | |
| 150 | - <div class="footer"> | |
| 151 | - {% if locale == "fr" %} | |
| 152 | - <p>Cet email a été envoyé automatiquement pour votre sécurité.</p> | |
| 153 | - <p>Si vous avez des questions, contactez le support technique.</p> | |
| 154 | - {% else %} | |
| 155 | - <p>This email was sent automatically for your security.</p> | |
| 156 | - <p>If you have any questions, please contact technical support.</p> | |
| 157 | - {% endif %} | |
| 158 | - <p style="margin-top: 10px; font-size: 12px; color: #9ca3af;"> | |
| 159 | - © {{ current_year }} {{ app_name }}. Tous droits réservés. | |
| 160 | - </p> | |
| 161 | - </div> | |
| 162 | - </div> | |
| 163 | -</body> | |
| 164 | -</html> | |
| 165 | - |
D
crates/rustwarden-ui/templates/emails/admin_new_user.html
+0
-151
@@ -1,151 +0,0 @@
| 1 | -<!DOCTYPE html> | |
| 2 | -<html lang="{{ locale }}"> | |
| 3 | -<head> | |
| 4 | - <meta charset="UTF-8"> | |
| 5 | - <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| 6 | - <title>{% if locale == "fr" %}Nouvel utilisateur inscrit{% else %}New User Registered{% endif %}</title> | |
| 7 | - <style> | |
| 8 | - body { | |
| 9 | - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | |
| 10 | - line-height: 1.6; | |
| 11 | - color: #333; | |
| 12 | - max-width: 600px; | |
| 13 | - margin: 0 auto; | |
| 14 | - padding: 20px; | |
| 15 | - background-color: #f4f4f4; | |
| 16 | - } | |
| 17 | - .email-container { | |
| 18 | - background-color: #ffffff; | |
| 19 | - border-radius: 8px; | |
| 20 | - padding: 30px; | |
| 21 | - box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| 22 | - } | |
| 23 | - .header { | |
| 24 | - text-align: center; | |
| 25 | - margin-bottom: 30px; | |
| 26 | - } | |
| 27 | - .logo { | |
| 28 | - font-size: 24px; | |
| 29 | - font-weight: bold; | |
| 30 | - color: #2563eb; | |
| 31 | - margin-bottom: 10px; | |
| 32 | - } | |
| 33 | - h1 { | |
| 34 | - color: #1f2937; | |
| 35 | - font-size: 24px; | |
| 36 | - margin-bottom: 20px; | |
| 37 | - } | |
| 38 | - .content { | |
| 39 | - margin-bottom: 30px; | |
| 40 | - } | |
| 41 | - .user-info { | |
| 42 | - margin: 20px 0; | |
| 43 | - padding: 15px; | |
| 44 | - background-color: #f3f4f6; | |
| 45 | - border-radius: 6px; | |
| 46 | - } | |
| 47 | - .info-row { | |
| 48 | - margin: 10px 0; | |
| 49 | - padding: 8px 0; | |
| 50 | - border-bottom: 1px solid #e5e7eb; | |
| 51 | - } | |
| 52 | - .info-row:last-child { | |
| 53 | - border-bottom: none; | |
| 54 | - } | |
| 55 | - .info-label { | |
| 56 | - font-weight: 600; | |
| 57 | - color: #1f2937; | |
| 58 | - display: inline-block; | |
| 59 | - width: 120px; | |
| 60 | - } | |
| 61 | - .info-value { | |
| 62 | - color: #6b7280; | |
| 63 | - } | |
| 64 | - .action-box { | |
| 65 | - margin-top: 20px; | |
| 66 | - padding: 15px; | |
| 67 | - background-color: #dbeafe; | |
| 68 | - border-left: 4px solid #3b82f6; | |
| 69 | - border-radius: 4px; | |
| 70 | - font-size: 14px; | |
| 71 | - } | |
| 72 | - .footer { | |
| 73 | - margin-top: 30px; | |
| 74 | - padding-top: 20px; | |
| 75 | - border-top: 1px solid #e5e7eb; | |
| 76 | - font-size: 14px; | |
| 77 | - color: #6b7280; | |
| 78 | - text-align: center; | |
| 79 | - } | |
| 80 | - </style> | |
| 81 | -</head> | |
| 82 | -<body> | |
| 83 | - <div class="email-container"> | |
| 84 | - <div class="header"> | |
| 85 | - <div class="logo">{{ app_name }}</div> | |
| 86 | - </div> | |
| 87 | - | |
| 88 | - <h1> | |
| 89 | - {% if locale == "fr" %} | |
| 90 | - Nouvel utilisateur inscrit | |
| 91 | - {% else %} | |
| 92 | - New User Registered | |
| 93 | - {% endif %} | |
| 94 | - </h1> | |
| 95 | - | |
| 96 | - <div class="content"> | |
| 97 | - {% if locale == "fr" %} | |
| 98 | - <p>Bonjour,</p> | |
| 99 | - <p>Un nouvel utilisateur s'est inscrit sur <strong>{{ app_name }}</strong>.</p> | |
| 100 | - {% else %} | |
| 101 | - <p>Hello,</p> | |
| 102 | - <p>A new user has registered on <strong>{{ app_name }}</strong>.</p> | |
| 103 | - {% endif %} | |
| 104 | - </div> | |
| 105 | - | |
| 106 | - <div class="user-info"> | |
| 107 | - {% if locale == "fr" %} | |
| 108 | - <p style="font-weight: 600; margin-bottom: 15px;">Informations de l'utilisateur :</p> | |
| 109 | - {% else %} | |
| 110 | - <p style="font-weight: 600; margin-bottom: 15px;">User Information:</p> | |
| 111 | - {% endif %} | |
| 112 | - <div class="info-row"> | |
| 113 | - <span class="info-label">{% if locale == "fr" %}Nom d'utilisateur{% else %}Username{% endif %}:</span> | |
| 114 | - <span class="info-value">{{ username }}</span> | |
| 115 | - </div> | |
| 116 | - <div class="info-row"> | |
| 117 | - <span class="info-label">Email:</span> | |
| 118 | - <span class="info-value">{{ email }}</span> | |
| 119 | - </div> | |
| 120 | - <div class="info-row"> | |
| 121 | - <span class="info-label">{% if locale == "fr" %}Date d'inscription{% else %}Registration Date{% endif %}:</span> | |
| 122 | - <span class="info-value">{{ registered_at }}</span> | |
| 123 | - </div> | |
| 124 | - </div> | |
| 125 | - | |
| 126 | - <div class="action-box"> | |
| 127 | - {% if locale == "fr" %} | |
| 128 | - <p><strong>ℹ️ Action requise :</strong></p> | |
| 129 | - <p>Vous pouvez consulter et gérer cet utilisateur depuis le panneau d'administration.</p> | |
| 130 | - {% else %} | |
| 131 | - <p><strong>ℹ️ Action Required:</strong></p> | |
| 132 | - <p>You can view and manage this user from the admin panel.</p> | |
| 133 | - {% endif %} | |
| 134 | - </div> | |
| 135 | - | |
| 136 | - <div class="footer"> | |
| 137 | - {% if locale == "fr" %} | |
| 138 | - <p>Cet email a été envoyé automatiquement aux administrateurs.</p> | |
| 139 | - <p>Si vous avez des questions, contactez le support technique.</p> | |
| 140 | - {% else %} | |
| 141 | - <p>This email was sent automatically to administrators.</p> | |
| 142 | - <p>If you have any questions, please contact technical support.</p> | |
| 143 | - {% endif %} | |
| 144 | - <p style="margin-top: 10px; font-size: 12px; color: #9ca3af;"> | |
| 145 | - © {{ current_year }} {{ app_name }}. Tous droits réservés. | |
| 146 | - </p> | |
| 147 | - </div> | |
| 148 | - </div> | |
| 149 | -</body> | |
| 150 | -</html> | |
| 151 | - |
D
crates/rustwarden-ui/templates/emails/email_validation.html
+0
-164
@@ -1,164 +0,0 @@
| 1 | -<!DOCTYPE html> | |
| 2 | -<html lang="{{ locale }}"> | |
| 3 | -<head> | |
| 4 | - <meta charset="UTF-8"> | |
| 5 | - <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| 6 | - <title>{% if locale == "fr" %}Vérification de votre adresse email{% else %}Verify Your Email Address{% endif %}</title> | |
| 7 | - <style> | |
| 8 | - body { | |
| 9 | - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | |
| 10 | - line-height: 1.6; | |
| 11 | - color: #333; | |
| 12 | - max-width: 600px; | |
| 13 | - margin: 0 auto; | |
| 14 | - padding: 20px; | |
| 15 | - background-color: #f4f4f4; | |
| 16 | - } | |
| 17 | - .email-container { | |
| 18 | - background-color: #ffffff; | |
| 19 | - border-radius: 8px; | |
| 20 | - padding: 30px; | |
| 21 | - box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| 22 | - } | |
| 23 | - .header { | |
| 24 | - text-align: center; | |
| 25 | - margin-bottom: 30px; | |
| 26 | - } | |
| 27 | - .logo { | |
| 28 | - font-size: 24px; | |
| 29 | - font-weight: bold; | |
| 30 | - color: #2563eb; | |
| 31 | - margin-bottom: 10px; | |
| 32 | - } | |
| 33 | - h1 { | |
| 34 | - color: #1f2937; | |
| 35 | - font-size: 24px; | |
| 36 | - margin-bottom: 20px; | |
| 37 | - } | |
| 38 | - .content { | |
| 39 | - margin-bottom: 30px; | |
| 40 | - } | |
| 41 | - .button-container { | |
| 42 | - text-align: center; | |
| 43 | - margin: 30px 0; | |
| 44 | - } | |
| 45 | - .button { | |
| 46 | - display: inline-block; | |
| 47 | - padding: 12px 30px; | |
| 48 | - background-color: #10b981; | |
| 49 | - color: #ffffff !important; | |
| 50 | - text-decoration: none; | |
| 51 | - border-radius: 6px; | |
| 52 | - font-weight: 600; | |
| 53 | - font-size: 16px; | |
| 54 | - } | |
| 55 | - .button:hover { | |
| 56 | - background-color: #059669; | |
| 57 | - } | |
| 58 | - .link-fallback { | |
| 59 | - margin-top: 20px; | |
| 60 | - padding: 15px; | |
| 61 | - background-color: #f3f4f6; | |
| 62 | - border-radius: 6px; | |
| 63 | - word-break: break-all; | |
| 64 | - font-size: 14px; | |
| 65 | - color: #6b7280; | |
| 66 | - } | |
| 67 | - .footer { | |
| 68 | - margin-top: 30px; | |
| 69 | - padding-top: 20px; | |
| 70 | - border-top: 1px solid #e5e7eb; | |
| 71 | - font-size: 14px; | |
| 72 | - color: #6b7280; | |
| 73 | - text-align: center; | |
| 74 | - } | |
| 75 | - .info { | |
| 76 | - margin-top: 20px; | |
| 77 | - padding: 15px; | |
| 78 | - background-color: #dbeafe; | |
| 79 | - border-left: 4px solid #3b82f6; | |
| 80 | - border-radius: 4px; | |
| 81 | - font-size: 14px; | |
| 82 | - } | |
| 83 | - </style> | |
| 84 | -</head> | |
| 85 | -<body> | |
| 86 | - <div class="email-container"> | |
| 87 | - <div class="header"> | |
| 88 | - <div class="logo">{{ app_name }}</div> | |
| 89 | - </div> | |
| 90 | - | |
| 91 | - <h1> | |
| 92 | - {% if locale == "fr" %} | |
| 93 | - Vérification de votre adresse email | |
| 94 | - {% else %} | |
| 95 | - Verify Your Email Address | |
| 96 | - {% endif %} | |
| 97 | - </h1> | |
| 98 | - | |
| 99 | - <div class="content"> | |
| 100 | - {% if locale == "fr" %} | |
| 101 | - <p>Bonjour {{ username }},</p> | |
| 102 | - <p>Merci de vous être inscrit sur <strong>{{ app_name }}</strong>.</p> | |
| 103 | - <p>Pour activer votre compte, veuillez vérifier votre adresse email en cliquant sur le bouton ci-dessous :</p> | |
| 104 | - {% else %} | |
| 105 | - <p>Hello {{ username }},</p> | |
| 106 | - <p>Thank you for signing up for <strong>{{ app_name }}</strong>.</p> | |
| 107 | - <p>To activate your account, please verify your email address by clicking the button below:</p> | |
| 108 | - {% endif %} | |
| 109 | - </div> | |
| 110 | - | |
| 111 | - <div class="button-container"> | |
| 112 | - <a href="{{ validation_url }}" class="button"> | |
| 113 | - {% if locale == "fr" %} | |
| 114 | - Vérifier mon email | |
| 115 | - {% else %} | |
| 116 | - Verify My Email | |
| 117 | - {% endif %} | |
| 118 | - </a> | |
| 119 | - </div> | |
| 120 | - | |
| 121 | - <div class="link-fallback"> | |
| 122 | - {% if locale == "fr" %} | |
| 123 | - <p><strong>Le bouton ne fonctionne pas ?</strong></p> | |
| 124 | - <p>Copiez et collez ce lien dans votre navigateur :</p> | |
| 125 | - {% else %} | |
| 126 | - <p><strong>Button not working?</strong></p> | |
| 127 | - <p>Copy and paste this link into your browser:</p> | |
| 128 | - {% endif %} | |
| 129 | - <p style="margin-top: 10px; word-break: break-all;">{{ validation_url }}</p> | |
| 130 | - </div> | |
| 131 | - | |
| 132 | - <div class="info"> | |
| 133 | - {% if locale == "fr" %} | |
| 134 | - <p><strong>ℹ️ Important :</strong></p> | |
| 135 | - <ul style="margin: 10px 0; padding-left: 20px;"> | |
| 136 | - <li>Ce lien est valide pendant <strong>24 heures</strong></li> | |
| 137 | - <li>Si vous n'avez pas créé de compte, ignorez cet email</li> | |
| 138 | - <li>Vous devez vérifier votre email avant de pouvoir vous connecter</li> | |
| 139 | - </ul> | |
| 140 | - {% else %} | |
| 141 | - <p><strong>ℹ️ Important:</strong></p> | |
| 142 | - <ul style="margin: 10px 0; padding-left: 20px;"> | |
| 143 | - <li>This link is valid for <strong>24 hours</strong></li> | |
| 144 | - <li>If you did not create an account, please ignore this email</li> | |
| 145 | - <li>You must verify your email before you can log in</li> | |
| 146 | - </ul> | |
| 147 | - {% endif %} | |
| 148 | - </div> | |
| 149 | - | |
| 150 | - <div class="footer"> | |
| 151 | - {% if locale == "fr" %} | |
| 152 | - <p>Cet email a été envoyé automatiquement, merci de ne pas y répondre.</p> | |
| 153 | - <p>Si vous avez des questions, contactez le support technique.</p> | |
| 154 | - {% else %} | |
| 155 | - <p>This email was sent automatically, please do not reply.</p> | |
| 156 | - <p>If you have any questions, please contact technical support.</p> | |
| 157 | - {% endif %} | |
| 158 | - <p style="margin-top: 10px; font-size: 12px; color: #9ca3af;"> | |
| 159 | - © {{ current_year }} {{ app_name }}. Tous droits réservés. | |
| 160 | - </p> | |
| 161 | - </div> | |
| 162 | - </div> | |
| 163 | -</body> | |
| 164 | -</html> |
D
crates/rustwarden-ui/templates/emails/password_change_confirmation.html
+0
-167
@@ -1,167 +0,0 @@
| 1 | -<!DOCTYPE html> | |
| 2 | -<html lang="{{ locale }}"> | |
| 3 | -<head> | |
| 4 | - <meta charset="UTF-8"> | |
| 5 | - <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| 6 | - <title>{% if locale == "fr" %}Confirmation de changement de mot de passe{% else %}Password Change Confirmation{% endif %}</title> | |
| 7 | - <style> | |
| 8 | - body { | |
| 9 | - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | |
| 10 | - line-height: 1.6; | |
| 11 | - color: #333; | |
| 12 | - max-width: 600px; | |
| 13 | - margin: 0 auto; | |
| 14 | - padding: 20px; | |
| 15 | - background-color: #f4f4f4; | |
| 16 | - } | |
| 17 | - .email-container { | |
| 18 | - background-color: #ffffff; | |
| 19 | - border-radius: 8px; | |
| 20 | - padding: 30px; | |
| 21 | - box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| 22 | - } | |
| 23 | - .header { | |
| 24 | - text-align: center; | |
| 25 | - margin-bottom: 30px; | |
| 26 | - } | |
| 27 | - .logo { | |
| 28 | - font-size: 24px; | |
| 29 | - font-weight: bold; | |
| 30 | - color: #2563eb; | |
| 31 | - margin-bottom: 10px; | |
| 32 | - } | |
| 33 | - h1 { | |
| 34 | - color: #1f2937; | |
| 35 | - font-size: 24px; | |
| 36 | - margin-bottom: 20px; | |
| 37 | - } | |
| 38 | - .content { | |
| 39 | - margin-bottom: 30px; | |
| 40 | - } | |
| 41 | - .button-container { | |
| 42 | - text-align: center; | |
| 43 | - margin: 30px 0; | |
| 44 | - } | |
| 45 | - .button { | |
| 46 | - display: inline-block; | |
| 47 | - padding: 12px 30px; | |
| 48 | - background-color: #2563eb; | |
| 49 | - color: #ffffff !important; | |
| 50 | - text-decoration: none; | |
| 51 | - border-radius: 6px; | |
| 52 | - font-weight: 600; | |
| 53 | - font-size: 16px; | |
| 54 | - } | |
| 55 | - .button:hover { | |
| 56 | - background-color: #1d4ed8; | |
| 57 | - } | |
| 58 | - .link-fallback { | |
| 59 | - margin-top: 20px; | |
| 60 | - padding: 15px; | |
| 61 | - background-color: #f3f4f6; | |
| 62 | - border-radius: 6px; | |
| 63 | - word-break: break-all; | |
| 64 | - font-size: 14px; | |
| 65 | - color: #6b7280; | |
| 66 | - } | |
| 67 | - .footer { | |
| 68 | - margin-top: 30px; | |
| 69 | - padding-top: 20px; | |
| 70 | - border-top: 1px solid #e5e7eb; | |
| 71 | - font-size: 14px; | |
| 72 | - color: #6b7280; | |
| 73 | - text-align: center; | |
| 74 | - } | |
| 75 | - .warning { | |
| 76 | - margin-top: 20px; | |
| 77 | - padding: 15px; | |
| 78 | - background-color: #fef3c7; | |
| 79 | - border-left: 4px solid #f59e0b; | |
| 80 | - border-radius: 4px; | |
| 81 | - font-size: 14px; | |
| 82 | - } | |
| 83 | - </style> | |
| 84 | -</head> | |
| 85 | -<body> | |
| 86 | - <div class="email-container"> | |
| 87 | - <div class="header"> | |
| 88 | - <div class="logo">{{ app_name }}</div> | |
| 89 | - </div> | |
| 90 | - | |
| 91 | - <h1> | |
| 92 | - {% if locale == "fr" %} | |
| 93 | - Confirmation de changement de mot de passe | |
| 94 | - {% else %} | |
| 95 | - Password Change Confirmation | |
| 96 | - {% endif %} | |
| 97 | - </h1> | |
| 98 | - | |
| 99 | - <div class="content"> | |
| 100 | - {% if locale == "fr" %} | |
| 101 | - <p>Bonjour {{ username }},</p> | |
| 102 | - <p>Vous avez demandé à changer votre mot de passe pour votre compte {{ app_name }}.</p> | |
| 103 | - <p>Pour confirmer ce changement, cliquez sur le bouton ci-dessous :</p> | |
| 104 | - {% else %} | |
| 105 | - <p>Hello {{ username }},</p> | |
| 106 | - <p>You have requested to change your password for your {{ app_name }} account.</p> | |
| 107 | - <p>To confirm this change, click the button below:</p> | |
| 108 | - {% endif %} | |
| 109 | - </div> | |
| 110 | - | |
| 111 | - <div class="button-container"> | |
| 112 | - <a href="{{ confirmation_url }}" class="button"> | |
| 113 | - {% if locale == "fr" %} | |
| 114 | - Confirmer le changement de mot de passe | |
| 115 | - {% else %} | |
| 116 | - Confirm Password Change | |
| 117 | - {% endif %} | |
| 118 | - </a> | |
| 119 | - </div> | |
| 120 | - | |
| 121 | - <div class="link-fallback"> | |
| 122 | - {% if locale == "fr" %} | |
| 123 | - <p><strong>Le bouton ne fonctionne pas ?</strong></p> | |
| 124 | - <p>Copiez et collez ce lien dans votre navigateur :</p> | |
| 125 | - {% else %} | |
| 126 | - <p><strong>Button not working?</strong></p> | |
| 127 | - <p>Copy and paste this link into your browser:</p> | |
| 128 | - {% endif %} | |
| 129 | - <p style="margin-top: 10px; word-break: break-all;">{{ confirmation_url }}</p> | |
| 130 | - </div> | |
| 131 | - | |
| 132 | - <div class="warning"> | |
| 133 | - {% if locale == "fr" %} | |
| 134 | - <p><strong>⚠️ Important :</strong></p> | |
| 135 | - <ul style="margin: 10px 0; padding-left: 20px;"> | |
| 136 | - <li>Ce lien est valide pendant <strong>{{ expires_in_hours }} heures</strong> uniquement</li> | |
| 137 | - <li>Si vous n'avez pas demandé ce changement, ignorez cet email</li> | |
| 138 | - <li>Votre mot de passe actuel restera inchangé si vous n'utilisez pas ce lien</li> | |
| 139 | - <li>Ce lien est à <strong>usage unique</strong> et ne peut être utilisé qu'une seule fois</li> | |
| 140 | - </ul> | |
| 141 | - {% else %} | |
| 142 | - <p><strong>⚠️ Important:</strong></p> | |
| 143 | - <ul style="margin: 10px 0; padding-left: 20px;"> | |
| 144 | - <li>This link is valid for <strong>{{ expires_in_hours }} hours</strong> only</li> | |
| 145 | - <li>If you did not request this change, please ignore this email</li> | |
| 146 | - <li>Your current password will remain unchanged if you don't use this link</li> | |
| 147 | - <li>This link is <strong>single-use</strong> and can only be used once</li> | |
| 148 | - </ul> | |
| 149 | - {% endif %} | |
| 150 | - </div> | |
| 151 | - | |
| 152 | - <div class="footer"> | |
| 153 | - {% if locale == "fr" %} | |
| 154 | - <p>Cet email a été envoyé automatiquement, merci de ne pas y répondre.</p> | |
| 155 | - <p>Si vous avez des questions, contactez le support technique.</p> | |
| 156 | - {% else %} | |
| 157 | - <p>This email was sent automatically, please do not reply.</p> | |
| 158 | - <p>If you have any questions, please contact technical support.</p> | |
| 159 | - {% endif %} | |
| 160 | - <p style="margin-top: 10px; font-size: 12px; color: #9ca3af;"> | |
| 161 | - © {{ current_year }} {{ app_name }}. {% if locale == "fr" %}Tous droits réservés.{% else %}All rights reserved.{% endif %} | |
| 162 | - </p> | |
| 163 | - </div> | |
| 164 | - </div> | |
| 165 | -</body> | |
| 166 | -</html> | |
| 167 | - |
D
crates/rustwarden-ui/templates/emails/password_changed.html
+0
-135
@@ -1,135 +0,0 @@
| 1 | -<!DOCTYPE html> | |
| 2 | -<html lang="{{ locale }}"> | |
| 3 | -<head> | |
| 4 | - <meta charset="UTF-8"> | |
| 5 | - <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| 6 | - <title>{% if locale == "fr" %}Mot de passe modifié{% else %}Password Changed{% endif %}</title> | |
| 7 | - <style> | |
| 8 | - body { | |
| 9 | - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | |
| 10 | - line-height: 1.6; | |
| 11 | - color: #333; | |
| 12 | - max-width: 600px; | |
| 13 | - margin: 0 auto; | |
| 14 | - padding: 20px; | |
| 15 | - background-color: #f4f4f4; | |
| 16 | - } | |
| 17 | - .email-container { | |
| 18 | - background-color: #ffffff; | |
| 19 | - border-radius: 8px; | |
| 20 | - padding: 30px; | |
| 21 | - box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| 22 | - } | |
| 23 | - .header { | |
| 24 | - text-align: center; | |
| 25 | - margin-bottom: 30px; | |
| 26 | - } | |
| 27 | - .logo { | |
| 28 | - font-size: 24px; | |
| 29 | - font-weight: bold; | |
| 30 | - color: #2563eb; | |
| 31 | - margin-bottom: 10px; | |
| 32 | - } | |
| 33 | - h1 { | |
| 34 | - color: #1f2937; | |
| 35 | - font-size: 24px; | |
| 36 | - margin-bottom: 20px; | |
| 37 | - } | |
| 38 | - .content { | |
| 39 | - margin-bottom: 30px; | |
| 40 | - } | |
| 41 | - .info-box { | |
| 42 | - margin-top: 20px; | |
| 43 | - padding: 15px; | |
| 44 | - background-color: #fef3c7; | |
| 45 | - border-left: 4px solid #f59e0b; | |
| 46 | - border-radius: 4px; | |
| 47 | - font-size: 14px; | |
| 48 | - } | |
| 49 | - .warning-box { | |
| 50 | - margin-top: 20px; | |
| 51 | - padding: 15px; | |
| 52 | - background-color: #fee2e2; | |
| 53 | - border-left: 4px solid #ef4444; | |
| 54 | - border-radius: 4px; | |
| 55 | - font-size: 14px; | |
| 56 | - } | |
| 57 | - .footer { | |
| 58 | - margin-top: 30px; | |
| 59 | - padding-top: 20px; | |
| 60 | - border-top: 1px solid #e5e7eb; | |
| 61 | - font-size: 14px; | |
| 62 | - color: #6b7280; | |
| 63 | - text-align: center; | |
| 64 | - } | |
| 65 | - </style> | |
| 66 | -</head> | |
| 67 | -<body> | |
| 68 | - <div class="email-container"> | |
| 69 | - <div class="header"> | |
| 70 | - <div class="logo">{{ app_name }}</div> | |
| 71 | - </div> | |
| 72 | - | |
| 73 | - <h1> | |
| 74 | - {% if locale == "fr" %} | |
| 75 | - Mot de passe modifié | |
| 76 | - {% else %} | |
| 77 | - Password Changed | |
| 78 | - {% endif %} | |
| 79 | - </h1> | |
| 80 | - | |
| 81 | - <div class="content"> | |
| 82 | - {% if locale == "fr" %} | |
| 83 | - <p>Bonjour {{ username }},</p> | |
| 84 | - <p>Nous vous informons que le mot de passe de votre compte <strong>{{ app_name }}</strong> a été modifié avec succès.</p> | |
| 85 | - <p><strong>Date et heure de modification :</strong> {{ changed_at }}</p> | |
| 86 | - {% else %} | |
| 87 | - <p>Hello {{ username }},</p> | |
| 88 | - <p>We are informing you that the password for your <strong>{{ app_name }}</strong> account has been successfully changed.</p> | |
| 89 | - <p><strong>Date and time of change:</strong> {{ changed_at }}</p> | |
| 90 | - {% endif %} | |
| 91 | - </div> | |
| 92 | - | |
| 93 | - <div class="info-box"> | |
| 94 | - {% if locale == "fr" %} | |
| 95 | - <p><strong>ℹ️ Information :</strong></p> | |
| 96 | - <p>Si vous n'avez pas effectué cette modification, veuillez contacter immédiatement le support technique et réinitialiser votre mot de passe.</p> | |
| 97 | - {% else %} | |
| 98 | - <p><strong>ℹ️ Information:</strong></p> | |
| 99 | - <p>If you did not make this change, please contact technical support immediately and reset your password.</p> | |
| 100 | - {% endif %} | |
| 101 | - </div> | |
| 102 | - | |
| 103 | - <div class="warning-box"> | |
| 104 | - {% if locale == "fr" %} | |
| 105 | - <p><strong>⚠️ Sécurité :</strong></p> | |
| 106 | - <ul style="margin: 10px 0; padding-left: 20px;"> | |
| 107 | - <li>Ne partagez jamais votre mot de passe avec qui que ce soit</li> | |
| 108 | - <li>Utilisez un mot de passe unique et fort</li> | |
| 109 | - <li>Si vous suspectez une activité suspecte, changez votre mot de passe immédiatement</li> | |
| 110 | - </ul> | |
| 111 | - {% else %} | |
| 112 | - <p><strong>⚠️ Security:</strong></p> | |
| 113 | - <ul style="margin: 10px 0; padding-left: 20px;"> | |
| 114 | - <li>Never share your password with anyone</li> | |
| 115 | - <li>Use a unique and strong password</li> | |
| 116 | - <li>If you suspect suspicious activity, change your password immediately</li> | |
| 117 | - </ul> | |
| 118 | - {% endif %} | |
| 119 | - </div> | |
| 120 | - | |
| 121 | - <div class="footer"> | |
| 122 | - {% if locale == "fr" %} | |
| 123 | - <p>Cet email a été envoyé automatiquement pour votre sécurité.</p> | |
| 124 | - <p>Si vous avez des questions, contactez le support technique.</p> | |
| 125 | - {% else %} | |
| 126 | - <p>This email was sent automatically for your security.</p> | |
| 127 | - <p>If you have any questions, please contact technical support.</p> | |
| 128 | - {% endif %} | |
| 129 | - <p style="margin-top: 10px; font-size: 12px; color: #9ca3af;"> | |
| 130 | - © {{ current_year }} {{ app_name }}. Tous droits réservés. | |
| 131 | - </p> | |
| 132 | - </div> | |
| 133 | - </div> | |
| 134 | -</body> | |
| 135 | -</html> |
D
crates/rustwarden-ui/templates/emails/password_reset.html
+0
-164
@@ -1,164 +0,0 @@
| 1 | -<!DOCTYPE html> | |
| 2 | -<html lang="{{ locale }}"> | |
| 3 | -<head> | |
| 4 | - <meta charset="UTF-8"> | |
| 5 | - <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| 6 | - <title>{% if locale == "fr" %}Réinitialisation de mot de passe{% else %}Password Reset{% endif %}</title> | |
| 7 | - <style> | |
| 8 | - body { | |
| 9 | - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | |
| 10 | - line-height: 1.6; | |
| 11 | - color: #333; | |
| 12 | - max-width: 600px; | |
| 13 | - margin: 0 auto; | |
| 14 | - padding: 20px; | |
| 15 | - background-color: #f4f4f4; | |
| 16 | - } | |
| 17 | - .email-container { | |
| 18 | - background-color: #ffffff; | |
| 19 | - border-radius: 8px; | |
| 20 | - padding: 30px; | |
| 21 | - box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| 22 | - } | |
| 23 | - .header { | |
| 24 | - text-align: center; | |
| 25 | - margin-bottom: 30px; | |
| 26 | - } | |
| 27 | - .logo { | |
| 28 | - font-size: 24px; | |
| 29 | - font-weight: bold; | |
| 30 | - color: #2563eb; | |
| 31 | - margin-bottom: 10px; | |
| 32 | - } | |
| 33 | - h1 { | |
| 34 | - color: #1f2937; | |
| 35 | - font-size: 24px; | |
| 36 | - margin-bottom: 20px; | |
| 37 | - } | |
| 38 | - .content { | |
| 39 | - margin-bottom: 30px; | |
| 40 | - } | |
| 41 | - .button-container { | |
| 42 | - text-align: center; | |
| 43 | - margin: 30px 0; | |
| 44 | - } | |
| 45 | - .button { | |
| 46 | - display: inline-block; | |
| 47 | - padding: 12px 30px; | |
| 48 | - background-color: #2563eb; | |
| 49 | - color: #ffffff !important; | |
| 50 | - text-decoration: none; | |
| 51 | - border-radius: 6px; | |
| 52 | - font-weight: 600; | |
| 53 | - font-size: 16px; | |
| 54 | - } | |
| 55 | - .button:hover { | |
| 56 | - background-color: #1d4ed8; | |
| 57 | - } | |
| 58 | - .link-fallback { | |
| 59 | - margin-top: 20px; | |
| 60 | - padding: 15px; | |
| 61 | - background-color: #f3f4f6; | |
| 62 | - border-radius: 6px; | |
| 63 | - word-break: break-all; | |
| 64 | - font-size: 14px; | |
| 65 | - color: #6b7280; | |
| 66 | - } | |
| 67 | - .footer { | |
| 68 | - margin-top: 30px; | |
| 69 | - padding-top: 20px; | |
| 70 | - border-top: 1px solid #e5e7eb; | |
| 71 | - font-size: 14px; | |
| 72 | - color: #6b7280; | |
| 73 | - text-align: center; | |
| 74 | - } | |
| 75 | - .warning { | |
| 76 | - margin-top: 20px; | |
| 77 | - padding: 15px; | |
| 78 | - background-color: #fef3c7; | |
| 79 | - border-left: 4px solid #f59e0b; | |
| 80 | - border-radius: 4px; | |
| 81 | - font-size: 14px; | |
| 82 | - } | |
| 83 | - </style> | |
| 84 | -</head> | |
| 85 | -<body> | |
| 86 | - <div class="email-container"> | |
| 87 | - <div class="header"> | |
| 88 | - <div class="logo">{{ app_name }}</div> | |
| 89 | - </div> | |
| 90 | - | |
| 91 | - <h1> | |
| 92 | - {% if locale == "fr" %} | |
| 93 | - Réinitialisation de votre mot de passe | |
| 94 | - {% else %} | |
| 95 | - Reset Your Password | |
| 96 | - {% endif %} | |
| 97 | - </h1> | |
| 98 | - | |
| 99 | - <div class="content"> | |
| 100 | - {% if locale == "fr" %} | |
| 101 | - <p>Bonjour {{ username }},</p> | |
| 102 | - <p>Vous avez demandé à réinitialiser votre mot de passe pour votre compte {{ app_name }}.</p> | |
| 103 | - <p>Cliquez sur le bouton ci-dessous pour réinitialiser votre mot de passe :</p> | |
| 104 | - {% else %} | |
| 105 | - <p>Hello {{ username }},</p> | |
| 106 | - <p>You have requested to reset your password for your {{ app_name }} account.</p> | |
| 107 | - <p>Click the button below to reset your password:</p> | |
| 108 | - {% endif %} | |
| 109 | - </div> | |
| 110 | - | |
| 111 | - <div class="button-container"> | |
| 112 | - <a href="{{ reset_url }}" class="button"> | |
| 113 | - {% if locale == "fr" %} | |
| 114 | - Réinitialiser mon mot de passe | |
| 115 | - {% else %} | |
| 116 | - Reset My Password | |
| 117 | - {% endif %} | |
| 118 | - </a> | |
| 119 | - </div> | |
| 120 | - | |
| 121 | - <div class="link-fallback"> | |
| 122 | - {% if locale == "fr" %} | |
| 123 | - <p><strong>Le bouton ne fonctionne pas ?</strong></p> | |
| 124 | - <p>Copiez et collez ce lien dans votre navigateur :</p> | |
| 125 | - {% else %} | |
| 126 | - <p><strong>Button not working?</strong></p> | |
| 127 | - <p>Copy and paste this link into your browser:</p> | |
| 128 | - {% endif %} | |
| 129 | - <p style="margin-top: 10px; word-break: break-all;">{{ reset_url }}</p> | |
| 130 | - </div> | |
| 131 | - | |
| 132 | - <div class="warning"> | |
| 133 | - {% if locale == "fr" %} | |
| 134 | - <p><strong>⚠️ Important :</strong></p> | |
| 135 | - <ul style="margin: 10px 0; padding-left: 20px;"> | |
| 136 | - <li>Ce lien est valide pendant <strong>1 heure</strong> uniquement</li> | |
| 137 | - <li>Si vous n'avez pas demandé cette réinitialisation, ignorez cet email</li> | |
| 138 | - <li>Votre mot de passe actuel restera inchangé si vous n'utilisez pas ce lien</li> | |
| 139 | - </ul> | |
| 140 | - {% else %} | |
| 141 | - <p><strong>⚠️ Important:</strong></p> | |
| 142 | - <ul style="margin: 10px 0; padding-left: 20px;"> | |
| 143 | - <li>This link is valid for <strong>1 hour</strong> only</li> | |
| 144 | - <li>If you did not request this reset, please ignore this email</li> | |
| 145 | - <li>Your current password will remain unchanged if you don't use this link</li> | |
| 146 | - </ul> | |
| 147 | - {% endif %} | |
| 148 | - </div> | |
| 149 | - | |
| 150 | - <div class="footer"> | |
| 151 | - {% if locale == "fr" %} | |
| 152 | - <p>Cet email a été envoyé automatiquement, merci de ne pas y répondre.</p> | |
| 153 | - <p>Si vous avez des questions, contactez le support technique.</p> | |
| 154 | - {% else %} | |
| 155 | - <p>This email was sent automatically, please do not reply.</p> | |
| 156 | - <p>If you have any questions, please contact technical support.</p> | |
| 157 | - {% endif %} | |
| 158 | - <p style="margin-top: 10px; font-size: 12px; color: #9ca3af;"> | |
| 159 | - © {{ current_year }} {{ app_name }}. Tous droits réservés. | |
| 160 | - </p> | |
| 161 | - </div> | |
| 162 | - </div> | |
| 163 | -</body> | |
| 164 | -</html> |
D
crates/rustwarden-ui/templates/emails/welcome.html
+0
-176
@@ -1,176 +0,0 @@
| 1 | -<!DOCTYPE html> | |
| 2 | -<html lang="{{ locale }}"> | |
| 3 | -<head> | |
| 4 | - <meta charset="UTF-8"> | |
| 5 | - <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| 6 | - <title>{% if locale == "fr" %}Bienvenue{% else %}Welcome{% endif %}</title> | |
| 7 | - <style> | |
| 8 | - body { | |
| 9 | - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | |
| 10 | - line-height: 1.6; | |
| 11 | - color: #333; | |
| 12 | - max-width: 600px; | |
| 13 | - margin: 0 auto; | |
| 14 | - padding: 20px; | |
| 15 | - background-color: #f4f4f4; | |
| 16 | - } | |
| 17 | - .email-container { | |
| 18 | - background-color: #ffffff; | |
| 19 | - border-radius: 8px; | |
| 20 | - padding: 30px; | |
| 21 | - box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
| 22 | - } | |
| 23 | - .header { | |
| 24 | - text-align: center; | |
| 25 | - margin-bottom: 30px; | |
| 26 | - } | |
| 27 | - .logo { | |
| 28 | - font-size: 28px; | |
| 29 | - font-weight: bold; | |
| 30 | - color: #2563eb; | |
| 31 | - margin-bottom: 10px; | |
| 32 | - } | |
| 33 | - .welcome-icon { | |
| 34 | - font-size: 48px; | |
| 35 | - margin-bottom: 20px; | |
| 36 | - } | |
| 37 | - h1 { | |
| 38 | - color: #1f2937; | |
| 39 | - font-size: 28px; | |
| 40 | - margin-bottom: 20px; | |
| 41 | - text-align: center; | |
| 42 | - } | |
| 43 | - .content { | |
| 44 | - margin-bottom: 30px; | |
| 45 | - } | |
| 46 | - .button-container { | |
| 47 | - text-align: center; | |
| 48 | - margin: 30px 0; | |
| 49 | - } | |
| 50 | - .button { | |
| 51 | - display: inline-block; | |
| 52 | - padding: 14px 32px; | |
| 53 | - background-color: #10b981; | |
| 54 | - color: #ffffff !important; | |
| 55 | - text-decoration: none; | |
| 56 | - border-radius: 6px; | |
| 57 | - font-weight: 600; | |
| 58 | - font-size: 16px; | |
| 59 | - } | |
| 60 | - .button:hover { | |
| 61 | - background-color: #059669; | |
| 62 | - } | |
| 63 | - .features-list { | |
| 64 | - margin: 20px 0; | |
| 65 | - padding: 20px; | |
| 66 | - background-color: #f3f4f6; | |
| 67 | - border-radius: 6px; | |
| 68 | - } | |
| 69 | - .feature-item { | |
| 70 | - margin: 12px 0; | |
| 71 | - padding-left: 25px; | |
| 72 | - position: relative; | |
| 73 | - } | |
| 74 | - .feature-item::before { | |
| 75 | - content: "✓"; | |
| 76 | - position: absolute; | |
| 77 | - left: 0; | |
| 78 | - color: #10b981; | |
| 79 | - font-weight: bold; | |
| 80 | - font-size: 18px; | |
| 81 | - } | |
| 82 | - .info-box { | |
| 83 | - margin-top: 20px; | |
| 84 | - padding: 15px; | |
| 85 | - background-color: #dbeafe; | |
| 86 | - border-left: 4px solid #3b82f6; | |
| 87 | - border-radius: 4px; | |
| 88 | - font-size: 14px; | |
| 89 | - } | |
| 90 | - .footer { | |
| 91 | - margin-top: 30px; | |
| 92 | - padding-top: 20px; | |
| 93 | - border-top: 1px solid #e5e7eb; | |
| 94 | - font-size: 14px; | |
| 95 | - color: #6b7280; | |
| 96 | - text-align: center; | |
| 97 | - } | |
| 98 | - </style> | |
| 99 | -</head> | |
| 100 | -<body> | |
| 101 | - <div class="email-container"> | |
| 102 | - <div class="header"> | |
| 103 | - <div class="logo">{{ app_name }}</div> | |
| 104 | - <div class="welcome-icon">🎉</div> | |
| 105 | - </div> | |
| 106 | - | |
| 107 | - <h1> | |
| 108 | - {% if locale == "fr" %} | |
| 109 | - Bienvenue sur {{ app_name }} ! | |
| 110 | - {% else %} | |
| 111 | - Welcome to {{ app_name }}! | |
| 112 | - {% endif %} | |
| 113 | - </h1> | |
| 114 | - | |
| 115 | - <div class="content"> | |
| 116 | - {% if locale == "fr" %} | |
| 117 | - <p>Bonjour {{ username }},</p> | |
| 118 | - <p>Nous sommes ravis de vous accueillir sur <strong>{{ app_name }}</strong> ! Votre compte a été activé avec succès.</p> | |
| 119 | - <p>Vous pouvez maintenant profiter de toutes les fonctionnalités de la plateforme.</p> | |
| 120 | - {% else %} | |
| 121 | - <p>Hello {{ username }},</p> | |
| 122 | - <p>We are delighted to welcome you to <strong>{{ app_name }}</strong>! Your account has been successfully activated.</p> | |
| 123 | - <p>You can now enjoy all the platform's features.</p> | |
| 124 | - {% endif %} | |
| 125 | - </div> | |
| 126 | - | |
| 127 | - <div class="button-container"> | |
| 128 | - <a href="{{ login_url }}" class="button"> | |
| 129 | - {% if locale == "fr" %} | |
| 130 | - Se connecter | |
| 131 | - {% else %} | |
| 132 | - Log In | |
| 133 | - {% endif %} | |
| 134 | - </a> | |
| 135 | - </div> | |
| 136 | - | |
| 137 | - <div class="features-list"> | |
| 138 | - {% if locale == "fr" %} | |
| 139 | - <p style="font-weight: 600; margin-bottom: 15px;">Ce que vous pouvez faire :</p> | |
| 140 | - <div class="feature-item">Accéder à votre tableau de bord personnalisé</div> | |
| 141 | - <div class="feature-item">Gérer votre profil et vos préférences</div> | |
| 142 | - <div class="feature-item">Profiter de toutes les fonctionnalités disponibles</div> | |
| 143 | - {% else %} | |
| 144 | - <p style="font-weight: 600; margin-bottom: 15px;">What you can do:</p> | |
| 145 | - <div class="feature-item">Access your personalized dashboard</div> | |
| 146 | - <div class="feature-item">Manage your profile and preferences</div> | |
| 147 | - <div class="feature-item">Enjoy all available features</div> | |
| 148 | - {% endif %} | |
| 149 | - </div> | |
| 150 | - | |
| 151 | - <div class="info-box"> | |
| 152 | - {% if locale == "fr" %} | |
| 153 | - <p><strong>ℹ️ Besoin d'aide ?</strong></p> | |
| 154 | - <p>Si vous avez des questions ou besoin d'assistance, n'hésitez pas à consulter notre documentation ou à contacter le support technique.</p> | |
| 155 | - {% else %} | |
| 156 | - <p><strong>ℹ️ Need Help?</strong></p> | |
| 157 | - <p>If you have any questions or need assistance, feel free to check our documentation or contact technical support.</p> | |
| 158 | - {% endif %} | |
| 159 | - </div> | |
| 160 | - | |
| 161 | - <div class="footer"> | |
| 162 | - {% if locale == "fr" %} | |
| 163 | - <p>Merci de nous faire confiance et bonne utilisation de {{ app_name }} !</p> | |
| 164 | - <p>Si vous avez des questions, contactez le support technique.</p> | |
| 165 | - {% else %} | |
| 166 | - <p>Thank you for trusting us and enjoy using {{ app_name }}!</p> | |
| 167 | - <p>If you have any questions, please contact technical support.</p> | |
| 168 | - {% endif %} | |
| 169 | - <p style="margin-top: 10px; font-size: 12px; color: #9ca3af;"> | |
| 170 | - © {{ current_year }} {{ app_name }}. Tous droits réservés. | |
| 171 | - </p> | |
| 172 | - </div> | |
| 173 | - </div> | |
| 174 | -</body> | |
| 175 | -</html> | |
| 176 | - |
D
crates/rustwarden-ui/templates/errors/401.html
+0
-36
@@ -1,36 +0,0 @@
| 1 | -<!DOCTYPE html> | |
| 2 | -<html lang="fr"> | |
| 3 | -<head> | |
| 4 | - <meta charset="UTF-8"> | |
| 5 | - <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| 6 | - <title>401 - Non Authentifié</title> | |
| 7 | - <link href="/static/css/tailwind.css" rel="stylesheet"> | |
| 8 | -</head> | |
| 9 | -<body class="bg-base-200 min-h-screen flex items-center justify-center px-4"> | |
| 10 | - <div class="max-w-md w-full text-center"> | |
| 11 | - <!-- Error Icon --> | |
| 12 | - <div class="mb-8"> | |
| 13 | - <div class="inline-flex items-center justify-center w-24 h-24 rounded-full bg-error/20"> | |
| 14 | - <span class="text-6xl">🔒</span> | |
| 15 | - </div> | |
| 16 | - </div> | |
| 17 | - | |
| 18 | - <!-- Error Message --> | |
| 19 | - <h1 class="text-4xl font-bold text-base-content mb-4">401</h1> | |
| 20 | - <h2 class="text-2xl font-semibold text-base-content mb-4">Non Authentifié</h2> | |
| 21 | - <p class="text-base-content/70 mb-8"> | |
| 22 | - Vous devez vous connecter pour accéder à cette page. | |
| 23 | - </p> | |
| 24 | - | |
| 25 | - <!-- Actions --> | |
| 26 | - <div class="space-y-3"> | |
| 27 | - <a href="/login" class="btn btn-primary w-full"> | |
| 28 | - Se Connecter | |
| 29 | - </a> | |
| 30 | - <a href="/" class="btn btn-ghost w-full"> | |
| 31 | - Retour à l'Accueil | |
| 32 | - </a> | |
| 33 | - </div> | |
| 34 | - </div> | |
| 35 | -</body> | |
| 36 | -</html> |
D
crates/rustwarden-ui/templates/errors/403.html
+0
-36
@@ -1,36 +0,0 @@
| 1 | -<!DOCTYPE html> | |
| 2 | -<html lang="fr"> | |
| 3 | -<head> | |
| 4 | - <meta charset="UTF-8"> | |
| 5 | - <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| 6 | - <title>403 - Accès Refusé</title> | |
| 7 | - <link href="/static/css/tailwind.css" rel="stylesheet"> | |
| 8 | -</head> | |
| 9 | -<body class="bg-base-200 min-h-screen flex items-center justify-center px-4"> | |
| 10 | - <div class="max-w-md w-full text-center"> | |
| 11 | - <!-- Error Icon --> | |
| 12 | - <div class="mb-8"> | |
| 13 | - <div class="inline-flex items-center justify-center w-24 h-24 rounded-full bg-warning/20"> | |
| 14 | - <span class="text-6xl">⛔</span> | |
| 15 | - </div> | |
| 16 | - </div> | |
| 17 | - | |
| 18 | - <!-- Error Message --> | |
| 19 | - <h1 class="text-4xl font-bold text-base-content mb-4">403</h1> | |
| 20 | - <h2 class="text-2xl font-semibold text-base-content mb-4">Accès Refusé</h2> | |
| 21 | - <p class="text-base-content/70 mb-8"> | |
| 22 | - Vous n'avez pas les permissions nécessaires pour accéder à cette page. | |
| 23 | - </p> | |
| 24 | - | |
| 25 | - <!-- Actions --> | |
| 26 | - <div class="space-y-3"> | |
| 27 | - <a href="/home" class="btn btn-primary w-full"> | |
| 28 | - Retour à l'Accueil | |
| 29 | - </a> | |
| 30 | - <a href="/profile" class="btn btn-ghost w-full"> | |
| 31 | - Mon profil | |
| 32 | - </a> | |
| 33 | - </div> | |
| 34 | - </div> | |
| 35 | -</body> | |
| 36 | -</html> |
D
crates/rustwarden-ui/templates/errors/500.html
+0
-41
@@ -1,41 +0,0 @@
| 1 | -<!DOCTYPE html> | |
| 2 | -<html lang="fr"> | |
| 3 | -<head> | |
| 4 | - <meta charset="UTF-8"> | |
| 5 | - <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| 6 | - <title>500 - Erreur Serveur</title> | |
| 7 | - <link href="/static/css/tailwind.css" rel="stylesheet"> | |
| 8 | -</head> | |
| 9 | -<body class="bg-base-200 min-h-screen flex items-center justify-center px-4"> | |
| 10 | - <div class="max-w-md w-full text-center"> | |
| 11 | - <!-- Error Icon --> | |
| 12 | - <div class="mb-8"> | |
| 13 | - <div class="inline-flex items-center justify-center w-24 h-24 rounded-full bg-error/20"> | |
| 14 | - <span class="text-6xl">⚠️</span> | |
| 15 | - </div> | |
| 16 | - </div> | |
| 17 | - | |
| 18 | - <!-- Error Message --> | |
| 19 | - <h1 class="text-4xl font-bold text-base-content mb-4">500</h1> | |
| 20 | - <h2 class="text-2xl font-semibold text-base-content mb-4">Erreur Serveur</h2> | |
| 21 | - <p class="text-base-content/70 mb-8"> | |
| 22 | - Une erreur interne est survenue. Nos équipes ont été notifiées et travaillent sur le problème. | |
| 23 | - </p> | |
| 24 | - | |
| 25 | - <!-- Actions --> | |
| 26 | - <div class="space-y-3"> | |
| 27 | - <button onclick="window.location.reload()" class="btn btn-primary w-full"> | |
| 28 | - Réessayer | |
| 29 | - </button> | |
| 30 | - <a href="/home" class="btn btn-ghost w-full"> | |
| 31 | - Retour à l'Accueil | |
| 32 | - </a> | |
| 33 | - </div> | |
| 34 | - | |
| 35 | - <!-- Support Info --> | |
| 36 | - <div class="mt-8 text-sm text-base-content/50"> | |
| 37 | - <p>Si le problème persiste, contactez le support.</p> | |
| 38 | - </div> | |
| 39 | - </div> | |
| 40 | -</body> | |
| 41 | -</html> |
A
crates/rustwarden-ui/templates/forgot_password.html
+108
-0
@@ -0,0 +1,108 @@
| 1 | +{% extends "base.html" %} | |
| 2 | + | |
| 3 | +{% block title %}{{ page_title }}{% endblock %} | |
| 4 | + | |
| 5 | +{% block content %} | |
| 6 | +<div class="min-h-screen flex items-center justify-center bg-base-200 px-4"> | |
| 7 | + <div class="max-w-lg w-full space-y-6"> | |
| 8 | + <div class="text-center"> | |
| 9 | + <h1 class="text-3xl font-bold text-base-content mb-2">{{ title }}</h1> | |
| 10 | + <p class="text-base-content/70">{{ subtitle }}</p> | |
| 11 | + </div> | |
| 12 | + | |
| 13 | + <div id="alert-error" class="hidden alert alert-error"> | |
| 14 | + <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> | |
| 15 | + <span id="alert-error-text"></span> | |
| 16 | + </div> | |
| 17 | + | |
| 18 | + <div id="alert-success" class="hidden alert alert-success"> | |
| 19 | + <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg> | |
| 20 | + <span id="alert-success-text"></span> | |
| 21 | + </div> | |
| 22 | + | |
| 23 | + <div class="card bg-base-100 shadow-xl p-8"> | |
| 24 | + <form id="forgot-form" class="space-y-4" | |
| 25 | + data-error-default="{{ error_default }}" | |
| 26 | + data-success-default="{{ success_default }}" | |
| 27 | + data-error-missing-email="{{ error_missing_email }}" | |
| 28 | + data-error-cannot-send="{{ error_cannot_send }}"> | |
| 29 | + <div class="form-control"> | |
| 30 | + <label for="email" class="label"> | |
| 31 | + <span class="label-text">{{ email_label }}</span> | |
| 32 | + </label> | |
| 33 | + <input | |
| 34 | + type="email" | |
| 35 | + id="email" | |
| 36 | + name="email" | |
| 37 | + required | |
| 38 | + autocomplete="email" | |
| 39 | + class="input input-bordered w-full" | |
| 40 | + placeholder="{{ email_placeholder }}"> | |
| 41 | + </div> | |
| 42 | + | |
| 43 | + <div class="form-control mt-6"> | |
| 44 | + <button type="submit" class="btn btn-primary w-full"> | |
| 45 | + {{ submit }} | |
| 46 | + </button> | |
| 47 | + </div> | |
| 48 | + </form> | |
| 49 | + | |
| 50 | + <div class="mt-4 text-sm text-center"> | |
| 51 | + <a href="/login" class="text-primary hover:underline">{{ back_to_login }}</a> | |
| 52 | + </div> | |
| 53 | + </div> | |
| 54 | + </div> | |
| 55 | +</div> | |
| 56 | + | |
| 57 | +<script> | |
| 58 | + const form = document.getElementById('forgot-form'); | |
| 59 | + const MSG_ERROR_DEFAULT = form?.dataset.errorDefault || ""; | |
| 60 | + const MSG_SUCCESS_DEFAULT = form?.dataset.successDefault || ""; | |
| 61 | + const MSG_ERROR_MISSING_EMAIL = form?.dataset.errorMissingEmail || ""; | |
| 62 | + const MSG_ERROR_CANNOT_SEND = form?.dataset.errorCannotSend || ""; | |
| 63 | + const alertError = document.getElementById('alert-error'); | |
| 64 | + const alertErrorText = document.getElementById('alert-error-text'); | |
| 65 | + const alertSuccess = document.getElementById('alert-success'); | |
| 66 | + const alertSuccessText = document.getElementById('alert-success-text'); | |
| 67 | + | |
| 68 | + function showError(msg) { | |
| 69 | + if (alertError && alertErrorText) { | |
| 70 | + alertErrorText.textContent = msg || MSG_ERROR_DEFAULT; | |
| 71 | + alertError.classList.remove('hidden'); | |
| 72 | + } | |
| 73 | + if (alertSuccess) alertSuccess.classList.add('hidden'); | |
| 74 | + } | |
| 75 | + | |
| 76 | + function showSuccess(msg) { | |
| 77 | + if (alertSuccess && alertSuccessText) { | |
| 78 | + alertSuccessText.textContent = msg || MSG_SUCCESS_DEFAULT; | |
| 79 | + alertSuccess.classList.remove('hidden'); | |
| 80 | + } | |
| 81 | + if (alertError) alertError.classList.add('hidden'); | |
| 82 | + if (form) form.classList.add('hidden'); | |
| 83 | + } | |
| 84 | + | |
| 85 | + form?.addEventListener('submit', (e) => { | |
| 86 | + e.preventDefault(); | |
| 87 | + const email = document.getElementById('email')?.value; | |
| 88 | + if (!email) { | |
| 89 | + showError(MSG_ERROR_MISSING_EMAIL); | |
| 90 | + return; | |
| 91 | + } | |
| 92 | + fetch('/api/v1/auth/forgot-password', { | |
| 93 | + method: 'POST', | |
| 94 | + headers: { 'Content-Type': 'application/json' }, | |
| 95 | + body: JSON.stringify({ email }), | |
| 96 | + credentials: 'include' | |
| 97 | + }) | |
| 98 | + .then(async (resp) => { | |
| 99 | + if (resp.ok) { | |
| 100 | + showSuccess(MSG_SUCCESS_DEFAULT); | |
| 101 | + } else { | |
| 102 | + showError(MSG_ERROR_CANNOT_SEND); | |
| 103 | + } | |
| 104 | + }) | |
| 105 | + .catch(() => showError(MSG_ERROR_CANNOT_SEND)); | |
| 106 | + }); | |
| 107 | +</script> | |
| 108 | +{% endblock %} |
M
crates/rustwarden-ui/templates/login.html
+2
-1
@@ -16,7 +16,8 @@
| 16 | 16 | <script src="/static/js/htmx.min.js"></script> |
| 17 | 17 | |
| 18 | 18 | <!-- Alpine.js (local) --> |
| 19 | - <script src="/static/js/alpine.min.js" defer></script> | |
| 19 | + <script src="/static/js/alpine-components.js" defer></script> | |
| 20 | + <script src="/static/js/alpine-csp.min.js" defer></script> | |
| 20 | 21 | </head> |
| 21 | 22 | <body class="bg-base-200 min-h-screen flex items-center justify-center px-4"> |
| 22 | 23 | <div class="max-w-md w-full"> |
M
crates/rustwarden-ui/templates/login_2fa.html
+4
-47
@@ -6,10 +6,11 @@
| 6 | 6 | <title>{{ page_title }}</title> |
| 7 | 7 | <link href="/static/css/tailwind.css" rel="stylesheet"> |
| 8 | 8 | <link href="/static/css/custom.css" rel="stylesheet"> |
| 9 | - <script src="/static/js/alpine.min.js" defer></script> | |
| 9 | + <script src="/static/js/alpine-components.js" defer></script> | |
| 10 | + <script src="/static/js/alpine-csp.min.js" defer></script> | |
| 10 | 11 | </head> |
| 11 | 12 | <body class="min-h-screen bg-base-200"> |
| 12 | - <div class="min-h-screen flex items-center justify-center px-4" x-data="twoFactorPage()"> | |
| 13 | + <div class="min-h-screen flex items-center justify-center px-4" x-data="twoFactorPage" data-error-invalid="{{ error_invalid_code }}" data-error-network="{{ error_network }}"> | |
| 13 | 14 | <div class="card bg-base-100 shadow-xl w-full max-w-md"> |
| 14 | 15 | <div class="card-body"> |
| 15 | 16 | <div class="text-center mb-6"> |
@@ -61,7 +62,7 @@
| 61 | 62 | |
| 62 | 63 | <!-- Toggle backup code --> |
| 63 | 64 | <div class="text-center mt-4"> |
| 64 | - <button class="btn btn-ghost btn-sm" @click="useBackupCode = !useBackupCode; code = '';"> | |
| 65 | + <button class="btn btn-ghost btn-sm" @click="toggleBackup()"> | |
| 65 | 66 | <span x-show="!useBackupCode">{{ use_backup_code }}</span> |
| 66 | 67 | <span x-show="useBackupCode">{{ use_totp_code }}</span> |
| 67 | 68 | </button> |
@@ -75,49 +76,5 @@
| 75 | 76 | </div> |
| 76 | 77 | </div> |
| 77 | 78 | |
| 78 | - <script> | |
| 79 | - function twoFactorPage() { | |
| 80 | - return { | |
| 81 | - code: '', | |
| 82 | - error: '', | |
| 83 | - verifying: false, | |
| 84 | - useBackupCode: false, | |
| 85 | - challengeToken: new URLSearchParams(window.location.search).get('challenge') || '', | |
| 86 | - | |
| 87 | - async verify() { | |
| 88 | - if (!this.code || !this.challengeToken) return; | |
| 89 | - this.verifying = true; | |
| 90 | - this.error = ''; | |
| 91 | - try { | |
| 92 | - const res = await fetch('/api/v1/auth/2fa/verify', { | |
| 93 | - method: 'POST', | |
| 94 | - headers: { 'Content-Type': 'application/json' }, | |
| 95 | - body: JSON.stringify({ | |
| 96 | - challenge_token: this.challengeToken, | |
| 97 | - code: this.code, | |
| 98 | - }), | |
| 99 | - }); | |
| 100 | - const data = await res.json(); | |
| 101 | - if (res.ok) { | |
| 102 | - if (data.refresh_token) { | |
| 103 | - localStorage.setItem('refresh_token', data.refresh_token); | |
| 104 | - } | |
| 105 | - if (data.user && data.user.must_change_password === true) { | |
| 106 | - window.location.href = '/settings/password?must_change=true'; | |
| 107 | - } else { | |
| 108 | - window.location.href = '/home'; | |
| 109 | - } | |
| 110 | - } else { | |
| 111 | - this.error = data.error || '{{ error_invalid_code }}'; | |
| 112 | - this.code = ''; | |
| 113 | - } | |
| 114 | - } catch (e) { | |
| 115 | - this.error = '{{ error_network }}'; | |
| 116 | - } | |
| 117 | - this.verifying = false; | |
| 118 | - } | |
| 119 | - }; | |
| 120 | - } | |
| 121 | - </script> | |
| 122 | 79 | </body> |
| 123 | 80 | </html> |
M
crates/rustwarden-ui/templates/profile.html
+118
-54
@@ -28,21 +28,25 @@
| 28 | 28 | <div class="card-body"> |
| 29 | 29 | <h2 class="card-title mb-4">{{ account_info }}</h2> |
| 30 | 30 | |
| 31 | - <form id="profileForm" method="POST" action="/profile" class="space-y-6"> | |
| 32 | - <!-- CSRF Token --> | |
| 31 | + <form id="profileForm" class="space-y-6" | |
| 32 | + data-saved="{{ profile_saved }}" | |
| 33 | + data-error="{{ profile_error }}" | |
| 34 | + data-network="{{ network_error }}" | |
| 35 | + data-saving="{{ saving }}" | |
| 36 | + data-save-label="{{ save_button }}"> | |
| 33 | 37 | <input type="hidden" name="csrf_token" value="{{ csrf_token }}"> |
| 34 | 38 | |
| 35 | - <!-- Username --> | |
| 36 | 39 | <div class="form-control"> |
| 37 | - <label class="label"> | |
| 40 | + <label class="label" for="username"> | |
| 38 | 41 | <span class="label-text font-semibold">{{ username_label }}</span> |
| 39 | 42 | <span class="label-text-alt text-warning">{{ username_public }}</span> |
| 40 | 43 | </label> |
| 41 | - <input | |
| 42 | - type="text" | |
| 43 | - name="username" | |
| 44 | - value="{{ username }}" | |
| 45 | - placeholder="{{ username_placeholder }}" | |
| 44 | + <input | |
| 45 | + type="text" | |
| 46 | + id="username" | |
| 47 | + name="username" | |
| 48 | + value="{{ username }}" | |
| 49 | + placeholder="{{ username_placeholder }}" | |
| 46 | 50 | class="input input-bordered w-full" |
| 47 | 51 | minlength="3" |
| 48 | 52 | maxlength="39" |
@@ -55,38 +59,73 @@
| 55 | 59 | </label> |
| 56 | 60 | </div> |
| 57 | 61 | |
| 58 | - <!-- Email --> | |
| 59 | 62 | <div class="form-control"> |
| 60 | 63 | <label class="label"> |
| 61 | 64 | <span class="label-text font-semibold">{{ email_label }}</span> |
| 62 | 65 | <span class="label-text-alt text-info">{{ email_used_for }}</span> |
| 63 | 66 | </label> |
| 64 | - <input | |
| 65 | - type="email" | |
| 66 | - name="email" | |
| 67 | - value="{{ email }}" | |
| 68 | - placeholder="{{ email_placeholder }}" | |
| 67 | + <input | |
| 68 | + type="email" | |
| 69 | + value="{{ email }}" | |
| 69 | 70 | class="input input-bordered w-full" |
| 70 | - required | |
| 71 | + readonly | |
| 71 | 72 | > |
| 72 | 73 | <label class="label"> |
| 73 | 74 | <span class="label-text-alt">{{ email_warning }}</span> |
| 74 | 75 | </label> |
| 75 | 76 | </div> |
| 76 | 77 | |
| 77 | - <!-- Alert Info --> | |
| 78 | - <div class="alert alert-info"> | |
| 79 | - <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg> | |
| 80 | - <span>{{ email_change_info }}</span> | |
| 78 | + <div class="card-actions justify-end"> | |
| 79 | + <a href="/profile" class="btn btn-ghost">{{ cancel }}</a> | |
| 80 | + <button type="submit" class="btn btn-primary">{{ save_button }}</button> | |
| 81 | 81 | </div> |
| 82 | + </form> | |
| 83 | + </div> | |
| 84 | + </div> | |
| 82 | 85 | |
| 83 | - <!-- Form Actions --> | |
| 86 | + <div class="card bg-base-100 shadow-xl max-w-2xl mt-6"> | |
| 87 | + <div class="card-body"> | |
| 88 | + <h2 class="card-title mb-4">{{ email_change_title }}</h2> | |
| 89 | + <div class="alert alert-info mb-4"> | |
| 90 | + <span>{{ email_change_info }}</span> | |
| 91 | + </div> | |
| 92 | + <div id="email-change-alert" class="hidden alert mb-4"> | |
| 93 | + <span id="email-change-alert-text"></span> | |
| 94 | + </div> | |
| 95 | + <form id="emailChangeForm" class="space-y-4" | |
| 96 | + data-error="{{ email_change_error }}" | |
| 97 | + data-network="{{ network_error }}" | |
| 98 | + data-saving="{{ saving }}" | |
| 99 | + data-submit-label="{{ request_button }}"> | |
| 100 | + <div class="form-control"> | |
| 101 | + <label class="label" for="new-email"> | |
| 102 | + <span class="label-text font-semibold">{{ new_email_label }}</span> | |
| 103 | + </label> | |
| 104 | + <input | |
| 105 | + type="email" | |
| 106 | + id="new-email" | |
| 107 | + name="new_email" | |
| 108 | + placeholder="{{ email_placeholder }}" | |
| 109 | + class="input input-bordered w-full" | |
| 110 | + required | |
| 111 | + autocomplete="email" | |
| 112 | + > | |
| 113 | + </div> | |
| 114 | + <div class="form-control"> | |
| 115 | + <label class="label" for="current-password"> | |
| 116 | + <span class="label-text font-semibold">{{ current_password_label }}</span> | |
| 117 | + </label> | |
| 118 | + <input | |
| 119 | + type="password" | |
| 120 | + id="current-password" | |
| 121 | + name="current_password" | |
| 122 | + class="input input-bordered w-full" | |
| 123 | + required | |
| 124 | + autocomplete="current-password" | |
| 125 | + > | |
| 126 | + </div> | |
| 84 | 127 | <div class="card-actions justify-end"> |
| 85 | - <a href="/profile" class="btn btn-ghost">{{ cancel }}</a> | |
| 86 | - <button type="submit" class="btn btn-primary"> | |
| 87 | - <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg> | |
| 88 | - {{ save_button }} | |
| 89 | - </button> | |
| 128 | + <button type="submit" class="btn btn-primary">{{ request_button }}</button> | |
| 90 | 129 | </div> |
| 91 | 130 | </form> |
| 92 | 131 | </div> |
@@ -135,52 +174,77 @@
| 135 | 174 | </div> |
| 136 | 175 | |
| 137 | 176 | <script> |
| 138 | -// Form submission handling | |
| 139 | 177 | document.getElementById('profileForm').addEventListener('submit', async function(e) { |
| 140 | 178 | e.preventDefault(); |
| 141 | - | |
| 142 | - const formData = new FormData(this); | |
| 143 | 179 | const submitBtn = this.querySelector('button[type="submit"]'); |
| 144 | - | |
| 145 | - // Disable submit button | |
| 180 | + const username = document.getElementById('username').value; | |
| 146 | 181 | submitBtn.disabled = true; |
| 147 | - submitBtn.innerHTML = '<span class="loading loading-spinner"></span> Enregistrement...'; | |
| 148 | - | |
| 182 | + submitBtn.textContent = this.dataset.saving; | |
| 149 | 183 | try { |
| 150 | 184 | const response = await fetch('/profile', { |
| 151 | 185 | method: 'PUT', |
| 152 | 186 | headers: { |
| 153 | - 'Content-Type': 'application/x-www-form-urlencoded', | |
| 187 | + 'Content-Type': 'application/json', | |
| 188 | + 'Accept': 'application/json', | |
| 154 | 189 | 'X-CSRF-Token': '{{ csrf_token }}', |
| 155 | 190 | }, |
| 156 | - body: new URLSearchParams(formData) | |
| 191 | + credentials: 'include', | |
| 192 | + body: JSON.stringify({ username }) | |
| 157 | 193 | }); |
| 158 | - | |
| 159 | - const result = await response.json(); | |
| 160 | - | |
| 194 | + const result = await response.json().catch(() => ({})); | |
| 161 | 195 | if (response.ok) { |
| 162 | - // Show success message | |
| 163 | - alert('✅ Profil mis à jour avec succès !'); | |
| 164 | - | |
| 165 | - // Reload if email changed (will redirect to login) | |
| 166 | - const originalEmail = '{{ email }}'; | |
| 167 | - if (formData.get('email') !== originalEmail) { | |
| 168 | - alert('⚠️ Votre email a été modifié. Vous allez être déconnecté.'); | |
| 169 | - window.location.href = '/login'; | |
| 170 | - } else { | |
| 171 | - window.location.reload(); | |
| 172 | - } | |
| 196 | + alert(this.dataset.saved); | |
| 197 | + window.location.reload(); | |
| 173 | 198 | } else { |
| 174 | - alert('❌ Erreur : ' + (result.error || result.message || 'Échec de la mise à jour')); | |
| 199 | + alert(result.error || result.message || this.dataset.error); | |
| 175 | 200 | submitBtn.disabled = false; |
| 176 | - submitBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg> Enregistrer les modifications'; | |
| 201 | + submitBtn.textContent = this.dataset.saveLabel; | |
| 177 | 202 | } |
| 178 | 203 | } catch (error) { |
| 179 | - console.error('Error:', error); | |
| 180 | - alert('❌ Erreur réseau. Veuillez réessayer.'); | |
| 204 | + alert(this.dataset.network); | |
| 181 | 205 | submitBtn.disabled = false; |
| 182 | - submitBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" /></svg> Enregistrer les modifications'; | |
| 206 | + submitBtn.textContent = this.dataset.saveLabel; | |
| 207 | + } | |
| 208 | +}); | |
| 209 | + | |
| 210 | +document.getElementById('emailChangeForm').addEventListener('submit', async function(e) { | |
| 211 | + e.preventDefault(); | |
| 212 | + const submitBtn = this.querySelector('button[type="submit"]'); | |
| 213 | + const alertBox = document.getElementById('email-change-alert'); | |
| 214 | + const alertText = document.getElementById('email-change-alert-text'); | |
| 215 | + submitBtn.disabled = true; | |
| 216 | + submitBtn.textContent = this.dataset.saving; | |
| 217 | + alertBox.classList.add('hidden'); | |
| 218 | + try { | |
| 219 | + const response = await fetch('/api/v1/auth/email-change', { | |
| 220 | + method: 'POST', | |
| 221 | + headers: { | |
| 222 | + 'Content-Type': 'application/json', | |
| 223 | + 'Accept': 'application/json', | |
| 224 | + }, | |
| 225 | + credentials: 'include', | |
| 226 | + body: JSON.stringify({ | |
| 227 | + new_email: document.getElementById('new-email').value, | |
| 228 | + current_password: document.getElementById('current-password').value | |
| 229 | + }) | |
| 230 | + }); | |
| 231 | + const result = await response.json().catch(() => ({})); | |
| 232 | + alertBox.classList.remove('hidden', 'alert-success', 'alert-error'); | |
| 233 | + if (response.ok) { | |
| 234 | + alertBox.classList.add('alert-success'); | |
| 235 | + alertText.textContent = result.message || ''; | |
| 236 | + this.reset(); | |
| 237 | + } else { | |
| 238 | + alertBox.classList.add('alert-error'); | |
| 239 | + alertText.textContent = result.error || result.message || this.dataset.error; | |
| 240 | + } | |
| 241 | + } catch (error) { | |
| 242 | + alertBox.classList.remove('hidden', 'alert-success'); | |
| 243 | + alertBox.classList.add('alert-error'); | |
| 244 | + alertText.textContent = this.dataset.network; | |
| 183 | 245 | } |
| 246 | + submitBtn.disabled = false; | |
| 247 | + submitBtn.textContent = this.dataset.submitLabel; | |
| 184 | 248 | }); |
| 185 | 249 | </script> |
| 186 | 250 | {% endblock %} |
M
crates/rustwarden-ui/templates/settings/security.html
+4
-126
@@ -36,7 +36,7 @@
| 36 | 36 | </div> |
| 37 | 37 | </div> |
| 38 | 38 | |
| 39 | - <div x-data="securityPage()" class="space-y-6"> | |
| 39 | + <div x-data="securityPage" data-csrf="{{ csrf_token }}" data-regenerate-confirm="{{ regenerate_confirm }}" class="space-y-6"> | |
| 40 | 40 | <!-- Status Card --> |
| 41 | 41 | <div class="card bg-base-100 shadow-lg"> |
| 42 | 42 | <div class="card-body"> |
@@ -148,10 +148,10 @@
| 148 | 148 | </button> |
| 149 | 149 | </div> |
| 150 | 150 | <div class="modal-action"> |
| 151 | - <button class="btn btn-primary" @click="showBackupCodes = false; window.location.reload();">{{ saved_codes_button }}</button> | |
| 151 | + <button class="btn btn-primary" @click="acknowledgeBackupCodes()">{{ saved_codes_button }}</button> | |
| 152 | 152 | </div> |
| 153 | 153 | </div> |
| 154 | - <form method="dialog" class="modal-backdrop" @click="showBackupCodes = false; window.location.reload();"> | |
| 154 | + <form method="dialog" class="modal-backdrop" @click="acknowledgeBackupCodes()"> | |
| 155 | 155 | <button>close</button> |
| 156 | 156 | </form> |
| 157 | 157 | </dialog> |
@@ -166,7 +166,7 @@
| 166 | 166 | </div> |
| 167 | 167 | <p x-show="disableError" class="text-error text-sm mt-2" x-text="disableError"></p> |
| 168 | 168 | <div class="modal-action"> |
| 169 | - <button class="btn" @click="showDisableModal = false; disablePassword = ''; disableError = '';">{{ cancel_button }}</button> | |
| 169 | + <button class="btn" @click="closeDisableModal()">{{ cancel_button }}</button> | |
| 170 | 170 | <button class="btn btn-error" @click="disableTotp()" :disabled="!disablePassword || disabling"> |
| 171 | 171 | <span x-show="!disabling">{{ disable_button }}</span> |
| 172 | 172 | <span x-show="disabling" class="loading loading-spinner loading-sm"></span> |
@@ -179,126 +179,4 @@
| 179 | 179 | </dialog> |
| 180 | 180 | </div> |
| 181 | 181 | </div> |
| 182 | - | |
| 183 | -<script> | |
| 184 | -function securityPage() { | |
| 185 | - return { | |
| 186 | - setupStarted: false, | |
| 187 | - setupComplete: false, | |
| 188 | - setupError: '', | |
| 189 | - qrCode: '', | |
| 190 | - secretBase32: '', | |
| 191 | - verifyCode: '', | |
| 192 | - verifying: false, | |
| 193 | - showBackupCodes: false, | |
| 194 | - backupCodes: [], | |
| 195 | - regenerating: false, | |
| 196 | - showDisableModal: false, | |
| 197 | - disablePassword: '', | |
| 198 | - disableError: '', | |
| 199 | - disabling: false, | |
| 200 | - | |
| 201 | - async startSetup() { | |
| 202 | - this.setupStarted = true; | |
| 203 | - this.setupError = ''; | |
| 204 | - try { | |
| 205 | - const res = await fetch('/settings/security/totp/enable', { | |
| 206 | - method: 'POST', | |
| 207 | - headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': '{{ csrf_token }}' }, | |
| 208 | - }); | |
| 209 | - const data = await res.json(); | |
| 210 | - if (res.ok) { | |
| 211 | - this.qrCode = data.qr_code_data_uri; | |
| 212 | - this.secretBase32 = data.secret_base32; | |
| 213 | - } else { | |
| 214 | - this.setupError = data.error || 'Error'; | |
| 215 | - this.setupStarted = false; | |
| 216 | - } | |
| 217 | - } catch (e) { | |
| 218 | - this.setupError = 'Network error'; | |
| 219 | - this.setupStarted = false; | |
| 220 | - } | |
| 221 | - }, | |
| 222 | - | |
| 223 | - async verifyAndEnable() { | |
| 224 | - if (this.verifyCode.length !== 6) return; | |
| 225 | - this.verifying = true; | |
| 226 | - this.setupError = ''; | |
| 227 | - try { | |
| 228 | - const res = await fetch('/settings/security/totp/verify', { | |
| 229 | - method: 'POST', | |
| 230 | - headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': '{{ csrf_token }}' }, | |
| 231 | - body: JSON.stringify({ code: this.verifyCode }), | |
| 232 | - }); | |
| 233 | - const data = await res.json(); | |
| 234 | - if (res.ok) { | |
| 235 | - this.setupComplete = true; | |
| 236 | - this.backupCodes = data.backup_codes; | |
| 237 | - this.showBackupCodes = true; | |
| 238 | - } else { | |
| 239 | - this.setupError = data.error || 'Invalid code'; | |
| 240 | - this.verifyCode = ''; | |
| 241 | - } | |
| 242 | - } catch (e) { | |
| 243 | - this.setupError = 'Network error'; | |
| 244 | - } | |
| 245 | - this.verifying = false; | |
| 246 | - }, | |
| 247 | - | |
| 248 | - async regenerateBackupCodes() { | |
| 249 | - if (!confirm('{{ regenerate_confirm }}')) return; | |
| 250 | - this.regenerating = true; | |
| 251 | - try { | |
| 252 | - const res = await fetch('/settings/security/backup-codes', { | |
| 253 | - method: 'POST', | |
| 254 | - headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': '{{ csrf_token }}' }, | |
| 255 | - }); | |
| 256 | - const data = await res.json(); | |
| 257 | - if (res.ok) { | |
| 258 | - this.backupCodes = data.backup_codes; | |
| 259 | - this.showBackupCodes = true; | |
| 260 | - } | |
| 261 | - } catch (e) {} | |
| 262 | - this.regenerating = false; | |
| 263 | - }, | |
| 264 | - | |
| 265 | - async disableTotp() { | |
| 266 | - this.disabling = true; | |
| 267 | - this.disableError = ''; | |
| 268 | - try { | |
| 269 | - const res = await fetch('/settings/security/totp/disable', { | |
| 270 | - method: 'POST', | |
| 271 | - headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': '{{ csrf_token }}' }, | |
| 272 | - body: JSON.stringify({ password: this.disablePassword }), | |
| 273 | - }); | |
| 274 | - if (res.ok) { | |
| 275 | - window.location.reload(); | |
| 276 | - } else { | |
| 277 | - const data = await res.json(); | |
| 278 | - this.disableError = data.error || 'Error'; | |
| 279 | - } | |
| 280 | - } catch (e) { | |
| 281 | - this.disableError = 'Network error'; | |
| 282 | - } | |
| 283 | - this.disabling = false; | |
| 284 | - }, | |
| 285 | - | |
| 286 | - copyBackupCodes() { | |
| 287 | - const text = this.backupCodes.join('\n'); | |
| 288 | - if (navigator.clipboard && navigator.clipboard.writeText) { | |
| 289 | - navigator.clipboard.writeText(text); | |
| 290 | - } else { | |
| 291 | - const ta = document.createElement('textarea'); | |
| 292 | - ta.value = text; | |
| 293 | - ta.style.position = 'fixed'; | |
| 294 | - ta.style.opacity = '0'; | |
| 295 | - document.body.appendChild(ta); | |
| 296 | - ta.select(); | |
| 297 | - document.execCommand('copy'); | |
| 298 | - document.body.removeChild(ta); | |
| 299 | - } | |
| 300 | - } | |
| 301 | - }; | |
| 302 | -} | |
| 303 | -</script> | |
| 304 | 182 | {% endblock %} |
M
docs/MIGRATION_GUIDE.md
+31
-0
@@ -9,6 +9,37 @@ Les entrees sont classees par date, de la plus recente a la plus ancienne.
| 9 | 9 | |
| 10 | 10 | --- |
| 11 | 11 | |
| 12 | +## 2026-10-01 — RFC 0039 phase 2 : CSP par défaut sans `unsafe-eval` | |
| 13 | + | |
| 14 | +Le défaut de `SecurityHeadersConfig` est maintenant `CSP_WITHOUT_UNSAFE_EVAL` | |
| 15 | +(`script-src 'self' 'unsafe-inline'`). `'unsafe-inline'` reste : les scripts | |
| 16 | +de boot (thème, langue) et HTMX sont encore inline. HSTS, `X-Frame-Options`, | |
| 17 | +COOP et CORP ne changent pas. | |
| 18 | + | |
| 19 | +L'UI de référence charge `@alpinejs/csp` (`/static/js/alpine-csp.min.js`) et | |
| 20 | +enregistre ses composants dans `/static/js/alpine-components.js`. Les | |
| 21 | +expressions Alpine qui appellent `window`, `document` ou une fonction globale | |
| 22 | +sont interdites par ce build : elles vivent dans `Alpine.data(...)`. | |
| 23 | + | |
| 24 | +**Retour arrière** si une app dépend encore du build Alpine standard (`new Function`) : | |
| 25 | + | |
| 26 | +```rust | |
| 27 | +use std::borrow::Cow; | |
| 28 | +use rustwarden_core::config::security_headers::{ | |
| 29 | + DEFAULT_CONTENT_SECURITY_POLICY, SecurityHeadersConfig, | |
| 30 | +}; | |
| 31 | + | |
| 32 | +let headers = SecurityHeadersConfig { | |
| 33 | + content_security_policy: Cow::Borrowed(DEFAULT_CONTENT_SECURITY_POLICY), | |
| 34 | + ..SecurityHeadersConfig::default() | |
| 35 | +}; | |
| 36 | +// .security_headers(headers) sur RustwardenBuilder | |
| 37 | +``` | |
| 38 | + | |
| 39 | +`DEFAULT_CONTENT_SECURITY_POLICY` est la politique 1.0.1, avec `'unsafe-eval'`. | |
| 40 | + | |
| 41 | +--- | |
| 42 | + | |
| 12 | 43 | ## 2026-09-18 — RFC 0036-0041 (1.1.0) |
| 13 | 44 | |
| 14 | 45 | Minor de securite. Les RFC 0036 a 0040 sont **Accepted (2026-09-18)**, la RFC |
M
docs/rfcs/0032-fediverse-event-observer-delete-actor.md
+4
-3
@@ -1,10 +1,11 @@
| 1 | -# RFC 0004 — Ajouter `on_delete_actor` à `FediverseEventObserver` | |
| 1 | +# RFC 0032 — Ajouter `on_delete_actor` à `FediverseEventObserver` | |
| 2 | 2 | |
| 3 | -> Numéro local gitrust. Upstream assignera vraisemblablement 0032+ à la livraison. | |
| 3 | +> Ancien numéro local gitrust : RFC 0004. Le numéro upstream est 0032. | |
| 4 | 4 | |
| 5 | 5 | - **Auteur** : EBO (gitrust) |
| 6 | 6 | - **Date** : 2026-05-17 |
| 7 | -- **Statut** : Draft (à pousser upstream) | |
| 7 | +- **Statut** : Implemented (2026-10-01) | |
| 8 | +- **Signature livrée** : `on_delete_actor(domain, ap_id, remote_identity_id)` — pas `rows_disabled`. Appelée depuis `apply_delete_actor`. | |
| 8 | 9 | - **Crate cible** : `rustwarden-fediverse` |
| 9 | 10 | - **Type** : extension trait additive (pas de breaking change pour les consommateurs existants — ajout d'une méthode `default` au trait `FediverseEventObserver`) |
| 10 | 11 |
M
docs/rfcs/0034-edition-2024-fmt-cleanup.md
+1
-1
@@ -2,7 +2,7 @@
| 2 | 2 | |
| 3 | 3 | - **Auteur** : EBO (gitrust) |
| 4 | 4 | - **Date** : 2026-05-18 |
| 5 | -- **Statut** : Draft | |
| 5 | +- **Statut** : Implemented (2026-10-01) — `cargo fmt --all` appliqué sur le workspace. Pas de commit dans ce train (le versioning reste manuel). | |
| 6 | 6 | - **Apps consommatrices concernées** : gitrust (immédiat — blocage CI sur `cargo fmt --all -- --check`), tout futur consommateur qui pose une CI stricte sur le formatage du workspace. |
| 7 | 7 | - **Crate cible** : `rustwarden-core`, `rustwarden-fediverse`, `rustwarden-fediverse-guard` (les 3 crates upstream vendorées dans le workspace gitrust) |
| 8 | 8 |
M
docs/rfcs/0036-username-charset-validation.md
+1
-1
@@ -2,7 +2,7 @@
| 2 | 2 | |
| 3 | 3 | - **Auteur** : EBO (origine audit Gitrust / Rustwarden 1.0.1) |
| 4 | 4 | - **Date** : 2026-09-18 |
| 5 | -- **Statut** : Accepted (2026-09-18) | |
| 5 | +- **Statut** : Implemented (2026-10-01 ; Accepted 2026-09-18) | |
| 6 | 6 | - **Apps consommatrices concernées** : Gitrust (newtype local à retirer après Accept + impl) ; tout consommateur qui appelle `UserService::create_user` / `update_profile` ou `POST /api/v1/auth/register` |
| 7 | 7 | - **Crate cible** : `rustwarden-core` |
| 8 | 8 | - **Origine** : constat M-1 du plan [`docs/plans/2026-09-17-securisation-upstream-audit-gitrust.md`](../plans/2026-09-17-securisation-upstream-audit-gitrust.md) (Task 13) |
M
docs/rfcs/0037-totp-last-used-step.md
+1
-1
@@ -2,7 +2,7 @@
| 2 | 2 | |
| 3 | 3 | - **Auteur** : EBO (origine audit Gitrust / patch 1.0.1) |
| 4 | 4 | - **Date** : 2026-09-18 |
| 5 | -- **Statut** : Accepted (2026-09-18) | |
| 5 | +- **Statut** : Implemented (2026-10-01 ; Accepted 2026-09-18) | |
| 6 | 6 | - **Apps consommatrices concernées** : Gitrust (déploiements multi-réplicas) ; tout consommateur de `rustwarden-core` qui appelle `TotpService::verify_code` / `verify_and_enable` (API `POST /api/v1/auth/2fa/verify`, UI login 2FA, apps headless) |
| 7 | 7 | - **Crate cible** : `rustwarden-core` |
| 8 | 8 | - **Origine** : constat M-7 du plan [`docs/plans/2026-09-17-securisation-upstream-audit-gitrust.md`](../plans/2026-09-17-securisation-upstream-audit-gitrust.md) (Task 14). RFC **obligatoire** : changement de schéma DB ([`RUSTWARDEN_EVOLUTION_PROCESS.md` §2](../RUSTWARDEN_EVOLUTION_PROCESS.md#2-quand-une-rfc-est-obligatoire)). |
M
docs/rfcs/0039-security-headers-config.md
+3
-3
@@ -2,7 +2,7 @@
| 2 | 2 | |
| 3 | 3 | - **Auteur** : EBO (origine audit Gitrust / patch 1.0.1) |
| 4 | 4 | - **Date** : 2026-09-18 |
| 5 | -- **Statut** : Accepted (2026-09-18) | |
| 5 | +- **Statut** : Implemented (2026-10-01) — phase 1 (API) et phase 2 (défaut sans `'unsafe-eval'`). `'unsafe-inline'` reste. | |
| 6 | 6 | - **Apps consommatrices concernées** : Gitrust (pourra retirer `SetResponseHeaderLayer::overriding`) ; `rustwarden-ui` / `rustwarden-app` ; tout consommateur de `RustwardenApp::builder()` |
| 7 | 7 | - **Crate cible** : `rustwarden-core` |
| 8 | 8 | - **Origine** : constats M-10 + F-UI du plan [`docs/plans/2026-09-17-securisation-upstream-audit-gitrust.md`](../plans/2026-09-17-securisation-upstream-audit-gitrust.md) (Task 16). CSP Alpine : [`docs/plans/PLAN_CSP_HTMX.md`](../plans/PLAN_CSP_HTMX.md). |
@@ -220,9 +220,9 @@ async fn security_headers_middleware(
| 220 | 220 | |
| 221 | 221 | Constitution : pas d’`unwrap`/`expect` hors tests. |
| 222 | 222 | |
| 223 | -### Phase 2 (après Accept de cette RFC **et** Alpine CSP) | |
| 223 | +### Phase 2 (livrée le 2026-10-01) | |
| 224 | 224 | |
| 225 | -Prérequis : [`PLAN_CSP_HTMX.md`](../plans/PLAN_CSP_HTMX.md) Phase 1 (`@alpinejs/csp`, inventaire `x-data` inline → `Alpine.data`). | |
| 225 | +Prérequis : [`PLAN_CSP_HTMX.md`](../plans/PLAN_CSP_HTMX.md) Phase 1 (`@alpinejs/csp`, inventaire `x-data` inline → `Alpine.data`). **Fait** : `static/js/alpine-csp.min.js`, composants dans `static/js/alpine-components.js`, `SecurityHeadersConfig::default` pointe sur `CSP_WITHOUT_UNSAFE_EVAL`. La constante `DEFAULT_CONTENT_SECURITY_POLICY` conserve la politique 1.0.1 pour un retour arrière explicite. | |
| 226 | 226 | |
| 227 | 227 | Alors seulement : `Default` pointe sur `CSP_WITHOUT_UNSAFE_EVAL` (retirer `'unsafe-eval'`, **garder** `'unsafe-inline'` jusqu’aux phases suivantes du plan). C’est un **changement de défaut** : PR séparé, mention CHANGELOG `Security`, éventuellement minor. Hors du premier PR d’implémentation de **cette** RFC. |
| 228 | 228 |
M
docs/rfcs/0040-authenticate-user-email-verified.md
+1
-1
@@ -2,7 +2,7 @@
| 2 | 2 | |
| 3 | 3 | - **Auteur** : EBO (origine audit Gitrust / patch 1.0.1) |
| 4 | 4 | - **Date** : 2026-09-18 |
| 5 | -- **Statut** : Accepted (2026-09-18) | |
| 5 | +- **Statut** : Implemented (2026-10-01 ; Accepted 2026-09-18) | |
| 6 | 6 | - **Apps consommatrices concernées** : Gitrust (SSR / appels directs au service) ; `rustwarden-ui` (déjà via `POST /api/v1/auth/login`) ; tout consommateur de `rustwarden-core` qui appelle `AuthService::authenticate_user` |
| 7 | 7 | - **Crate cible** : `rustwarden-core` |
| 8 | 8 | - **Origine** : plan [`docs/plans/2026-09-17-securisation-upstream-audit-gitrust.md`](../plans/2026-09-17-securisation-upstream-audit-gitrust.md) (Task 17). Contrat limiteur : finding E-2 / layer `auth_ip_rate_limit` 1.0.1. |
M
docs/rfcs/0041-pending-email-changes.md
+1
-1
@@ -2,7 +2,7 @@
| 2 | 2 | |
| 3 | 3 | - **Auteur** : EBO (origine audit Gitrust / suite de la RFC 0038) |
| 4 | 4 | - **Date** : 2026-09-19 |
| 5 | -- **Statut** : Accepted (2026-09-19) | |
| 5 | +- **Statut** : Implemented (2026-10-01 ; Accepted 2026-09-19) | |
| 6 | 6 | - **Apps consommatrices concernées** : Gitrust ; `rustwarden-ui` (`PUT /settings/profile`, `PUT /admin/users/{id}`) ; tout consommateur headless qui appelle `UserService::update_profile` avec un e-mail |
| 7 | 7 | - **Crate cible** : `rustwarden-core` (**core uniquement**) |
| 8 | 8 | - **Origine** : § « v2 (follow-up) » de la [RFC 0038](0038-email-change-revalidation.md) (L117-121), déclenchée par le mode de panne documenté dans [`docs/MIGRATION_GUIDE.md`](../MIGRATION_GUIDE.md) (§ RFC 0038, « Faute de frappe sur la nouvelle adresse = lock-out définitif sans recours self-service ») |
M
docs/rfcs/README.md
+7
-7
@@ -88,16 +88,16 @@ Score minimum **25/35** pour passer en `Accepted`. Voir grille détaillée dans
| 88 | 88 | | [0029](0029-fediverse-remote-resource-shares.md) | Fediverse — `RemoteResourceShareService` (partages cross-instance de ressources génériques) | **Accepted** (33/35, livré + 11 tests E2E verts) | EBO (origine gitrust) | 2026-05-15 | |
| 89 | 89 | | [0030](0030-fediverse-remote-personal-access-tokens.md) | Fediverse — `RemotePersonalAccessTokenService` (PAT pour identités remote) | **Accepted** (34/35, livré + 20 tests E2E verts) | EBO (origine gitrust) | 2026-05-15 | |
| 90 | 90 | | [0031](0031-auth-service-optional-fallback.md) | Auth — fallback `AuthConfig::from_env()` sur la branche optional de `AuthService` | **Accepted** (implémenté 2026-05-16) | EBO (gitrust) | 2026-05-16 | |
| 91 | -| [0032](0032-fediverse-event-observer-delete-actor.md) | Fediverse — `on_delete_actor` sur `FediverseEventObserver` | **Draft** | EBO (gitrust) | 2026-05-17 | | |
| 91 | +| [0032](0032-fediverse-event-observer-delete-actor.md) | Fediverse — `on_delete_actor` sur `FediverseEventObserver` | **Implemented** | EBO (gitrust) | 2026-05-17 | | |
| 92 | 92 | | [0033](0033-fediverse-delete-actor-hooks-unification.md) | Fediverse — unifier `apply_delete_actor*` derrière `DeleteActorHooks` | **Implemented** | EBO | 2026-05-17 | |
| 93 | -| [0034](0034-edition-2024-fmt-cleanup.md) | Workspace — cleanup rustfmt après migration édition 2024 | **Draft** | EBO (gitrust) | 2026-05-18 | | |
| 93 | +| [0034](0034-edition-2024-fmt-cleanup.md) | Workspace — cleanup rustfmt après migration édition 2024 | **Implemented** | EBO (gitrust) | 2026-05-18 | | |
| 94 | 94 | | [0035](0035-oauth-pkce-code-verifier.md) | OAuth — envoyer `code_verifier` PKCE à l'échange (GitHub & Discord) | **Implemented** (2026-06-18) | PasterMan | 2026-06-18 | |
| 95 | -| [0036](0036-username-charset-validation.md) | Auth — newtype `Username` (charset GitHub-like, audit M-1) | **Accepted** (2026-09-18) | EBO (origine audit Gitrust / Rustwarden 1.0.1) | 2026-09-18 | | |
| 96 | -| [0037](0037-totp-last-used-step.md) | Auth — persister `last_used_step` TOTP (anti-rejeu multi-instance, audit M-7) | **Accepted** (2026-09-18) | EBO (origine audit Gitrust / patch 1.0.1) | 2026-09-18 | | |
| 95 | +| [0036](0036-username-charset-validation.md) | Auth — newtype `Username` (charset GitHub-like, audit M-1) | **Implemented** | EBO (origine audit Gitrust / Rustwarden 1.0.1) | 2026-09-18 | | |
| 96 | +| [0037](0037-totp-last-used-step.md) | Auth — persister `last_used_step` TOTP (anti-rejeu multi-instance, audit M-7) | **Implemented** | EBO (origine audit Gitrust / patch 1.0.1) | 2026-09-18 | | |
| 97 | 97 | | [0038](0038-email-change-revalidation.md) | Auth — revalidation e-mail à `UserService::update_profile` | **Superseded by [0041](0041-pending-email-changes.md)** (2026-09-19 ; était Accepted 2026-09-18) | EBO (origine audit Gitrust / patch 1.0.1) | 2026-09-18 | |
| 98 | -| [0039](0039-security-headers-config.md) | HTTP — `SecurityHeadersConfig` (CSP / COOP / CORP additifs) | **Accepted** (2026-09-18) | EBO (origine audit Gitrust / patch 1.0.1) | 2026-09-18 | | |
| 99 | -| [0040](0040-authenticate-user-email-verified.md) | Auth — `authenticate_user` honore `validation_email_required` | **Accepted** (2026-09-18) | EBO (origine audit Gitrust / patch 1.0.1) | 2026-09-18 | | |
| 100 | -| [0041](0041-pending-email-changes.md) | Auth — changement d'e-mail en deux temps (`pending_email_changes`) — **remplace la [0038](0038-email-change-revalidation.md)** | **Accepted** (2026-09-19) | EBO (origine audit Gitrust / suite de la RFC 0038) | 2026-09-19 | | |
| 98 | +| [0039](0039-security-headers-config.md) | HTTP — `SecurityHeadersConfig` (CSP / COOP / CORP additifs) | **Implemented** (phase 2, sans `unsafe-eval`) | EBO (origine audit Gitrust / patch 1.0.1) | 2026-09-18 | | |
| 99 | +| [0040](0040-authenticate-user-email-verified.md) | Auth — `authenticate_user` honore `validation_email_required` | **Implemented** | EBO (origine audit Gitrust / patch 1.0.1) | 2026-09-18 | | |
| 100 | +| [0041](0041-pending-email-changes.md) | Auth — changement d'e-mail en deux temps (`pending_email_changes`) — **remplace la [0038](0038-email-change-revalidation.md)** | **Implemented** | EBO (origine audit Gitrust / suite de la RFC 0038) | 2026-09-19 | | |
| 101 | 101 | |
| 102 | 102 | À mesure que les RFCs sont créées, elles doivent apparaître dans cette table avec leur statut courant. |
| 103 | 103 |
M
static/css/custom.css
+1
-1
@@ -1,4 +1,4 @@
| 1 | -/* RustCloud Custom Styles */ | |
| 1 | +/* Rustwarden custom styles */ | |
| 2 | 2 | |
| 3 | 3 | :root { |
| 4 | 4 | --rw-primary: #0082c9; |
M
static/css/tailwind.css
+891
-548
@@ -10,10 +10,15 @@
| 10 | 10 | --color-red-500: oklch(63.7% 0.237 25.331); |
| 11 | 11 | --color-gray-800: oklch(27.8% 0.033 256.848); |
| 12 | 12 | --color-black: #000; |
| 13 | + --color-white: #fff; | |
| 13 | 14 | --spacing: 0.25rem; |
| 15 | + --container-xs: 20rem; | |
| 16 | + --container-sm: 24rem; | |
| 14 | 17 | --container-md: 28rem; |
| 15 | 18 | --container-lg: 32rem; |
| 19 | + --container-xl: 36rem; | |
| 16 | 20 | --container-2xl: 42rem; |
| 21 | + --container-3xl: 48rem; | |
| 17 | 22 | --container-4xl: 56rem; |
| 18 | 23 | --container-6xl: 72rem; |
| 19 | 24 | --text-xs: 0.75rem; |
@@ -32,13 +37,17 @@
| 32 | 37 | --text-3xl--line-height: calc(2.25 / 1.875); |
| 33 | 38 | --text-4xl: 2.25rem; |
| 34 | 39 | --text-4xl--line-height: calc(2.5 / 2.25); |
| 40 | + --text-5xl: 3rem; | |
| 41 | + --text-5xl--line-height: 1; | |
| 35 | 42 | --text-6xl: 3.75rem; |
| 36 | 43 | --text-6xl--line-height: 1; |
| 37 | 44 | --font-weight-normal: 400; |
| 38 | 45 | --font-weight-medium: 500; |
| 39 | 46 | --font-weight-semibold: 600; |
| 40 | 47 | --font-weight-bold: 700; |
| 48 | + --tracking-wide: 0.025em; | |
| 41 | 49 | --tracking-wider: 0.05em; |
| 50 | + --tracking-widest: 0.1em; | |
| 42 | 51 | --radius-lg: 0.5rem; |
| 43 | 52 | --ease-in: cubic-bezier(0.4, 0, 1, 1); |
| 44 | 53 | --ease-out: cubic-bezier(0, 0, 0.2, 1); |
@@ -197,6 +206,59 @@
| 197 | 206 | } |
| 198 | 207 | } |
| 199 | 208 | @layer utilities { |
| 209 | + .diff { | |
| 210 | + @layer daisyui.l1.l2.l3 { | |
| 211 | + position: relative; | |
| 212 | + display: grid; | |
| 213 | + width: 100%; | |
| 214 | + overflow: hidden; | |
| 215 | + webkit-user-select: none; | |
| 216 | + user-select: none; | |
| 217 | + grid-template-rows: 1fr 1.8rem 1fr; | |
| 218 | + direction: ltr; | |
| 219 | + container-type: inline-size; | |
| 220 | + grid-template-columns: auto 1fr; | |
| 221 | + &:focus-visible, &:has(.diff-item-1:focus-visible) { | |
| 222 | + outline-style: var(--tw-outline-style); | |
| 223 | + outline-width: 2px; | |
| 224 | + outline-offset: 1px; | |
| 225 | + outline-color: var(--color-base-content); | |
| 226 | + } | |
| 227 | + &:focus-visible { | |
| 228 | + outline-style: var(--tw-outline-style); | |
| 229 | + outline-width: 2px; | |
| 230 | + outline-offset: 1px; | |
| 231 | + outline-color: var(--color-base-content); | |
| 232 | + .diff-resizer { | |
| 233 | + min-width: 95cqi; | |
| 234 | + max-width: 95cqi; | |
| 235 | + } | |
| 236 | + } | |
| 237 | + &:has(.diff-item-1:focus-visible) { | |
| 238 | + outline-style: var(--tw-outline-style); | |
| 239 | + outline-width: 2px; | |
| 240 | + outline-offset: 1px; | |
| 241 | + .diff-resizer { | |
| 242 | + min-width: 5cqi; | |
| 243 | + max-width: 5cqi; | |
| 244 | + } | |
| 245 | + } | |
| 246 | + @supports (-webkit-overflow-scrolling: touch) and (overflow: -webkit-paged-x) { | |
| 247 | + &:focus { | |
| 248 | + .diff-resizer { | |
| 249 | + min-width: 5cqi; | |
| 250 | + max-width: 5cqi; | |
| 251 | + } | |
| 252 | + } | |
| 253 | + &:has(.diff-item-1:focus) { | |
| 254 | + .diff-resizer { | |
| 255 | + min-width: 95cqi; | |
| 256 | + max-width: 95cqi; | |
| 257 | + } | |
| 258 | + } | |
| 259 | + } | |
| 260 | + } | |
| 261 | + } | |
| 200 | 262 | .modal { |
| 201 | 263 | @layer daisyui.l1.l2.l3 { |
| 202 | 264 | pointer-events: none; |
@@ -583,6 +645,28 @@
| 583 | 645 | } |
| 584 | 646 | } |
| 585 | 647 | } |
| 648 | + .collapse-arrow { | |
| 649 | + @layer daisyui.l1.l2 { | |
| 650 | + > .collapse-title:after { | |
| 651 | + position: absolute; | |
| 652 | + display: block; | |
| 653 | + height: 0.5rem; | |
| 654 | + width: 0.5rem; | |
| 655 | + transform: translateY(-100%) rotate(45deg); | |
| 656 | + @media (prefers-reduced-motion: no-preference) { | |
| 657 | + transition-property: all; | |
| 658 | + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); | |
| 659 | + transition-duration: 0.2s; | |
| 660 | + } | |
| 661 | + top: 50%; | |
| 662 | + inset-inline-end: 1.4rem; | |
| 663 | + content: ""; | |
| 664 | + transform-origin: 75% 75%; | |
| 665 | + box-shadow: 2px 2px; | |
| 666 | + pointer-events: none; | |
| 667 | + } | |
| 668 | + } | |
| 669 | + } | |
| 586 | 670 | .dropdown { |
| 587 | 671 | @layer daisyui.l1.l2.l3 { |
| 588 | 672 | position: relative; |
@@ -1036,6 +1120,27 @@
| 1036 | 1120 | } |
| 1037 | 1121 | } |
| 1038 | 1122 | } |
| 1123 | + .collapse-content { | |
| 1124 | + @layer daisyui.l1.l2.l3 { | |
| 1125 | + grid-column-start: 1; | |
| 1126 | + grid-row-start: 1; | |
| 1127 | + } | |
| 1128 | + @layer daisyui.l1.l2.l3 { | |
| 1129 | + content-visibility: hidden; | |
| 1130 | + grid-column-start: 1; | |
| 1131 | + grid-row-start: 2; | |
| 1132 | + min-height: 0; | |
| 1133 | + padding-left: 1rem; | |
| 1134 | + padding-right: 1rem; | |
| 1135 | + cursor: unset; | |
| 1136 | + @supports not (content-visibility: hidden) { | |
| 1137 | + visibility: hidden; | |
| 1138 | + } | |
| 1139 | + @media (prefers-reduced-motion: no-preference) { | |
| 1140 | + transition: content-visibility 0.2s allow-discrete, visibility 0.2s allow-discrete, min-height 0.2s ease-out allow-discrete, padding 0.1s ease-out 20ms, background-color 0.2s ease-out; | |
| 1141 | + } | |
| 1142 | + } | |
| 1143 | + } | |
| 1039 | 1144 | .validator { |
| 1040 | 1145 | @layer daisyui.l1.l2.l3 { |
| 1041 | 1146 | &:user-valid, &:has(:user-valid) { |
@@ -1062,6 +1167,9 @@
| 1062 | 1167 | .collapse { |
| 1063 | 1168 | visibility: collapse; |
| 1064 | 1169 | } |
| 1170 | + .invisible { | |
| 1171 | + visibility: hidden; | |
| 1172 | + } | |
| 1065 | 1173 | .visible { |
| 1066 | 1174 | visibility: visible; |
| 1067 | 1175 | } |
@@ -1124,6 +1232,285 @@
| 1124 | 1232 | } |
| 1125 | 1233 | } |
| 1126 | 1234 | } |
| 1235 | + .toast { | |
| 1236 | + @layer daisyui.l1.l2.l3 { | |
| 1237 | + position: fixed; | |
| 1238 | + inset-inline-start: auto; | |
| 1239 | + inset-inline-end: calc(0.25rem * 4); | |
| 1240 | + top: auto; | |
| 1241 | + bottom: calc(0.25rem * 4); | |
| 1242 | + display: flex; | |
| 1243 | + flex-direction: column; | |
| 1244 | + gap: calc(0.25rem * 2); | |
| 1245 | + background-color: transparent; | |
| 1246 | + translate: var(--toast-x, 0) var(--toast-y, 0); | |
| 1247 | + width: max-content; | |
| 1248 | + max-width: calc(100vw - 2rem); | |
| 1249 | + & > * { | |
| 1250 | + @media (prefers-reduced-motion: no-preference) { | |
| 1251 | + animation: toast 0.25s ease-out; | |
| 1252 | + } | |
| 1253 | + } | |
| 1254 | + } | |
| 1255 | + } | |
| 1256 | + .toggle { | |
| 1257 | + @layer daisyui.l1.l2.l3 { | |
| 1258 | + border: var(--border) solid currentColor; | |
| 1259 | + color: var(--input-color); | |
| 1260 | + position: relative; | |
| 1261 | + display: inline-grid; | |
| 1262 | + flex-shrink: 0; | |
| 1263 | + cursor: pointer; | |
| 1264 | + appearance: none; | |
| 1265 | + place-content: center; | |
| 1266 | + vertical-align: middle; | |
| 1267 | + webkit-user-select: none; | |
| 1268 | + user-select: none; | |
| 1269 | + grid-template-columns: 0fr 1fr 1fr; | |
| 1270 | + --radius-selector-max: calc( | |
| 1271 | + var(--radius-selector) + var(--radius-selector) + var(--radius-selector) | |
| 1272 | + ); | |
| 1273 | + border-radius: calc( var(--radius-selector) + min(var(--toggle-p), var(--radius-selector-max)) + min(var(--border), var(--radius-selector-max)) ); | |
| 1274 | + padding: var(--toggle-p); | |
| 1275 | + box-shadow: 0 1px currentColor inset; | |
| 1276 | + @supports (color: color-mix(in lab, red, red)) { | |
| 1277 | + box-shadow: 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000) inset; | |
| 1278 | + } | |
| 1279 | + transition: color 0.3s, grid-template-columns 0.2s; | |
| 1280 | + --input-color: var(--color-base-content); | |
| 1281 | + @supports (color: color-mix(in lab, red, red)) { | |
| 1282 | + --input-color: color-mix(in oklab, var(--color-base-content) 50%, #0000); | |
| 1283 | + } | |
| 1284 | + --toggle-p: calc(var(--size) * 0.125); | |
| 1285 | + --size: calc(var(--size-selector, 0.25rem) * 6); | |
| 1286 | + width: calc((var(--size) * 2) - (var(--border) + var(--toggle-p)) * 2); | |
| 1287 | + height: var(--size); | |
| 1288 | + > * { | |
| 1289 | + z-index: 1; | |
| 1290 | + grid-column: span 1 / span 1; | |
| 1291 | + grid-column-start: 2; | |
| 1292 | + grid-row-start: 1; | |
| 1293 | + height: 100%; | |
| 1294 | + cursor: pointer; | |
| 1295 | + appearance: none; | |
| 1296 | + background-color: transparent; | |
| 1297 | + padding: calc(0.25rem * 0.5); | |
| 1298 | + transition: opacity 0.2s, rotate 0.4s; | |
| 1299 | + border: none; | |
| 1300 | + &:focus { | |
| 1301 | + --tw-outline-style: none; | |
| 1302 | + outline-style: none; | |
| 1303 | + @media (forced-colors: active) { | |
| 1304 | + outline: 2px solid transparent; | |
| 1305 | + outline-offset: 2px; | |
| 1306 | + } | |
| 1307 | + } | |
| 1308 | + &:nth-child(2) { | |
| 1309 | + color: var(--color-base-100); | |
| 1310 | + rotate: 0deg; | |
| 1311 | + } | |
| 1312 | + &:nth-child(3) { | |
| 1313 | + color: var(--color-base-100); | |
| 1314 | + opacity: 0%; | |
| 1315 | + rotate: -15deg; | |
| 1316 | + } | |
| 1317 | + } | |
| 1318 | + &:has(:checked) { | |
| 1319 | + > :nth-child(2) { | |
| 1320 | + opacity: 0%; | |
| 1321 | + rotate: 15deg; | |
| 1322 | + } | |
| 1323 | + > :nth-child(3) { | |
| 1324 | + opacity: 100%; | |
| 1325 | + rotate: 0deg; | |
| 1326 | + } | |
| 1327 | + } | |
| 1328 | + &:before { | |
| 1329 | + position: relative; | |
| 1330 | + inset-inline-start: calc(0.25rem * 0); | |
| 1331 | + grid-column-start: 2; | |
| 1332 | + grid-row-start: 1; | |
| 1333 | + aspect-ratio: 1 / 1; | |
| 1334 | + height: 100%; | |
| 1335 | + width: 100%; | |
| 1336 | + border-radius: var(--radius-selector); | |
| 1337 | + background-color: currentcolor; | |
| 1338 | + translate: 0; | |
| 1339 | + --tw-content: ""; | |
| 1340 | + content: var(--tw-content); | |
| 1341 | + transition: background-color 0.1s, translate 0.2s, inset-inline-start 0.2s; | |
| 1342 | + box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px currentColor; | |
| 1343 | + @supports (color: color-mix(in lab, red, red)) { | |
| 1344 | + box-shadow: 0 -1px oklch(0% 0 0 / calc(var(--depth) * 0.1)) inset, 0 8px 0 -4px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset, 0 1px color-mix(in oklab, currentColor calc(var(--depth) * 10%), #0000); | |
| 1345 | + } | |
| 1346 | + background-size: auto, calc(var(--noise) * 100%); | |
| 1347 | + background-image: none, var(--fx-noise); | |
| 1348 | + } | |
| 1349 | + @media (forced-colors: active) { | |
| 1350 | + &:before { | |
| 1351 | + outline-style: var(--tw-outline-style); | |
| 1352 | + outline-width: 1px; | |
| 1353 | + outline-offset: calc(1px * -1); | |
| 1354 | + } | |
| 1355 | + } | |
| 1356 | + @media print { | |
| 1357 | + &:before { | |
| 1358 | + outline: 0.25rem solid; | |
| 1359 | + outline-offset: -1rem; | |
| 1360 | + } | |
| 1361 | + } | |
| 1362 | + &:focus-visible, &:has(:focus-visible) { | |
| 1363 | + outline: 2px solid currentColor; | |
| 1364 | + outline-offset: 2px; | |
| 1365 | + } | |
| 1366 | + &:checked, &[aria-checked="true"], &:has(> input:checked) { | |
| 1367 | + grid-template-columns: 1fr 1fr 0fr; | |
| 1368 | + background-color: var(--color-base-100); | |
| 1369 | + --input-color: var(--color-base-content); | |
| 1370 | + &:before { | |
| 1371 | + background-color: currentcolor; | |
| 1372 | + } | |
| 1373 | + @starting-style { | |
| 1374 | + &:before { | |
| 1375 | + opacity: 0; | |
| 1376 | + } | |
| 1377 | + } | |
| 1378 | + } | |
| 1379 | + &:indeterminate { | |
| 1380 | + grid-template-columns: 0.5fr 1fr 0.5fr; | |
| 1381 | + } | |
| 1382 | + &:disabled { | |
| 1383 | + cursor: not-allowed; | |
| 1384 | + opacity: 30%; | |
| 1385 | + &:before { | |
| 1386 | + background-color: transparent; | |
| 1387 | + border: var(--border) solid currentColor; | |
| 1388 | + } | |
| 1389 | + } | |
| 1390 | + } | |
| 1391 | + } | |
| 1392 | + .\!input { | |
| 1393 | + @layer daisyui.l1.l2.l3 { | |
| 1394 | + cursor: text !important; | |
| 1395 | + border: var(--border) solid #0000 !important; | |
| 1396 | + position: relative !important; | |
| 1397 | + display: inline-flex !important; | |
| 1398 | + flex-shrink: 1 !important; | |
| 1399 | + appearance: none !important; | |
| 1400 | + align-items: center !important; | |
| 1401 | + gap: calc(0.25rem * 2) !important; | |
| 1402 | + background-color: var(--color-base-100) !important; | |
| 1403 | + padding-inline: calc(0.25rem * 3) !important; | |
| 1404 | + vertical-align: middle !important; | |
| 1405 | + white-space: nowrap !important; | |
| 1406 | + width: clamp(3rem, 20rem, 100%) !important; | |
| 1407 | + height: var(--size) !important; | |
| 1408 | + font-size: max(var(--font-size, 0.875rem), 0.875rem) !important; | |
| 1409 | + touch-action: manipulation !important; | |
| 1410 | + border-start-start-radius: var(--join-ss, var(--radius-field)) !important; | |
| 1411 | + border-start-end-radius: var(--join-se, var(--radius-field)) !important; | |
| 1412 | + border-end-start-radius: var(--join-es, var(--radius-field)) !important; | |
| 1413 | + border-end-end-radius: var(--join-ee, var(--radius-field)) !important; | |
| 1414 | + border-color: var(--input-color) !important; | |
| 1415 | + box-shadow: 0 1px var(--input-color) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset !important; | |
| 1416 | + @supports (color: color-mix(in lab, red, red)) { | |
| 1417 | + box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) inset, 0 -1px oklch(100% 0 0 / calc(var(--depth) * 0.1)) inset !important; | |
| 1418 | + } | |
| 1419 | + --size: calc(var(--size-field, 0.25rem) * 10) !important; | |
| 1420 | + --input-color: var(--color-base-content) !important; | |
| 1421 | + @supports (color: color-mix(in lab, red, red)) { | |
| 1422 | + --input-color: color-mix(in oklab, var(--color-base-content) 20%, #0000) !important; | |
| 1423 | + } | |
| 1424 | + &:where(input) { | |
| 1425 | + display: inline-flex !important; | |
| 1426 | + } | |
| 1427 | + :where(input) { | |
| 1428 | + display: inline-flex !important; | |
| 1429 | + height: 100% !important; | |
| 1430 | + width: 100% !important; | |
| 1431 | + appearance: none !important; | |
| 1432 | + background-color: transparent !important; | |
| 1433 | + border: none !important; | |
| 1434 | + &:focus, &:focus-within { | |
| 1435 | + --tw-outline-style: none !important; | |
| 1436 | + outline-style: none !important; | |
| 1437 | + @media (forced-colors: active) { | |
| 1438 | + outline: 2px solid transparent !important; | |
| 1439 | + outline-offset: 2px !important; | |
| 1440 | + } | |
| 1441 | + } | |
| 1442 | + } | |
| 1443 | + :where(input[type="url"]), :where(input[type="email"]) { | |
| 1444 | + direction: ltr !important; | |
| 1445 | + } | |
| 1446 | + :where(input[type="date"]) { | |
| 1447 | + display: inline-flex !important; | |
| 1448 | + } | |
| 1449 | + &:focus, &:focus-within { | |
| 1450 | + --input-color: var(--color-base-content) !important; | |
| 1451 | + box-shadow: 0 1px var(--input-color) !important; | |
| 1452 | + @supports (color: color-mix(in lab, red, red)) { | |
| 1453 | + box-shadow: 0 1px color-mix(in oklab, var(--input-color) calc(var(--depth) * 10%), #0000) !important; | |
| 1454 | + } | |
| 1455 | + outline: 2px solid var(--input-color) !important; | |
| 1456 | + outline-offset: 2px !important; | |
| 1457 | + isolation: isolate !important; | |
| 1458 | + } | |
| 1459 | + @media (pointer: coarse) { | |
| 1460 | + @supports (-webkit-touch-callout: none) { | |
| 1461 | + &:focus, &:focus-within { | |
| 1462 | + --font-size: 1rem !important; | |
| 1463 | + } | |
| 1464 | + } | |
| 1465 | + } | |
| 1466 | + &:has(> input[disabled]), &:is(:disabled, [disabled]), fieldset:disabled & { | |
| 1467 | + cursor: not-allowed !important; | |
| 1468 | + border-color: var(--color-base-200) !important; | |
| 1469 | + background-color: var(--color-base-200) !important; | |
| 1470 | + color: var(--color-base-content) !important; | |
| 1471 | + @supports (color: color-mix(in lab, red, red)) { | |
| 1472 | + color: color-mix(in oklab, var(--color-base-content) 40%, transparent) !important; | |
| 1473 | + } | |
| 1474 | + &::placeholder { | |
| 1475 | + color: var(--color-base-content) !important; | |
| 1476 | + @supports (color: color-mix(in lab, red, red)) { | |
| 1477 | + color: color-mix(in oklab, var(--color-base-content) 20%, transparent) !important; | |
| 1478 | + } | |
| 1479 | + } | |
| 1480 | + box-shadow: none !important; | |
| 1481 | + } | |
| 1482 | + &:has(> input[disabled]) > input[disabled] { | |
| 1483 | + cursor: not-allowed !important; | |
| 1484 | + } | |
| 1485 | + &::-webkit-date-and-time-value { | |
| 1486 | + text-align: inherit !important; | |
| 1487 | + } | |
| 1488 | + &[type="number"] { | |
| 1489 | + &::-webkit-inner-spin-button { | |
| 1490 | + margin-block: calc(0.25rem * -3) !important; | |
| 1491 | + margin-inline-end: calc(0.25rem * -3) !important; | |
| 1492 | + } | |
| 1493 | + } | |
| 1494 | + &::-webkit-calendar-picker-indicator { | |
| 1495 | + position: absolute !important; | |
| 1496 | + inset-inline-end: 0.75em !important; | |
| 1497 | + } | |
| 1498 | + &:has(> input[type="date"]) { | |
| 1499 | + :where(input[type="date"]) { | |
| 1500 | + display: inline-flex !important; | |
| 1501 | + webkit-appearance: none !important; | |
| 1502 | + appearance: none !important; | |
| 1503 | + } | |
| 1504 | + input[type="date"]::-webkit-calendar-picker-indicator { | |
| 1505 | + position: absolute !important; | |
| 1506 | + inset-inline-end: 0.75em !important; | |
| 1507 | + width: 1em !important; | |
| 1508 | + height: 1em !important; | |
| 1509 | + cursor: pointer !important; | |
| 1510 | + } | |
| 1511 | + } | |
| 1512 | + } | |
| 1513 | + } | |
| 1127 | 1514 | .input { |
| 1128 | 1515 | @layer daisyui.l1.l2.l3 { |
| 1129 | 1516 | cursor: text; |
@@ -1706,28 +2093,6 @@
| 1706 | 2093 | } |
| 1707 | 2094 | } |
| 1708 | 2095 | } |
| 1709 | - .image-full { | |
| 1710 | - @layer daisyui.l1.l2 { | |
| 1711 | - display: grid; | |
| 1712 | - > * { | |
| 1713 | - grid-column-start: 1; | |
| 1714 | - grid-row-start: 1; | |
| 1715 | - } | |
| 1716 | - > .card-body { | |
| 1717 | - position: relative; | |
| 1718 | - color: var(--color-neutral-content); | |
| 1719 | - } | |
| 1720 | - :where(figure) { | |
| 1721 | - overflow: hidden; | |
| 1722 | - border-radius: inherit; | |
| 1723 | - } | |
| 1724 | - > figure img { | |
| 1725 | - height: 100%; | |
| 1726 | - object-fit: cover; | |
| 1727 | - filter: brightness(28%); | |
| 1728 | - } | |
| 1729 | - } | |
| 1730 | - } | |
| 1731 | 2096 | .swap { |
| 1732 | 2097 | @layer daisyui.l1.l2.l3 { |
| 1733 | 2098 | position: relative; |
@@ -1764,6 +2129,20 @@
| 1764 | 2129 | } |
| 1765 | 2130 | } |
| 1766 | 2131 | } |
| 2132 | + .collapse-title { | |
| 2133 | + @layer daisyui.l1.l2.l3 { | |
| 2134 | + grid-column-start: 1; | |
| 2135 | + grid-row-start: 1; | |
| 2136 | + } | |
| 2137 | + @layer daisyui.l1.l2.l3 { | |
| 2138 | + position: relative; | |
| 2139 | + width: 100%; | |
| 2140 | + padding: 1rem; | |
| 2141 | + padding-inline-end: 3rem; | |
| 2142 | + min-height: 1lh; | |
| 2143 | + transition: background-color 0.2s ease-out; | |
| 2144 | + } | |
| 2145 | + } | |
| 1767 | 2146 | .menu-vertical { |
| 1768 | 2147 | @layer daisyui.l1.l2 { |
| 1769 | 2148 | display: inline-flex; |
@@ -2098,9 +2477,6 @@
| 2098 | 2477 | } |
| 2099 | 2478 | } |
| 2100 | 2479 | } |
| 2101 | - .inset-0 { | |
| 2102 | - inset: calc(var(--spacing) * 0); | |
| 2103 | - } | |
| 2104 | 2480 | .start { |
| 2105 | 2481 | inset-inline-start: var(--spacing); |
| 2106 | 2482 | } |
@@ -2145,18 +2521,12 @@
| 2145 | 2521 | } |
| 2146 | 2522 | } |
| 2147 | 2523 | } |
| 2148 | - .top-0 { | |
| 2149 | - top: calc(var(--spacing) * 0); | |
| 2150 | - } | |
| 2151 | 2524 | .top-16 { |
| 2152 | 2525 | top: calc(var(--spacing) * 16); |
| 2153 | 2526 | } |
| 2154 | 2527 | .top-full { |
| 2155 | 2528 | top: 100%; |
| 2156 | 2529 | } |
| 2157 | - .right-0 { | |
| 2158 | - right: calc(var(--spacing) * 0); | |
| 2159 | - } | |
| 2160 | 2530 | .right-4 { |
| 2161 | 2531 | right: calc(var(--spacing) * 4); |
| 2162 | 2532 | } |
@@ -2236,6 +2606,17 @@
| 2236 | 2606 | } |
| 2237 | 2607 | } |
| 2238 | 2608 | } |
| 2609 | + .hero-content { | |
| 2610 | + @layer daisyui.l1.l2.l3 { | |
| 2611 | + isolation: isolate; | |
| 2612 | + display: flex; | |
| 2613 | + max-width: 80rem; | |
| 2614 | + align-items: center; | |
| 2615 | + justify-content: center; | |
| 2616 | + gap: calc(0.25rem * 4); | |
| 2617 | + padding: calc(0.25rem * 4); | |
| 2618 | + } | |
| 2619 | + } | |
| 2239 | 2620 | .textarea { |
| 2240 | 2621 | @layer daisyui.l1.l2.l3 { |
| 2241 | 2622 | border: var(--border) solid #0000; |
@@ -2402,29 +2783,120 @@
| 2402 | 2783 | } |
| 2403 | 2784 | } |
| 2404 | 2785 | } |
| 2405 | - .modal-backdrop { | |
| 2786 | + .stack\! { | |
| 2406 | 2787 | @layer daisyui.l1.l2.l3 { |
| 2407 | - grid-column-start: 1; | |
| 2408 | - grid-row-start: 1; | |
| 2409 | - display: grid; | |
| 2410 | - align-self: stretch; | |
| 2411 | - justify-self: stretch; | |
| 2412 | - color: transparent; | |
| 2788 | + display: inline-grid !important; | |
| 2789 | + grid-template-columns: 3px 4px 1fr 4px 3px !important; | |
| 2790 | + grid-template-rows: 3px 4px 1fr 4px 3px !important; | |
| 2791 | + & > * { | |
| 2792 | + height: 100% !important; | |
| 2793 | + width: 100% !important; | |
| 2794 | + &:nth-child(n + 2) { | |
| 2795 | + width: 100% !important; | |
| 2796 | + opacity: 70% !important; | |
| 2797 | + } | |
| 2798 | + &:nth-child(2) { | |
| 2799 | + z-index: 2 !important; | |
| 2800 | + opacity: 90% !important; | |
| 2801 | + } | |
| 2802 | + &:nth-child(1) { | |
| 2803 | + z-index: 3 !important; | |
| 2804 | + width: 100% !important; | |
| 2805 | + } | |
| 2806 | + } | |
| 2807 | + } | |
| 2808 | + @layer daisyui.l1.l2 { | |
| 2809 | + &, &.stack-bottom { | |
| 2810 | + > * { | |
| 2811 | + grid-column: 3 / 4 !important; | |
| 2812 | + grid-row: 3 / 6 !important; | |
| 2813 | + &:nth-child(2) { | |
| 2814 | + grid-column: 2 / 5 !important; | |
| 2815 | + grid-row: 2 / 5 !important; | |
| 2816 | + } | |
| 2817 | + &:nth-child(1) { | |
| 2818 | + grid-column: 1 / 6 !important; | |
| 2819 | + grid-row: 1 / 4 !important; | |
| 2820 | + } | |
| 2821 | + } | |
| 2822 | + } | |
| 2823 | + &.stack-top { | |
| 2824 | + > * { | |
| 2825 | + grid-column: 3 / 4 !important; | |
| 2826 | + grid-row: 1 / 4 !important; | |
| 2827 | + &:nth-child(2) { | |
| 2828 | + grid-column: 2 / 5 !important; | |
| 2829 | + grid-row: 2 / 5 !important; | |
| 2830 | + } | |
| 2831 | + &:nth-child(1) { | |
| 2832 | + grid-column: 1 / 6 !important; | |
| 2833 | + grid-row: 3 / 6 !important; | |
| 2834 | + } | |
| 2835 | + } | |
| 2836 | + } | |
| 2837 | + &.stack-start { | |
| 2838 | + > * { | |
| 2839 | + grid-column: 1 / 4 !important; | |
| 2840 | + grid-row: 3 / 4 !important; | |
| 2841 | + &:nth-child(2) { | |
| 2842 | + grid-column: 2 / 5 !important; | |
| 2843 | + grid-row: 2 / 5 !important; | |
| 2844 | + } | |
| 2845 | + &:nth-child(1) { | |
| 2846 | + grid-column: 3 / 6 !important; | |
| 2847 | + grid-row: 1 / 6 !important; | |
| 2848 | + } | |
| 2849 | + } | |
| 2850 | + } | |
| 2851 | + &.stack-end { | |
| 2852 | + > * { | |
| 2853 | + grid-column: 3 / 6 !important; | |
| 2854 | + grid-row: 3 / 4 !important; | |
| 2855 | + &:nth-child(2) { | |
| 2856 | + grid-column: 2 / 5 !important; | |
| 2857 | + grid-row: 2 / 5 !important; | |
| 2858 | + } | |
| 2859 | + &:nth-child(1) { | |
| 2860 | + grid-column: 1 / 4 !important; | |
| 2861 | + grid-row: 1 / 6 !important; | |
| 2862 | + } | |
| 2863 | + } | |
| 2864 | + } | |
| 2865 | + } | |
| 2866 | + } | |
| 2867 | + .modal-backdrop { | |
| 2868 | + @layer daisyui.l1.l2.l3 { | |
| 2869 | + grid-column-start: 1; | |
| 2870 | + grid-row-start: 1; | |
| 2871 | + display: grid; | |
| 2872 | + align-self: stretch; | |
| 2873 | + justify-self: stretch; | |
| 2874 | + color: transparent; | |
| 2413 | 2875 | z-index: -1; |
| 2414 | 2876 | button { |
| 2415 | 2877 | cursor: pointer; |
| 2416 | 2878 | } |
| 2417 | 2879 | } |
| 2418 | 2880 | } |
| 2419 | - .z-40 { | |
| 2420 | - z-index: 40; | |
| 2421 | - } | |
| 2422 | 2881 | .z-50 { |
| 2423 | 2882 | z-index: 50; |
| 2424 | 2883 | } |
| 2425 | 2884 | .z-\[1\] { |
| 2426 | 2885 | z-index: 1; |
| 2427 | 2886 | } |
| 2887 | + .hero { | |
| 2888 | + @layer daisyui.l1.l2.l3 { | |
| 2889 | + display: grid; | |
| 2890 | + width: 100%; | |
| 2891 | + place-items: center; | |
| 2892 | + background-size: cover; | |
| 2893 | + background-position: center; | |
| 2894 | + & > * { | |
| 2895 | + grid-column-start: 1; | |
| 2896 | + grid-row-start: 1; | |
| 2897 | + } | |
| 2898 | + } | |
| 2899 | + } | |
| 2428 | 2900 | .modal-box { |
| 2429 | 2901 | @layer daisyui.l1.l2.l3 { |
| 2430 | 2902 | grid-column-start: 1; |
@@ -2483,8 +2955,35 @@
| 2483 | 2955 | max-width: 96rem; |
| 2484 | 2956 | } |
| 2485 | 2957 | } |
| 2486 | - .m-4 { | |
| 2487 | - margin: calc(var(--spacing) * 4); | |
| 2958 | + .divider { | |
| 2959 | + @layer daisyui.l1.l2.l3 { | |
| 2960 | + display: flex; | |
| 2961 | + height: calc(0.25rem * 4); | |
| 2962 | + flex-direction: row; | |
| 2963 | + align-items: center; | |
| 2964 | + align-self: stretch; | |
| 2965 | + white-space: nowrap; | |
| 2966 | + margin: var(--divider-m, 1rem 0); | |
| 2967 | + --divider-color: var(--color-base-content); | |
| 2968 | + @supports (color: color-mix(in lab, red, red)) { | |
| 2969 | + --divider-color: color-mix(in oklab, var(--color-base-content) 10%, transparent); | |
| 2970 | + } | |
| 2971 | + &:before, &:after { | |
| 2972 | + content: ""; | |
| 2973 | + height: calc(0.25rem * 0.5); | |
| 2974 | + width: 100%; | |
| 2975 | + flex-grow: 1; | |
| 2976 | + background-color: var(--divider-color); | |
| 2977 | + } | |
| 2978 | + @media print { | |
| 2979 | + &:before, &:after { | |
| 2980 | + border: 0.5px solid; | |
| 2981 | + } | |
| 2982 | + } | |
| 2983 | + &:not(:empty) { | |
| 2984 | + gap: calc(0.25rem * 4); | |
| 2985 | + } | |
| 2986 | + } | |
| 2488 | 2987 | } |
| 2489 | 2988 | .filter { |
| 2490 | 2989 | @layer daisyui.l1.l2.l3 { |
@@ -2531,36 +3030,9 @@
| 2531 | 3030 | } |
| 2532 | 3031 | } |
| 2533 | 3032 | } |
| 2534 | - .mx-4 { | |
| 2535 | - margin-inline: calc(var(--spacing) * 4); | |
| 2536 | - } | |
| 2537 | 3033 | .mx-auto { |
| 2538 | 3034 | margin-inline: auto; |
| 2539 | 3035 | } |
| 2540 | - .input-lg { | |
| 2541 | - @layer daisyui.l1.l2 { | |
| 2542 | - --size: calc(var(--size-field, 0.25rem) * 12); | |
| 2543 | - font-size: max(var(--font-size, 1.125rem), 1.125rem); | |
| 2544 | - &[type="number"] { | |
| 2545 | - &::-webkit-inner-spin-button { | |
| 2546 | - margin-block: calc(0.25rem * -3); | |
| 2547 | - margin-inline-end: calc(0.25rem * -3); | |
| 2548 | - } | |
| 2549 | - } | |
| 2550 | - } | |
| 2551 | - } | |
| 2552 | - .input-md { | |
| 2553 | - @layer daisyui.l1.l2 { | |
| 2554 | - --size: calc(var(--size-field, 0.25rem) * 10); | |
| 2555 | - font-size: max(var(--font-size, 0.875rem), 0.875rem); | |
| 2556 | - &[type="number"] { | |
| 2557 | - &::-webkit-inner-spin-button { | |
| 2558 | - margin-block: calc(0.25rem * -3); | |
| 2559 | - margin-inline-end: calc(0.25rem * -3); | |
| 2560 | - } | |
| 2561 | - } | |
| 2562 | - } | |
| 2563 | - } | |
| 2564 | 3036 | .input-sm { |
| 2565 | 3037 | @layer daisyui.l1.l2 { |
| 2566 | 3038 | --size: calc(var(--size-field, 0.25rem) * 8); |
@@ -2573,24 +3045,9 @@
| 2573 | 3045 | } |
| 2574 | 3046 | } |
| 2575 | 3047 | } |
| 2576 | - .input-xs { | |
| 2577 | - @layer daisyui.l1.l2 { | |
| 2578 | - --size: calc(var(--size-field, 0.25rem) * 6); | |
| 2579 | - font-size: max(var(--font-size, 0.6875rem), 0.6875rem); | |
| 2580 | - &[type="number"] { | |
| 2581 | - &::-webkit-inner-spin-button { | |
| 2582 | - margin-block: calc(0.25rem * -1); | |
| 2583 | - margin-inline-end: calc(0.25rem * -3); | |
| 2584 | - } | |
| 2585 | - } | |
| 2586 | - } | |
| 2587 | - } | |
| 2588 | 3048 | .my-2 { |
| 2589 | 3049 | margin-block: calc(var(--spacing) * 2); |
| 2590 | 3050 | } |
| 2591 | - .my-4 { | |
| 2592 | - margin-block: calc(var(--spacing) * 4); | |
| 2593 | - } | |
| 2594 | 3051 | .breadcrumbs { |
| 2595 | 3052 | @layer daisyui.l1.l2.l3 { |
| 2596 | 3053 | max-width: 100%; |
@@ -2703,6 +3160,9 @@
| 2703 | 3160 | gap: calc(0.25rem * 2); |
| 2704 | 3161 | } |
| 2705 | 3162 | } |
| 3163 | + .mt-0\.5 { | |
| 3164 | + margin-top: calc(var(--spacing) * 0.5); | |
| 3165 | + } | |
| 2706 | 3166 | .mt-1 { |
| 2707 | 3167 | margin-top: calc(var(--spacing) * 1); |
| 2708 | 3168 | } |
@@ -2721,15 +3181,9 @@
| 2721 | 3181 | .mt-8 { |
| 2722 | 3182 | margin-top: calc(var(--spacing) * 8); |
| 2723 | 3183 | } |
| 2724 | - .mt-12 { | |
| 2725 | - margin-top: calc(var(--spacing) * 12); | |
| 2726 | - } | |
| 2727 | 3184 | .mr-2 { |
| 2728 | 3185 | margin-right: calc(var(--spacing) * 2); |
| 2729 | 3186 | } |
| 2730 | - .mr-4 { | |
| 2731 | - margin-right: calc(var(--spacing) * 4); | |
| 2732 | - } | |
| 2733 | 3187 | .mb-1 { |
| 2734 | 3188 | margin-bottom: calc(var(--spacing) * 1); |
| 2735 | 3189 | } |
@@ -2751,8 +3205,11 @@
| 2751 | 3205 | .mb-12 { |
| 2752 | 3206 | margin-bottom: calc(var(--spacing) * 12); |
| 2753 | 3207 | } |
| 2754 | - .ml-4 { | |
| 2755 | - margin-left: calc(var(--spacing) * 4); | |
| 3208 | + .ml-1 { | |
| 3209 | + margin-left: calc(var(--spacing) * 1); | |
| 3210 | + } | |
| 3211 | + .ml-2 { | |
| 3212 | + margin-left: calc(var(--spacing) * 2); | |
| 2756 | 3213 | } |
| 2757 | 3214 | .ml-auto { |
| 2758 | 3215 | margin-left: auto; |
@@ -2967,6 +3424,62 @@
| 2967 | 3424 | font-weight: 600; |
| 2968 | 3425 | } |
| 2969 | 3426 | } |
| 3427 | + .prose { | |
| 3428 | + :root & { | |
| 3429 | + --tw-prose-body: var(--color-base-content); | |
| 3430 | + @supports (color: color-mix(in lab, red, red)) { | |
| 3431 | + --tw-prose-body: color-mix(in oklab, var(--color-base-content) 80%, #0000); | |
| 3432 | + } | |
| 3433 | + --tw-prose-headings: var(--color-base-content); | |
| 3434 | + --tw-prose-lead: var(--color-base-content); | |
| 3435 | + --tw-prose-links: var(--color-base-content); | |
| 3436 | + --tw-prose-bold: var(--color-base-content); | |
| 3437 | + --tw-prose-counters: var(--color-base-content); | |
| 3438 | + --tw-prose-bullets: var(--color-base-content); | |
| 3439 | + @supports (color: color-mix(in lab, red, red)) { | |
| 3440 | + --tw-prose-bullets: color-mix(in oklab, var(--color-base-content) 50%, #0000); | |
| 3441 | + } | |
| 3442 | + --tw-prose-hr: var(--color-base-content); | |
| 3443 | + @supports (color: color-mix(in lab, red, red)) { | |
| 3444 | + --tw-prose-hr: color-mix(in oklab, var(--color-base-content) 20%, #0000); | |
| 3445 | + } | |
| 3446 | + --tw-prose-quotes: var(--color-base-content); | |
| 3447 | + --tw-prose-quote-borders: var(--color-base-content); | |
| 3448 | + @supports (color: color-mix(in lab, red, red)) { | |
| 3449 | + --tw-prose-quote-borders: color-mix(in oklab, var(--color-base-content) 20%, #0000); | |
| 3450 | + } | |
| 3451 | + --tw-prose-captions: var(--color-base-content); | |
| 3452 | + @supports (color: color-mix(in lab, red, red)) { | |
| 3453 | + --tw-prose-captions: color-mix(in oklab, var(--color-base-content) 50%, #0000); | |
| 3454 | + } | |
| 3455 | + --tw-prose-code: var(--color-base-content); | |
| 3456 | + --tw-prose-pre-code: var(--color-neutral-content); | |
| 3457 | + --tw-prose-pre-bg: var(--color-neutral); | |
| 3458 | + --tw-prose-th-borders: var(--color-base-content); | |
| 3459 | + @supports (color: color-mix(in lab, red, red)) { | |
| 3460 | + --tw-prose-th-borders: color-mix(in oklab, var(--color-base-content) 50%, #0000); | |
| 3461 | + } | |
| 3462 | + --tw-prose-td-borders: var(--color-base-content); | |
| 3463 | + @supports (color: color-mix(in lab, red, red)) { | |
| 3464 | + --tw-prose-td-borders: color-mix(in oklab, var(--color-base-content) 20%, #0000); | |
| 3465 | + } | |
| 3466 | + --tw-prose-kbd: var(--color-base-content); | |
| 3467 | + @supports (color: color-mix(in lab, red, red)) { | |
| 3468 | + --tw-prose-kbd: color-mix(in oklab, var(--color-base-content) 80%, #0000); | |
| 3469 | + } | |
| 3470 | + :where(code):not(pre > code) { | |
| 3471 | + background-color: var(--color-base-200); | |
| 3472 | + border-radius: var(--radius-selector); | |
| 3473 | + border: var(--border) solid var(--color-base-300); | |
| 3474 | + padding-inline: 0.5em; | |
| 3475 | + padding-block: 0.2em; | |
| 3476 | + font-weight: inherit; | |
| 3477 | + &:before, &:after { | |
| 3478 | + display: none; | |
| 3479 | + } | |
| 3480 | + } | |
| 3481 | + } | |
| 3482 | + } | |
| 2970 | 3483 | .mask { |
| 2971 | 3484 | @layer daisyui.l1.l2.l3 { |
| 2972 | 3485 | display: inline-block; |
@@ -2979,6 +3492,9 @@
| 2979 | 3492 | .block { |
| 2980 | 3493 | display: block; |
| 2981 | 3494 | } |
| 3495 | + .contents { | |
| 3496 | + display: contents; | |
| 3497 | + } | |
| 2982 | 3498 | .flex { |
| 2983 | 3499 | display: flex; |
| 2984 | 3500 | } |
@@ -2997,34 +3513,6 @@
| 2997 | 3513 | .table { |
| 2998 | 3514 | display: table; |
| 2999 | 3515 | } |
| 3000 | - .card-side { | |
| 3001 | - @layer daisyui.l1.l2 { | |
| 3002 | - align-items: stretch; | |
| 3003 | - flex-direction: row; | |
| 3004 | - :where(figure:first-child) { | |
| 3005 | - overflow: hidden; | |
| 3006 | - border-start-start-radius: inherit; | |
| 3007 | - border-start-end-radius: unset; | |
| 3008 | - border-end-start-radius: inherit; | |
| 3009 | - border-end-end-radius: unset; | |
| 3010 | - } | |
| 3011 | - :where(figure:last-child) { | |
| 3012 | - overflow: hidden; | |
| 3013 | - border-start-start-radius: unset; | |
| 3014 | - border-start-end-radius: inherit; | |
| 3015 | - border-end-start-radius: unset; | |
| 3016 | - border-end-end-radius: inherit; | |
| 3017 | - } | |
| 3018 | - figure > * { | |
| 3019 | - max-width: unset; | |
| 3020 | - } | |
| 3021 | - :where(figure > *) { | |
| 3022 | - width: 100%; | |
| 3023 | - height: 100%; | |
| 3024 | - object-fit: cover; | |
| 3025 | - } | |
| 3026 | - } | |
| 3027 | - } | |
| 3028 | 3516 | .btn-circle { |
| 3029 | 3517 | @layer daisyui.l1.l2 { |
| 3030 | 3518 | border-radius: calc(infinity * 1px); |
@@ -3033,12 +3521,8 @@
| 3033 | 3521 | height: var(--size); |
| 3034 | 3522 | } |
| 3035 | 3523 | } |
| 3036 | - .btn-square { | |
| 3037 | - @layer daisyui.l1.l2 { | |
| 3038 | - padding-inline: calc(0.25rem * 0); | |
| 3039 | - width: var(--size); | |
| 3040 | - height: var(--size); | |
| 3041 | - } | |
| 3524 | + .h-3 { | |
| 3525 | + height: calc(var(--spacing) * 3); | |
| 3042 | 3526 | } |
| 3043 | 3527 | .h-4 { |
| 3044 | 3528 | height: calc(var(--spacing) * 4); |
@@ -3064,24 +3548,23 @@
| 3064 | 3548 | .h-20 { |
| 3065 | 3549 | height: calc(var(--spacing) * 20); |
| 3066 | 3550 | } |
| 3067 | - .h-24 { | |
| 3068 | - height: calc(var(--spacing) * 24); | |
| 3551 | + .h-48 { | |
| 3552 | + height: calc(var(--spacing) * 48); | |
| 3069 | 3553 | } |
| 3070 | - .h-full { | |
| 3071 | - height: 100%; | |
| 3554 | + .min-h-\[60vh\] { | |
| 3555 | + min-height: 60vh; | |
| 3072 | 3556 | } |
| 3073 | 3557 | .min-h-screen { |
| 3074 | 3558 | min-height: 100vh; |
| 3075 | 3559 | } |
| 3076 | - .btn-wide { | |
| 3560 | + .btn-block { | |
| 3077 | 3561 | @layer daisyui.l1.l2 { |
| 3078 | 3562 | width: 100%; |
| 3079 | - max-width: calc(0.25rem * 64); | |
| 3080 | 3563 | } |
| 3081 | 3564 | } |
| 3082 | - .btn-block { | |
| 3565 | + .loading-lg { | |
| 3083 | 3566 | @layer daisyui.l1.l2 { |
| 3084 | - width: 100%; | |
| 3567 | + width: calc(var(--size-selector, 0.25rem) * 7); | |
| 3085 | 3568 | } |
| 3086 | 3569 | } |
| 3087 | 3570 | .loading-sm { |
@@ -3089,6 +3572,9 @@
| 3089 | 3572 | width: calc(var(--size-selector, 0.25rem) * 5); |
| 3090 | 3573 | } |
| 3091 | 3574 | } |
| 3575 | + .w-3 { | |
| 3576 | + width: calc(var(--spacing) * 3); | |
| 3577 | + } | |
| 3092 | 3578 | .w-4 { |
| 3093 | 3579 | width: calc(var(--spacing) * 4); |
| 3094 | 3580 | } |
@@ -3113,8 +3599,8 @@
| 3113 | 3599 | .w-20 { |
| 3114 | 3600 | width: calc(var(--spacing) * 20); |
| 3115 | 3601 | } |
| 3116 | - .w-24 { | |
| 3117 | - width: calc(var(--spacing) * 24); | |
| 3602 | + .w-48 { | |
| 3603 | + width: calc(var(--spacing) * 48); | |
| 3118 | 3604 | } |
| 3119 | 3605 | .w-52 { |
| 3120 | 3606 | width: calc(var(--spacing) * 52); |
@@ -3128,6 +3614,9 @@
| 3128 | 3614 | .max-w-2xl { |
| 3129 | 3615 | max-width: var(--container-2xl); |
| 3130 | 3616 | } |
| 3617 | + .max-w-3xl { | |
| 3618 | + max-width: var(--container-3xl); | |
| 3619 | + } | |
| 3131 | 3620 | .max-w-4xl { |
| 3132 | 3621 | max-width: var(--container-4xl); |
| 3133 | 3622 | } |
@@ -3140,6 +3629,15 @@
| 3140 | 3629 | .max-w-md { |
| 3141 | 3630 | max-width: var(--container-md); |
| 3142 | 3631 | } |
| 3632 | + .max-w-sm { | |
| 3633 | + max-width: var(--container-sm); | |
| 3634 | + } | |
| 3635 | + .max-w-xl { | |
| 3636 | + max-width: var(--container-xl); | |
| 3637 | + } | |
| 3638 | + .max-w-xs { | |
| 3639 | + max-width: var(--container-xs); | |
| 3640 | + } | |
| 3143 | 3641 | .min-w-0 { |
| 3144 | 3642 | min-width: calc(var(--spacing) * 0); |
| 3145 | 3643 | } |
@@ -3149,14 +3647,6 @@
| 3149 | 3647 | .shrink-0 { |
| 3150 | 3648 | flex-shrink: 0; |
| 3151 | 3649 | } |
| 3152 | - .translate-x-0 { | |
| 3153 | - --tw-translate-x: calc(var(--spacing) * 0); | |
| 3154 | - translate: var(--tw-translate-x) var(--tw-translate-y); | |
| 3155 | - } | |
| 3156 | - .translate-x-full { | |
| 3157 | - --tw-translate-x: 100%; | |
| 3158 | - translate: var(--tw-translate-x) var(--tw-translate-y); | |
| 3159 | - } | |
| 3160 | 3650 | .scale-95 { |
| 3161 | 3651 | --tw-scale-x: 95%; |
| 3162 | 3652 | --tw-scale-y: 95%; |
@@ -3175,6 +3665,22 @@
| 3175 | 3665 | .transform { |
| 3176 | 3666 | transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); |
| 3177 | 3667 | } |
| 3668 | + .skeleton { | |
| 3669 | + @layer daisyui.l1.l2.l3 { | |
| 3670 | + border-radius: var(--radius-box); | |
| 3671 | + background-color: var(--color-base-300); | |
| 3672 | + @media (prefers-reduced-motion: reduce) { | |
| 3673 | + transition-duration: 15s; | |
| 3674 | + } | |
| 3675 | + will-change: background-position; | |
| 3676 | + background-image: linear-gradient( 105deg, #0000 0% 40%, var(--color-base-100) 50%, #0000 60% 100% ); | |
| 3677 | + background-size: 200% auto; | |
| 3678 | + background-position-x: -50%; | |
| 3679 | + @media (prefers-reduced-motion: no-preference) { | |
| 3680 | + animation: skeleton 1.8s ease-in-out infinite; | |
| 3681 | + } | |
| 3682 | + } | |
| 3683 | + } | |
| 3178 | 3684 | .link { |
| 3179 | 3685 | @layer daisyui.l1.l2.l3 { |
| 3180 | 3686 | cursor: pointer; |
@@ -3193,37 +3699,27 @@
| 3193 | 3699 | } |
| 3194 | 3700 | } |
| 3195 | 3701 | } |
| 3196 | - .cursor-default { | |
| 3197 | - cursor: default; | |
| 3198 | - } | |
| 3199 | 3702 | .cursor-pointer { |
| 3200 | 3703 | cursor: pointer; |
| 3201 | 3704 | } |
| 3705 | + .resize { | |
| 3706 | + resize: both; | |
| 3707 | + } | |
| 3202 | 3708 | .list-inside { |
| 3203 | 3709 | list-style-position: inside; |
| 3204 | 3710 | } |
| 3205 | 3711 | .list-disc { |
| 3206 | 3712 | list-style-type: disc; |
| 3207 | 3713 | } |
| 3208 | - .stats-vertical { | |
| 3209 | - @layer daisyui.l1.l2 { | |
| 3210 | - grid-auto-flow: row; | |
| 3211 | - overflow-y: auto; | |
| 3212 | - .stat:not(:last-child) { | |
| 3213 | - border-inline-end: none; | |
| 3214 | - border-block-end: var(--border) dashed currentColor; | |
| 3215 | - @supports (color: color-mix(in lab, red, red)) { | |
| 3216 | - border-block-end: var(--border) dashed color-mix(in oklab, currentColor 10%, #0000); | |
| 3217 | - } | |
| 3218 | - } | |
| 3219 | - } | |
| 3220 | - } | |
| 3221 | 3714 | .grid-cols-1 { |
| 3222 | 3715 | grid-template-columns: repeat(1, minmax(0, 1fr)); |
| 3223 | 3716 | } |
| 3224 | 3717 | .grid-cols-2 { |
| 3225 | 3718 | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| 3226 | 3719 | } |
| 3720 | + .grid-cols-3 { | |
| 3721 | + grid-template-columns: repeat(3, minmax(0, 1fr)); | |
| 3722 | + } | |
| 3227 | 3723 | .flex-col { |
| 3228 | 3724 | flex-direction: column; |
| 3229 | 3725 | } |
@@ -3263,11 +3759,14 @@
| 3263 | 3759 | .gap-4 { |
| 3264 | 3760 | gap: calc(var(--spacing) * 4); |
| 3265 | 3761 | } |
| 3762 | + .gap-5 { | |
| 3763 | + gap: calc(var(--spacing) * 5); | |
| 3764 | + } | |
| 3266 | 3765 | .gap-6 { |
| 3267 | 3766 | gap: calc(var(--spacing) * 6); |
| 3268 | 3767 | } |
| 3269 | - .gap-8 { | |
| 3270 | - gap: calc(var(--spacing) * 8); | |
| 3768 | + .gap-7 { | |
| 3769 | + gap: calc(var(--spacing) * 7); | |
| 3271 | 3770 | } |
| 3272 | 3771 | .space-y-2 { |
| 3273 | 3772 | :where(& > :not(:last-child)) { |
@@ -3304,20 +3803,6 @@
| 3304 | 3803 | margin-inline-end: calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-x-reverse))); |
| 3305 | 3804 | } |
| 3306 | 3805 | } |
| 3307 | - .space-x-2 { | |
| 3308 | - :where(& > :not(:last-child)) { | |
| 3309 | - --tw-space-x-reverse: 0; | |
| 3310 | - margin-inline-start: calc(calc(var(--spacing) * 2) * var(--tw-space-x-reverse)); | |
| 3311 | - margin-inline-end: calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-x-reverse))); | |
| 3312 | - } | |
| 3313 | - } | |
| 3314 | - .space-x-3 { | |
| 3315 | - :where(& > :not(:last-child)) { | |
| 3316 | - --tw-space-x-reverse: 0; | |
| 3317 | - margin-inline-start: calc(calc(var(--spacing) * 3) * var(--tw-space-x-reverse)); | |
| 3318 | - margin-inline-end: calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-x-reverse))); | |
| 3319 | - } | |
| 3320 | - } | |
| 3321 | 3806 | .truncate { |
| 3322 | 3807 | overflow: hidden; |
| 3323 | 3808 | text-overflow: ellipsis; |
@@ -3332,6 +3817,9 @@
| 3332 | 3817 | .overflow-y-auto { |
| 3333 | 3818 | overflow-y: auto; |
| 3334 | 3819 | } |
| 3820 | + .rounded { | |
| 3821 | + border-radius: 0.25rem; | |
| 3822 | + } | |
| 3335 | 3823 | .rounded-box { |
| 3336 | 3824 | border-radius: var(--radius-box); |
| 3337 | 3825 | } |
@@ -3356,35 +3844,13 @@
| 3356 | 3844 | border-top-style: var(--tw-border-style); |
| 3357 | 3845 | border-top-width: 1px; |
| 3358 | 3846 | } |
| 3359 | - .border-r { | |
| 3360 | - border-right-style: var(--tw-border-style); | |
| 3361 | - border-right-width: 1px; | |
| 3362 | - } | |
| 3363 | 3847 | .border-b { |
| 3364 | 3848 | border-bottom-style: var(--tw-border-style); |
| 3365 | 3849 | border-bottom-width: 1px; |
| 3366 | 3850 | } |
| 3367 | - .border-l { | |
| 3851 | + .border-l-4 { | |
| 3368 | 3852 | border-left-style: var(--tw-border-style); |
| 3369 | - border-left-width: 1px; | |
| 3370 | - } | |
| 3371 | - .badge-dash { | |
| 3372 | - @layer daisyui.l1.l2 { | |
| 3373 | - color: var(--badge-color); | |
| 3374 | - --badge-bg: #0000; | |
| 3375 | - background-image: none; | |
| 3376 | - border-color: currentColor; | |
| 3377 | - border-style: dashed; | |
| 3378 | - } | |
| 3379 | - } | |
| 3380 | - .alert-dash { | |
| 3381 | - @layer daisyui.l1 { | |
| 3382 | - background-color: transparent; | |
| 3383 | - color: var(--alert-color); | |
| 3384 | - border-style: dashed; | |
| 3385 | - box-shadow: none; | |
| 3386 | - background-image: none; | |
| 3387 | - } | |
| 3853 | + border-left-width: 4px; | |
| 3388 | 3854 | } |
| 3389 | 3855 | .badge-ghost { |
| 3390 | 3856 | @layer daisyui.l1.l2 { |
@@ -3394,33 +3860,6 @@
| 3394 | 3860 | background-image: none; |
| 3395 | 3861 | } |
| 3396 | 3862 | } |
| 3397 | - .badge-soft { | |
| 3398 | - @layer daisyui.l1.l2 { | |
| 3399 | - color: var(--badge-color, var(--color-base-content)); | |
| 3400 | - background-color: var(--badge-color, var(--color-base-content)); | |
| 3401 | - @supports (color: color-mix(in lab, red, red)) { | |
| 3402 | - background-color: color-mix( in oklab, var(--badge-color, var(--color-base-content)) 8%, var(--color-base-100) ); | |
| 3403 | - } | |
| 3404 | - border-color: var(--badge-color, var(--color-base-content)); | |
| 3405 | - @supports (color: color-mix(in lab, red, red)) { | |
| 3406 | - border-color: color-mix( in oklab, var(--badge-color, var(--color-base-content)) 10%, var(--color-base-100) ); | |
| 3407 | - } | |
| 3408 | - background-image: none; | |
| 3409 | - } | |
| 3410 | - } | |
| 3411 | - .input-ghost { | |
| 3412 | - @layer daisyui.l1.l2 { | |
| 3413 | - background-color: transparent; | |
| 3414 | - box-shadow: none; | |
| 3415 | - border-color: #0000; | |
| 3416 | - &:focus, &:focus-within { | |
| 3417 | - background-color: var(--color-base-100); | |
| 3418 | - color: var(--color-base-content); | |
| 3419 | - border-color: #0000; | |
| 3420 | - box-shadow: none; | |
| 3421 | - } | |
| 3422 | - } | |
| 3423 | - } | |
| 3424 | 3863 | .badge-outline { |
| 3425 | 3864 | @layer daisyui.l1.l2 { |
| 3426 | 3865 | color: var(--badge-color); |
@@ -3432,23 +3871,12 @@
| 3432 | 3871 | .border-base-300 { |
| 3433 | 3872 | border-color: var(--color-base-300); |
| 3434 | 3873 | } |
| 3435 | - .border-info\/20 { | |
| 3436 | - border-color: var(--color-info); | |
| 3437 | - @supports (color: color-mix(in lab, red, red)) { | |
| 3438 | - border-color: color-mix(in oklab, var(--color-info) 20%, transparent); | |
| 3439 | - } | |
| 3874 | + .border-error { | |
| 3875 | + border-color: var(--color-error); | |
| 3440 | 3876 | } |
| 3441 | 3877 | .border-primary { |
| 3442 | 3878 | border-color: var(--color-primary); |
| 3443 | 3879 | } |
| 3444 | - .alert-outline { | |
| 3445 | - @layer daisyui.l1 { | |
| 3446 | - background-color: transparent; | |
| 3447 | - color: var(--alert-color); | |
| 3448 | - box-shadow: none; | |
| 3449 | - background-image: none; | |
| 3450 | - } | |
| 3451 | - } | |
| 3452 | 3880 | .table-zebra { |
| 3453 | 3881 | @layer daisyui.l1.l2 { |
| 3454 | 3882 | tbody { |
@@ -3478,9 +3906,6 @@
| 3478 | 3906 | .bg-base-200 { |
| 3479 | 3907 | background-color: var(--color-base-200); |
| 3480 | 3908 | } |
| 3481 | - .bg-black { | |
| 3482 | - background-color: var(--color-black); | |
| 3483 | - } | |
| 3484 | 3909 | .bg-error\/20 { |
| 3485 | 3910 | background-color: var(--color-error); |
| 3486 | 3911 | @supports (color: color-mix(in lab, red, red)) { |
@@ -3490,24 +3915,9 @@
| 3490 | 3915 | .bg-info { |
| 3491 | 3916 | background-color: var(--color-info); |
| 3492 | 3917 | } |
| 3493 | - .bg-info\/10 { | |
| 3494 | - background-color: var(--color-info); | |
| 3495 | - @supports (color: color-mix(in lab, red, red)) { | |
| 3496 | - background-color: color-mix(in oklab, var(--color-info) 10%, transparent); | |
| 3497 | - } | |
| 3498 | - } | |
| 3499 | 3918 | .bg-primary { |
| 3500 | 3919 | background-color: var(--color-primary); |
| 3501 | 3920 | } |
| 3502 | - .bg-primary\/20 { | |
| 3503 | - background-color: var(--color-primary); | |
| 3504 | - @supports (color: color-mix(in lab, red, red)) { | |
| 3505 | - background-color: color-mix(in oklab, var(--color-primary) 20%, transparent); | |
| 3506 | - } | |
| 3507 | - } | |
| 3508 | - .bg-red-500\! { | |
| 3509 | - background-color: var(--color-red-500) !important; | |
| 3510 | - } | |
| 3511 | 3921 | .bg-success\/20 { |
| 3512 | 3922 | background-color: var(--color-success); |
| 3513 | 3923 | @supports (color: color-mix(in lab, red, red)) { |
@@ -3520,29 +3930,13 @@
| 3520 | 3930 | background-color: color-mix(in oklab, var(--color-warning) 20%, transparent); |
| 3521 | 3931 | } |
| 3522 | 3932 | } |
| 3933 | + .bg-white { | |
| 3934 | + background-color: var(--color-white); | |
| 3935 | + } | |
| 3523 | 3936 | .bg-gradient-to-br { |
| 3524 | 3937 | --tw-gradient-position: to bottom right in oklab; |
| 3525 | 3938 | background-image: linear-gradient(var(--tw-gradient-stops)); |
| 3526 | 3939 | } |
| 3527 | - .alert-soft { | |
| 3528 | - @layer daisyui.l1 { | |
| 3529 | - color: var(--alert-color, var(--color-base-content)); | |
| 3530 | - background: var(--alert-color, var(--color-base-content)); | |
| 3531 | - @supports (color: color-mix(in lab, red, red)) { | |
| 3532 | - background: color-mix( in oklab, var(--alert-color, var(--color-base-content)) 8%, var(--color-base-100) ); | |
| 3533 | - } | |
| 3534 | - --alert-border-color: var(--alert-color, var(--color-base-content)); | |
| 3535 | - @supports (color: color-mix(in lab, red, red)) { | |
| 3536 | - --alert-border-color: color-mix( | |
| 3537 | - in oklab, | |
| 3538 | - var(--alert-color, var(--color-base-content)) 10%, | |
| 3539 | - var(--color-base-100) | |
| 3540 | - ); | |
| 3541 | - } | |
| 3542 | - box-shadow: none; | |
| 3543 | - background-image: none; | |
| 3544 | - } | |
| 3545 | - } | |
| 3546 | 3940 | .from-base-300 { |
| 3547 | 3941 | --tw-gradient-from: var(--color-base-300); |
| 3548 | 3942 | --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); |
@@ -3570,6 +3964,9 @@
| 3570 | 3964 | --size: calc(var(--size-selector, 0.25rem) * 5); |
| 3571 | 3965 | } |
| 3572 | 3966 | } |
| 3967 | + .p-0 { | |
| 3968 | + padding: calc(var(--spacing) * 0); | |
| 3969 | + } | |
| 3573 | 3970 | .p-2 { |
| 3574 | 3971 | padding: calc(var(--spacing) * 2); |
| 3575 | 3972 | } |
@@ -3594,6 +3991,17 @@
| 3594 | 3991 | font-weight: 600; |
| 3595 | 3992 | } |
| 3596 | 3993 | } |
| 3994 | + .table-sm { | |
| 3995 | + @layer daisyui.l1.l2 { | |
| 3996 | + :not(thead, tfoot) tr { | |
| 3997 | + font-size: 0.75rem; | |
| 3998 | + } | |
| 3999 | + :where(th, td) { | |
| 4000 | + padding-inline: calc(0.25rem * 3); | |
| 4001 | + padding-block: calc(0.25rem * 2); | |
| 4002 | + } | |
| 4003 | + } | |
| 4004 | + } | |
| 3597 | 4005 | .px-2 { |
| 3598 | 4006 | padding-inline: calc(var(--spacing) * 2); |
| 3599 | 4007 | } |
@@ -3615,27 +4023,30 @@
| 3615 | 4023 | .py-8 { |
| 3616 | 4024 | padding-block: calc(var(--spacing) * 8); |
| 3617 | 4025 | } |
| 4026 | + .py-10 { | |
| 4027 | + padding-block: calc(var(--spacing) * 10); | |
| 4028 | + } | |
| 4029 | + .py-12 { | |
| 4030 | + padding-block: calc(var(--spacing) * 12); | |
| 4031 | + } | |
| 4032 | + .py-16 { | |
| 4033 | + padding-block: calc(var(--spacing) * 16); | |
| 4034 | + } | |
| 3618 | 4035 | .pt-2 { |
| 3619 | 4036 | padding-top: calc(var(--spacing) * 2); |
| 3620 | 4037 | } |
| 3621 | 4038 | .pt-4 { |
| 3622 | 4039 | padding-top: calc(var(--spacing) * 4); |
| 3623 | 4040 | } |
| 3624 | - .pr-4 { | |
| 3625 | - padding-right: calc(var(--spacing) * 4); | |
| 3626 | - } | |
| 3627 | - .pb-4 { | |
| 3628 | - padding-bottom: calc(var(--spacing) * 4); | |
| 3629 | - } | |
| 3630 | - .pl-4 { | |
| 3631 | - padding-left: calc(var(--spacing) * 4); | |
| 3632 | - } | |
| 3633 | 4041 | .text-center { |
| 3634 | 4042 | text-align: center; |
| 3635 | 4043 | } |
| 3636 | 4044 | .text-left { |
| 3637 | 4045 | text-align: left; |
| 3638 | 4046 | } |
| 4047 | + .text-right { | |
| 4048 | + text-align: right; | |
| 4049 | + } | |
| 3639 | 4050 | .font-mono { |
| 3640 | 4051 | font-family: var(--font-mono); |
| 3641 | 4052 | } |
@@ -3651,9 +4062,9 @@
| 3651 | 4062 | font-size: var(--text-4xl); |
| 3652 | 4063 | line-height: var(--tw-leading, var(--text-4xl--line-height)); |
| 3653 | 4064 | } |
| 3654 | - .text-6xl { | |
| 3655 | - font-size: var(--text-6xl); | |
| 3656 | - line-height: var(--tw-leading, var(--text-6xl--line-height)); | |
| 4065 | + .text-5xl { | |
| 4066 | + font-size: var(--text-5xl); | |
| 4067 | + line-height: var(--tw-leading, var(--text-5xl--line-height)); | |
| 3657 | 4068 | } |
| 3658 | 4069 | .text-base { |
| 3659 | 4070 | font-size: var(--text-base); |
@@ -3681,30 +4092,12 @@
| 3681 | 4092 | font-size: 1rem; |
| 3682 | 4093 | } |
| 3683 | 4094 | } |
| 3684 | - .badge-md { | |
| 3685 | - @layer daisyui.l1.l2 { | |
| 3686 | - --size: calc(var(--size-selector, 0.25rem) * 6); | |
| 3687 | - font-size: 0.875rem; | |
| 3688 | - } | |
| 3689 | - } | |
| 3690 | 4095 | .badge-sm { |
| 3691 | 4096 | @layer daisyui.l1.l2 { |
| 3692 | 4097 | --size: calc(var(--size-selector, 0.25rem) * 5); |
| 3693 | 4098 | font-size: 0.75rem; |
| 3694 | 4099 | } |
| 3695 | 4100 | } |
| 3696 | - .badge-xl { | |
| 3697 | - @layer daisyui.l1.l2 { | |
| 3698 | - --size: calc(var(--size-selector, 0.25rem) * 8); | |
| 3699 | - font-size: 1.125rem; | |
| 3700 | - } | |
| 3701 | - } | |
| 3702 | - .badge-xs { | |
| 3703 | - @layer daisyui.l1.l2 { | |
| 3704 | - --size: calc(var(--size-selector, 0.25rem) * 4); | |
| 3705 | - font-size: 0.625rem; | |
| 3706 | - } | |
| 3707 | - } | |
| 3708 | 4101 | .font-bold { |
| 3709 | 4102 | --tw-font-weight: var(--font-weight-bold); |
| 3710 | 4103 | font-weight: var(--font-weight-bold); |
@@ -3713,17 +4106,20 @@
| 3713 | 4106 | --tw-font-weight: var(--font-weight-medium); |
| 3714 | 4107 | font-weight: var(--font-weight-medium); |
| 3715 | 4108 | } |
| 3716 | - .font-normal { | |
| 3717 | - --tw-font-weight: var(--font-weight-normal); | |
| 3718 | - font-weight: var(--font-weight-normal); | |
| 3719 | - } | |
| 3720 | 4109 | .font-semibold { |
| 3721 | 4110 | --tw-font-weight: var(--font-weight-semibold); |
| 3722 | 4111 | font-weight: var(--font-weight-semibold); |
| 3723 | 4112 | } |
| 3724 | - .tracking-wider { | |
| 3725 | - --tw-tracking: var(--tracking-wider); | |
| 3726 | - letter-spacing: var(--tracking-wider); | |
| 4113 | + .tracking-wide { | |
| 4114 | + --tw-tracking: var(--tracking-wide); | |
| 4115 | + letter-spacing: var(--tracking-wide); | |
| 4116 | + } | |
| 4117 | + .tracking-widest { | |
| 4118 | + --tw-tracking: var(--tracking-widest); | |
| 4119 | + letter-spacing: var(--tracking-widest); | |
| 4120 | + } | |
| 4121 | + .break-all { | |
| 4122 | + word-break: break-all; | |
| 3727 | 4123 | } |
| 3728 | 4124 | .whitespace-nowrap { |
| 3729 | 4125 | white-space: nowrap; |
@@ -3801,12 +4197,6 @@
| 3801 | 4197 | color: color-mix(in oklab, var(--color-base-content) 30%, transparent); |
| 3802 | 4198 | } |
| 3803 | 4199 | } |
| 3804 | - .text-base-content\/40 { | |
| 3805 | - color: var(--color-base-content); | |
| 3806 | - @supports (color: color-mix(in lab, red, red)) { | |
| 3807 | - color: color-mix(in oklab, var(--color-base-content) 40%, transparent); | |
| 3808 | - } | |
| 3809 | - } | |
| 3810 | 4200 | .text-base-content\/50 { |
| 3811 | 4201 | color: var(--color-base-content); |
| 3812 | 4202 | @supports (color: color-mix(in lab, red, red)) { |
@@ -3834,9 +4224,6 @@
| 3834 | 4224 | .text-error { |
| 3835 | 4225 | color: var(--color-error); |
| 3836 | 4226 | } |
| 3837 | - .text-gray-800 { | |
| 3838 | - color: var(--color-gray-800); | |
| 3839 | - } | |
| 3840 | 4227 | .text-info { |
| 3841 | 4228 | color: var(--color-info); |
| 3842 | 4229 | } |
@@ -3858,6 +4245,9 @@
| 3858 | 4245 | .text-warning { |
| 3859 | 4246 | color: var(--color-warning); |
| 3860 | 4247 | } |
| 4248 | + .capitalize { | |
| 4249 | + text-transform: capitalize; | |
| 4250 | + } | |
| 3861 | 4251 | .lowercase { |
| 3862 | 4252 | text-transform: lowercase; |
| 3863 | 4253 | } |
@@ -3867,26 +4257,30 @@
| 3867 | 4257 | .uppercase { |
| 3868 | 4258 | text-transform: uppercase; |
| 3869 | 4259 | } |
| 3870 | - .btn-link { | |
| 3871 | - @layer daisyui.l1 { | |
| 3872 | - text-decoration-line: underline; | |
| 3873 | - outline-color: currentcolor; | |
| 3874 | - --btn-border: #0000; | |
| 3875 | - --btn-bg: #0000; | |
| 3876 | - --btn-noise: none; | |
| 3877 | - --btn-shadow: ""; | |
| 3878 | - &:not(.btn-disabled, .btn:disabled, .btn[disabled]) { | |
| 3879 | - --btn-fg: var(--btn-color, var(--color-primary)); | |
| 3880 | - } | |
| 3881 | - &:is(.btn-active, :hover, :active:focus, :focus-visible) { | |
| 3882 | - --btn-border: #0000; | |
| 3883 | - --btn-bg: #0000; | |
| 4260 | + .link-hover { | |
| 4261 | + @layer daisyui.l1.l2 { | |
| 4262 | + text-decoration-line: none; | |
| 4263 | + &:hover { | |
| 4264 | + @media (hover: hover) { | |
| 4265 | + text-decoration-line: underline; | |
| 4266 | + } | |
| 3884 | 4267 | } |
| 3885 | 4268 | } |
| 3886 | 4269 | } |
| 4270 | + .prose { | |
| 4271 | + & :where(a.btn:not(.btn-link)):not(:where([class~="not-prose"], [class~="not-prose"] *)) { | |
| 4272 | + text-decoration-line: none; | |
| 4273 | + } | |
| 4274 | + } | |
| 3887 | 4275 | .opacity-0 { |
| 3888 | 4276 | opacity: 0%; |
| 3889 | 4277 | } |
| 4278 | + .opacity-40 { | |
| 4279 | + opacity: 40%; | |
| 4280 | + } | |
| 4281 | + .opacity-50 { | |
| 4282 | + opacity: 50%; | |
| 4283 | + } | |
| 3890 | 4284 | .opacity-100 { |
| 3891 | 4285 | opacity: 100%; |
| 3892 | 4286 | } |
@@ -3898,6 +4292,10 @@
| 3898 | 4292 | --tw-shadow: 0 25px 50px -12px var(--tw-shadow-color, rgb(0 0 0 / 0.25)); |
| 3899 | 4293 | box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); |
| 3900 | 4294 | } |
| 4295 | + .shadow-inner { | |
| 4296 | + --tw-shadow: inset 0 2px 4px 0 var(--tw-shadow-color, rgb(0 0 0 / 0.05)); | |
| 4297 | + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); | |
| 4298 | + } | |
| 3901 | 4299 | .shadow-lg { |
| 3902 | 4300 | --tw-shadow: 0 10px 15px -3px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 4px 6px -4px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); |
| 3903 | 4301 | box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); |
@@ -3914,10 +4312,6 @@
| 3914 | 4312 | --tw-shadow: 0 20px 25px -5px var(--tw-shadow-color, rgb(0 0 0 / 0.1)), 0 8px 10px -6px var(--tw-shadow-color, rgb(0 0 0 / 0.1)); |
| 3915 | 4313 | box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); |
| 3916 | 4314 | } |
| 3917 | - .outline { | |
| 3918 | - outline-style: var(--tw-outline-style); | |
| 3919 | - outline-width: 1px; | |
| 3920 | - } | |
| 3921 | 4315 | .btn-ghost { |
| 3922 | 4316 | @layer daisyui.l1 { |
| 3923 | 4317 | &:not(.btn-active, :hover, :active:focus, :focus-visible, input:checked:not(.filter .btn)) { |
@@ -3981,10 +4375,6 @@
| 3981 | 4375 | --tw-duration: 200ms; |
| 3982 | 4376 | transition-duration: 200ms; |
| 3983 | 4377 | } |
| 3984 | - .duration-300 { | |
| 3985 | - --tw-duration: 300ms; | |
| 3986 | - transition-duration: 300ms; | |
| 3987 | - } | |
| 3988 | 4378 | .ease-in { |
| 3989 | 4379 | --tw-ease: var(--ease-in); |
| 3990 | 4380 | transition-timing-function: var(--ease-in); |
@@ -4013,20 +4403,6 @@
| 4013 | 4403 | } |
| 4014 | 4404 | } |
| 4015 | 4405 | } |
| 4016 | - .btn-lg { | |
| 4017 | - @layer daisyui.l1.l2 { | |
| 4018 | - --fontsize: 1.125rem; | |
| 4019 | - --btn-p: 1.25rem; | |
| 4020 | - --size: calc(var(--size-field, 0.25rem) * 12); | |
| 4021 | - } | |
| 4022 | - } | |
| 4023 | - .btn-md { | |
| 4024 | - @layer daisyui.l1.l2 { | |
| 4025 | - --fontsize: 0.875rem; | |
| 4026 | - --btn-p: 1rem; | |
| 4027 | - --size: calc(var(--size-field, 0.25rem) * 10); | |
| 4028 | - } | |
| 4029 | - } | |
| 4030 | 4406 | .btn-sm { |
| 4031 | 4407 | @layer daisyui.l1.l2 { |
| 4032 | 4408 | --fontsize: 0.75rem; |
@@ -4034,81 +4410,6 @@
| 4034 | 4410 | --size: calc(var(--size-field, 0.25rem) * 8); |
| 4035 | 4411 | } |
| 4036 | 4412 | } |
| 4037 | - .btn-xl { | |
| 4038 | - @layer daisyui.l1.l2 { | |
| 4039 | - --fontsize: 1.375rem; | |
| 4040 | - --btn-p: 1.5rem; | |
| 4041 | - --size: calc(var(--size-field, 0.25rem) * 14); | |
| 4042 | - } | |
| 4043 | - } | |
| 4044 | - .btn-xs { | |
| 4045 | - @layer daisyui.l1.l2 { | |
| 4046 | - --fontsize: 0.6875rem; | |
| 4047 | - --btn-p: 0.5rem; | |
| 4048 | - --size: calc(var(--size-field, 0.25rem) * 6); | |
| 4049 | - } | |
| 4050 | - } | |
| 4051 | - .card-lg { | |
| 4052 | - @layer daisyui.l1.l2 { | |
| 4053 | - .card-body { | |
| 4054 | - --card-p: 2rem; | |
| 4055 | - --card-fs: 1rem; | |
| 4056 | - } | |
| 4057 | - .card-title { | |
| 4058 | - --cardtitle-fs: 1.25rem; | |
| 4059 | - } | |
| 4060 | - } | |
| 4061 | - } | |
| 4062 | - .card-md { | |
| 4063 | - @layer daisyui.l1.l2 { | |
| 4064 | - .card-body { | |
| 4065 | - --card-p: 1.5rem; | |
| 4066 | - --card-fs: 0.875rem; | |
| 4067 | - } | |
| 4068 | - .card-title { | |
| 4069 | - --cardtitle-fs: 1.125rem; | |
| 4070 | - } | |
| 4071 | - } | |
| 4072 | - } | |
| 4073 | - .card-sm { | |
| 4074 | - @layer daisyui.l1.l2 { | |
| 4075 | - .card-body { | |
| 4076 | - --card-p: 1rem; | |
| 4077 | - --card-fs: 0.75rem; | |
| 4078 | - } | |
| 4079 | - .card-title { | |
| 4080 | - --cardtitle-fs: 1rem; | |
| 4081 | - } | |
| 4082 | - } | |
| 4083 | - } | |
| 4084 | - .card-xl { | |
| 4085 | - @layer daisyui.l1.l2 { | |
| 4086 | - .card-body { | |
| 4087 | - --card-p: 2.5rem; | |
| 4088 | - --card-fs: 1.125rem; | |
| 4089 | - } | |
| 4090 | - .card-title { | |
| 4091 | - --cardtitle-fs: 1.375rem; | |
| 4092 | - } | |
| 4093 | - } | |
| 4094 | - } | |
| 4095 | - .card-xs { | |
| 4096 | - @layer daisyui.l1.l2 { | |
| 4097 | - .card-body { | |
| 4098 | - --card-p: 0.5rem; | |
| 4099 | - --card-fs: 0.6875rem; | |
| 4100 | - } | |
| 4101 | - .card-title { | |
| 4102 | - --cardtitle-fs: 0.875rem; | |
| 4103 | - } | |
| 4104 | - } | |
| 4105 | - } | |
| 4106 | - .badge-accent { | |
| 4107 | - @layer daisyui.l1.l2 { | |
| 4108 | - --badge-color: var(--color-accent); | |
| 4109 | - --badge-fg: var(--color-accent-content); | |
| 4110 | - } | |
| 4111 | - } | |
| 4112 | 4413 | .badge-error { |
| 4113 | 4414 | @layer daisyui.l1.l2 { |
| 4114 | 4415 | --badge-color: var(--color-error); |
@@ -4127,12 +4428,6 @@
| 4127 | 4428 | --badge-fg: var(--color-primary-content); |
| 4128 | 4429 | } |
| 4129 | 4430 | } |
| 4130 | - .badge-secondary { | |
| 4131 | - @layer daisyui.l1.l2 { | |
| 4132 | - --badge-color: var(--color-secondary); | |
| 4133 | - --badge-fg: var(--color-secondary-content); | |
| 4134 | - } | |
| 4135 | - } | |
| 4136 | 4431 | .badge-success { |
| 4137 | 4432 | @layer daisyui.l1.l2 { |
| 4138 | 4433 | --badge-color: var(--color-success); |
@@ -4175,61 +4470,42 @@
| 4175 | 4470 | --btn-fg: var(--color-secondary-content); |
| 4176 | 4471 | } |
| 4177 | 4472 | } |
| 4178 | - .btn-success { | |
| 4179 | - @layer daisyui.l1.l2.l3 { | |
| 4180 | - --btn-color: var(--color-success); | |
| 4181 | - --btn-fg: var(--color-success-content); | |
| 4182 | - } | |
| 4183 | - } | |
| 4184 | 4473 | .btn-warning { |
| 4185 | 4474 | @layer daisyui.l1.l2.l3 { |
| 4186 | 4475 | --btn-color: var(--color-warning); |
| 4187 | 4476 | --btn-fg: var(--color-warning-content); |
| 4188 | 4477 | } |
| 4189 | 4478 | } |
| 4190 | - .card-border { | |
| 4191 | - @layer daisyui.l1.l2 { | |
| 4192 | - border: var(--border) solid var(--color-base-200); | |
| 4193 | - } | |
| 4194 | - } | |
| 4195 | - .card-dash { | |
| 4196 | - @layer daisyui.l1.l2 { | |
| 4197 | - border: var(--border) dashed var(--color-base-200); | |
| 4198 | - } | |
| 4479 | + .select-all { | |
| 4480 | + -webkit-user-select: all; | |
| 4481 | + user-select: all; | |
| 4199 | 4482 | } |
| 4200 | - .input-error { | |
| 4483 | + .radio-primary { | |
| 4201 | 4484 | @layer daisyui.l1.l2 { |
| 4202 | - &, &:focus, &:focus-within { | |
| 4203 | - --input-color: var(--color-error); | |
| 4204 | - } | |
| 4485 | + --input-color: var(--color-primary); | |
| 4205 | 4486 | } |
| 4206 | 4487 | } |
| 4207 | - .input-info { | |
| 4488 | + .toggle-primary { | |
| 4208 | 4489 | @layer daisyui.l1.l2 { |
| 4209 | - &, &:focus, &:focus-within { | |
| 4210 | - --input-color: var(--color-info); | |
| 4490 | + &:checked, &[aria-checked="true"] { | |
| 4491 | + --input-color: var(--color-primary); | |
| 4211 | 4492 | } |
| 4212 | 4493 | } |
| 4213 | 4494 | } |
| 4214 | - .input-success { | |
| 4495 | + .toggle-secondary { | |
| 4215 | 4496 | @layer daisyui.l1.l2 { |
| 4216 | - &, &:focus, &:focus-within { | |
| 4217 | - --input-color: var(--color-success); | |
| 4497 | + &:checked, &[aria-checked="true"] { | |
| 4498 | + --input-color: var(--color-secondary); | |
| 4218 | 4499 | } |
| 4219 | 4500 | } |
| 4220 | 4501 | } |
| 4221 | - .input-warning { | |
| 4502 | + .toggle-sm { | |
| 4222 | 4503 | @layer daisyui.l1.l2 { |
| 4223 | - &, &:focus, &:focus-within { | |
| 4224 | - --input-color: var(--color-warning); | |
| 4504 | + &:is([type="checkbox"]), &:has([type="checkbox"]) { | |
| 4505 | + --size: calc(var(--size-selector, 0.25rem) * 5); | |
| 4225 | 4506 | } |
| 4226 | 4507 | } |
| 4227 | 4508 | } |
| 4228 | - .radio-primary { | |
| 4229 | - @layer daisyui.l1.l2 { | |
| 4230 | - --input-color: var(--color-primary); | |
| 4231 | - } | |
| 4232 | - } | |
| 4233 | 4509 | .hover\:bg-base-200 { |
| 4234 | 4510 | &:hover { |
| 4235 | 4511 | @media (hover: hover) { |
@@ -4261,13 +4537,6 @@
| 4261 | 4537 | } |
| 4262 | 4538 | } |
| 4263 | 4539 | } |
| 4264 | - .hover\:opacity-80 { | |
| 4265 | - &:hover { | |
| 4266 | - @media (hover: hover) { | |
| 4267 | - opacity: 80%; | |
| 4268 | - } | |
| 4269 | - } | |
| 4270 | - } | |
| 4271 | 4540 | .hover\:shadow-2xl { |
| 4272 | 4541 | &:hover { |
| 4273 | 4542 | @media (hover: hover) { |
@@ -4292,6 +4561,23 @@
| 4292 | 4561 | } |
| 4293 | 4562 | } |
| 4294 | 4563 | } |
| 4564 | + .focus\:ring-2 { | |
| 4565 | + &:focus { | |
| 4566 | + --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor); | |
| 4567 | + box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); | |
| 4568 | + } | |
| 4569 | + } | |
| 4570 | + .focus\:ring-primary { | |
| 4571 | + &:focus { | |
| 4572 | + --tw-ring-color: var(--color-primary); | |
| 4573 | + } | |
| 4574 | + } | |
| 4575 | + .focus\:outline-none { | |
| 4576 | + &:focus { | |
| 4577 | + --tw-outline-style: none; | |
| 4578 | + outline-style: none; | |
| 4579 | + } | |
| 4580 | + } | |
| 4295 | 4581 | .md\:col-span-2 { |
| 4296 | 4582 | @media (width >= 48rem) { |
| 4297 | 4583 | grid-column: span 2 / span 2; |
@@ -4317,14 +4603,14 @@
| 4317 | 4603 | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| 4318 | 4604 | } |
| 4319 | 4605 | } |
| 4320 | - .md\:grid-cols-4 { | |
| 4606 | + .md\:grid-cols-3 { | |
| 4321 | 4607 | @media (width >= 48rem) { |
| 4322 | - grid-template-columns: repeat(4, minmax(0, 1fr)); | |
| 4608 | + grid-template-columns: repeat(3, minmax(0, 1fr)); | |
| 4323 | 4609 | } |
| 4324 | 4610 | } |
| 4325 | - .md\:flex-row { | |
| 4611 | + .md\:grid-cols-4 { | |
| 4326 | 4612 | @media (width >= 48rem) { |
| 4327 | - flex-direction: row; | |
| 4613 | + grid-template-columns: repeat(4, minmax(0, 1fr)); | |
| 4328 | 4614 | } |
| 4329 | 4615 | } |
| 4330 | 4616 | .lg\:block { |
@@ -4332,26 +4618,6 @@
| 4332 | 4618 | display: block; |
| 4333 | 4619 | } |
| 4334 | 4620 | } |
| 4335 | - .lg\:hidden { | |
| 4336 | - @media (width >= 64rem) { | |
| 4337 | - display: none; | |
| 4338 | - } | |
| 4339 | - } | |
| 4340 | - .lg\:stats-horizontal { | |
| 4341 | - @media (width >= 64rem) { | |
| 4342 | - @layer daisyui.l1.l2 { | |
| 4343 | - grid-auto-flow: column; | |
| 4344 | - overflow-x: auto; | |
| 4345 | - .stat:not(:last-child) { | |
| 4346 | - border-inline-end: var(--border) dashed currentColor; | |
| 4347 | - @supports (color: color-mix(in lab, red, red)) { | |
| 4348 | - border-inline-end: var(--border) dashed color-mix(in oklab, currentColor 10%, #0000); | |
| 4349 | - } | |
| 4350 | - border-block-end: none; | |
| 4351 | - } | |
| 4352 | - } | |
| 4353 | - } | |
| 4354 | - } | |
| 4355 | 4621 | .lg\:grid-cols-2 { |
| 4356 | 4622 | @media (width >= 64rem) { |
| 4357 | 4623 | grid-template-columns: repeat(2, minmax(0, 1fr)); |
@@ -4369,6 +4635,39 @@
| 4369 | 4635 | } |
| 4370 | 4636 | } |
| 4371 | 4637 | @layer base { |
| 4638 | + :where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme=light] { | |
| 4639 | + color-scheme: light; | |
| 4640 | + --color-base-100: oklch(100% 0 0); | |
| 4641 | + --color-base-200: oklch(98% 0 0); | |
| 4642 | + --color-base-300: oklch(95% 0 0); | |
| 4643 | + --color-base-content: oklch(21% 0.006 285.885); | |
| 4644 | + --color-primary: oklch(45% 0.24 277.023); | |
| 4645 | + --color-primary-content: oklch(93% 0.034 272.788); | |
| 4646 | + --color-secondary: oklch(65% 0.241 354.308); | |
| 4647 | + --color-secondary-content: oklch(94% 0.028 342.258); | |
| 4648 | + --color-accent: oklch(77% 0.152 181.912); | |
| 4649 | + --color-accent-content: oklch(38% 0.063 188.416); | |
| 4650 | + --color-neutral: oklch(14% 0.005 285.823); | |
| 4651 | + --color-neutral-content: oklch(92% 0.004 286.32); | |
| 4652 | + --color-info: oklch(74% 0.16 232.661); | |
| 4653 | + --color-info-content: oklch(29% 0.066 243.157); | |
| 4654 | + --color-success: oklch(76% 0.177 163.223); | |
| 4655 | + --color-success-content: oklch(37% 0.077 168.94); | |
| 4656 | + --color-warning: oklch(82% 0.189 84.429); | |
| 4657 | + --color-warning-content: oklch(41% 0.112 45.904); | |
| 4658 | + --color-error: oklch(71% 0.194 13.428); | |
| 4659 | + --color-error-content: oklch(27% 0.105 12.094); | |
| 4660 | + --radius-selector: 0.5rem; | |
| 4661 | + --radius-field: 0.25rem; | |
| 4662 | + --radius-box: 0.5rem; | |
| 4663 | + --size-selector: 0.25rem; | |
| 4664 | + --size-field: 0.25rem; | |
| 4665 | + --border: 1px; | |
| 4666 | + --depth: 1; | |
| 4667 | + --noise: 0; | |
| 4668 | + } | |
| 4669 | +} | |
| 4670 | +@layer base { | |
| 4372 | 4671 | :root:has(input.theme-controller[value=light]:checked),[data-theme=light] { |
| 4373 | 4672 | color-scheme: light; |
| 4374 | 4673 | --color-base-100: oklch(100% 0 0); |
@@ -4547,28 +4846,28 @@
| 4547 | 4846 | } |
| 4548 | 4847 | } |
| 4549 | 4848 | @layer base { |
| 4550 | - :where(:root),:root:has(input.theme-controller[value=rustwarden]:checked),[data-theme="rustwarden"] { | |
| 4849 | + :where(:root),:root:has(input.theme-controller[value=light]:checked),[data-theme="light"] { | |
| 4551 | 4850 | color-scheme: light; |
| 4552 | - --color-base-100: oklch(100% 0 0); | |
| 4553 | - --color-base-200: oklch(96% 0.01 240); | |
| 4554 | - --color-base-300: oklch(92% 0.02 240); | |
| 4555 | - --color-base-content: oklch(20% 0.02 240); | |
| 4556 | - --color-primary: oklch(55% 0.15 240); | |
| 4557 | - --color-primary-content: oklch(98% 0.01 240); | |
| 4558 | - --color-secondary: oklch(25% 0.02 240); | |
| 4559 | - --color-secondary-content: oklch(98% 0.01 240); | |
| 4560 | - --color-accent: oklch(70% 0.18 150); | |
| 4561 | - --color-accent-content: oklch(98% 0.01 150); | |
| 4562 | - --color-neutral: oklch(96% 0.01 240); | |
| 4563 | - --color-neutral-content: oklch(20% 0.02 240); | |
| 4564 | - --color-info: oklch(55% 0.15 240); | |
| 4565 | - --color-info-content: oklch(98% 0.01 240); | |
| 4566 | - --color-success: oklch(70% 0.18 150); | |
| 4567 | - --color-success-content: oklch(98% 0.01 150); | |
| 4568 | - --color-warning: oklch(80% 0.15 85); | |
| 4569 | - --color-warning-content: oklch(20% 0.05 85); | |
| 4570 | - --color-error: oklch(65% 0.20 25); | |
| 4571 | - --color-error-content: oklch(98% 0.01 25); | |
| 4851 | + --color-base-100: #ffffff; | |
| 4852 | + --color-base-200: #f9fbfa; | |
| 4853 | + --color-base-300: #e1e5e8; | |
| 4854 | + --color-base-content: #001e2b; | |
| 4855 | + --color-primary: #00ed64; | |
| 4856 | + --color-primary-content: #001e2b; | |
| 4857 | + --color-secondary: #001e2b; | |
| 4858 | + --color-secondary-content: #ffffff; | |
| 4859 | + --color-accent: #00684a; | |
| 4860 | + --color-accent-content: #ffffff; | |
| 4861 | + --color-neutral: #1c2d38; | |
| 4862 | + --color-neutral-content: #ffffff; | |
| 4863 | + --color-info: #3d4f9f; | |
| 4864 | + --color-info-content: #ffffff; | |
| 4865 | + --color-success: #00a35c; | |
| 4866 | + --color-success-content: #001e2b; | |
| 4867 | + --color-warning: #fa6e39; | |
| 4868 | + --color-warning-content: #001e2b; | |
| 4869 | + --color-error: #e74856; | |
| 4870 | + --color-error-content: #ffffff; | |
| 4572 | 4871 | --radius-selector: 0.5rem; |
| 4573 | 4872 | --radius-field: 0.25rem; |
| 4574 | 4873 | --radius-box: 0.5rem; |
@@ -4579,20 +4878,73 @@
| 4579 | 4878 | --noise: 0; |
| 4580 | 4879 | } |
| 4581 | 4880 | } |
| 4582 | -@property --tw-translate-x { | |
| 4583 | - syntax: "*"; | |
| 4584 | - inherits: false; | |
| 4585 | - initial-value: 0; | |
| 4586 | -} | |
| 4587 | -@property --tw-translate-y { | |
| 4588 | - syntax: "*"; | |
| 4589 | - inherits: false; | |
| 4590 | - initial-value: 0; | |
| 4881 | +@layer base { | |
| 4882 | + @media (prefers-color-scheme: dark) { | |
| 4883 | + :root:not([data-theme]) { | |
| 4884 | + color-scheme: dark; | |
| 4885 | + --color-base-100: #001e2b; | |
| 4886 | + --color-base-200: #1c2d38; | |
| 4887 | + --color-base-300: #003d4f; | |
| 4888 | + --color-base-content: #ffffff; | |
| 4889 | + --color-primary: #00ed64; | |
| 4890 | + --color-primary-content: #001e2b; | |
| 4891 | + --color-secondary: #003d4f; | |
| 4892 | + --color-secondary-content: #ffffff; | |
| 4893 | + --color-accent: #00ed64; | |
| 4894 | + --color-accent-content: #001e2b; | |
| 4895 | + --color-neutral: #0d1c26; | |
| 4896 | + --color-neutral-content: #ffffff; | |
| 4897 | + --color-info: #8ea0e8; | |
| 4898 | + --color-info-content: #001e2b; | |
| 4899 | + --color-success: #00ed64; | |
| 4900 | + --color-success-content: #001e2b; | |
| 4901 | + --color-warning: #fa6e39; | |
| 4902 | + --color-warning-content: #001e2b; | |
| 4903 | + --color-error: #ff8b96; | |
| 4904 | + --color-error-content: #001e2b; | |
| 4905 | + --radius-selector: 0.5rem; | |
| 4906 | + --radius-field: 0.25rem; | |
| 4907 | + --radius-box: 0.5rem; | |
| 4908 | + --size-selector: 0.25rem; | |
| 4909 | + --size-field: 0.25rem; | |
| 4910 | + --border: 1px; | |
| 4911 | + --depth: 1; | |
| 4912 | + --noise: 0; | |
| 4913 | + } | |
| 4914 | + } | |
| 4591 | 4915 | } |
| 4592 | -@property --tw-translate-z { | |
| 4593 | - syntax: "*"; | |
| 4594 | - inherits: false; | |
| 4595 | - initial-value: 0; | |
| 4916 | +@layer base { | |
| 4917 | + :root:has(input.theme-controller[value=dark]:checked),[data-theme="dark"] { | |
| 4918 | + color-scheme: dark; | |
| 4919 | + --color-base-100: #001e2b; | |
| 4920 | + --color-base-200: #1c2d38; | |
| 4921 | + --color-base-300: #003d4f; | |
| 4922 | + --color-base-content: #ffffff; | |
| 4923 | + --color-primary: #00ed64; | |
| 4924 | + --color-primary-content: #001e2b; | |
| 4925 | + --color-secondary: #003d4f; | |
| 4926 | + --color-secondary-content: #ffffff; | |
| 4927 | + --color-accent: #00ed64; | |
| 4928 | + --color-accent-content: #001e2b; | |
| 4929 | + --color-neutral: #0d1c26; | |
| 4930 | + --color-neutral-content: #ffffff; | |
| 4931 | + --color-info: #8ea0e8; | |
| 4932 | + --color-info-content: #001e2b; | |
| 4933 | + --color-success: #00ed64; | |
| 4934 | + --color-success-content: #001e2b; | |
| 4935 | + --color-warning: #fa6e39; | |
| 4936 | + --color-warning-content: #001e2b; | |
| 4937 | + --color-error: #ff8b96; | |
| 4938 | + --color-error-content: #001e2b; | |
| 4939 | + --radius-selector: 0.5rem; | |
| 4940 | + --radius-field: 0.25rem; | |
| 4941 | + --radius-box: 0.5rem; | |
| 4942 | + --size-selector: 0.25rem; | |
| 4943 | + --size-field: 0.25rem; | |
| 4944 | + --border: 1px; | |
| 4945 | + --depth: 1; | |
| 4946 | + --noise: 0; | |
| 4947 | + } | |
| 4596 | 4948 | } |
| 4597 | 4949 | @property --tw-scale-x { |
| 4598 | 4950 | syntax: "*"; |
@@ -4759,11 +5111,6 @@
| 4759 | 5111 | inherits: false; |
| 4760 | 5112 | initial-value: 0 0 #0000; |
| 4761 | 5113 | } |
| 4762 | -@property --tw-outline-style { | |
| 4763 | - syntax: "*"; | |
| 4764 | - inherits: false; | |
| 4765 | - initial-value: solid; | |
| 4766 | -} | |
| 4767 | 5114 | @property --tw-blur { |
| 4768 | 5115 | syntax: "*"; |
| 4769 | 5116 | inherits: false; |
@@ -4828,9 +5175,6 @@
| 4828 | 5175 | @layer properties { |
| 4829 | 5176 | @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { |
| 4830 | 5177 | *, ::before, ::after, ::backdrop { |
| 4831 | - --tw-translate-x: 0; | |
| 4832 | - --tw-translate-y: 0; | |
| 4833 | - --tw-translate-z: 0; | |
| 4834 | 5178 | --tw-scale-x: 1; |
| 4835 | 5179 | --tw-scale-y: 1; |
| 4836 | 5180 | --tw-scale-z: 1; |
@@ -4867,7 +5211,6 @@
| 4867 | 5211 | --tw-ring-offset-width: 0px; |
| 4868 | 5212 | --tw-ring-offset-color: #fff; |
| 4869 | 5213 | --tw-ring-offset-shadow: 0 0 #0000; |
| 4870 | - --tw-outline-style: solid; | |
| 4871 | 5214 | --tw-blur: initial; |
| 4872 | 5215 | --tw-brightness: initial; |
| 4873 | 5216 | --tw-contrast: initial; |
M
static/css/tailwind.src.css
+78
-40
@@ -1,67 +1,59 @@
| 1 | 1 | /* Tailwind CSS 4 + DaisyUI 5 Configuration */ |
| 2 | 2 | @import "tailwindcss"; |
| 3 | 3 | |
| 4 | -/* Source files for class extraction - Tailwind CSS 4 syntax */ | |
| 5 | -/* Path is relative to this CSS file location (static/css/) */ | |
| 6 | -@source "../../templates/**/*.html"; | |
| 4 | +/* Source files for class extraction. Paths are relative to static/css/. */ | |
| 5 | +@source "../../crates/rustwarden-ui/templates/**/*.html"; | |
| 6 | +@source "../../crates/rustwarden-fediverse-ui/templates/**/*.html"; | |
| 7 | +@source "../../crates/rustwarden-fediverse-guard/templates/**/*.html"; | |
| 7 | 8 | |
| 8 | -/* Custom RustCloud colors for Tailwind utilities */ | |
| 9 | +/* Brand tokens from docs/INFORMATIF/design/design.md */ | |
| 9 | 10 | @theme { |
| 10 | - --color-rustwarden-blue: #0082c9; | |
| 11 | - --color-rustwarden-rust: #ce422b; | |
| 11 | + --color-brand-green: #00ed64; | |
| 12 | + --color-brand-teal: #001e2b; | |
| 12 | 13 | } |
| 13 | 14 | |
| 14 | -/* DaisyUI Plugin */ | |
| 15 | +/* DaisyUI Plugin. The server sets data-theme to light or dark. */ | |
| 15 | 16 | @plugin "daisyui" { |
| 16 | - themes: rustwarden --default, light, dark; | |
| 17 | + themes: light --default, dark; | |
| 17 | 18 | root: ":root"; |
| 18 | - logs: true; | |
| 19 | + logs: false; | |
| 19 | 20 | } |
| 20 | 21 | |
| 21 | -/* Custom RustCloud Theme */ | |
| 22 | +/* Light theme — canvas, ink, primary green from the design palette. */ | |
| 22 | 23 | @plugin "daisyui/theme" { |
| 23 | - name: "rustwarden"; | |
| 24 | + name: "light"; | |
| 24 | 25 | default: true; |
| 25 | 26 | prefersdark: false; |
| 26 | 27 | color-scheme: light; |
| 27 | 28 | |
| 28 | - /* Base colors - converted from hex to oklch approximations */ | |
| 29 | - --color-base-100: oklch(100% 0 0); /* #ffffff - white */ | |
| 30 | - --color-base-200: oklch(96% 0.01 240); /* #f5f5f5 - light gray */ | |
| 31 | - --color-base-300: oklch(92% 0.02 240); /* darker gray for elevations */ | |
| 32 | - --color-base-content: oklch(20% 0.02 240); /* dark text on light background */ | |
| 29 | + --color-base-100: #ffffff; | |
| 30 | + --color-base-200: #f9fbfa; | |
| 31 | + --color-base-300: #e1e5e8; | |
| 32 | + --color-base-content: #001e2b; | |
| 33 | 33 | |
| 34 | - /* Primary - RustCloud blue #0082c9 */ | |
| 35 | - --color-primary: oklch(55% 0.15 240); | |
| 36 | - --color-primary-content: oklch(98% 0.01 240); | |
| 34 | + --color-primary: #00ed64; | |
| 35 | + --color-primary-content: #001e2b; | |
| 37 | 36 | |
| 38 | - /* Secondary - Dark gray #1d2d35 */ | |
| 39 | - --color-secondary: oklch(25% 0.02 240); | |
| 40 | - --color-secondary-content: oklch(98% 0.01 240); | |
| 37 | + --color-secondary: #001e2b; | |
| 38 | + --color-secondary-content: #ffffff; | |
| 41 | 39 | |
| 42 | - /* Accent - Success green #46ba61 */ | |
| 43 | - --color-accent: oklch(70% 0.18 150); | |
| 44 | - --color-accent-content: oklch(98% 0.01 150); | |
| 40 | + --color-accent: #00684a; | |
| 41 | + --color-accent-content: #ffffff; | |
| 45 | 42 | |
| 46 | - /* Neutral - Light gray #f5f5f5 */ | |
| 47 | - --color-neutral: oklch(96% 0.01 240); | |
| 48 | - --color-neutral-content: oklch(20% 0.02 240); | |
| 43 | + --color-neutral: #1c2d38; | |
| 44 | + --color-neutral-content: #ffffff; | |
| 49 | 45 | |
| 50 | - /* Info - RustCloud blue #0082c9 */ | |
| 51 | - --color-info: oklch(55% 0.15 240); | |
| 52 | - --color-info-content: oklch(98% 0.01 240); | |
| 46 | + --color-info: #3d4f9f; | |
| 47 | + --color-info-content: #ffffff; | |
| 53 | 48 | |
| 54 | - /* Success - Green #46ba61 */ | |
| 55 | - --color-success: oklch(70% 0.18 150); | |
| 56 | - --color-success-content: oklch(98% 0.01 150); | |
| 49 | + --color-success: #00a35c; | |
| 50 | + --color-success-content: #001e2b; | |
| 57 | 51 | |
| 58 | - /* Warning - Yellow #f0b400 */ | |
| 59 | - --color-warning: oklch(80% 0.15 85); | |
| 60 | - --color-warning-content: oklch(20% 0.05 85); | |
| 52 | + --color-warning: #fa6e39; | |
| 53 | + --color-warning-content: #001e2b; | |
| 61 | 54 | |
| 62 | - /* Error - Red #e74856 */ | |
| 63 | - --color-error: oklch(65% 0.20 25); | |
| 64 | - --color-error-content: oklch(98% 0.01 25); | |
| 55 | + --color-error: #e74856; | |
| 56 | + --color-error-content: #ffffff; | |
| 65 | 57 | |
| 66 | 58 | /* Border radius */ |
| 67 | 59 | --radius-selector: 0.5rem; |
@@ -79,3 +71,49 @@
| 79 | 71 | --depth: 1; |
| 80 | 72 | --noise: 0; |
| 81 | 73 | } |
| 74 | + | |
| 75 | +/* Dark theme — same primary, teal canvas. */ | |
| 76 | +@plugin "daisyui/theme" { | |
| 77 | + name: "dark"; | |
| 78 | + default: false; | |
| 79 | + prefersdark: true; | |
| 80 | + color-scheme: dark; | |
| 81 | + | |
| 82 | + --color-base-100: #001e2b; | |
| 83 | + --color-base-200: #1c2d38; | |
| 84 | + --color-base-300: #003d4f; | |
| 85 | + --color-base-content: #ffffff; | |
| 86 | + | |
| 87 | + --color-primary: #00ed64; | |
| 88 | + --color-primary-content: #001e2b; | |
| 89 | + | |
| 90 | + --color-secondary: #003d4f; | |
| 91 | + --color-secondary-content: #ffffff; | |
| 92 | + | |
| 93 | + --color-accent: #00ed64; | |
| 94 | + --color-accent-content: #001e2b; | |
| 95 | + | |
| 96 | + --color-neutral: #0d1c26; | |
| 97 | + --color-neutral-content: #ffffff; | |
| 98 | + | |
| 99 | + --color-info: #8ea0e8; | |
| 100 | + --color-info-content: #001e2b; | |
| 101 | + | |
| 102 | + --color-success: #00ed64; | |
| 103 | + --color-success-content: #001e2b; | |
| 104 | + | |
| 105 | + --color-warning: #fa6e39; | |
| 106 | + --color-warning-content: #001e2b; | |
| 107 | + | |
| 108 | + --color-error: #ff8b96; | |
| 109 | + --color-error-content: #001e2b; | |
| 110 | + | |
| 111 | + --radius-selector: 0.5rem; | |
| 112 | + --radius-field: 0.25rem; | |
| 113 | + --radius-box: 0.5rem; | |
| 114 | + --size-selector: 0.25rem; | |
| 115 | + --size-field: 0.25rem; | |
| 116 | + --border: 1px; | |
| 117 | + --depth: 1; | |
| 118 | + --noise: 0; | |
| 119 | +} |
A
static/js/alpine-components.js
+268
-0
@@ -0,0 +1,268 @@
| 1 | +/* Alpine components for the CSP build (@alpinejs/csp). | |
| 2 | + Loaded before alpine-csp.min.js so these listeners are registered | |
| 3 | + before Alpine.start() dispatches alpine:init. | |
| 4 | + Method bodies here are real JavaScript: fetch, window and document | |
| 5 | + are allowed. They must not appear in HTML Alpine expressions. */ | |
| 6 | +document.addEventListener("alpine:init", () => { | |
| 7 | + Alpine.data("twoFactorPage", () => ({ | |
| 8 | + code: "", | |
| 9 | + error: "", | |
| 10 | + verifying: false, | |
| 11 | + useBackupCode: false, | |
| 12 | + challengeToken: "", | |
| 13 | + errorInvalid: "", | |
| 14 | + errorNetwork: "", | |
| 15 | + | |
| 16 | + init() { | |
| 17 | + const params = new URLSearchParams(window.location.search); | |
| 18 | + this.challengeToken = params.get("challenge") || ""; | |
| 19 | + this.errorInvalid = this.$el.dataset.errorInvalid || ""; | |
| 20 | + this.errorNetwork = this.$el.dataset.errorNetwork || ""; | |
| 21 | + }, | |
| 22 | + | |
| 23 | + toggleBackup() { | |
| 24 | + this.useBackupCode = !this.useBackupCode; | |
| 25 | + this.code = ""; | |
| 26 | + }, | |
| 27 | + | |
| 28 | + async verify() { | |
| 29 | + if (!this.code || !this.challengeToken) return; | |
| 30 | + this.verifying = true; | |
| 31 | + this.error = ""; | |
| 32 | + try { | |
| 33 | + const res = await fetch("/api/v1/auth/2fa/verify", { | |
| 34 | + method: "POST", | |
| 35 | + headers: { "Content-Type": "application/json" }, | |
| 36 | + body: JSON.stringify({ | |
| 37 | + challenge_token: this.challengeToken, | |
| 38 | + code: this.code, | |
| 39 | + }), | |
| 40 | + }); | |
| 41 | + const data = await res.json(); | |
| 42 | + if (res.ok) { | |
| 43 | + if (data.refresh_token) { | |
| 44 | + localStorage.setItem("refresh_token", data.refresh_token); | |
| 45 | + } | |
| 46 | + if (data.user && data.user.must_change_password === true) { | |
| 47 | + window.location.href = "/settings/password?must_change=true"; | |
| 48 | + } else { | |
| 49 | + window.location.href = "/home"; | |
| 50 | + } | |
| 51 | + } else { | |
| 52 | + this.error = data.error || this.errorInvalid; | |
| 53 | + this.code = ""; | |
| 54 | + } | |
| 55 | + } catch (e) { | |
| 56 | + this.error = this.errorNetwork; | |
| 57 | + } | |
| 58 | + this.verifying = false; | |
| 59 | + }, | |
| 60 | + })); | |
| 61 | + | |
| 62 | + Alpine.data("securityPage", () => ({ | |
| 63 | + setupStarted: false, | |
| 64 | + setupComplete: false, | |
| 65 | + setupError: "", | |
| 66 | + qrCode: "", | |
| 67 | + secretBase32: "", | |
| 68 | + verifyCode: "", | |
| 69 | + verifying: false, | |
| 70 | + showBackupCodes: false, | |
| 71 | + backupCodes: [], | |
| 72 | + regenerating: false, | |
| 73 | + showDisableModal: false, | |
| 74 | + disablePassword: "", | |
| 75 | + disableError: "", | |
| 76 | + disabling: false, | |
| 77 | + csrf: "", | |
| 78 | + regenerateConfirm: "", | |
| 79 | + | |
| 80 | + init() { | |
| 81 | + this.csrf = this.$el.dataset.csrf || ""; | |
| 82 | + this.regenerateConfirm = this.$el.dataset.regenerateConfirm || ""; | |
| 83 | + }, | |
| 84 | + | |
| 85 | + async startSetup() { | |
| 86 | + this.setupStarted = true; | |
| 87 | + this.setupError = ""; | |
| 88 | + try { | |
| 89 | + const res = await fetch("/settings/security/totp/enable", { | |
| 90 | + method: "POST", | |
| 91 | + headers: { | |
| 92 | + "Content-Type": "application/json", | |
| 93 | + "X-CSRF-Token": this.csrf, | |
| 94 | + }, | |
| 95 | + }); | |
| 96 | + const data = await res.json(); | |
| 97 | + if (res.ok) { | |
| 98 | + this.qrCode = data.qr_code_data_uri; | |
| 99 | + this.secretBase32 = data.secret_base32; | |
| 100 | + } else { | |
| 101 | + this.setupError = data.error || "Error"; | |
| 102 | + this.setupStarted = false; | |
| 103 | + } | |
| 104 | + } catch (e) { | |
| 105 | + this.setupError = "Network error"; | |
| 106 | + this.setupStarted = false; | |
| 107 | + } | |
| 108 | + }, | |
| 109 | + | |
| 110 | + async verifyAndEnable() { | |
| 111 | + if (this.verifyCode.length !== 6) return; | |
| 112 | + this.verifying = true; | |
| 113 | + this.setupError = ""; | |
| 114 | + try { | |
| 115 | + const res = await fetch("/settings/security/totp/verify", { | |
| 116 | + method: "POST", | |
| 117 | + headers: { | |
| 118 | + "Content-Type": "application/json", | |
| 119 | + "X-CSRF-Token": this.csrf, | |
| 120 | + }, | |
| 121 | + body: JSON.stringify({ code: this.verifyCode }), | |
| 122 | + }); | |
| 123 | + const data = await res.json(); | |
| 124 | + if (res.ok) { | |
| 125 | + this.setupComplete = true; | |
| 126 | + this.backupCodes = data.backup_codes; | |
| 127 | + this.showBackupCodes = true; | |
| 128 | + } else { | |
| 129 | + this.setupError = data.error || "Invalid code"; | |
| 130 | + this.verifyCode = ""; | |
| 131 | + } | |
| 132 | + } catch (e) { | |
| 133 | + this.setupError = "Network error"; | |
| 134 | + } | |
| 135 | + this.verifying = false; | |
| 136 | + }, | |
| 137 | + | |
| 138 | + async regenerateBackupCodes() { | |
| 139 | + if (!confirm(this.regenerateConfirm)) return; | |
| 140 | + this.regenerating = true; | |
| 141 | + try { | |
| 142 | + const res = await fetch("/settings/security/backup-codes", { | |
| 143 | + method: "POST", | |
| 144 | + headers: { | |
| 145 | + "Content-Type": "application/json", | |
| 146 | + "X-CSRF-Token": this.csrf, | |
| 147 | + }, | |
| 148 | + }); | |
| 149 | + const data = await res.json(); | |
| 150 | + if (res.ok) { | |
| 151 | + this.backupCodes = data.backup_codes; | |
| 152 | + this.showBackupCodes = true; | |
| 153 | + } | |
| 154 | + } catch (e) {} | |
| 155 | + this.regenerating = false; | |
| 156 | + }, | |
| 157 | + | |
| 158 | + async disableTotp() { | |
| 159 | + this.disabling = true; | |
| 160 | + this.disableError = ""; | |
| 161 | + try { | |
| 162 | + const res = await fetch("/settings/security/totp/disable", { | |
| 163 | + method: "POST", | |
| 164 | + headers: { | |
| 165 | + "Content-Type": "application/json", | |
| 166 | + "X-CSRF-Token": this.csrf, | |
| 167 | + }, | |
| 168 | + body: JSON.stringify({ password: this.disablePassword }), | |
| 169 | + }); | |
| 170 | + if (res.ok) { | |
| 171 | + window.location.reload(); | |
| 172 | + } else { | |
| 173 | + const data = await res.json(); | |
| 174 | + this.disableError = data.error || "Error"; | |
| 175 | + } | |
| 176 | + } catch (e) { | |
| 177 | + this.disableError = "Network error"; | |
| 178 | + } | |
| 179 | + this.disabling = false; | |
| 180 | + }, | |
| 181 | + | |
| 182 | + copyBackupCodes() { | |
| 183 | + const text = this.backupCodes.join("\n"); | |
| 184 | + if (navigator.clipboard && navigator.clipboard.writeText) { | |
| 185 | + navigator.clipboard.writeText(text); | |
| 186 | + } else { | |
| 187 | + const ta = document.createElement("textarea"); | |
| 188 | + ta.value = text; | |
| 189 | + ta.style.position = "fixed"; | |
| 190 | + ta.style.opacity = "0"; | |
| 191 | + document.body.appendChild(ta); | |
| 192 | + ta.select(); | |
| 193 | + document.execCommand("copy"); | |
| 194 | + document.body.removeChild(ta); | |
| 195 | + } | |
| 196 | + }, | |
| 197 | + | |
| 198 | + acknowledgeBackupCodes() { | |
| 199 | + this.showBackupCodes = false; | |
| 200 | + window.location.reload(); | |
| 201 | + }, | |
| 202 | + | |
| 203 | + closeDisableModal() { | |
| 204 | + this.showDisableModal = false; | |
| 205 | + this.disablePassword = ""; | |
| 206 | + this.disableError = ""; | |
| 207 | + }, | |
| 208 | + })); | |
| 209 | + | |
| 210 | + Alpine.data("analyticsOptOut", () => ({ | |
| 211 | + storageKey: "", | |
| 212 | + optedOut: false, | |
| 213 | + | |
| 214 | + init() { | |
| 215 | + this.storageKey = this.$el.dataset.storageKey || ""; | |
| 216 | + try { | |
| 217 | + this.optedOut = localStorage.getItem(this.storageKey) !== null; | |
| 218 | + } catch (e) {} | |
| 219 | + }, | |
| 220 | + | |
| 221 | + toggle() { | |
| 222 | + try { | |
| 223 | + if (this.optedOut) { | |
| 224 | + localStorage.removeItem(this.storageKey); | |
| 225 | + this.optedOut = false; | |
| 226 | + } else { | |
| 227 | + localStorage.setItem(this.storageKey, "1"); | |
| 228 | + this.optedOut = true; | |
| 229 | + } | |
| 230 | + } catch (e) {} | |
| 231 | + }, | |
| 232 | + })); | |
| 233 | + | |
| 234 | + Alpine.data("fediversePage", () => ({ | |
| 235 | + saved: false, | |
| 236 | + deleteTarget: null, | |
| 237 | + revokeTarget: null, | |
| 238 | + killDomain: null, | |
| 239 | + | |
| 240 | + init() { | |
| 241 | + this.saved = this.$el.dataset.saved === "true"; | |
| 242 | + this.$watch("saved", (value) => { | |
| 243 | + if (value) { | |
| 244 | + setTimeout(() => { | |
| 245 | + this.saved = false; | |
| 246 | + }, 4000); | |
| 247 | + } | |
| 248 | + }); | |
| 249 | + }, | |
| 250 | + | |
| 251 | + revokeHandle() { | |
| 252 | + if (!this.revokeTarget) return ""; | |
| 253 | + const parts = this.revokeTarget.split("|"); | |
| 254 | + return parts.length > 1 ? parts[1] : ""; | |
| 255 | + }, | |
| 256 | + | |
| 257 | + openKillModal() { | |
| 258 | + if (!this.killDomain) return; | |
| 259 | + const modal = document.getElementById("kill-domain-modal"); | |
| 260 | + if (modal) modal.showModal(); | |
| 261 | + }, | |
| 262 | + | |
| 263 | + closeKillModal() { | |
| 264 | + const modal = document.getElementById("kill-domain-modal"); | |
| 265 | + if (modal) modal.close(); | |
| 266 | + }, | |
| 267 | + })); | |
| 268 | +}); |
A
static/js/alpine-csp.min.js
+23
-0
@@ -0,0 +1,23 @@
| 1 | +(()=>{var bt=!1,vt=!1,K=[],wt=-1,Fe=!1,At=!1;function wr(e){_n(e)}function Ar(){At=!0}function Or(){At=!1,Sr()}function _n(e){K.includes(e)||(K.push(e),e._x_schedulerPriority!==void 0&&(Fe=!0)),Sr()}function he(e){let t=K.indexOf(e);t!==-1&&t>wt&&K.splice(t,1)}function Sr(){if(!vt&&!bt){if(At)return;bt=!0,queueMicrotask(gn)}}function gn(){bt=!1,vt=!0;for(let e=0;e<K.length;e++)Fe&&yn(e),K[e](),wt=e;K.length=0,wt=-1,Fe=!1,vt=!1}function yn(e){let t=new Map,r=K.slice(e).sort((i,n)=>xn(i,n,t));for(let i=0;i<r.length;i++)K[e+i]=r[i];Fe=!1}function xn(e,t,r){return Et(e)?Et(t)?vr(e._x_schedulerPriority.el,r)-vr(t._x_schedulerPriority.el,r)||e._x_schedulerPriority.order-t._x_schedulerPriority.order:-1:Et(t)?1:0}function Et(e){return e._x_schedulerPriority!==void 0}function vr(e,t){if(t.has(e))return t.get(e);let r=0,i=e;for(;e;)r++,e._x_teleportBack?e=e._x_teleportBack:typeof ShadowRoot=="function"&&e.parentNode instanceof ShadowRoot?e=e.parentNode.host:e=e.parentElement;return t.set(i,r),r}var D,j,q,St,En=0,Ot=!0;function Nr(e){Ot=!1,e(),Ot=!0}function Tr(e){D=e.reactive,q=e.release,j=t=>e.effect(t,{scheduler:r=>{Ot?wr(r):r()}}),St=e.raw}function Nt(e){j=e}function Cr(e){let t=()=>{};return[(i,n)=>{let s=n?.priority==="structural"?En++:void 0,o=j(i);return s!==void 0&&o!==void 0&&(o._x_schedulerPriority={el:e,order:s}),e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(a=>a())}),e._x_effects.add(o),t=()=>{o!==void 0&&(e._x_effects.delete(o),q(o))},o},()=>{t()}]}function $e(e,t){let r=!0,i,n,s=j(()=>{let o=e(),a=JSON.stringify(o);if(!r&&(typeof o=="object"||o!==i)){let c=typeof i=="object"?JSON.parse(n):i;queueMicrotask(()=>{t(o,c)})}i=o,n=a,r=!1});return()=>{he(s),q(s)}}async function Rr(e){Ar();try{await e(),await Promise.resolve()}finally{Or()}}var kr=[],Dr=[],Pr=[];function Ir(e){Pr.push(e)}function me(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,Dr.push(t))}function He(e){kr.push(e)}function Be(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(r)}function Tt(e,t){e._x_attributeCleanups&&Object.entries(e._x_attributeCleanups).forEach(([r,i])=>{(t===void 0||t.includes(r))&&(i.forEach(n=>n()),delete e._x_attributeCleanups[r])})}function Mr(e){for(e._x_effects?.forEach(he);e._x_cleanups?.length;)e._x_cleanups.pop()()}var Ke=new MutationObserver(qe),Ct=!1;function Oe(){Ke.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),Ct=!0}function Rt(){bn(),Ke.disconnect(),Ct=!1}var te=[];function bn(){let e=Ke.takeRecords();te.push(()=>e.length>0&&qe(e));let t=te.length;queueMicrotask(()=>{if(te.length===t)for(;te.length>0;)te.shift()()})}function Lr(){for(;te.length>0;)te.shift()();let e=Ke.takeRecords();e.length>0&&qe(e)}function _(e){if(!Ct)return e();Rt();try{return e()}finally{Oe()}}var kt=!1,Ve=[];function Ur(){kt=!0}function jr(){kt=!1,qe(Ve),Ve=[]}function qe(e){if(kt){Ve=Ve.concat(e);return}let t=[],r=new Set,i=new Map,n=new Map;for(let s=0;s<e.length;s++)if(!e[s].target._x_ignoreMutationObserver&&(e[s].type==="childList"&&(e[s].removedNodes.forEach(o=>{o.nodeType===1&&o._x_marker&&r.add(o)}),e[s].addedNodes.forEach(o=>{if(o.nodeType===1){if(r.has(o)){r.delete(o);return}o._x_marker||t.push(o)}})),e[s].type==="attributes")){let o=e[s].target,a=e[s].attributeName,c=e[s].oldValue,l=()=>{i.has(o)||i.set(o,[]),i.get(o).push({name:a,value:o.getAttribute(a)})},u=()=>{n.has(o)||n.set(o,[]),n.get(o).push(a)};o.hasAttribute(a)&&c===null?l():o.hasAttribute(a)?(u(),l()):u()}n.forEach((s,o)=>{Tt(o,s)}),i.forEach((s,o)=>{kr.forEach(a=>a(o,s))});for(let s of r)t.some(o=>o.contains(s))||Dr.forEach(o=>o(s));for(let s of t)s.isConnected&&Pr.forEach(o=>o(s));t=null,r=null,i=null,n=null}function ze(e){return P(z(e))}function F(e,t,r){return e._x_dataStack=[t,...z(r||e)],()=>{e._x_dataStack=e._x_dataStack.filter(i=>i!==t)}}function z(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?z(e.host):e.parentNode?z(e.parentNode):[]}function P(e){return new Proxy({objects:e},vn)}function Fr(e,t){return e===null||e===Object.prototype?null:Object.prototype.hasOwnProperty.call(e,t)?e:Fr(Object.getPrototypeOf(e),t)}var vn={ownKeys({objects:e}){return Array.from(new Set(e.flatMap(t=>Object.keys(t))))},has({objects:e},t){return t==Symbol.unscopables?!1:e.some(r=>Object.prototype.hasOwnProperty.call(r,t)||Reflect.has(r,t))},get({objects:e},t,r){return t=="toJSON"?wn:Reflect.get(e.find(i=>Reflect.has(i,t))||{},t,r)},set({objects:e},t,r,i){let n;for(let o of e)if(n=Fr(o,t),n)break;n||(n=e[e.length-1]);let s=Object.getOwnPropertyDescriptor(n,t);return s?.set&&s?.get?s.set.call(i,r)||!0:Reflect.set(n,t,r)}};function wn(){return Reflect.ownKeys(this).reduce((t,r)=>(t[r]=Reflect.get(this,r),t),{})}function _e(e,t=()=>{}){let r=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,i=(n,s="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:a,enumerable:c}])=>{if(c===!1||a===void 0||typeof a=="object"&&a!==null&&a.__v_skip)return;let l=s===""?o:`${s}.${o}`;typeof a=="object"&&a!==null&&a._x_interceptor?n[o]=a.initialize(e,l,o,t):r(a)&&a!==n&&!(a instanceof Element)&&i(a,l)})};return i(e)}function We(e,t=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(i,n,s,o){return e(this.initialValue,()=>An(i,n),a=>Dt(i,n,a),n,s,o)}};return t(r),i=>{if(typeof i=="object"&&i!==null&&i._x_interceptor){let n=r.initialize.bind(r);r.initialize=(s,o,a,c)=>{let l=i.initialize(s,o,a,c);return r.initialValue=l,n(s,o,a,c)}}else r.initialValue=i;return r}}function An(e,t){return t.split(".").reduce((r,i)=>r[i],e)}function Dt(e,t,r){if(typeof t=="string"&&(t=t.split(".")),t.length===1)e[t[0]]=r;else{if(t.length===0)throw error;return e[t[0]]||(e[t[0]]={}),Dt(e[t[0]],t.slice(1),r)}}var $r={};function v(e,t){$r[e]=t}function X(e,t){let r=On(t);return Object.entries($r).forEach(([i,n])=>{Object.defineProperty(e,`$${i}`,{get(){return n(t,r)},enumerable:!1})}),e}function On(e){let[t,r]=Pt(e),i={interceptor:We,...t};return me(e,r),i}function It(e,t,r,...i){try{return r(...i)}catch(n){re(n,e,t)}}function re(...e){return Vr(...e)}var Vr=Sn;function Hr(e){Vr=e}function Sn(e,t,r=void 0){e=Object.assign(e??{message:"No error message given."},{el:t,expression:r}),console.warn(`Alpine Expression Error: ${e.message} | |
| 2 | + | |
| 3 | +${r?'Expression: "'+r+`" | |
| 4 | + | |
| 5 | +`:""}`,t),setTimeout(()=>{throw e},0)}var Z=!0;function Ge(e){let t=Z;Z=!1;let r=e();return Z=t,r}function I(e,t,r={}){let i;return b(e,t)(n=>i=n,r),i}function b(...e){return Br(...e)}var Br=()=>{};function Kr(e){Br=e}var qr;function zr(e){qr=e}function Wr(e,t){return(r=()=>{},{scope:i={},params:n=[],context:s}={})=>{if(!Z){Mt(r,t,P([i,...e]),n);return}let o=t.apply(P([i,...e]),n);Mt(r,o)}}function Mt(e,t,r,i,n){if(Z&&typeof t=="function"){let s=t.apply(r,i);s instanceof Promise?s.then(o=>Mt(e,o,r,i)).catch(o=>re(o,n,t)):e(s)}else typeof t=="object"&&t instanceof Promise?t.then(s=>e(s)):e(t)}function Gr(...e){return qr(...e)}var jt="x-";function R(e=""){return jt+e}function Yr(e){jt=e}var Ye={};function h(e,t){return Ye[e]=t,{before(r){if(!Ye[r]){console.warn(String.raw`Cannot find directive \`${r}\`. \`${e}\` will use the default order of execution`);return}let i=ie.indexOf(r);ie.splice(i>=0?i:ie.indexOf("DEFAULT"),0,e)}}}function Jr(e){return Object.keys(Ye).includes(e)}function ne(e,t,r){if(t=Array.from(t),e._x_virtualDirectives){let s=Object.entries(e._x_virtualDirectives).map(([a,c])=>({name:a,value:c})),o=Ft(s);s=s.map(a=>o.find(c=>c.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),t=t.concat(s)}let i={};return t.map(Qr((s,o)=>i[s]=o)).filter(ti).map(Tn(i,r)).sort(Cn).map(s=>Nn(e,s))}function Ft(e){return Array.from(e).map(Qr()).filter(t=>!ti(t))}var Lt=!1,Se=new Map,Xr=Symbol();function Zr(e){Lt=!0;let t=Symbol();Xr=t,Se.set(t,[]);let r=()=>{for(;Se.get(t).length;)Se.get(t).shift()();Se.delete(t)},i=()=>{Lt=!1,r()};e(r),i()}function Pt(e){let t=[],r=a=>t.push(a),[i,n]=Cr(e);return t.push(n),[{Alpine:W,effect:i,cleanup:r,evaluateLater:b.bind(b,e),evaluate:I.bind(I,e)},()=>t.forEach(a=>a())]}function Nn(e,t){let r=()=>{},i=Ye[t.type]||r,[n,s]=Pt(e);Be(e,t.original,s);let o=()=>{e._x_ignore||e._x_ignoreSelf||(i.inline&&i.inline(e,t,n),i=i.bind(i,e,t,n),Lt?Se.get(Xr).push(i):i())};return o.runCleanups=s,o}var Je=(e,t)=>({name:r,value:i})=>(r.startsWith(e)&&(r=r.replace(e,t)),{name:r,value:i}),Xe=e=>e;function Qr(e=()=>{}){return({name:t,value:r})=>{let{name:i,value:n}=ei.reduce((s,o)=>o(s),{name:t,value:r});return i!==t&&e(i,t),{name:i,value:n}}}var ei=[];function ge(e){ei.push(e)}function ti({name:e}){return ri().test(e)}var ri=()=>new RegExp(`^${jt}([^:^.]+)\\b`);function Tn(e,t){return({name:r,value:i})=>{r===i&&(i="");let n=r.match(ri()),s=r.match(/:([a-zA-Z0-9\-_:]+)/),o=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=t||e[r]||r;return{type:n?n[1]:null,value:s?s[1]:null,modifiers:o.map(c=>c.replace(".","")),expression:i,original:a}}}var Ut="DEFAULT",ie=["ignore","ref","id","data","anchor","bind","init","for","model","modelable","transition","show","if",Ut,"teleport"];function Cn(e,t){let r=ie.indexOf(e.type)===-1?Ut:e.type,i=ie.indexOf(t.type)===-1?Ut:t.type;return ie.indexOf(r)-ie.indexOf(i)}function $(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(n=>$(n,t));return}let r=!1;if(t(e,()=>r=!0),r)return;let i=e.firstElementChild;for(;i;)$(i,t,!1),i=i.nextElementSibling}var V=!1;function O(e,t=()=>{}){return(...r)=>V?t(...r):e(...r)}function ii(e){return(...t)=>V&&e(...t)}var ni=[];function H(e){ni.push(e)}function si(e,t){ni.forEach(r=>r(e,t)),V=!0,ai(()=>{S(t,(r,i)=>{i(r,()=>{})})}),V=!1}var Ze=!1;function oi(e,t){t._x_dataStack||(t._x_dataStack=e._x_dataStack),V=!0,Ze=!0,ai(()=>{Rn(t)}),V=!1,Ze=!1}function Rn(e){let t=!1;S(e,(i,n)=>{$(i,(s,o)=>{if(t&&ci(s))return o();t=!0,n(s,o)})})}function ai(e){let t=j;Nt((r,i)=>{let n=t(r);return q(n),()=>{}}),e(),Nt(t)}var Qe=0;function li(e,t){let r=e._x_deferInit;r||(r=e._x_deferInit={pending:0,ownsIgnore:!e._x_ignore,queuedAttributes:new Map},r.ownsIgnore&&(e._x_ignore=!0),Qe++),r.pending++,Promise.resolve(t).catch(i=>{try{re(i,e)}catch(n){setTimeout(()=>{throw n},0)}}).then(()=>kn(e,r))}function kn(e,t){t.pending--,!(t.pending>0)&&(Lr(),!(t.pending>0)&&e._x_deferInit===t&&(delete e._x_deferInit,t.ownsIgnore&&delete e._x_ignore,Qe--,e.isConnected&&(Dn(t),S(e))))}function ui(e,t){if(Qe===0)return!1;let r=N(e,i=>i._x_deferInit);return r?(fi(r._x_deferInit,e,t.map(({name:i})=>i)),!0):!1}function fi(e,t,r){let i=e.queuedAttributes.get(t);(!i||i.marker!==t._x_marker)&&(i={marker:t._x_marker,names:new Set},e.queuedAttributes.set(t,i)),r.forEach(n=>i.names.add(n))}function Dn(e){e.queuedAttributes.forEach((t,r)=>{if(!r.isConnected||!r._x_marker||r._x_marker!==t.marker)return;let i=N(r,s=>s._x_deferInit);if(i){fi(i._x_deferInit,r,Array.from(t.names));return}let n=Array.from(t.names).filter(s=>r.hasAttribute(s)).map(s=>({name:s,value:r.getAttribute(s)}));n.length!==0&&ne(r,n).forEach(s=>s())})}H((e,t)=>{Qe!==0&&(!e||e.nodeType!==1||!t||t.nodeType!==1||N(e,r=>r._x_deferInit)&&(t._x_ignore=!0))});function se(e,t,r={},i={}){return e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0,cancelable:!0,...i}))}function T(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}var pi=!1;function di(){pi&&T("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),pi=!0,document.body||T("Unable to initialize. Trying to load Alpine before `<body>` is available. Did you forget to add `defer` in Alpine's `<script>` tag?"),se(document,"alpine:init"),se(document,"alpine:initializing"),Oe(),Ir(t=>S(t,$)),me(t=>k(t)),He((t,r)=>{ui(t,r)||ne(t,r).forEach(i=>i())});let e=t=>!oe(t.parentElement,!0);Array.from(document.querySelectorAll(_i().join(","))).filter(e).forEach(t=>{S(t)}),se(document,"alpine:initialized"),setTimeout(()=>{In()})}var $t=[],hi=[];function mi(){return $t.map(e=>e())}function _i(){return $t.concat(hi).map(e=>e())}function et(e){$t.push(e)}function tt(e){hi.push(e)}function oe(e,t=!1){return N(e,r=>{if((t?_i():mi()).some(n=>r.matches(n)))return!0})}function N(e,t){if(e){if(t(e))return e;if(e._x_teleportBack)return N(e._x_teleportBack,t);if(e.parentNode instanceof ShadowRoot)return N(e.parentNode.host,t);if(e.parentElement)return N(e.parentElement,t)}}function ci(e){return mi().some(t=>e.matches(t))}var gi=[];function yi(e){gi.push(e)}var Pn=1;function S(e,t=$,r=()=>{}){N(e,i=>i._x_ignore)||Zr(()=>{t(e,(i,n)=>{i._x_marker||(r(i,n),gi.forEach(s=>s(i,n)),ne(i,i.attributes).forEach(s=>s()),i._x_ignore||(i._x_marker=Pn++),i._x_ignore&&n())})})}function k(e,t=$){t(e,r=>{Mr(r),Tt(r),delete r._x_marker})}function In(){[["ui","dialog",["[x-dialog], [x-popover]"]],["anchor","anchor",["[x-anchor]"]],["sort","sort",["[x-sort]"]]].forEach(([t,r,i])=>{Jr(r)||i.some(n=>{if(document.querySelector(n))return T(`found "${n}", but missing ${t} plugin`),!0})})}var Vt=[],Ht=!1;function ye(e=()=>{}){return queueMicrotask(()=>{Ht||setTimeout(()=>{rt()})}),new Promise(t=>{Vt.push(()=>{e(),t()})})}function rt(){for(Ht=!1;Vt.length;)Vt.shift()()}function xi(){Ht=!0}function Ne(e,t){return Array.isArray(t)?Ei(e,t.join(" ")):typeof t=="object"&&t!==null?Mn(e,t):typeof t=="function"?Ne(e,t()):Ei(e,t)}function Bt(e){return e.split(/\s/).filter(Boolean)}function Ei(e,t){let r=n=>Bt(n).filter(s=>!e.classList.contains(s)).filter(Boolean),i=n=>(e.classList.add(...n),()=>{e.classList.remove(...n)});return t=t===!0?t="":t||"",i(r(t))}function Mn(e,t){let r=Object.entries(t).flatMap(([o,a])=>a?Bt(o):!1).filter(Boolean),i=Object.entries(t).flatMap(([o,a])=>a?!1:Bt(o)).filter(Boolean),n=[],s=[];return i.forEach(o=>{e.classList.contains(o)&&(e.classList.remove(o),s.push(o))}),r.forEach(o=>{e.classList.contains(o)||(e.classList.add(o),n.push(o))}),()=>{s.forEach(o=>e.classList.add(o)),n.forEach(o=>e.classList.remove(o))}}function ae(e,t){return typeof t=="object"&&t!==null?Ln(e,t):Un(e,t)}function Ln(e,t){let r={};return Object.entries(t).forEach(([i,n])=>{r[i]=e.style[i],i.startsWith("--")||(i=jn(i)),e.style.setProperty(i,n)}),setTimeout(()=>{e.style.length===0&&e.removeAttribute("style")}),()=>{ae(e,r)}}function Un(e,t){let r=e.getAttribute("style",t);return e.setAttribute("style",t),()=>{e.setAttribute("style",r||"")}}function jn(e){return e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase()}function Te(e,t=()=>{}){let r=!1;return function(){r?t.apply(this,arguments):(r=!0,e.apply(this,arguments))}}h("transition",(e,{value:t,modifiers:r,expression:i},{evaluate:n})=>{typeof i=="function"&&(i=n(i)),i!==!1&&(!i||typeof i=="boolean"?$n(e,r,t):Fn(e,i,t))});function Fn(e,t,r){bi(e,Ne,""),{enter:n=>{e._x_transition.enter.during=n},"enter-start":n=>{e._x_transition.enter.start=n},"enter-end":n=>{e._x_transition.enter.end=n},leave:n=>{e._x_transition.leave.during=n},"leave-start":n=>{e._x_transition.leave.start=n},"leave-end":n=>{e._x_transition.leave.end=n}}[r](t)}function $n(e,t,r){bi(e,ae);let i=!t.includes("in")&&!t.includes("out")&&!r,n=i||t.includes("in")||["enter"].includes(r),s=i||t.includes("out")||["leave"].includes(r);t.includes("in")&&!i&&(t=t.filter((p,y)=>y<t.indexOf("out"))),t.includes("out")&&!i&&(t=t.filter((p,y)=>y>t.indexOf("out")));let o=!t.includes("opacity")&&!t.includes("scale"),a=o||t.includes("opacity"),c=o||t.includes("scale"),l=a?0:1,u=c?Ce(t,"scale",95)/100:1,f=Ce(t,"delay",0)/1e3,g=Ce(t,"origin","center"),m="opacity, transform",L=Ce(t,"duration",150)/1e3,d=Ce(t,"duration",75)/1e3,w="cubic-bezier(0.4, 0.0, 0.2, 1)";n&&(e._x_transition.enter.during={transformOrigin:g,transitionDelay:`${f}s`,transitionProperty:m,transitionDuration:`${L}s`,transitionTimingFunction:w},e._x_transition.enter.start={opacity:l,transform:`scale(${u})`},e._x_transition.enter.end={opacity:1,transform:"scale(1)"}),s&&(e._x_transition.leave.during={transformOrigin:g,transitionDelay:`${f}s`,transitionProperty:m,transitionDuration:`${d}s`,transitionTimingFunction:w},e._x_transition.leave.start={opacity:1,transform:"scale(1)"},e._x_transition.leave.end={opacity:l,transform:`scale(${u})`})}function bi(e,t,r={}){e._x_transition||(e._x_transition={enter:{during:r,start:r,end:r},leave:{during:r,start:r,end:r},in(i=()=>{},n=()=>{}){it(e,t,{during:this.enter.during,start:this.enter.start,end:this.enter.end},i,n)},out(i=()=>{},n=()=>{}){it(e,t,{during:this.leave.during,start:this.leave.start,end:this.leave.end},i,n)}})}window.Element.prototype._x_toggleAndCascadeWithTransitions=function(e,t,r,i){let n=document.visibilityState==="visible"?requestAnimationFrame:setTimeout,s=()=>n(r);if(t){e._x_transition&&(e._x_transition.enter||e._x_transition.leave)?e._x_transition.enter&&(Object.entries(e._x_transition.enter.during).length||Object.entries(e._x_transition.enter.start).length||Object.entries(e._x_transition.enter.end).length)?e._x_transition.in(r):s():e._x_transition?e._x_transition.in(r):s();return}e._x_hidePromise=e._x_transition?new Promise((o,a)=>{e._x_transition.out(()=>{},()=>o(i)),e._x_transitioning&&e._x_transitioning.beforeCancel(()=>a({isFromCancelledTransition:!0}))}):Promise.resolve(i),queueMicrotask(()=>{let o=vi(e);o?(o._x_hideChildren||(o._x_hideChildren=[]),o._x_hideChildren.push(e)):n(()=>{let a=c=>{let l=Promise.all([c._x_hidePromise,...(c._x_hideChildren||[]).map(a)]).then(([u])=>u?.());return delete c._x_hidePromise,delete c._x_hideChildren,l};a(e).catch(c=>{if(!c.isFromCancelledTransition)throw c})})})};function vi(e){let t=e.parentNode;if(t)return t._x_hidePromise?t:vi(t)}function it(e,t,{during:r,start:i,end:n}={},s=()=>{},o=()=>{}){if(e._x_transitioning&&e._x_transitioning.cancel(),Object.keys(r).length===0&&Object.keys(i).length===0&&Object.keys(n).length===0){s(),o();return}let a,c,l;Vn(e,{start(){a=t(e,i)},during(){c=t(e,r)},before:s,end(){a(),l=t(e,n)},after:o,cleanup(){c(),l()}})}function Vn(e,t){let r,i,n,s=Te(()=>{_(()=>{r=!0,i||t.before(),n||(t.end(),rt()),t.after(),e.isConnected&&t.cleanup(),delete e._x_transitioning})});e._x_transitioning={beforeCancels:[],beforeCancel(o){this.beforeCancels.push(o)},cancel:Te(function(){for(;this.beforeCancels.length;)this.beforeCancels.shift()();s()}),finish:s},_(()=>{t.start(),t.during()}),xi(),requestAnimationFrame(()=>{if(r)return;let o=Number(getComputedStyle(e).transitionDuration.replace(/,.*/,"").replace("s",""))*1e3,a=Number(getComputedStyle(e).transitionDelay.replace(/,.*/,"").replace("s",""))*1e3;o===0&&(o=Number(getComputedStyle(e).animationDuration.replace("s",""))*1e3),_(()=>{t.before()}),i=!0,requestAnimationFrame(()=>{r||(_(()=>{t.end()}),rt(),setTimeout(e._x_transitioning.finish,o+a),n=!0)})})}function Ce(e,t,r){if(e.indexOf(t)===-1)return r;let i=e[e.indexOf(t)+1];if(!i||t==="scale"&&isNaN(i))return r;if(t==="duration"||t==="delay"){let n=i.match(/([0-9]+)ms/);if(n)return n[1]}return t==="origin"&&["top","right","left","center","bottom"].includes(e[e.indexOf(t)+2])?[i,e[e.indexOf(t)+2]].join(" "):i}function Re(e,t,r,i=[]){switch(e._x_bindings||(e._x_bindings=D({})),e._x_bindings[t]=r,t=i.includes("camel")?Yn(t):t,t){case"value":Hn(e,r);break;case"style":Kn(e,r);break;case"class":Bn(e,r);break;case"selected":case"checked":qn(e,t,r);break;default:Kt(e,t,r);break}}function Hn(e,t){if(nt(e))e.attributes.value===void 0&&(e.value=t);else if(De(e))Number.isInteger(t)?e.value=t:!Array.isArray(t)&&typeof t!="boolean"&&![null,void 0].includes(t)?e.value=String(t):Array.isArray(t)?e.checked=t.some(r=>Jn(r,e.value)):e.checked=!!t;else if(e.tagName==="SELECT")Gn(e,t);else if(e.tagName==="OPTION")Kt(e,"value",t);else{if(e.value===t&&(typeof t!="object"||t===null))return;e.value=t===void 0?"":t}}function Bn(e,t){e._x_undoAddedClasses&&e._x_undoAddedClasses(),e._x_undoAddedClasses=Ne(e,t)}function Kn(e,t){e._x_undoAddedStyles&&e._x_undoAddedStyles(),e._x_undoAddedStyles=ae(e,t)}function qn(e,t,r){Kt(e,t,r),Wn(e,t,r)}function Kt(e,t,r){[null,void 0,!1].includes(r)&&Zn(t)?e.removeAttribute(t):(wi(t)&&(r=t),Qn(r)&&(r=JSON.stringify(r)),zn(e,t,r))}function zn(e,t,r){e.getAttribute(t)!=r&&e.setAttribute(t,r)}function Wn(e,t,r){e[t]!==r&&(e[t]=r)}function Gn(e,t){let r=[].concat(t).map(i=>i+"");Array.from(e.options).forEach(i=>{i.selected=r.includes(i.value)})}function Yn(e){return e.toLowerCase().replace(/-(\w)/g,(t,r)=>r.toUpperCase())}function Jn(e,t){return e==t}function ke(e){return[1,"1","true","on","yes",!0].includes(e)?!0:[0,"0","false","off","no",!1].includes(e)?!1:e?Boolean(e):null}var Xn=new Set(["allowfullscreen","async","autofocus","autoplay","checked","controls","default","defer","disabled","formnovalidate","inert","ismap","itemscope","loop","multiple","muted","nomodule","novalidate","open","playsinline","readonly","required","reversed","selected","shadowrootclonable","shadowrootdelegatesfocus","shadowrootserializable"]);function wi(e){return Xn.has(e)}function Zn(e){return!["aria-pressed","aria-checked","aria-expanded","aria-selected"].includes(e)}function Qn(e){return typeof e=="object"&&e!==null}function Ai(e,t,r){return e._x_bindings&&e._x_bindings[t]!==void 0?e._x_bindings[t]:Si(e,t,r)}function Oi(e,t,r,i=!0){if(e._x_bindings&&e._x_bindings[t]!==void 0)return e._x_bindings[t];if(e._x_inlineBindings&&e._x_inlineBindings[t]!==void 0){let n=e._x_inlineBindings[t];return n.extract=i,Ge(()=>I(e,n.expression))}return Si(e,t,r)}function Si(e,t,r){let i=e.getAttribute(t);return i===null?typeof r=="function"?r():r:i===""?!0:wi(t)?!![t,"true"].includes(i):i}function De(e){return e.type==="checkbox"||e.localName==="ui-checkbox"||e.localName==="ui-switch"}function nt(e){return e.type==="radio"||e.localName==="ui-radio"}function st(e,t){let r;return function(){let i=this,n=arguments,s=function(){r=null,e.apply(i,n)};clearTimeout(r),r=setTimeout(s,t)}}function ot(e,t){let r;return function(){let i=this,n=arguments;r||(e.apply(i,n),r=!0,setTimeout(()=>r=!1,t))}}function at({get:e,set:t},{get:r,set:i}){let n=!0,s,o,a=j(()=>{let c=e(),l=r();if(n)i(qt(c)),n=!1;else{let u=JSON.stringify(c),f=JSON.stringify(l);u!==s?i(qt(c)):u!==f&&t(qt(l))}s=JSON.stringify(e()),o=JSON.stringify(r())});return()=>{he(a),q(a)}}function qt(e){return typeof e=="object"?JSON.parse(JSON.stringify(e)):e}function Ni(e){(Array.isArray(e)?e:[e]).forEach(r=>r(W))}var G={},Ti=!1;function Ci(e,t){if(Ti||(G=D(G),Ti=!0),t===void 0)return G[e];G[e]=t,typeof t=="object"&&t!==null&&t._x_interceptor?G[e]=t.initialize(G,e,e,()=>{}):_e(G[e]),typeof t=="object"&&t!==null&&t.hasOwnProperty("init")&&typeof t.init=="function"&&G[e].init()}function Ri(){return G}var ki={};function Di(e,t){let r=typeof t!="function"?()=>t:t;return e instanceof Element?zt(e,r()):(ki[e]=r,()=>{})}function Pi(e){return Object.entries(ki).forEach(([t,r])=>{Object.defineProperty(e,t,{get(){return(...i)=>r(...i)}})}),e}function zt(e,t,r){let i=[];for(;i.length;)i.pop()();let n=Object.entries(t).map(([o,a])=>({name:o,value:a})),s=Ft(n);return n=n.map(o=>s.find(a=>a.name===o.name)?{name:`x-bind:${o.name}`,value:`"${o.value}"`}:o),ne(e,n,r).map(o=>{i.push(o.runCleanups),o()}),()=>{for(;i.length;)i.pop()()}}var Ii={};function Mi(e,t){Ii[e]=t}function Li(e,t){return Object.entries(Ii).forEach(([r,i])=>{Object.defineProperty(e,r,{get(){return(...n)=>i.bind(t)(...n)},enumerable:!1})}),e}var es={get reactive(){return D},get release(){return q},get effect(){return j},get raw(){return St},get transaction(){return Rr},version:"3.17.4",flushAndStopDeferringMutations:jr,dontAutoEvaluateFunctions:Ge,disableEffectScheduling:Nr,startObservingMutations:Oe,stopObservingMutations:Rt,setReactivityEngine:Tr,onAttributeRemoved:Be,onAttributesAdded:He,closestDataStack:z,skipDuringClone:O,onlyDuringClone:ii,addRootSelector:et,addInitSelector:tt,setErrorHandler:Hr,interceptClone:H,addScopeToNode:F,deferMutations:Ur,mapAttributes:ge,evaluateLater:b,interceptInit:yi,initInterceptors:_e,injectMagics:X,setEvaluator:Kr,setRawEvaluator:zr,mergeProxies:P,extractProp:Oi,findClosest:N,onElRemoved:me,closestRoot:oe,destroyTree:k,interceptor:We,transition:it,setStyles:ae,mutateDom:_,deferInit:li,directive:h,entangle:at,throttle:ot,debounce:st,evaluate:I,evaluateRaw:Gr,initTree:S,nextTick:ye,prefixed:R,prefix:Yr,plugin:Ni,magic:v,store:Ci,start:di,clone:oi,cloneNode:si,bound:Ai,$data:ze,watch:$e,walk:$,data:Mi,bind:Di},W=es;var Ui=new WeakMap,ji=new Set;Object.getOwnPropertyNames(globalThis).forEach(e=>{if(e==="styleMedia"||e==="sharedStorage")return;let t;try{t=globalThis[e]}catch{return}ji.add(t)});var A=class{constructor(t,r,i,n){this.type=t,this.value=r,this.start=i,this.end=n}},ts=["IDENTIFIER","KEYWORD","BOOLEAN","NULL","UNDEFINED"],Wt=class{constructor(t){this.input=t,this.position=0,this.tokens=[]}tokenize(){for(;this.position<this.input.length&&(this.skipWhitespace(),!(this.position>=this.input.length));){let t=this.input[this.position];this.isDigit(t)?this.readNumber():this.isAlpha(t)||t==="_"||t==="$"?this.readIdentifierOrKeyword():t==='"'||t==="'"?this.readString():t==="/"&&this.peek()==="/"?this.skipLineComment():this.readOperatorOrPunctuation()}return this.tokens.push(new A("EOF",null,this.position,this.position)),this.tokens}skipWhitespace(){for(;this.position<this.input.length&&/\s/.test(this.input[this.position]);)this.position++}skipLineComment(){for(;this.position<this.input.length&&this.input[this.position]!==` | |
| 6 | +`;)this.position++}isDigit(t){return/[0-9]/.test(t)}isAlpha(t){return/[a-zA-Z]/.test(t)}isAlphaNumeric(t){return/[a-zA-Z0-9_$]/.test(t)}peek(t=1){return this.input[this.position+t]||""}readNumber(){let t=this.position,r=!1;for(;this.position<this.input.length;){let n=this.input[this.position];if(this.isDigit(n))this.position++;else if(n==="."&&!r)r=!0,this.position++;else break}let i=this.input.slice(t,this.position);this.tokens.push(new A("NUMBER",parseFloat(i),t,this.position))}readIdentifierOrKeyword(){let t=this.position;for(;this.position<this.input.length&&this.isAlphaNumeric(this.input[this.position]);)this.position++;let r=this.input.slice(t,this.position);["true","false","null","undefined","new","typeof","void","delete","in","instanceof"].includes(r)?r==="true"||r==="false"?this.tokens.push(new A("BOOLEAN",r==="true",t,this.position)):r==="null"?this.tokens.push(new A("NULL",null,t,this.position)):r==="undefined"?this.tokens.push(new A("UNDEFINED",void 0,t,this.position)):this.tokens.push(new A("KEYWORD",r,t,this.position)):this.tokens.push(new A("IDENTIFIER",r,t,this.position))}readString(){let t=this.position,r=this.input[this.position];this.position++;let i="",n=!1;for(;this.position<this.input.length;){let s=this.input[this.position];if(n){switch(s){case"n":i+=` | |
| 7 | +`;break;case"t":i+=" ";break;case"r":i+="\r";break;case"\\":i+="\\";break;case r:i+=r;break;case"u":{let o=this.input.slice(this.position+1,this.position+5);/^[0-9a-fA-F]{4}$/.test(o)?(i+=String.fromCharCode(parseInt(o,16)),this.position+=4):i+=s;break}default:i+=s}n=!1}else if(s==="\\")n=!0;else if(s===r){this.position++,this.tokens.push(new A("STRING",i,t,this.position));return}else i+=s;this.position++}throw new Error(`Unterminated string starting at position ${t}`)}readOperatorOrPunctuation(){let t=this.position,r=this.input[this.position],i=this.peek(),n=this.peek(2);if(r==="="&&i==="="&&n==="=")this.position+=3,this.tokens.push(new A("OPERATOR","===",t,this.position));else if(r==="!"&&i==="="&&n==="=")this.position+=3,this.tokens.push(new A("OPERATOR","!==",t,this.position));else if(r==="="&&i==="=")this.position+=2,this.tokens.push(new A("OPERATOR","==",t,this.position));else if(r==="!"&&i==="=")this.position+=2,this.tokens.push(new A("OPERATOR","!=",t,this.position));else if(r==="<"&&i==="=")this.position+=2,this.tokens.push(new A("OPERATOR","<=",t,this.position));else if(r===">"&&i==="=")this.position+=2,this.tokens.push(new A("OPERATOR",">=",t,this.position));else if(r==="&"&&i==="&")this.position+=2,this.tokens.push(new A("OPERATOR","&&",t,this.position));else if(r==="|"&&i==="|")this.position+=2,this.tokens.push(new A("OPERATOR","||",t,this.position));else if(r==="+"&&i==="+")this.position+=2,this.tokens.push(new A("OPERATOR","++",t,this.position));else if(r==="-"&&i==="-")this.position+=2,this.tokens.push(new A("OPERATOR","--",t,this.position));else{this.position++;let s="()[]{},.;:?".includes(r)?"PUNCTUATION":"OPERATOR";this.tokens.push(new A(s,r,t,this.position))}}},Gt=class{constructor(t){this.tokens=t,this.position=0}parse(){if(this.isAtEnd())throw new Error("Empty expression");let t=this.parseExpression();if(this.match("PUNCTUATION",";"),!this.isAtEnd())throw new Error(`Unexpected token: ${this.current().value}`);return t}parseExpression(){return this.parseAssignment()}parseAssignment(){let t=this.parseTernary();if(this.match("OPERATOR","=")){let r=this.parseAssignment();if(t.type==="Identifier"||t.type==="MemberExpression")return{type:"AssignmentExpression",left:t,operator:"=",right:r};throw new Error("Invalid assignment target")}return t}parseTernary(){let t=this.parseLogicalOr();if(this.match("PUNCTUATION","?")){let r=this.parseExpression();this.consume("PUNCTUATION",":");let i=this.parseExpression();return{type:"ConditionalExpression",test:t,consequent:r,alternate:i}}return t}parseLogicalOr(){let t=this.parseLogicalAnd();for(;this.match("OPERATOR","||");){let r=this.previous().value,i=this.parseLogicalAnd();t={type:"BinaryExpression",operator:r,left:t,right:i}}return t}parseLogicalAnd(){let t=this.parseEquality();for(;this.match("OPERATOR","&&");){let r=this.previous().value,i=this.parseEquality();t={type:"BinaryExpression",operator:r,left:t,right:i}}return t}parseEquality(){let t=this.parseRelational();for(;this.match("OPERATOR","==","!=","===","!==");){let r=this.previous().value,i=this.parseRelational();t={type:"BinaryExpression",operator:r,left:t,right:i}}return t}parseRelational(){let t=this.parseAdditive();for(;this.match("OPERATOR","<",">","<=",">=");){let r=this.previous().value,i=this.parseAdditive();t={type:"BinaryExpression",operator:r,left:t,right:i}}return t}parseAdditive(){let t=this.parseMultiplicative();for(;this.match("OPERATOR","+","-");){let r=this.previous().value,i=this.parseMultiplicative();t={type:"BinaryExpression",operator:r,left:t,right:i}}return t}parseMultiplicative(){let t=this.parseUnary();for(;this.match("OPERATOR","*","/","%");){let r=this.previous().value,i=this.parseUnary();t={type:"BinaryExpression",operator:r,left:t,right:i}}return t}parseUnary(){if(this.match("OPERATOR","++","--")){let t=this.previous().value,r=this.parseUnary();return{type:"UpdateExpression",operator:t,argument:r,prefix:!0}}if(this.match("OPERATOR","!","-","+")){let t=this.previous().value,r=this.parseUnary();return{type:"UnaryExpression",operator:t,argument:r,prefix:!0}}return this.parsePostfix()}parsePostfix(){let t=this.parseMember();return this.match("OPERATOR","++","--")?{type:"UpdateExpression",operator:this.previous().value,argument:t,prefix:!1}:t}parseMember(){let t=this.parsePrimary();for(;;)if(this.match("PUNCTUATION",".")){let r=this.consumeIdentifierName();t={type:"MemberExpression",object:t,property:{type:"Identifier",name:r},computed:!1}}else if(this.match("PUNCTUATION","[")){let r=this.parseExpression();this.consume("PUNCTUATION","]"),t={type:"MemberExpression",object:t,property:r,computed:!0}}else if(this.match("PUNCTUATION","(")){let r=this.parseArguments();t={type:"CallExpression",callee:t,arguments:r}}else break;return t}parseArguments(){let t=[];if(!this.check("PUNCTUATION",")"))do t.push(this.parseExpression());while(this.match("PUNCTUATION",","));return this.consume("PUNCTUATION",")"),t}parsePrimary(){if(this.match("NUMBER"))return{type:"Literal",value:this.previous().value};if(this.match("STRING"))return{type:"Literal",value:this.previous().value};if(this.match("BOOLEAN"))return{type:"Literal",value:this.previous().value};if(this.match("NULL"))return{type:"Literal",value:null};if(this.match("UNDEFINED"))return{type:"Literal",value:void 0};if(this.match("IDENTIFIER"))return{type:"Identifier",name:this.previous().value};if(this.match("PUNCTUATION","(")){let t=this.parseExpression();return this.consume("PUNCTUATION",")"),t}if(this.match("PUNCTUATION","["))return this.parseArrayLiteral();if(this.match("PUNCTUATION","{"))return this.parseObjectLiteral();throw new Error(`Unexpected token: ${this.current().type} "${this.current().value}"`)}parseArrayLiteral(){let t=[];for(;!this.check("PUNCTUATION","]")&&!this.isAtEnd()&&(t.push(this.parseExpression()),this.match("PUNCTUATION",","));)if(this.check("PUNCTUATION","]"))break;return this.consume("PUNCTUATION","]"),{type:"ArrayExpression",elements:t}}parseObjectLiteral(){let t=[];for(;!this.check("PUNCTUATION","}")&&!this.isAtEnd();){let r,i=!1;if(this.match("STRING"))r={type:"Literal",value:this.previous().value};else if(this.checkIdentifierName())r={type:"Identifier",name:this.consumeIdentifierName()};else if(this.match("PUNCTUATION","["))r=this.parseExpression(),i=!0,this.consume("PUNCTUATION","]");else throw new Error("Expected property key");this.consume("PUNCTUATION",":");let n=this.parseExpression();if(t.push({type:"Property",key:r,value:n,computed:i,shorthand:!1}),this.match("PUNCTUATION",",")){if(this.check("PUNCTUATION","}"))break}else break}return this.consume("PUNCTUATION","}"),{type:"ObjectExpression",properties:t}}match(...t){for(let r=0;r<t.length;r++){let i=t[r];if(r===0&&t.length>1){let n=i;for(let s=1;s<t.length;s++)if(this.check(n,t[s]))return this.advance(),!0;return!1}else if(t.length===1)return this.checkType(i)?(this.advance(),!0):!1}return!1}check(t,r){return this.isAtEnd()?!1:r!==void 0?this.current().type===t&&this.current().value===r:this.current().type===t}checkType(t){return this.isAtEnd()?!1:this.current().type===t}advance(){return this.isAtEnd()||this.position++,this.previous()}isAtEnd(){return this.current().type==="EOF"}current(){return this.tokens[this.position]}previous(){return this.tokens[this.position-1]}consume(t,r){if(r!==void 0){if(this.check(t,r))return this.advance();throw new Error(`Expected ${t} "${r}" but got ${this.current().type} "${this.current().value}"`)}if(this.check(t))return this.advance();throw new Error(`Expected ${t} but got ${this.current().type} "${this.current().value}"`)}checkIdentifierName(){return this.isAtEnd()?!1:ts.includes(this.current().type)}consumeIdentifierName(){if(!this.checkIdentifierName())throw new Error(`Expected an identifier name but got ${this.current().type} "${this.current().value}"`);let t=this.advance();switch(t.type){case"BOOLEAN":return t.value?"true":"false";case"NULL":return"null";case"UNDEFINED":return"undefined";default:return t.value}}},Yt=class{evaluate({node:t,scope:r={},context:i=null,forceBindingRootScopeToFunctions:n=!0}){switch(t.type){case"Literal":return t.value;case"Identifier":if(t.name in r){let p=r[t.name];return this.checkForDangerousValues(p),typeof p=="function"?p.bind(r):p}throw new Error(`Undefined variable: ${t.name}`);case"MemberExpression":let s=this.evaluate({node:t.object,scope:r,context:i,forceBindingRootScopeToFunctions:n});if(s==null)throw new Error("Cannot read property of null or undefined");let o;t.computed?o=this.evaluate({node:t.property,scope:r,context:i,forceBindingRootScopeToFunctions:n}):o=t.property.name,this.checkForDangerousKeywords(o);let a=s[o];return this.checkForDangerousValues(a),typeof a=="function"?n?a.bind(r):a.bind(s):a;case"CallExpression":let c=t.arguments.map(p=>this.evaluate({node:p,scope:r,context:i,forceBindingRootScopeToFunctions:n})),l;if(t.callee.type==="MemberExpression"){let p=this.evaluate({node:t.callee.object,scope:r,context:i,forceBindingRootScopeToFunctions:n}),y;t.callee.computed?y=this.evaluate({node:t.callee.property,scope:r,context:i,forceBindingRootScopeToFunctions:n}):y=t.callee.property.name,this.checkForDangerousKeywords(y);let U=p[y];if(typeof U!="function")throw new Error("Value is not a function");l=U.apply(p,c)}else if(t.callee.type==="Identifier"){let p=t.callee.name,y;if(p in r)y=r[p];else throw new Error(`Undefined variable: ${p}`);if(typeof y!="function")throw new Error("Value is not a function");let U=i!==null?i:r;l=y.apply(U,c)}else{let p=this.evaluate({node:t.callee,scope:r,context:i,forceBindingRootScopeToFunctions:n});if(typeof p!="function")throw new Error("Value is not a function");l=p.apply(i,c)}return this.checkForDangerousValues(l),l;case"UnaryExpression":let u=this.evaluate({node:t.argument,scope:r,context:i,forceBindingRootScopeToFunctions:n});switch(t.operator){case"!":return!u;case"-":return-u;case"+":return+u;default:throw new Error(`Unknown unary operator: ${t.operator}`)}case"UpdateExpression":if(t.argument.type==="Identifier"){let p=t.argument.name;if(!(p in r))throw new Error(`Undefined variable: ${p}`);let y=r[p];return t.operator==="++"?r[p]=y+1:t.operator==="--"&&(r[p]=y-1),t.prefix?r[p]:y}else if(t.argument.type==="MemberExpression"){let p=this.evaluate({node:t.argument.object,scope:r,context:i,forceBindingRootScopeToFunctions:n}),y=t.argument.computed?this.evaluate({node:t.argument.property,scope:r,context:i,forceBindingRootScopeToFunctions:n}):t.argument.property.name;if(this.isDOMObject(p))throw new Error("Property assignments on DOM objects are prohibited in the CSP build");this.checkForDangerousKeywords(y);let U=p[y];return t.operator==="++"?p[y]=U+1:t.operator==="--"&&(p[y]=U-1),t.prefix?p[y]:U}throw new Error("Invalid update expression target");case"BinaryExpression":let f=this.evaluate({node:t.left,scope:r,context:i,forceBindingRootScopeToFunctions:n}),g=()=>this.evaluate({node:t.right,scope:r,context:i,forceBindingRootScopeToFunctions:n});if(t.operator==="&&")return f&&g();if(t.operator==="||")return f||g();let m=g();switch(t.operator){case"+":return f+m;case"-":return f-m;case"*":return f*m;case"/":return f/m;case"%":return f%m;case"==":return f==m;case"!=":return f!=m;case"===":return f===m;case"!==":return f!==m;case"<":return f<m;case">":return f>m;case"<=":return f<=m;case">=":return f>=m;default:throw new Error(`Unknown binary operator: ${t.operator}`)}case"ConditionalExpression":return this.evaluate({node:t.test,scope:r,context:i,forceBindingRootScopeToFunctions:n})?this.evaluate({node:t.consequent,scope:r,context:i,forceBindingRootScopeToFunctions:n}):this.evaluate({node:t.alternate,scope:r,context:i,forceBindingRootScopeToFunctions:n});case"AssignmentExpression":let d=this.evaluate({node:t.right,scope:r,context:i,forceBindingRootScopeToFunctions:n});if(t.left.type==="Identifier")return r[t.left.name]=d,d;if(t.left.type==="MemberExpression"){let p=this.evaluate({node:t.left.object,scope:r,context:i,forceBindingRootScopeToFunctions:n}),y=t.left.computed?this.evaluate({node:t.left.property,scope:r,context:i,forceBindingRootScopeToFunctions:n}):t.left.property.name;if(this.isDOMObject(p))throw new Error("Property assignments on DOM objects are prohibited in the CSP build");return this.checkForDangerousKeywords(y),p[y]=d,d}throw new Error("Invalid assignment target");case"ArrayExpression":return t.elements.map(p=>this.evaluate({node:p,scope:r,context:i,forceBindingRootScopeToFunctions:n}));case"ObjectExpression":let w={};for(let p of t.properties){let y=p.computed?this.evaluate({node:p.key,scope:r,context:i,forceBindingRootScopeToFunctions:n}):p.key.type==="Identifier"?p.key.name:this.evaluate({node:p.key,scope:r,context:i,forceBindingRootScopeToFunctions:n}),U=this.evaluate({node:p.value,scope:r,context:i,forceBindingRootScopeToFunctions:n});w[y]=U}return w;default:throw new Error(`Unknown node type: ${t.type}`)}}isDOMObject(t){if(t instanceof Node||typeof CSSStyleDeclaration<"u"&&t instanceof CSSStyleDeclaration||typeof DOMStringMap<"u"&&t instanceof DOMStringMap||typeof DOMTokenList<"u"&&t instanceof DOMTokenList||typeof NamedNodeMap<"u"&&t instanceof NamedNodeMap)return!0;let r=t!=null&&t.constructor;return!!(r&&typeof r.name=="string"&&/^(SVG|CSS|DOM)[A-Z]/.test(r.name))}checkForDangerousKeywords(t){if(["constructor","prototype","__proto__","__defineGetter__","__defineSetter__","insertAdjacentHTML","insertAdjacentElement","insertAdjacentText","setAttribute","setAttributeNS","setAttributeNode","setAttributeNodeNS","createElement","createElementNS","createDocumentFragment","createContextualFragment","appendChild","insertBefore","replaceChild"].includes(t))throw new Error(`Accessing "${t}" is prohibited in the CSP build`)}checkForDangerousValues(t){if(t!==null&&!(typeof t!="object"&&typeof t!="function")&&!Ui.has(t)){if(t instanceof HTMLIFrameElement||t instanceof HTMLScriptElement||typeof SVGScriptElement<"u"&&t instanceof SVGScriptElement)throw new Error("Accessing iframes and scripts is prohibited in the CSP build");if(typeof Document<"u"&&t instanceof Document)throw new Error("Accessing document objects is prohibited in the CSP build");if(ji.has(t))throw new Error("Accessing global variables is prohibited in the CSP build");return Ui.set(t,!0),!0}}};function Jt(e){try{let r=new Wt(e).tokenize(),n=new Gt(r).parse(),s=new Yt;return function(o={}){let{scope:a={},context:c=null,forceBindingRootScopeToFunctions:l=!1}=o;return s.evaluate({node:n,scope:a,context:c,forceBindingRootScopeToFunctions:l})}}catch(t){throw new Error(`CSP Parser Error: ${t.message}`)}}function Fi(e,t,r={}){let i=Vi(e),n=P([r.scope??{},...i]),s=r.params??[],a=Jt(t)({scope:n,forceBindingRootScopeToFunctions:!0});return typeof a=="function"&&Z?a.apply(n,s):a}function $i(e,t){let r=Vi(e);if(typeof t=="function")return Wr(r,t);let i=rs(e,t,r);return It.bind(null,e,t,i)}function Vi(e){let t={};return X(t,e),[t,...z(e)]}function rs(e,t,r){if(e instanceof HTMLIFrameElement)throw new Error("Evaluating expressions on an iframe is prohibited in the CSP build");if(e instanceof HTMLScriptElement)throw new Error("Evaluating expressions on a script is prohibited in the CSP build");return(i=()=>{},{scope:n={},params:s=[]}={})=>{let o=P([n,...r]),c=Jt(t)({scope:o,forceBindingRootScopeToFunctions:!0});if(Z&&typeof c=="function"){let l=c.apply(c,s);l instanceof Promise?l.then(u=>i(u)):i(l)}else typeof c=="object"&&c instanceof Promise?c.then(l=>i(l)):i(c)}}function Xt(e){let t=Object.create(null);for(let r of e.split(","))t[r]=1;return r=>r in t}var is=Object.freeze({}),Ka=Object.freeze([]);var xe=Object.assign;var ns=Object.prototype.hasOwnProperty,ct=(e,t)=>ns.call(e,t),Ee=Array.isArray,be=e=>Hi(e)==="[object Map]";var ss=e=>typeof e=="string",ve=e=>typeof e=="symbol",we=e=>e!==null&&typeof e=="object";var os=Object.prototype.toString,Hi=e=>os.call(e),Zt=e=>Hi(e).slice(8,-1);var lt=e=>ss(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e;var ut=e=>{let t=Object.create(null);return r=>t[r]||(t[r]=e(r))},as=/-\w/g,qa=ut(e=>e.replace(as,t=>t.slice(1).toUpperCase())),cs=/\B([A-Z])/g,za=ut(e=>e.replace(cs,"-$1").toLowerCase()),Qt=ut(e=>e.charAt(0).toUpperCase()+e.slice(1)),Wa=ut(e=>e?`on${Qt(e)}`:""),Q=(e,t)=>!Object.is(e,t);var ls="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly";var Ga=Xt(ls+",async,autofocus,autoplay,controls,default,defer,disabled,inert,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected");function le(e,...t){console.warn(`[Vue warn] ${e}`,...t)}var er;var x;var tr=new WeakSet,dt=class{constructor(t){this.fn=t,this.deps=void 0,this.depsTail=void 0,this.flags=5,this.next=void 0,this.cleanup=void 0,this.scheduler=void 0,er&&(er.active?er.effects.push(this):this.flags&=-2)}pause(){this.flags|=64}resume(){this.flags&64&&(this.flags&=-65,tr.has(this)&&(tr.delete(this),this.trigger()))}notify(){this.flags&2&&!(this.flags&32)||this.flags&8||us(this)}run(){if(!(this.flags&1))return this.fn();this.flags|=2,Bi(this),Wi(this);let t=x,r=M;x=this,M=!0;try{return this.fn()}finally{x!==this&&le("Active effect was not restored correctly - this is likely a Vue internal bug."),Gi(this),x=t,M=r,this.flags&=-3}}stop(){if(this.flags&1){for(let t=this.deps;t;t=t.nextDep)mr(t);this.deps=this.depsTail=void 0,Bi(this),this.onStop&&this.onStop(),this.flags&=-2}}trigger(){this.flags&64?tr.add(this):this.scheduler?this.scheduler():this.runIfDirty()}runIfDirty(){nr(this)&&this.run()}get dirty(){return nr(this)}},zi=0,Ie,Me;function us(e,t=!1){if(e.flags|=8,t){e.next=Me,Me=e;return}e.next=Ie,Ie=e}function dr(){zi++}function hr(){if(--zi>0)return;if(Me){let t=Me;for(Me=void 0;t;){let r=t.next;t.next=void 0,t.flags&=-9,t=r}}let e;for(;Ie;){let t=Ie;for(Ie=void 0;t;){let r=t.next;if(t.next=void 0,t.flags&=-9,t.flags&1)try{t.trigger()}catch(i){e||(e=i)}t=r}}if(e)throw e}function Wi(e){for(let t=e.deps;t;t=t.nextDep)t.version=-1,t.prevActiveLink=t.dep.activeLink,t.dep.activeLink=t}function Gi(e){let t,r=e.depsTail,i=r;for(;i;){let n=i.prevDep;i.version===-1?(i===r&&(r=n),mr(i),ps(i)):t=i,i.dep.activeLink=i.prevActiveLink,i.prevActiveLink=void 0,i=n}e.deps=t,e.depsTail=r}function nr(e){for(let t=e.deps;t;t=t.nextDep)if(t.dep.version!==t.version||t.dep.computed&&(fs(t.dep.computed)||t.dep.version!==t.version))return!0;return!!e._dirty}function fs(e){if(e.flags&4&&!(e.flags&16)||(e.flags&=-17,e.globalVersion===ht)||(e.globalVersion=ht,!e.isSSR&&e.flags&128&&(!e.deps&&!e._dirty||!nr(e))))return;e.flags|=2;let t=e.dep,r=x,i=M;x=e,M=!0;try{Wi(e);let n=e.fn(e._value);(t.version===0||Q(n,e._value))&&(e.flags|=128,e._value=n,t.version++)}catch(n){throw t.version++,n}finally{x=r,M=i,Gi(e),e.flags&=-3}}function mr(e,t=!1){let{dep:r,prevSub:i,nextSub:n}=e;if(i&&(i.nextSub=n,e.prevSub=void 0),n&&(n.prevSub=i,e.nextSub=void 0),r.subsHead===e&&(r.subsHead=n),r.subs===e&&(r.subs=i,!i&&r.computed)){r.computed.flags&=-5;for(let s=r.computed.deps;s;s=s.nextDep)mr(s,!0)}!t&&!--r.sc&&r.map&&r.map.delete(r.key)}function ps(e){let{prevDep:t,nextDep:r}=e;t&&(t.nextDep=r,e.prevDep=void 0),r&&(r.prevDep=t,e.nextDep=void 0)}function Yi(e,t){e.effect instanceof dt&&(e=e.effect.fn);let r=new dt(e);t&&xe(r,t);try{r.run()}catch(n){throw r.stop(),n}let i=r.run.bind(r);return i.effect=r,i}function Ji(e){e.effect.stop()}var M=!0,Xi=[];function ds(){Xi.push(M),M=!1}function hs(){let e=Xi.pop();M=e===void 0?!0:e}function Bi(e){let{cleanup:t}=e;if(e.cleanup=void 0,t){let r=x;x=void 0;try{t()}finally{x=r}}}var ht=0,sr=class{constructor(t,r){this.sub=t,this.dep=r,this.version=r.version,this.nextDep=this.prevDep=this.nextSub=this.prevSub=this.prevActiveLink=void 0}},or=class{constructor(t){this.computed=t,this.version=0,this.activeLink=void 0,this.subs=void 0,this.map=void 0,this.key=void 0,this.sc=0,this.__v_skip=!0,this.subsHead=void 0}track(t){if(!x||!M||x===this.computed)return;let r=this.activeLink;if(r===void 0||r.sub!==x)r=this.activeLink=new sr(x,this),x.deps?(r.prevDep=x.depsTail,x.depsTail.nextDep=r,x.depsTail=r):x.deps=x.depsTail=r,Zi(r);else if(r.version===-1&&(r.version=this.version,r.nextDep)){let i=r.nextDep;i.prevDep=r.prevDep,r.prevDep&&(r.prevDep.nextDep=i),r.prevDep=x.depsTail,r.nextDep=void 0,x.depsTail.nextDep=r,x.depsTail=r,x.deps===r&&(x.deps=i)}return x.onTrack&&x.onTrack(xe({effect:x},t)),r}trigger(t){this.version++,ht++,this.notify(t)}notify(t){dr();try{for(let r=this.subsHead;r;r=r.nextSub)r.sub.onTrigger&&!(r.sub.flags&8)&&r.sub.onTrigger(xe({effect:r.sub},t));for(let r=this.subs;r;r=r.prevSub)r.sub.notify()&&r.sub.dep.notify()}finally{hr()}}};function Zi(e){if(e.dep.sc++,e.sub.flags&4){let t=e.dep.computed;if(t&&!e.dep.subs){t.flags|=20;for(let i=t.deps;i;i=i.nextDep)Zi(i)}let r=e.dep.subs;r!==e&&(e.prevSub=r,r&&(r.nextSub=e)),e.dep.subsHead===void 0&&(e.dep.subsHead=e),e.dep.subs=e}}var ar=new WeakMap,ce=Symbol("Object iterate"),cr=Symbol("Map keys iterate"),Ue=Symbol("Array iterate");function C(e,t,r){if(M&&x){let i=ar.get(e);i||ar.set(e,i=new Map);let n=i.get(r);n||(i.set(r,n=new or),n.map=i,n.key=r),n.track({target:e,type:t,key:r})}}function ee(e,t,r,i,n,s){let o=ar.get(e);if(!o){ht++;return}let a=c=>{c&&c.trigger({target:e,type:t,key:r,newValue:i,oldValue:n,oldTarget:s})};if(dr(),t==="clear")o.forEach(a);else{let c=Ee(e),l=c&<(r);if(c&&r==="length"){let u=Number(i);o.forEach((f,g)=>{(g==="length"||g===Ue||!ve(g)&&g>=u)&&a(f)})}else switch((r!==void 0||o.has(void 0))&&a(o.get(r)),l&&a(o.get(Ue)),t){case"add":c?l&&a(o.get("length")):(a(o.get(ce)),be(e)&&a(o.get(cr)));break;case"delete":c||(a(o.get(ce)),be(e)&&a(o.get(cr)));break;case"set":be(e)&&a(o.get(ce));break}}hr()}function Ae(e){let t=E(e);return t===e?t:(C(t,"iterate",Ue),fe(e)?t:t.map(pe))}function _r(e){return C(e=E(e),"iterate",Ue),e}function B(e,t){return ue(e)?sn(e)?je(pe(t)):je(t):pe(t)}var ms={__proto__:null,[Symbol.iterator](){return rr(this,Symbol.iterator,e=>B(this,e))},concat(...e){return Ae(this).concat(...e.map(t=>Ee(t)?Ae(t):t))},entries(){return rr(this,"entries",e=>(e[1]=B(this,e[1]),e))},every(e,t){return Y(this,"every",e,t,void 0,arguments)},filter(e,t){return Y(this,"filter",e,t,r=>r.map(i=>B(this,i)),arguments)},find(e,t){return Y(this,"find",e,t,r=>B(this,r),arguments)},findIndex(e,t){return Y(this,"findIndex",e,t,void 0,arguments)},findLast(e,t){return Y(this,"findLast",e,t,r=>B(this,r),arguments)},findLastIndex(e,t){return Y(this,"findLastIndex",e,t,void 0,arguments)},forEach(e,t){return Y(this,"forEach",e,t,void 0,arguments)},includes(...e){return ir(this,"includes",e)},indexOf(...e){return ir(this,"indexOf",e)},join(e){return Ae(this).join(e)},lastIndexOf(...e){return ir(this,"lastIndexOf",e)},map(e,t){return Y(this,"map",e,t,void 0,arguments)},pop(){return Pe(this,"pop")},push(...e){return Pe(this,"push",e)},reduce(e,...t){return Ki(this,"reduce",e,t)},reduceRight(e,...t){return Ki(this,"reduceRight",e,t)},shift(){return Pe(this,"shift")},some(e,t){return Y(this,"some",e,t,void 0,arguments)},splice(...e){return Pe(this,"splice",e)},toReversed(){return Ae(this).toReversed()},toSorted(e){return Ae(this).toSorted(e)},toSpliced(...e){return Ae(this).toSpliced(...e)},unshift(...e){return Pe(this,"unshift",e)},values(){return rr(this,"values",e=>B(this,e))}};function rr(e,t,r){let i=_r(e),n=i[t]();return i!==e&&!fe(e)&&(n._next=n.next,n.next=()=>{let s=n._next();return s.done||(s.value=r(s.value)),s}),n}var _s=Array.prototype;function Y(e,t,r,i,n,s){let o=_r(e),a=o!==e&&!fe(e),c=o[t];if(c!==_s[t]){let f=c.apply(e,s);return a?pe(f):f}let l=r;o!==e&&(a?l=function(f,g){return r.call(this,B(e,f),g,e)}:r.length>2&&(l=function(f,g){return r.call(this,f,g,e)}));let u=c.call(o,l,i);return a&&n?n(u):u}function Ki(e,t,r,i){let n=_r(e),s=n!==e&&!fe(e),o=r,a=!1;n!==e&&(s?(a=i.length===0,o=function(l,u,f){return a&&(a=!1,l=B(e,l)),r.call(this,l,B(e,u),f,e)}):r.length>3&&(o=function(l,u,f){return r.call(this,l,u,f,e)}));let c=n[t](o,...i);return a?B(e,c):c}function ir(e,t,r){let i=E(e);C(i,"iterate",Ue);let n=i[t](...r);return(n===-1||n===!1)&&Ts(r[0])?(r[0]=E(r[0]),i[t](...r)):n}function Pe(e,t,r=[]){ds(),dr();let i=E(e)[t].apply(e,r);return hr(),hs(),i}var gs=Xt("__proto__,__v_isRef,__isVue"),Qi=new Set(Object.getOwnPropertyNames(Symbol).filter(e=>e!=="arguments"&&e!=="caller").map(e=>Symbol[e]).filter(ve));function ys(e){ve(e)||(e=String(e));let t=E(this);return C(t,"has",e),t.hasOwnProperty(e)}var mt=class{constructor(t=!1,r=!1){this._isReadonly=t,this._isShallow=r}get(t,r,i){if(r==="__v_skip")return t.__v_skip;let n=this._isReadonly,s=this._isShallow;if(r==="__v_isReactive")return!n;if(r==="__v_isReadonly")return n;if(r==="__v_isShallow")return s;if(r==="__v_raw")return i===(n?s?Ss:rn:s?Os:tn).get(t)||Object.getPrototypeOf(t)===Object.getPrototypeOf(i)?t:void 0;let o=Ee(t);if(!n){let c;if(o&&(c=ms[r]))return c;if(r==="hasOwnProperty")return ys}let a=Reflect.get(t,r,Le(t)?t:i);if((ve(r)?Qi.has(r):gs(r))||(n||C(t,"get",r),s))return a;if(Le(a)){let c=o&<(r)?a:a.value;return n&&we(c)?pr(c):c}return we(a)?n?pr(a):_t(a):a}},lr=class extends mt{constructor(t=!1){super(!1,t)}set(t,r,i,n){let s=t[r],o=Ee(t)&<(r);if(!this._isShallow){let l=ue(s);if(!fe(i)&&!ue(i)&&(s=E(s),i=E(i)),!o&&Le(s)&&!Le(i))return l?(le(`Set operation on key "${String(r)}" failed: target is readonly.`,t[r]),!0):(s.value=i,!0)}let a=o?Number(r)<t.length:ct(t,r),c=Reflect.set(t,r,i,Le(t)?t:n);return t===E(n)&&c&&(a?Q(i,s)&&ee(t,"set",r,i,s):ee(t,"add",r,i)),c}deleteProperty(t,r){let i=ct(t,r),n=t[r],s=Reflect.deleteProperty(t,r);return s&&i&&ee(t,"delete",r,void 0,n),s}has(t,r){let i=Reflect.has(t,r);return(!ve(r)||!Qi.has(r))&&C(t,"has",r),i}ownKeys(t){return C(t,"iterate",Ee(t)?"length":ce),Reflect.ownKeys(t)}},ur=class extends mt{constructor(t=!1){super(!0,t)}set(t,r){return le(`Set operation on key "${String(r)}" failed: target is readonly.`,t),!0}deleteProperty(t,r){return le(`Delete operation on key "${String(r)}" failed: target is readonly.`,t),!0}},xs=new lr,Es=new ur;var fr=e=>e,ft=e=>Reflect.getPrototypeOf(e);function bs(e,t,r){return function(...i){let n=this.__v_raw,s=E(n),o=be(s),a=e==="entries"||e===Symbol.iterator&&o,c=e==="keys"&&o,l=n[e](...i),u=r?fr:t?je:pe;return!t&&C(s,"iterate",c?cr:ce),xe(Object.create(l),{next(){let{value:f,done:g}=l.next();return g?{value:f,done:g}:{value:a?[u(f[0]),u(f[1])]:u(f),done:g}}})}}function pt(e){return function(...t){{let r=t[0]?`on key "${t[0]}" `:"";le(`${Qt(e)} operation ${r}failed: target is readonly.`,E(this))}return e==="delete"?!1:e==="clear"?void 0:this}}function vs(e,t){let r={get(n){let s=this.__v_raw,o=E(s),a=E(n);e||(Q(n,a)&&C(o,"get",n),C(o,"get",a));let{has:c}=ft(o),l=t?fr:e?je:pe;if(c.call(o,n))return l(s.get(n));if(c.call(o,a))return l(s.get(a));s!==o&&s.get(n)},get size(){let n=this.__v_raw;return!e&&C(E(n),"iterate",ce),n.size},has(n){let s=this.__v_raw,o=E(s),a=E(n);return e||(Q(n,a)&&C(o,"has",n),C(o,"has",a)),n===a?s.has(n):s.has(n)||s.has(a)},forEach(n,s){let o=this,a=o.__v_raw,c=E(a),l=t?fr:e?je:pe;return!e&&C(c,"iterate",ce),a.forEach((u,f)=>n.call(s,l(u),l(f),o))}};return xe(r,e?{add:pt("add"),set:pt("set"),delete:pt("delete"),clear:pt("clear")}:{add(n){let s=E(this),o=ft(s),a=E(n),c=!t&&!fe(n)&&!ue(n)?a:n;return o.has.call(s,c)||Q(n,c)&&o.has.call(s,n)||Q(a,c)&&o.has.call(s,a)||(s.add(c),ee(s,"add",c,c)),this},set(n,s){!t&&!fe(s)&&!ue(s)&&(s=E(s));let o=E(this),{has:a,get:c}=ft(o),l=a.call(o,n);l?qi(o,a,n):(n=E(n),l=a.call(o,n));let u=c.call(o,n);return o.set(n,s),l?Q(s,u)&&ee(o,"set",n,s,u):ee(o,"add",n,s),this},delete(n){let s=E(this),{has:o,get:a}=ft(s),c=o.call(s,n);c?qi(s,o,n):(n=E(n),c=o.call(s,n));let l=a?a.call(s,n):void 0,u=s.delete(n);return c&&ee(s,"delete",n,void 0,l),u},clear(){let n=E(this),s=n.size!==0,o=be(n)?new Map(n):new Set(n),a=n.clear();return s&&ee(n,"clear",void 0,void 0,o),a}}),["keys","values","entries",Symbol.iterator].forEach(n=>{r[n]=bs(n,e,t)}),r}function en(e,t){let r=vs(e,t);return(i,n,s)=>n==="__v_isReactive"?!e:n==="__v_isReadonly"?e:n==="__v_raw"?i:Reflect.get(ct(r,n)&&n in i?r:i,n,s)}var ws={get:en(!1,!1)};var As={get:en(!0,!1)};function qi(e,t,r){let i=E(r);if(i!==r&&t.call(e,i)){let n=Zt(e);le(`Reactive ${n} contains both the raw and reactive versions of the same object${n==="Map"?" as keys":""}, which can lead to inconsistencies. Avoid differentiating between the raw and reactive versions of an object and only use the reactive version if possible.`)}}var tn=new WeakMap,Os=new WeakMap,rn=new WeakMap,Ss=new WeakMap;function Ns(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function _t(e){return ue(e)?e:nn(e,!1,xs,ws,tn)}function pr(e){return nn(e,!0,Es,As,rn)}function nn(e,t,r,i,n){if(!we(e))return le(`value cannot be made ${t?"readonly":"reactive"}: ${String(e)}`),e;if(e.__v_raw&&!(t&&e.__v_isReactive)||e.__v_skip||!Object.isExtensible(e))return e;let s=n.get(e);if(s)return s;let o=Ns(Zt(e));if(o===0)return e;let a=new Proxy(e,o===2?i:r);return n.set(e,a),a}function sn(e){return ue(e)?sn(e.__v_raw):!!(e&&e.__v_isReactive)}function ue(e){return!!(e&&e.__v_isReadonly)}function fe(e){return!!(e&&e.__v_isShallow)}function Ts(e){return e?!!e.__v_raw:!1}function E(e){let t=e&&e.__v_raw;return t?E(t):e}var pe=e=>we(e)?_t(e):e,je=e=>we(e)?pr(e):e;function Le(e){return e?e.__v_isRef===!0:!1}v("nextTick",()=>ye);v("dispatch",e=>se.bind(se,e));v("watch",(e,{evaluateLater:t,cleanup:r})=>(i,n)=>{let s=t(i),a=$e(()=>{let c;return s(l=>c=l),c},n);r(a)});v("store",Ri);v("data",e=>ze(e));v("root",e=>oe(e));v("refs",e=>(e._x_refs_proxy||(e._x_refs_proxy=P(Cs(e))),e._x_refs_proxy));function Cs(e){let t=[];return N(e,r=>{r._x_refs&&t.push(r._x_refs)}),t}var gr={};function yr(e){return gr[e]||(gr[e]=0),++gr[e]}function on(e,t){return N(e,r=>{if(r._x_ids&&r._x_ids[t])return!0})}function an(e,t){e._x_ids||(e._x_ids={}),e._x_ids[t]||(e._x_ids[t]=yr(t))}v("id",(e,{cleanup:t})=>(r,i=null)=>{let n=`${r}${i?`-${i}`:""}`;return Rs(e,n,t,()=>{let s=on(e,r),o=s?s._x_ids[r]:yr(r);return i?`${r}-${o}-${i}`:`${r}-${o}`})});H((e,t)=>{e._x_id&&(t._x_id=e._x_id)});function Rs(e,t,r,i){if(e._x_id||(e._x_id={}),e._x_id[t])return e._x_id[t];let n=i();return e._x_id[t]=n,r(()=>{delete e._x_id[t]}),n}v("el",e=>e);cn("Focus","focus","focus");cn("Persist","persist","persist");function cn(e,t,r){v(t,i=>T(`You can't use [$${t}] without first installing the "${e}" plugin here: https://alpinejs.dev/plugins/${r}`,i))}h("modelable",(e,{expression:t},{effect:r,evaluateLater:i,cleanup:n})=>{let s=i(t),o=()=>{let u;return s(f=>u=f),u},a=i(`${t} = __placeholder`),c=u=>a(()=>{},{scope:{__placeholder:u}}),l=o();c(l),queueMicrotask(()=>{if(!e._x_model)return;e._x_removeModelListeners.default();let u=e._x_model.get,f=e._x_model.setWithModifiers,g=at({get(){return u()},set(m){f(m)}},{get(){return o()},set(m){c(m)}});n(g)})});h("teleport",(e,{modifiers:t,expression:r},{cleanup:i})=>{e.tagName.toLowerCase()!=="template"&&T("x-teleport can only be used on a <template> tag",e);let n=ln(r),s=e.content.cloneNode(!0).firstElementChild;e._x_teleport=s,s._x_teleportBack=e,e.setAttribute("data-teleport-template",!0),s.setAttribute("data-teleport-target",!0),e._x_forwardEvents&&e._x_forwardEvents.forEach(a=>{s.addEventListener(a,c=>{c.stopPropagation(),e.dispatchEvent(new c.constructor(c.type,c))})}),F(s,{},e);let o=(a,c,l)=>{l.includes("prepend")?c.parentNode.insertBefore(a,c):l.includes("append")?c.parentNode.insertBefore(a,c.nextSibling):c.appendChild(a)};_(()=>{O(()=>{o(s,n,t),S(s)})()}),e._x_teleportPutBack=()=>{let a=ln(r);_(()=>{o(e._x_teleport,a,t)})},i(()=>_(()=>{s.remove(),k(s)}))});var ks=document.createElement("div");function ln(e){let t=O(()=>document.querySelector(e),()=>ks)();return t||T(`Cannot find x-teleport element for selector: "${e}"`),t}var un=()=>{};un.inline=(e,{modifiers:t},{cleanup:r})=>{t.includes("self")?e._x_ignoreSelf=!0:e._x_ignore=!0,r(()=>{t.includes("self")?delete e._x_ignoreSelf:delete e._x_ignore})};h("ignore",un);h("effect",O((e,{expression:t},{effect:r})=>{r(b(e,t))}));function J(e,t,r,i){let n=e,s=c=>i(c),o={},a=(c,l)=>u=>l(c,u);return r.includes("dot")&&(t=Ds(t)),r.includes("camel")&&(t=Ps(t)),r.includes("capture")&&(o.capture=!0),r.includes("window")&&(n=window),r.includes("document")&&(n=document),r.includes("passive")&&(o.passive=r[r.indexOf("passive")+1]!=="false"),s=xr(r,s),r.includes("prevent")&&(s=a(s,(c,l)=>{l.preventDefault(),c(l)})),r.includes("stop")&&(s=a(s,(c,l)=>{l.stopPropagation(),c(l)})),r.includes("once")&&(s=a(s,(c,l)=>{c(l),n.removeEventListener(t,s,o)})),(r.includes("away")||r.includes("outside"))&&(n=document,s=a(s,(c,l)=>{e.contains(l.target)||l.target.isConnected!==!1&&(e.offsetWidth<1&&e.offsetHeight<1||e._x_isShown!==!1&&c(l))})),r.includes("self")&&(s=a(s,(c,l)=>{l.target===e&&c(l)})),t==="submit"&&(s=a(s,(c,l)=>{l.target._x_pendingModelUpdates&&l.target._x_pendingModelUpdates.forEach(u=>u()),c(l)})),(Ms(t)||pn(t))&&(s=a(s,(c,l)=>{Us(l,r)||c(l)})),n.addEventListener(t,s,o),()=>{n.removeEventListener(t,s,o)}}function xr(e,t){if(e.includes("debounce")){let r=e[e.indexOf("debounce")+1]||"invalid-wait",i=gt(r.split("ms")[0])?Number(r.split("ms")[0]):250;t=st(t,i)}if(e.includes("throttle")){let r=e[e.indexOf("throttle")+1]||"invalid-wait",i=gt(r.split("ms")[0])?Number(r.split("ms")[0]):250;t=ot(t,i)}return t}function Ds(e){return e.replace(/-/g,".")}function Ps(e){return e.toLowerCase().replace(/-(\w)/g,(t,r)=>r.toUpperCase())}function gt(e){return!Array.isArray(e)&&!isNaN(e)}function Is(e){return[" ","_"].includes(e)?e:e.replace(/([a-z])([A-Z])/g,"$1-$2").replace(/[_\s]/,"-").toLowerCase()}function Ms(e){return["keydown","keyup"].includes(e)}function pn(e){return["contextmenu","click","mouse"].some(t=>e.includes(t))}var Ls=["window","document","prevent","stop","once","capture","self","away","outside","passive","dot","camel","preserve-scroll","blur","change","lazy","number","boolean","trim","fill","unintrusive","parent"];function Us(e,t){let r=t.filter((s,o)=>s==="false"&&t[o-1]==="passive"?!1:!Ls.includes(s));if(r.includes("debounce")){let s=r.indexOf("debounce");r.splice(s,gt((r[s+1]||"invalid-wait").split("ms")[0])?2:1)}if(r.includes("throttle")){let s=r.indexOf("throttle");r.splice(s,gt((r[s+1]||"invalid-wait").split("ms")[0])?2:1)}if(r.length===0||r.length===1&&fn(e.key).includes(r[0]))return!1;let n=["ctrl","shift","alt","meta","cmd","super"].filter(s=>r.includes(s));return r=r.filter(s=>!n.includes(s)),!(n.length>0&&n.filter(o=>((o==="cmd"||o==="super")&&(o="meta"),e[`${o}Key`])).length===n.length&&(pn(e.type)||fn(e.key).includes(r[0])))}function fn(e){if(!e)return[];e=Is(e);let t={ctrl:"control",slash:"/",space:" ",spacebar:" ",cmd:"meta",esc:"escape",up:"arrow-up",down:"arrow-down",left:"arrow-left",right:"arrow-right",period:".",comma:",",equal:"=",minus:"-",underscore:"_"};return t[e]=e,Object.keys(t).map(r=>{if(t[r]===e)return r}).filter(r=>r)}h("model",(e,{modifiers:t,expression:r},{effect:i,cleanup:n})=>{let s=e;t.includes("parent")&&(s=N(e,d=>d!==e));let o=b(s,r),a;typeof r=="string"?a=b(s,`${r} = __placeholder`):typeof r=="function"&&typeof r()=="string"?a=b(s,`${r()} = __placeholder`):a=()=>{};let c=()=>{let d;return o(w=>d=w),dn(d)?d.get():d},l=d=>{let w;o(p=>w=p),dn(w)?w.set(d):a(()=>{},{scope:{__placeholder:d}})};typeof r=="string"&&e.type==="radio"&&_(()=>{e.hasAttribute("name")||e.setAttribute("name",r)});let u=t.includes("change")||t.includes("lazy"),f=t.includes("blur"),g=t.includes("enter"),m=u||f||g,L;if(V)L=()=>{};else if(m){let d=[],w=p=>l(yt(e,t,p,c()));if(u&&d.push(J(e,"change",t,w)),f&&(d.push(J(e,"blur",t,w)),e.form)){let p=e.form,y=()=>w({target:e});p._x_pendingModelUpdates||(p._x_pendingModelUpdates=[]),p._x_pendingModelUpdates.push(y),n(()=>{p._x_pendingModelUpdates&&p._x_pendingModelUpdates.splice(p._x_pendingModelUpdates.indexOf(y),1)})}g&&d.push(J(e,"keydown",t,p=>{p.key==="Enter"&&w(p)})),L=()=>d.forEach(p=>p())}else{let d=e.tagName.toLowerCase()==="select"||["checkbox","radio"].includes(e.type)?"change":"input";L=J(e,d,t,w=>{l(yt(e,t,w,c()))})}if(t.includes("fill")&&([void 0,null,""].includes(c())||De(e)&&Array.isArray(c())||e.tagName.toLowerCase()==="select"&&e.multiple)&&l(yt(e,t,{target:e},c())),e._x_removeModelListeners||(e._x_removeModelListeners={}),e._x_removeModelListeners.default=L,n(()=>e._x_removeModelListeners.default()),e.form){let d=J(e.form,"reset",[],w=>{ye(()=>e._x_model&&e._x_model.set(yt(e,t,{target:e},c())))});n(()=>d())}if(e._x_model={get(){return c()},set(d){l(d)},setWithModifiers:xr(t,l)},e._x_forceModelUpdate=d=>{d===void 0&&typeof r=="string"&&r.match(/\./)&&(d=""),_(()=>{De(e)?Array.isArray(d)?e.checked=d.some(w=>w==e.value):e.checked=!!d:nt(e)?typeof d=="boolean"?e.checked=ke(e.value)===d:e.checked=e.value==d:Re(e,"value",d)})},e.tagName==="SELECT"){let d=new MutationObserver(()=>{e._x_forceModelUpdate(c())});d.observe(e,{childList:!0}),n(()=>d.disconnect())}i(()=>{let d=c();t.includes("unintrusive")&&document.activeElement.isSameNode(e)||e._x_forceModelUpdate(d)})});function yt(e,t,r,i){return _(()=>{if(r instanceof CustomEvent&&r.detail!==void 0)return r.detail!==null&&r.detail!==void 0?r.detail:r.target.value;if(De(e))if(Array.isArray(i)){let n=null;return t.includes("number")?n=Er(r.target.value):t.includes("boolean")?n=ke(r.target.value):n=r.target.value,r.target.checked?i.includes(n)?i:i.concat([n]):i.filter(s=>!js(s,n))}else return r.target.checked;else{if(e.tagName.toLowerCase()==="select"&&e.multiple)return t.includes("number")?Array.from(r.target.selectedOptions).map(n=>{let s=n.value||n.text;return Er(s)}):t.includes("boolean")?Array.from(r.target.selectedOptions).map(n=>{let s=n.value||n.text;return ke(s)}):Array.from(r.target.selectedOptions).map(n=>n.value||n.text);{let n;return nt(e)?r.target.checked?n=r.target.value:n=i:n=r.target.value,t.includes("number")?Er(n):t.includes("boolean")?ke(n):t.includes("trim")?n.trim():n}}})}function Er(e){let t=e?parseFloat(e):null;return Fs(t)?t:e}function js(e,t){return e==t}function Fs(e){return!Array.isArray(e)&&!isNaN(e)}function dn(e){return e!==null&&typeof e=="object"&&typeof e.get=="function"&&typeof e.set=="function"}h("cloak",e=>queueMicrotask(()=>_(()=>e.removeAttribute(R("cloak")))));tt(()=>`[${R("init")}]`);h("init",O((e,{expression:t},{evaluate:r})=>typeof t=="string"?!!t.trim()&&r(t,{},!1):r(t,{},!1)));h("text",(e,{expression:t},{effect:r,evaluateLater:i})=>{let n=i(t);r(()=>{n(s=>{_(()=>{e.textContent=s})})})});h("html",(e,{expression:t},{effect:r,evaluateLater:i})=>{let n=i(t);r(()=>{n(s=>{_(()=>{Array.from(e.children).forEach(o=>k(o)),e.innerHTML=s??"",e._x_ignoreSelf=!0,S(e),delete e._x_ignoreSelf})})},{priority:"structural"})});ge(Je(":",Xe(R("bind:"))));var hn=(e,{value:t,modifiers:r,expression:i,original:n},{effect:s,cleanup:o})=>{if(!t){let c={};Pi(c),b(e,i)(u=>{zt(e,u,n)},{scope:c});return}if(t==="key")return $s(e,i);if(e._x_inlineBindings&&e._x_inlineBindings[t]&&e._x_inlineBindings[t].extract)return;let a=b(e,i);s(()=>a(c=>{c===void 0&&typeof i=="string"&&i.match(/\./)&&(c=""),_(()=>Re(e,t,c,r))})),o(()=>{e._x_undoAddedClasses&&e._x_undoAddedClasses(),e._x_undoAddedStyles&&e._x_undoAddedStyles()})};hn.inline=(e,{value:t,modifiers:r,expression:i})=>{t&&(e._x_inlineBindings||(e._x_inlineBindings={}),e._x_inlineBindings[t]={expression:i,extract:!1})};h("bind",hn);function $s(e,t){e._x_keyExpression=t}et(()=>`[${R("data")}]`);var de=Symbol();h("data",(e,{expression:t},{cleanup:r})=>{if(Hs(e))return;let i=e[de];if(i?.expression===t)return;t=t===""?"{}":t;let n={};X(n,e);let s={};Li(s,n);let o=I(e,t,{scope:s});(o===void 0||o===!0)&&(o={}),X(o,e);let a;if(i?.reactiveData){a=i.reactiveData,Vs(a,o);let l={expression:t};e[de]=l,queueMicrotask(()=>{e[de]===l&&delete e[de]})}else a=D(o);_e(a,r);let c=F(e,a);O(()=>{a.init&&I(e,a.init)})(),r(()=>{a.destroy&&I(e,a.destroy),c();let l={reactiveData:a};e[de]=l,queueMicrotask(()=>{e[de]===l&&delete e[de]})})});function Vs(e,t){Object.keys(t).forEach(r=>{let i=Object.getOwnPropertyDescriptor(t,r),n=Object.getOwnPropertyDescriptor(e,r);i.get||i.set||n?.get||n?.set?(n&&delete e[r],n||(e[r]=void 0),i.get||i.set?Object.defineProperty(e,r,i):e[r]=t[r]):e[r]=t[r]}),Object.keys(e).filter(r=>!Object.prototype.hasOwnProperty.call(t,r)).forEach(r=>delete e[r])}H((e,t)=>{e._x_dataStack&&(t._x_dataStack=e._x_dataStack,t.setAttribute("data-has-alpine-state",!0))});function Hs(e){return V?Ze?!0:e.hasAttribute("data-has-alpine-state"):!1}h("show",(e,{modifiers:t,expression:r},{effect:i})=>{let n=b(e,r);e._x_doHide||(e._x_doHide=()=>{_(()=>{e.style.setProperty("display","none",t.includes("important")?"important":void 0)})}),e._x_doShow||(e._x_doShow=()=>{_(()=>{e.style.length===1&&e.style.display==="none"?e.removeAttribute("style"):e.style.removeProperty("display")})});let s=()=>{e._x_doHide(),e._x_isShown=!1},o=()=>{e._x_doShow(),e._x_isShown=!0},a=()=>setTimeout(o),c=Te(f=>f?o():s(),f=>{typeof e._x_toggleAndCascadeWithTransitions=="function"?e._x_toggleAndCascadeWithTransitions(e,f,o,s):f?a():s()}),l,u=!0;i(()=>n(f=>{!u&&f===l||(t.includes("immediate")&&(f?a():s()),c(f),l=f,u=!1)}))});h("for",O((e,{expression:t},{effect:r,cleanup:i})=>{let n=qs(t),s=b(e,n.items),o=b(e,e._x_keyExpression||"index");e._x_lookup=new Map,r(()=>Ks(e,n,s,o),{priority:"structural"}),i(()=>{e._x_lookup.forEach(a=>_(()=>{k(a),a.remove()})),delete e._x_lookup,delete e._x_lastRenderedEl})}));function Bs(e){return t=>{Object.entries(t).forEach(([r,i])=>{e[r]=i})}}function Ks(e,t,r,i){r(n=>{Ws(n)&&(n=Array.from({length:n},(l,u)=>u+1)),n==null&&(n=[]),n instanceof Set&&(n=Array.from(n)),n instanceof Map&&(n=Array.from(n));let s=e._x_lookup,o=new Map;e._x_lookup=o;let a=Gs(n),c=Object.entries(n).map(([l,u])=>{a||(l=parseInt(l));let f=zs(t,u,l,n),g;return i(m=>{typeof m=="object"&&T("x-for key cannot be an object, it must be a string or an integer",e),s.has(m)&&(o.set(m,s.get(m)),s.delete(m)),g=m},{scope:{index:l,...f}}),[g,f]});_(()=>{s.forEach(f=>{k(f),f.remove()});let l=new Set,u=e;c.forEach(([f,g])=>{if(o.has(f)){let d=o.get(f);d._x_refreshXForScope(g),u.nextElementSibling!==d&&(u.nextElementSibling&&d.replaceWith(u.nextElementSibling),u.after(d)),u=d,d._x_currentIfEl&&(d.nextElementSibling!==d._x_currentIfEl&&u.after(d._x_currentIfEl),u=d._x_currentIfEl);return}e.content.children.length>1&&T("x-for templates require a single root element, additional elements will be ignored.",e);let m=document.importNode(e.content,!0).firstElementChild,L=D(g);F(m,L,e),m._x_refreshXForScope=Bs(L),o.set(f,m),l.add(m),u.after(m),u=m}),l.forEach(f=>S(f)),u!==e?e._x_lastRenderedEl=u:delete e._x_lastRenderedEl})})}function qs(e){let t=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,r=/^\s*\(|\)\s*$/g,i=/([\s\S]*?)\b(?:in|of)\b([\s\S]*)/,n=e.match(i);if(!n)return;let s={};s.items=n[2].trim();let o=n[1].replace(r,"").trim(),a=o.match(t);return a?(s.item=o.replace(t,"").trim(),s.index=a[1].trim(),a[2]&&(s.collection=a[2].trim())):s.item=o,s}function zs(e,t,r,i){let n={};return/^\[.*\]$/.test(e.item)&&Array.isArray(t)?e.item.replace("[","").replace("]","").split(",").map(o=>o.trim()).forEach((o,a)=>{n[o]=t[a]}):/^\{.*\}$/.test(e.item)&&!Array.isArray(t)&&typeof t=="object"?e.item.replace("{","").replace("}","").split(",").map(o=>o.trim()).forEach(o=>{n[o]=t[o]}):n[e.item]=t,e.index&&(n[e.index]=r),e.collection&&(n[e.collection]=i),n}function Ws(e){return typeof e!="object"&&!isNaN(e)}function Gs(e){return typeof e=="object"&&!Array.isArray(e)}function mn(){}mn.inline=(e,{expression:t},{cleanup:r})=>{let i=oe(e);i&&(i._x_refs||(i._x_refs={}),i._x_refs[t]=e,r(()=>delete i._x_refs[t]))};h("ref",mn);h("if",O((e,{expression:t},{effect:r,cleanup:i})=>{e.tagName.toLowerCase()!=="template"&&T("x-if can only be used on a <template> tag",e);let n=b(e,t),s=()=>{if(e._x_currentIfEl)return e._x_currentIfEl;let a=e.content.cloneNode(!0).firstElementChild;return F(a,{},e),_(()=>{e.after(a),S(a)}),e._x_currentIfEl=a,e._x_lastRenderedEl=a,e._x_undoIf=()=>{_(()=>{k(a),a.remove()}),delete e._x_currentIfEl,delete e._x_lastRenderedEl},a},o=()=>{e._x_undoIf&&(e._x_undoIf(),delete e._x_undoIf)};r(()=>n(a=>{a?s():o()}),{priority:"structural"}),i(()=>e._x_undoIf&&e._x_undoIf())}));h("id",(e,{expression:t},{evaluate:r})=>{r(t).forEach(n=>an(e,n))});H((e,t)=>{e._x_ids&&(t._x_ids=e._x_ids)});ge(Je("@",Xe(R("on:"))));h("on",O((e,{value:t,modifiers:r,expression:i},{cleanup:n})=>{let s=i?b(e,i):()=>{};e.tagName.toLowerCase()==="template"&&(e._x_forwardEvents||(e._x_forwardEvents=[]),e._x_forwardEvents.includes(t)||e._x_forwardEvents.push(t));let o=J(e,t,r,a=>{s(()=>{},{scope:{$event:a},params:[a]})});n(()=>o())}));xt("Collapse","collapse","collapse");xt("Intersect","intersect","intersect");xt("Focus","trap","focus");xt("Mask","mask","mask");function xt(e,t,r){h(t,i=>T(`You can't use [x-${t}] without first installing the "${e}" plugin here: https://alpinejs.dev/plugins/${r}`,i))}h("html",(e,{expression:t})=>{re(new Error("Using the x-html directive is prohibited in the CSP build"),e)});W.setEvaluator($i);W.setRawEvaluator(Fi);W.setReactivityEngine({reactive:_t,effect:(e,t={})=>{let r;return r=Yi(e,{scheduler:()=>{r&&(t.scheduler?t.scheduler(r):r())}}),r},release:Ji,raw:E});var br=W;window.Alpine=br;queueMicrotask(()=>{br.start()});})(); | |
| 8 | +/*! Bundled license information: | |
| 9 | + | |
| 10 | +@vue/shared/dist/shared.esm-bundler.js: | |
| 11 | + (** | |
| 12 | + * @vue/shared v3.5.41 | |
| 13 | + * (c) 2018-present Yuxi (Evan) You and Vue contributors | |
| 14 | + * @license MIT | |
| 15 | + **) | |
| 16 | + | |
| 17 | +@vue/reactivity/dist/reactivity.esm-bundler.js: | |
| 18 | + (** | |
| 19 | + * @vue/reactivity v3.5.41 | |
| 20 | + * (c) 2018-present Yuxi (Evan) You and Vue contributors | |
| 21 | + * @license MIT | |
| 22 | + **) | |
| 23 | +*/ |
D
static/js/alpine.min.js
+0
-5
@@ -1,5 +0,0 @@
| 1 | -(()=>{var rt=!1,nt=!1,q=[],it=-1;function Vt(e){Sn(e)}function Sn(e){q.includes(e)||q.push(e),An()}function ve(e){let t=q.indexOf(e);t!==-1&&t>it&&q.splice(t,1)}function An(){!nt&&!rt&&(rt=!0,queueMicrotask(On))}function On(){rt=!1,nt=!0;for(let e=0;e<q.length;e++)q[e](),it=e;q.length=0,it=-1,nt=!1}var T,N,L,st,ot=!0;function qt(e){ot=!1,e(),ot=!0}function Ut(e){T=e.reactive,L=e.release,N=t=>e.effect(t,{scheduler:r=>{ot?Vt(r):r()}}),st=e.raw}function at(e){N=e}function Wt(e){let t=()=>{};return[n=>{let i=N(n);return e._x_effects||(e._x_effects=new Set,e._x_runEffects=()=>{e._x_effects.forEach(o=>o())}),e._x_effects.add(i),t=()=>{i!==void 0&&(e._x_effects.delete(i),L(i))},i},()=>{t()}]}function Se(e,t){let r=!0,n,i=N(()=>{let o=e();JSON.stringify(o),r?n=o:queueMicrotask(()=>{t(o,n),n=o}),r=!1});return()=>L(i)}function U(e,t,r={}){e.dispatchEvent(new CustomEvent(t,{detail:r,bubbles:!0,composed:!0,cancelable:!0}))}function O(e,t){if(typeof ShadowRoot=="function"&&e instanceof ShadowRoot){Array.from(e.children).forEach(i=>O(i,t));return}let r=!1;if(t(e,()=>r=!0),r)return;let n=e.firstElementChild;for(;n;)O(n,t,!1),n=n.nextElementSibling}function v(e,...t){console.warn(`Alpine Warning: ${e}`,...t)}var Gt=!1;function Jt(){Gt&&v("Alpine has already been initialized on this page. Calling Alpine.start() more than once can cause problems."),Gt=!0,document.body||v("Unable to initialize. Trying to load Alpine before `<body>` is available. Did you forget to add `defer` in Alpine's `<script>` tag?"),U(document,"alpine:init"),U(document,"alpine:initializing"),le(),rr(t=>S(t,O)),ee(t=>ce(t)),Ce((t,r)=>{ue(t,r).forEach(n=>n())});let e=t=>!W(t.parentElement,!0);Array.from(document.querySelectorAll(Zt().join(","))).filter(e).forEach(t=>{S(t)}),U(document,"alpine:initialized")}var ct=[],Yt=[];function Xt(){return ct.map(e=>e())}function Zt(){return ct.concat(Yt).map(e=>e())}function Ae(e){ct.push(e)}function Oe(e){Yt.push(e)}function W(e,t=!1){return Q(e,r=>{if((t?Zt():Xt()).some(i=>r.matches(i)))return!0})}function Q(e,t){if(e){if(t(e))return e;if(e._x_teleportBack&&(e=e._x_teleportBack),!!e.parentElement)return Q(e.parentElement,t)}}function Qt(e){return Xt().some(t=>e.matches(t))}var er=[];function tr(e){er.push(e)}function S(e,t=O,r=()=>{}){ir(()=>{t(e,(n,i)=>{r(n,i),er.forEach(o=>o(n,i)),ue(n,n.attributes).forEach(o=>o()),n._x_ignore&&i()})})}function ce(e){O(e,t=>{lt(t),nr(t)})}var or=[],sr=[],ar=[];function rr(e){ar.push(e)}function ee(e,t){typeof t=="function"?(e._x_cleanups||(e._x_cleanups=[]),e._x_cleanups.push(t)):(t=e,sr.push(t))}function Ce(e){or.push(e)}function Re(e,t,r){e._x_attributeCleanups||(e._x_attributeCleanups={}),e._x_attributeCleanups[t]||(e._x_attributeCleanups[t]=[]),e._x_attributeCleanups[t].push(r)}function lt(e,t){e._x_attributeCleanups&&Object.entries(e._x_attributeCleanups).forEach(([r,n])=>{(t===void 0||t.includes(r))&&(n.forEach(i=>i()),delete e._x_attributeCleanups[r])})}function nr(e){if(e._x_cleanups)for(;e._x_cleanups.length;)e._x_cleanups.pop()()}var ut=new MutationObserver(mt),ft=!1;function le(){ut.observe(document,{subtree:!0,childList:!0,attributes:!0,attributeOldValue:!0}),ft=!0}function dt(){Cn(),ut.disconnect(),ft=!1}var fe=[];function Cn(){let e=ut.takeRecords();fe.push(()=>e.length>0&&mt(e));let t=fe.length;queueMicrotask(()=>{if(fe.length===t)for(;fe.length>0;)fe.shift()()})}function h(e){if(!ft)return e();dt();let t=e();return le(),t}var pt=!1,Te=[];function cr(){pt=!0}function lr(){pt=!1,mt(Te),Te=[]}function mt(e){if(pt){Te=Te.concat(e);return}let t=new Set,r=new Set,n=new Map,i=new Map;for(let o=0;o<e.length;o++)if(!e[o].target._x_ignoreMutationObserver&&(e[o].type==="childList"&&(e[o].addedNodes.forEach(s=>s.nodeType===1&&t.add(s)),e[o].removedNodes.forEach(s=>s.nodeType===1&&r.add(s))),e[o].type==="attributes")){let s=e[o].target,a=e[o].attributeName,c=e[o].oldValue,l=()=>{n.has(s)||n.set(s,[]),n.get(s).push({name:a,value:s.getAttribute(a)})},u=()=>{i.has(s)||i.set(s,[]),i.get(s).push(a)};s.hasAttribute(a)&&c===null?l():s.hasAttribute(a)?(u(),l()):u()}i.forEach((o,s)=>{lt(s,o)}),n.forEach((o,s)=>{or.forEach(a=>a(s,o))});for(let o of r)t.has(o)||(sr.forEach(s=>s(o)),ce(o));t.forEach(o=>{o._x_ignoreSelf=!0,o._x_ignore=!0});for(let o of t)r.has(o)||o.isConnected&&(delete o._x_ignoreSelf,delete o._x_ignore,ar.forEach(s=>s(o)),o._x_ignore=!0,o._x_ignoreSelf=!0);t.forEach(o=>{delete o._x_ignoreSelf,delete o._x_ignore}),t=null,r=null,n=null,i=null}function Me(e){return F(j(e))}function P(e,t,r){return e._x_dataStack=[t,...j(r||e)],()=>{e._x_dataStack=e._x_dataStack.filter(n=>n!==t)}}function j(e){return e._x_dataStack?e._x_dataStack:typeof ShadowRoot=="function"&&e instanceof ShadowRoot?j(e.host):e.parentNode?j(e.parentNode):[]}function F(e){return new Proxy({objects:e},Tn)}var Tn={ownKeys({objects:e}){return Array.from(new Set(e.flatMap(t=>Object.keys(t))))},has({objects:e},t){return t==Symbol.unscopables?!1:e.some(r=>Object.prototype.hasOwnProperty.call(r,t))},get({objects:e},t,r){return t=="toJSON"?Rn:Reflect.get(e.find(n=>Object.prototype.hasOwnProperty.call(n,t))||{},t,r)},set({objects:e},t,r,n){let i=e.find(s=>Object.prototype.hasOwnProperty.call(s,t))||e[e.length-1],o=Object.getOwnPropertyDescriptor(i,t);return o?.set&&o?.get?Reflect.set(i,t,r,n):Reflect.set(i,t,r)}};function Rn(){return Reflect.ownKeys(this).reduce((t,r)=>(t[r]=Reflect.get(this,r),t),{})}function Ne(e){let t=n=>typeof n=="object"&&!Array.isArray(n)&&n!==null,r=(n,i="")=>{Object.entries(Object.getOwnPropertyDescriptors(n)).forEach(([o,{value:s,enumerable:a}])=>{if(a===!1||s===void 0)return;let c=i===""?o:`${i}.${o}`;typeof s=="object"&&s!==null&&s._x_interceptor?n[o]=s.initialize(e,c,o):t(s)&&s!==n&&!(s instanceof Element)&&r(s,c)})};return r(e)}function Pe(e,t=()=>{}){let r={initialValue:void 0,_x_interceptor:!0,initialize(n,i,o){return e(this.initialValue,()=>Mn(n,i),s=>ht(n,i,s),i,o)}};return t(r),n=>{if(typeof n=="object"&&n!==null&&n._x_interceptor){let i=r.initialize.bind(r);r.initialize=(o,s,a)=>{let c=n.initialize(o,s,a);return r.initialValue=c,i(o,s,a)}}else r.initialValue=n;return r}}function Mn(e,t){return t.split(".").reduce((r,n)=>r[n],e)}function ht(e,t,r){if(typeof t=="string"&&(t=t.split(".")),t.length===1)e[t[0]]=r;else{if(t.length===0)throw error;return e[t[0]]||(e[t[0]]={}),ht(e[t[0]],t.slice(1),r)}}var ur={};function y(e,t){ur[e]=t}function de(e,t){return Object.entries(ur).forEach(([r,n])=>{let i=null;function o(){if(i)return i;{let[s,a]=_t(t);return i={interceptor:Pe,...s},ee(t,a),i}}Object.defineProperty(e,`$${r}`,{get(){return n(t,o())},enumerable:!1})}),e}function fr(e,t,r,...n){try{return r(...n)}catch(i){te(i,e,t)}}function te(e,t,r=void 0){e=Object.assign(e??{message:"No error message given."},{el:t,expression:r}),console.warn(`Alpine Expression Error: ${e.message} | |
| 2 | - | |
| 3 | -${r?'Expression: "'+r+`" | |
| 4 | - | |
| 5 | -`:""}`,t),setTimeout(()=>{throw e},0)}var Ie=!0;function ke(e){let t=Ie;Ie=!1;let r=e();return Ie=t,r}function R(e,t,r={}){let n;return x(e,t)(i=>n=i,r),n}function x(...e){return dr(...e)}var dr=xt;function pr(e){dr=e}function xt(e,t){let r={};de(r,e);let n=[r,...j(e)],i=typeof t=="function"?Nn(n,t):In(n,t,e);return fr.bind(null,e,t,i)}function Nn(e,t){return(r=()=>{},{scope:n={},params:i=[]}={})=>{let o=t.apply(F([n,...e]),i);De(r,o)}}var gt={};function Pn(e,t){if(gt[e])return gt[e];let r=Object.getPrototypeOf(async function(){}).constructor,n=/^[\n\s]*if.*\(.*\)/.test(e.trim())||/^(let|const)\s/.test(e.trim())?`(async()=>{ ${e} })()`:e,o=(()=>{try{let s=new r(["__self","scope"],`with (scope) { __self.result = ${n} }; __self.finished = true; return __self.result;`);return Object.defineProperty(s,"name",{value:`[Alpine] ${e}`}),s}catch(s){return te(s,t,e),Promise.resolve()}})();return gt[e]=o,o}function In(e,t,r){let n=Pn(t,r);return(i=()=>{},{scope:o={},params:s=[]}={})=>{n.result=void 0,n.finished=!1;let a=F([o,...e]);if(typeof n=="function"){let c=n(n,a).catch(l=>te(l,r,t));n.finished?(De(i,n.result,a,s,r),n.result=void 0):c.then(l=>{De(i,l,a,s,r)}).catch(l=>te(l,r,t)).finally(()=>n.result=void 0)}}}function De(e,t,r,n,i){if(Ie&&typeof t=="function"){let o=t.apply(r,n);o instanceof Promise?o.then(s=>De(e,s,r,n)).catch(s=>te(s,i,t)):e(o)}else typeof t=="object"&&t instanceof Promise?t.then(o=>e(o)):e(t)}var Et="x-";function C(e=""){return Et+e}function mr(e){Et=e}var yt={};function d(e,t){return yt[e]=t,{before(r){if(!yt[r]){console.warn(String.raw`Cannot find directive \`${r}\`. \`${e}\` will use the default order of execution`);return}let n=G.indexOf(r);G.splice(n>=0?n:G.indexOf("DEFAULT"),0,e)}}}function ue(e,t,r){if(t=Array.from(t),e._x_virtualDirectives){let o=Object.entries(e._x_virtualDirectives).map(([a,c])=>({name:a,value:c})),s=vt(o);o=o.map(a=>s.find(c=>c.name===a.name)?{name:`x-bind:${a.name}`,value:`"${a.value}"`}:a),t=t.concat(o)}let n={};return t.map(_r((o,s)=>n[o]=s)).filter(xr).map(kn(n,r)).sort(Ln).map(o=>Dn(e,o))}function vt(e){return Array.from(e).map(_r()).filter(t=>!xr(t))}var bt=!1,pe=new Map,hr=Symbol();function ir(e){bt=!0;let t=Symbol();hr=t,pe.set(t,[]);let r=()=>{for(;pe.get(t).length;)pe.get(t).shift()();pe.delete(t)},n=()=>{bt=!1,r()};e(r),n()}function _t(e){let t=[],r=a=>t.push(a),[n,i]=Wt(e);return t.push(i),[{Alpine:B,effect:n,cleanup:r,evaluateLater:x.bind(x,e),evaluate:R.bind(R,e)},()=>t.forEach(a=>a())]}function Dn(e,t){let r=()=>{},n=yt[t.type]||r,[i,o]=_t(e);Re(e,t.original,o);let s=()=>{e._x_ignore||e._x_ignoreSelf||(n.inline&&n.inline(e,t,i),n=n.bind(n,e,t,i),bt?pe.get(hr).push(n):n())};return s.runCleanups=o,s}var Le=(e,t)=>({name:r,value:n})=>(r.startsWith(e)&&(r=r.replace(e,t)),{name:r,value:n}),$e=e=>e;function _r(e=()=>{}){return({name:t,value:r})=>{let{name:n,value:i}=gr.reduce((o,s)=>s(o),{name:t,value:r});return n!==t&&e(n,t),{name:n,value:i}}}var gr=[];function re(e){gr.push(e)}function xr({name:e}){return yr().test(e)}var yr=()=>new RegExp(`^${Et}([^:^.]+)\\b`);function kn(e,t){return({name:r,value:n})=>{let i=r.match(yr()),o=r.match(/:([a-zA-Z0-9\-_:]+)/),s=r.match(/\.[^.\]]+(?=[^\]]*$)/g)||[],a=t||e[r]||r;return{type:i?i[1]:null,value:o?o[1]:null,modifiers:s.map(c=>c.replace(".","")),expression:n,original:a}}}var wt="DEFAULT",G=["ignore","ref","data","id","anchor","bind","init","for","model","modelable","transition","show","if",wt,"teleport"];function Ln(e,t){let r=G.indexOf(e.type)===-1?wt:e.type,n=G.indexOf(t.type)===-1?wt:t.type;return G.indexOf(r)-G.indexOf(n)}var St=[],At=!1;function ne(e=()=>{}){return queueMicrotask(()=>{At||setTimeout(()=>{je()})}),new Promise(t=>{St.push(()=>{e(),t()})})}function je(){for(At=!1;St.length;)St.shift()()}function br(){At=!0}function me(e,t){return Array.isArray(t)?wr(e,t.join(" ")):typeof t=="object"&&t!==null?$n(e,t):typeof t=="function"?me(e,t()):wr(e,t)}function wr(e,t){let r=o=>o.split(" ").filter(Boolean),n=o=>o.split(" ").filter(s=>!e.classList.contains(s)).filter(Boolean),i=o=>(e.classList.add(...o),()=>{e.classList.remove(...o)});return t=t===!0?t="":t||"",i(n(t))}function $n(e,t){let r=a=>a.split(" ").filter(Boolean),n=Object.entries(t).flatMap(([a,c])=>c?r(a):!1).filter(Boolean),i=Object.entries(t).flatMap(([a,c])=>c?!1:r(a)).filter(Boolean),o=[],s=[];return i.forEach(a=>{e.classList.contains(a)&&(e.classList.remove(a),s.push(a))}),n.forEach(a=>{e.classList.contains(a)||(e.classList.add(a),o.push(a))}),()=>{s.forEach(a=>e.classList.add(a)),o.forEach(a=>e.classList.remove(a))}}function J(e,t){return typeof t=="object"&&t!==null?jn(e,t):Fn(e,t)}function jn(e,t){let r={};return Object.entries(t).forEach(([n,i])=>{r[n]=e.style[n],n.startsWith("--")||(n=Bn(n)),e.style.setProperty(n,i)}),setTimeout(()=>{e.style.length===0&&e.removeAttribute("style")}),()=>{J(e,r)}}function Fn(e,t){let r=e.getAttribute("style",t);return e.setAttribute("style",t),()=>{e.setAttribute("style",r||"")}}function Bn(e){return e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase()}function he(e,t=()=>{}){let r=!1;return function(){r?t.apply(this,arguments):(r=!0,e.apply(this,arguments))}}d("transition",(e,{value:t,modifiers:r,expression:n},{evaluate:i})=>{typeof n=="function"&&(n=i(n)),n!==!1&&(!n||typeof n=="boolean"?Kn(e,r,t):zn(e,n,t))});function zn(e,t,r){Er(e,me,""),{enter:i=>{e._x_transition.enter.during=i},"enter-start":i=>{e._x_transition.enter.start=i},"enter-end":i=>{e._x_transition.enter.end=i},leave:i=>{e._x_transition.leave.during=i},"leave-start":i=>{e._x_transition.leave.start=i},"leave-end":i=>{e._x_transition.leave.end=i}}[r](t)}function Kn(e,t,r){Er(e,J);let n=!t.includes("in")&&!t.includes("out")&&!r,i=n||t.includes("in")||["enter"].includes(r),o=n||t.includes("out")||["leave"].includes(r);t.includes("in")&&!n&&(t=t.filter((g,b)=>b<t.indexOf("out"))),t.includes("out")&&!n&&(t=t.filter((g,b)=>b>t.indexOf("out")));let s=!t.includes("opacity")&&!t.includes("scale"),a=s||t.includes("opacity"),c=s||t.includes("scale"),l=a?0:1,u=c?_e(t,"scale",95)/100:1,p=_e(t,"delay",0)/1e3,m=_e(t,"origin","center"),w="opacity, transform",$=_e(t,"duration",150)/1e3,Ee=_e(t,"duration",75)/1e3,f="cubic-bezier(0.4, 0.0, 0.2, 1)";i&&(e._x_transition.enter.during={transformOrigin:m,transitionDelay:`${p}s`,transitionProperty:w,transitionDuration:`${$}s`,transitionTimingFunction:f},e._x_transition.enter.start={opacity:l,transform:`scale(${u})`},e._x_transition.enter.end={opacity:1,transform:"scale(1)"}),o&&(e._x_transition.leave.during={transformOrigin:m,transitionDelay:`${p}s`,transitionProperty:w,transitionDuration:`${Ee}s`,transitionTimingFunction:f},e._x_transition.leave.start={opacity:1,transform:"scale(1)"},e._x_transition.leave.end={opacity:l,transform:`scale(${u})`})}function Er(e,t,r={}){e._x_transition||(e._x_transition={enter:{during:r,start:r,end:r},leave:{during:r,start:r,end:r},in(n=()=>{},i=()=>{}){Fe(e,t,{during:this.enter.during,start:this.enter.start,end:this.enter.end},n,i)},out(n=()=>{},i=()=>{}){Fe(e,t,{during:this.leave.during,start:this.leave.start,end:this.leave.end},n,i)}})}window.Element.prototype._x_toggleAndCascadeWithTransitions=function(e,t,r,n){let i=document.visibilityState==="visible"?requestAnimationFrame:setTimeout,o=()=>i(r);if(t){e._x_transition&&(e._x_transition.enter||e._x_transition.leave)?e._x_transition.enter&&(Object.entries(e._x_transition.enter.during).length||Object.entries(e._x_transition.enter.start).length||Object.entries(e._x_transition.enter.end).length)?e._x_transition.in(r):o():e._x_transition?e._x_transition.in(r):o();return}e._x_hidePromise=e._x_transition?new Promise((s,a)=>{e._x_transition.out(()=>{},()=>s(n)),e._x_transitioning&&e._x_transitioning.beforeCancel(()=>a({isFromCancelledTransition:!0}))}):Promise.resolve(n),queueMicrotask(()=>{let s=vr(e);s?(s._x_hideChildren||(s._x_hideChildren=[]),s._x_hideChildren.push(e)):i(()=>{let a=c=>{let l=Promise.all([c._x_hidePromise,...(c._x_hideChildren||[]).map(a)]).then(([u])=>u());return delete c._x_hidePromise,delete c._x_hideChildren,l};a(e).catch(c=>{if(!c.isFromCancelledTransition)throw c})})})};function vr(e){let t=e.parentNode;if(t)return t._x_hidePromise?t:vr(t)}function Fe(e,t,{during:r,start:n,end:i}={},o=()=>{},s=()=>{}){if(e._x_transitioning&&e._x_transitioning.cancel(),Object.keys(r).length===0&&Object.keys(n).length===0&&Object.keys(i).length===0){o(),s();return}let a,c,l;Hn(e,{start(){a=t(e,n)},during(){c=t(e,r)},before:o,end(){a(),l=t(e,i)},after:s,cleanup(){c(),l()}})}function Hn(e,t){let r,n,i,o=he(()=>{h(()=>{r=!0,n||t.before(),i||(t.end(),je()),t.after(),e.isConnected&&t.cleanup(),delete e._x_transitioning})});e._x_transitioning={beforeCancels:[],beforeCancel(s){this.beforeCancels.push(s)},cancel:he(function(){for(;this.beforeCancels.length;)this.beforeCancels.shift()();o()}),finish:o},h(()=>{t.start(),t.during()}),br(),requestAnimationFrame(()=>{if(r)return;let s=Number(getComputedStyle(e).transitionDuration.replace(/,.*/,"").replace("s",""))*1e3,a=Number(getComputedStyle(e).transitionDelay.replace(/,.*/,"").replace("s",""))*1e3;s===0&&(s=Number(getComputedStyle(e).animationDuration.replace("s",""))*1e3),h(()=>{t.before()}),n=!0,requestAnimationFrame(()=>{r||(h(()=>{t.end()}),je(),setTimeout(e._x_transitioning.finish,s+a),i=!0)})})}function _e(e,t,r){if(e.indexOf(t)===-1)return r;let n=e[e.indexOf(t)+1];if(!n||t==="scale"&&isNaN(n))return r;if(t==="duration"||t==="delay"){let i=n.match(/([0-9]+)ms/);if(i)return i[1]}return t==="origin"&&["top","right","left","center","bottom"].includes(e[e.indexOf(t)+2])?[n,e[e.indexOf(t)+2]].join(" "):n}var I=!1;function D(e,t=()=>{}){return(...r)=>I?t(...r):e(...r)}function Sr(e){return(...t)=>I&&e(...t)}var Ar=[];function z(e){Ar.push(e)}function Or(e,t){Ar.forEach(r=>r(e,t)),I=!0,Tr(()=>{S(t,(r,n)=>{n(r,()=>{})})}),I=!1}var Be=!1;function Cr(e,t){t._x_dataStack||(t._x_dataStack=e._x_dataStack),I=!0,Be=!0,Tr(()=>{Vn(t)}),I=!1,Be=!1}function Vn(e){let t=!1;S(e,(n,i)=>{O(n,(o,s)=>{if(t&&Qt(o))return s();t=!0,i(o,s)})})}function Tr(e){let t=N;at((r,n)=>{let i=t(r);return L(i),()=>{}}),e(),at(t)}function ge(e,t,r,n=[]){switch(e._x_bindings||(e._x_bindings=T({})),e._x_bindings[t]=r,t=n.includes("camel")?Zn(t):t,t){case"value":qn(e,r);break;case"style":Wn(e,r);break;case"class":Un(e,r);break;case"selected":case"checked":Gn(e,t,r);break;default:Mr(e,t,r);break}}function qn(e,t){if(e.type==="radio")e.attributes.value===void 0&&(e.value=t),window.fromModel&&(typeof t=="boolean"?e.checked=xe(e.value)===t:e.checked=Rr(e.value,t));else if(e.type==="checkbox")Number.isInteger(t)?e.value=t:!Array.isArray(t)&&typeof t!="boolean"&&![null,void 0].includes(t)?e.value=String(t):Array.isArray(t)?e.checked=t.some(r=>Rr(r,e.value)):e.checked=!!t;else if(e.tagName==="SELECT")Xn(e,t);else{if(e.value===t)return;e.value=t===void 0?"":t}}function Un(e,t){e._x_undoAddedClasses&&e._x_undoAddedClasses(),e._x_undoAddedClasses=me(e,t)}function Wn(e,t){e._x_undoAddedStyles&&e._x_undoAddedStyles(),e._x_undoAddedStyles=J(e,t)}function Gn(e,t,r){Mr(e,t,r),Yn(e,t,r)}function Mr(e,t,r){[null,void 0,!1].includes(r)&&Qn(t)?e.removeAttribute(t):(Nr(t)&&(r=t),Jn(e,t,r))}function Jn(e,t,r){e.getAttribute(t)!=r&&e.setAttribute(t,r)}function Yn(e,t,r){e[t]!==r&&(e[t]=r)}function Xn(e,t){let r=[].concat(t).map(n=>n+"");Array.from(e.options).forEach(n=>{n.selected=r.includes(n.value)})}function Zn(e){return e.toLowerCase().replace(/-(\w)/g,(t,r)=>r.toUpperCase())}function Rr(e,t){return e==t}function xe(e){return[1,"1","true","on","yes",!0].includes(e)?!0:[0,"0","false","off","no",!1].includes(e)?!1:e?Boolean(e):null}function Nr(e){return["disabled","checked","required","readonly","hidden","open","selected","autofocus","itemscope","multiple","novalidate","allowfullscreen","allowpaymentrequest","formnovalidate","autoplay","controls","loop","muted","playsinline","default","ismap","reversed","async","defer","nomodule"].includes(e)}function Qn(e){return!["aria-pressed","aria-checked","aria-expanded","aria-selected"].includes(e)}function Pr(e,t,r){return e._x_bindings&&e._x_bindings[t]!==void 0?e._x_bindings[t]:Dr(e,t,r)}function Ir(e,t,r,n=!0){if(e._x_bindings&&e._x_bindings[t]!==void 0)return e._x_bindings[t];if(e._x_inlineBindings&&e._x_inlineBindings[t]!==void 0){let i=e._x_inlineBindings[t];return i.extract=n,ke(()=>R(e,i.expression))}return Dr(e,t,r)}function Dr(e,t,r){let n=e.getAttribute(t);return n===null?typeof r=="function"?r():r:n===""?!0:Nr(t)?!![t,"true"].includes(n):n}function ze(e,t){var r;return function(){var n=this,i=arguments,o=function(){r=null,e.apply(n,i)};clearTimeout(r),r=setTimeout(o,t)}}function Ke(e,t){let r;return function(){let n=this,i=arguments;r||(e.apply(n,i),r=!0,setTimeout(()=>r=!1,t))}}function He({get:e,set:t},{get:r,set:n}){let i=!0,o,s,a=N(()=>{let c=e(),l=r();if(i)n(Ot(c)),i=!1;else{let u=JSON.stringify(c),p=JSON.stringify(l);u!==o?n(Ot(c)):u!==p&&t(Ot(l))}o=JSON.stringify(e()),s=JSON.stringify(r())});return()=>{L(a)}}function Ot(e){return typeof e=="object"?JSON.parse(JSON.stringify(e)):e}function kr(e){(Array.isArray(e)?e:[e]).forEach(r=>r(B))}var Y={},Lr=!1;function $r(e,t){if(Lr||(Y=T(Y),Lr=!0),t===void 0)return Y[e];Y[e]=t,typeof t=="object"&&t!==null&&t.hasOwnProperty("init")&&typeof t.init=="function"&&Y[e].init(),Ne(Y[e])}function jr(){return Y}var Fr={};function Br(e,t){let r=typeof t!="function"?()=>t:t;return e instanceof Element?Ct(e,r()):(Fr[e]=r,()=>{})}function zr(e){return Object.entries(Fr).forEach(([t,r])=>{Object.defineProperty(e,t,{get(){return(...n)=>r(...n)}})}),e}function Ct(e,t,r){let n=[];for(;n.length;)n.pop()();let i=Object.entries(t).map(([s,a])=>({name:s,value:a})),o=vt(i);return i=i.map(s=>o.find(a=>a.name===s.name)?{name:`x-bind:${s.name}`,value:`"${s.value}"`}:s),ue(e,i,r).map(s=>{n.push(s.runCleanups),s()}),()=>{for(;n.length;)n.pop()()}}var Kr={};function Hr(e,t){Kr[e]=t}function Vr(e,t){return Object.entries(Kr).forEach(([r,n])=>{Object.defineProperty(e,r,{get(){return(...i)=>n.bind(t)(...i)},enumerable:!1})}),e}var ei={get reactive(){return T},get release(){return L},get effect(){return N},get raw(){return st},version:"3.13.5",flushAndStopDeferringMutations:lr,dontAutoEvaluateFunctions:ke,disableEffectScheduling:qt,startObservingMutations:le,stopObservingMutations:dt,setReactivityEngine:Ut,onAttributeRemoved:Re,onAttributesAdded:Ce,closestDataStack:j,skipDuringClone:D,onlyDuringClone:Sr,addRootSelector:Ae,addInitSelector:Oe,interceptClone:z,addScopeToNode:P,deferMutations:cr,mapAttributes:re,evaluateLater:x,interceptInit:tr,setEvaluator:pr,mergeProxies:F,extractProp:Ir,findClosest:Q,onElRemoved:ee,closestRoot:W,destroyTree:ce,interceptor:Pe,transition:Fe,setStyles:J,mutateDom:h,directive:d,entangle:He,throttle:Ke,debounce:ze,evaluate:R,initTree:S,nextTick:ne,prefixed:C,prefix:mr,plugin:kr,magic:y,store:$r,start:Jt,clone:Cr,cloneNode:Or,bound:Pr,$data:Me,watch:Se,walk:O,data:Hr,bind:Br},B=ei;function Tt(e,t){let r=Object.create(null),n=e.split(",");for(let i=0;i<n.length;i++)r[n[i]]=!0;return t?i=>!!r[i.toLowerCase()]:i=>!!r[i]}var ti="itemscope,allowfullscreen,formnovalidate,ismap,nomodule,novalidate,readonly";var Ps=Tt(ti+",async,autofocus,autoplay,controls,default,defer,disabled,hidden,loop,open,required,reversed,scoped,seamless,checked,muted,multiple,selected");var qr=Object.freeze({}),Is=Object.freeze([]);var ri=Object.prototype.hasOwnProperty,ye=(e,t)=>ri.call(e,t),K=Array.isArray,ie=e=>Ur(e)==="[object Map]";var ni=e=>typeof e=="string",Ve=e=>typeof e=="symbol",be=e=>e!==null&&typeof e=="object";var ii=Object.prototype.toString,Ur=e=>ii.call(e),Rt=e=>Ur(e).slice(8,-1);var qe=e=>ni(e)&&e!=="NaN"&&e[0]!=="-"&&""+parseInt(e,10)===e;var Ue=e=>{let t=Object.create(null);return r=>t[r]||(t[r]=e(r))},oi=/-(\w)/g,Ds=Ue(e=>e.replace(oi,(t,r)=>r?r.toUpperCase():"")),si=/\B([A-Z])/g,ks=Ue(e=>e.replace(si,"-$1").toLowerCase()),Mt=Ue(e=>e.charAt(0).toUpperCase()+e.slice(1)),Ls=Ue(e=>e?`on${Mt(e)}`:""),Nt=(e,t)=>e!==t&&(e===e||t===t);var Pt=new WeakMap,we=[],k,X=Symbol("iterate"),It=Symbol("Map key iterate");function ai(e){return e&&e._isEffect===!0}function Zr(e,t=qr){ai(e)&&(e=e.raw);let r=li(e,t);return t.lazy||r(),r}function Qr(e){e.active&&(en(e),e.options.onStop&&e.options.onStop(),e.active=!1)}var ci=0;function li(e,t){let r=function(){if(!r.active)return e();if(!we.includes(r)){en(r);try{return fi(),we.push(r),k=r,e()}finally{we.pop(),tn(),k=we[we.length-1]}}};return r.id=ci++,r.allowRecurse=!!t.allowRecurse,r._isEffect=!0,r.active=!0,r.raw=e,r.deps=[],r.options=t,r}function en(e){let{deps:t}=e;if(t.length){for(let r=0;r<t.length;r++)t[r].delete(e);t.length=0}}var oe=!0,kt=[];function ui(){kt.push(oe),oe=!1}function fi(){kt.push(oe),oe=!0}function tn(){let e=kt.pop();oe=e===void 0?!0:e}function M(e,t,r){if(!oe||k===void 0)return;let n=Pt.get(e);n||Pt.set(e,n=new Map);let i=n.get(r);i||n.set(r,i=new Set),i.has(k)||(i.add(k),k.deps.push(i),k.options.onTrack&&k.options.onTrack({effect:k,target:e,type:t,key:r}))}function V(e,t,r,n,i,o){let s=Pt.get(e);if(!s)return;let a=new Set,c=u=>{u&&u.forEach(p=>{(p!==k||p.allowRecurse)&&a.add(p)})};if(t==="clear")s.forEach(c);else if(r==="length"&&K(e))s.forEach((u,p)=>{(p==="length"||p>=n)&&c(u)});else switch(r!==void 0&&c(s.get(r)),t){case"add":K(e)?qe(r)&&c(s.get("length")):(c(s.get(X)),ie(e)&&c(s.get(It)));break;case"delete":K(e)||(c(s.get(X)),ie(e)&&c(s.get(It)));break;case"set":ie(e)&&c(s.get(X));break}let l=u=>{u.options.onTrigger&&u.options.onTrigger({effect:u,target:e,key:r,type:t,newValue:n,oldValue:i,oldTarget:o}),u.options.scheduler?u.options.scheduler(u):u()};a.forEach(l)}var di=Tt("__proto__,__v_isRef,__isVue"),rn=new Set(Object.getOwnPropertyNames(Symbol).map(e=>Symbol[e]).filter(Ve)),pi=nn();var mi=nn(!0);var Wr=hi();function hi(){let e={};return["includes","indexOf","lastIndexOf"].forEach(t=>{e[t]=function(...r){let n=_(this);for(let o=0,s=this.length;o<s;o++)M(n,"get",o+"");let i=n[t](...r);return i===-1||i===!1?n[t](...r.map(_)):i}}),["push","pop","shift","unshift","splice"].forEach(t=>{e[t]=function(...r){ui();let n=_(this)[t].apply(this,r);return tn(),n}}),e}function nn(e=!1,t=!1){return function(n,i,o){if(i==="__v_isReactive")return!e;if(i==="__v_isReadonly")return e;if(i==="__v_raw"&&o===(e?t?Ni:cn:t?Mi:an).get(n))return n;let s=K(n);if(!e&&s&&ye(Wr,i))return Reflect.get(Wr,i,o);let a=Reflect.get(n,i,o);return(Ve(i)?rn.has(i):di(i))||(e||M(n,"get",i),t)?a:Dt(a)?!s||!qe(i)?a.value:a:be(a)?e?ln(a):Qe(a):a}}var _i=gi();function gi(e=!1){return function(r,n,i,o){let s=r[n];if(!e&&(i=_(i),s=_(s),!K(r)&&Dt(s)&&!Dt(i)))return s.value=i,!0;let a=K(r)&&qe(n)?Number(n)<r.length:ye(r,n),c=Reflect.set(r,n,i,o);return r===_(o)&&(a?Nt(i,s)&&V(r,"set",n,i,s):V(r,"add",n,i)),c}}function xi(e,t){let r=ye(e,t),n=e[t],i=Reflect.deleteProperty(e,t);return i&&r&&V(e,"delete",t,void 0,n),i}function yi(e,t){let r=Reflect.has(e,t);return(!Ve(t)||!rn.has(t))&&M(e,"has",t),r}function bi(e){return M(e,"iterate",K(e)?"length":X),Reflect.ownKeys(e)}var wi={get:pi,set:_i,deleteProperty:xi,has:yi,ownKeys:bi},Ei={get:mi,set(e,t){return console.warn(`Set operation on key "${String(t)}" failed: target is readonly.`,e),!0},deleteProperty(e,t){return console.warn(`Delete operation on key "${String(t)}" failed: target is readonly.`,e),!0}};var Lt=e=>be(e)?Qe(e):e,$t=e=>be(e)?ln(e):e,jt=e=>e,Ze=e=>Reflect.getPrototypeOf(e);function We(e,t,r=!1,n=!1){e=e.__v_raw;let i=_(e),o=_(t);t!==o&&!r&&M(i,"get",t),!r&&M(i,"get",o);let{has:s}=Ze(i),a=n?jt:r?$t:Lt;if(s.call(i,t))return a(e.get(t));if(s.call(i,o))return a(e.get(o));e!==i&&e.get(t)}function Ge(e,t=!1){let r=this.__v_raw,n=_(r),i=_(e);return e!==i&&!t&&M(n,"has",e),!t&&M(n,"has",i),e===i?r.has(e):r.has(e)||r.has(i)}function Je(e,t=!1){return e=e.__v_raw,!t&&M(_(e),"iterate",X),Reflect.get(e,"size",e)}function Gr(e){e=_(e);let t=_(this);return Ze(t).has.call(t,e)||(t.add(e),V(t,"add",e,e)),this}function Jr(e,t){t=_(t);let r=_(this),{has:n,get:i}=Ze(r),o=n.call(r,e);o?sn(r,n,e):(e=_(e),o=n.call(r,e));let s=i.call(r,e);return r.set(e,t),o?Nt(t,s)&&V(r,"set",e,t,s):V(r,"add",e,t),this}function Yr(e){let t=_(this),{has:r,get:n}=Ze(t),i=r.call(t,e);i?sn(t,r,e):(e=_(e),i=r.call(t,e));let o=n?n.call(t,e):void 0,s=t.delete(e);return i&&V(t,"delete",e,void 0,o),s}function Xr(){let e=_(this),t=e.size!==0,r=ie(e)?new Map(e):new Set(e),n=e.clear();return t&&V(e,"clear",void 0,void 0,r),n}function Ye(e,t){return function(n,i){let o=this,s=o.__v_raw,a=_(s),c=t?jt:e?$t:Lt;return!e&&M(a,"iterate",X),s.forEach((l,u)=>n.call(i,c(l),c(u),o))}}function Xe(e,t,r){return function(...n){let i=this.__v_raw,o=_(i),s=ie(o),a=e==="entries"||e===Symbol.iterator&&s,c=e==="keys"&&s,l=i[e](...n),u=r?jt:t?$t:Lt;return!t&&M(o,"iterate",c?It:X),{next(){let{value:p,done:m}=l.next();return m?{value:p,done:m}:{value:a?[u(p[0]),u(p[1])]:u(p),done:m}},[Symbol.iterator](){return this}}}}function H(e){return function(...t){{let r=t[0]?`on key "${t[0]}" `:"";console.warn(`${Mt(e)} operation ${r}failed: target is readonly.`,_(this))}return e==="delete"?!1:this}}function vi(){let e={get(o){return We(this,o)},get size(){return Je(this)},has:Ge,add:Gr,set:Jr,delete:Yr,clear:Xr,forEach:Ye(!1,!1)},t={get(o){return We(this,o,!1,!0)},get size(){return Je(this)},has:Ge,add:Gr,set:Jr,delete:Yr,clear:Xr,forEach:Ye(!1,!0)},r={get(o){return We(this,o,!0)},get size(){return Je(this,!0)},has(o){return Ge.call(this,o,!0)},add:H("add"),set:H("set"),delete:H("delete"),clear:H("clear"),forEach:Ye(!0,!1)},n={get(o){return We(this,o,!0,!0)},get size(){return Je(this,!0)},has(o){return Ge.call(this,o,!0)},add:H("add"),set:H("set"),delete:H("delete"),clear:H("clear"),forEach:Ye(!0,!0)};return["keys","values","entries",Symbol.iterator].forEach(o=>{e[o]=Xe(o,!1,!1),r[o]=Xe(o,!0,!1),t[o]=Xe(o,!1,!0),n[o]=Xe(o,!0,!0)}),[e,r,t,n]}var[Si,Ai,Oi,Ci]=vi();function on(e,t){let r=t?e?Ci:Oi:e?Ai:Si;return(n,i,o)=>i==="__v_isReactive"?!e:i==="__v_isReadonly"?e:i==="__v_raw"?n:Reflect.get(ye(r,i)&&i in n?r:n,i,o)}var Ti={get:on(!1,!1)};var Ri={get:on(!0,!1)};function sn(e,t,r){let n=_(r);if(n!==r&&t.call(e,n)){let i=Rt(e);console.warn(`Reactive ${i} contains both the raw and reactive versions of the same object${i==="Map"?" as keys":""}, which can lead to inconsistencies. Avoid differentiating between the raw and reactive versions of an object and only use the reactive version if possible.`)}}var an=new WeakMap,Mi=new WeakMap,cn=new WeakMap,Ni=new WeakMap;function Pi(e){switch(e){case"Object":case"Array":return 1;case"Map":case"Set":case"WeakMap":case"WeakSet":return 2;default:return 0}}function Ii(e){return e.__v_skip||!Object.isExtensible(e)?0:Pi(Rt(e))}function Qe(e){return e&&e.__v_isReadonly?e:un(e,!1,wi,Ti,an)}function ln(e){return un(e,!0,Ei,Ri,cn)}function un(e,t,r,n,i){if(!be(e))return console.warn(`value cannot be made reactive: ${String(e)}`),e;if(e.__v_raw&&!(t&&e.__v_isReactive))return e;let o=i.get(e);if(o)return o;let s=Ii(e);if(s===0)return e;let a=new Proxy(e,s===2?n:r);return i.set(e,a),a}function _(e){return e&&_(e.__v_raw)||e}function Dt(e){return Boolean(e&&e.__v_isRef===!0)}y("nextTick",()=>ne);y("dispatch",e=>U.bind(U,e));y("watch",(e,{evaluateLater:t,cleanup:r})=>(n,i)=>{let o=t(n),a=Se(()=>{let c;return o(l=>c=l),c},i);r(a)});y("store",jr);y("data",e=>Me(e));y("root",e=>W(e));y("refs",e=>(e._x_refs_proxy||(e._x_refs_proxy=F(Di(e))),e._x_refs_proxy));function Di(e){let t=[],r=e;for(;r;)r._x_refs&&t.push(r._x_refs),r=r.parentNode;return t}var Ft={};function Bt(e){return Ft[e]||(Ft[e]=0),++Ft[e]}function fn(e,t){return Q(e,r=>{if(r._x_ids&&r._x_ids[t])return!0})}function dn(e,t){e._x_ids||(e._x_ids={}),e._x_ids[t]||(e._x_ids[t]=Bt(t))}y("id",(e,{cleanup:t})=>(r,n=null)=>{let i=`${r}${n?`-${n}`:""}`;return ki(e,i,t,()=>{let o=fn(e,r),s=o?o._x_ids[r]:Bt(r);return n?`${r}-${s}-${n}`:`${r}-${s}`})});z((e,t)=>{e._x_id&&(t._x_id=e._x_id)});function ki(e,t,r,n){if(e._x_id||(e._x_id={}),e._x_id[t])return e._x_id[t];let i=n();return e._x_id[t]=i,r(()=>{delete e._x_id[t]}),i}y("el",e=>e);pn("Focus","focus","focus");pn("Persist","persist","persist");function pn(e,t,r){y(t,n=>v(`You can't use [$${t}] without first installing the "${e}" plugin here: https://alpinejs.dev/plugins/${r}`,n))}d("modelable",(e,{expression:t},{effect:r,evaluateLater:n,cleanup:i})=>{let o=n(t),s=()=>{let u;return o(p=>u=p),u},a=n(`${t} = __placeholder`),c=u=>a(()=>{},{scope:{__placeholder:u}}),l=s();c(l),queueMicrotask(()=>{if(!e._x_model)return;e._x_removeModelListeners.default();let u=e._x_model.get,p=e._x_model.set,m=He({get(){return u()},set(w){p(w)}},{get(){return s()},set(w){c(w)}});i(m)})});d("teleport",(e,{modifiers:t,expression:r},{cleanup:n})=>{e.tagName.toLowerCase()!=="template"&&v("x-teleport can only be used on a <template> tag",e);let i=mn(r),o=e.content.cloneNode(!0).firstElementChild;e._x_teleport=o,o._x_teleportBack=e,e.setAttribute("data-teleport-template",!0),o.setAttribute("data-teleport-target",!0),e._x_forwardEvents&&e._x_forwardEvents.forEach(a=>{o.addEventListener(a,c=>{c.stopPropagation(),e.dispatchEvent(new c.constructor(c.type,c))})}),P(o,{},e);let s=(a,c,l)=>{l.includes("prepend")?c.parentNode.insertBefore(a,c):l.includes("append")?c.parentNode.insertBefore(a,c.nextSibling):c.appendChild(a)};h(()=>{s(o,i,t),S(o),o._x_ignore=!0}),e._x_teleportPutBack=()=>{let a=mn(r);h(()=>{s(e._x_teleport,a,t)})},n(()=>o.remove())});var Li=document.createElement("div");function mn(e){let t=D(()=>document.querySelector(e),()=>Li)();return t||v(`Cannot find x-teleport element for selector: "${e}"`),t}var hn=()=>{};hn.inline=(e,{modifiers:t},{cleanup:r})=>{t.includes("self")?e._x_ignoreSelf=!0:e._x_ignore=!0,r(()=>{t.includes("self")?delete e._x_ignoreSelf:delete e._x_ignore})};d("ignore",hn);d("effect",D((e,{expression:t},{effect:r})=>{r(x(e,t))}));function se(e,t,r,n){let i=e,o=c=>n(c),s={},a=(c,l)=>u=>l(c,u);if(r.includes("dot")&&(t=$i(t)),r.includes("camel")&&(t=ji(t)),r.includes("passive")&&(s.passive=!0),r.includes("capture")&&(s.capture=!0),r.includes("window")&&(i=window),r.includes("document")&&(i=document),r.includes("debounce")){let c=r[r.indexOf("debounce")+1]||"invalid-wait",l=et(c.split("ms")[0])?Number(c.split("ms")[0]):250;o=ze(o,l)}if(r.includes("throttle")){let c=r[r.indexOf("throttle")+1]||"invalid-wait",l=et(c.split("ms")[0])?Number(c.split("ms")[0]):250;o=Ke(o,l)}return r.includes("prevent")&&(o=a(o,(c,l)=>{l.preventDefault(),c(l)})),r.includes("stop")&&(o=a(o,(c,l)=>{l.stopPropagation(),c(l)})),r.includes("self")&&(o=a(o,(c,l)=>{l.target===e&&c(l)})),(r.includes("away")||r.includes("outside"))&&(i=document,o=a(o,(c,l)=>{e.contains(l.target)||l.target.isConnected!==!1&&(e.offsetWidth<1&&e.offsetHeight<1||e._x_isShown!==!1&&c(l))})),r.includes("once")&&(o=a(o,(c,l)=>{c(l),i.removeEventListener(t,o,s)})),o=a(o,(c,l)=>{Bi(t)&&zi(l,r)||c(l)}),i.addEventListener(t,o,s),()=>{i.removeEventListener(t,o,s)}}function $i(e){return e.replace(/-/g,".")}function ji(e){return e.toLowerCase().replace(/-(\w)/g,(t,r)=>r.toUpperCase())}function et(e){return!Array.isArray(e)&&!isNaN(e)}function Fi(e){return[" ","_"].includes(e)?e:e.replace(/([a-z])([A-Z])/g,"$1-$2").replace(/[_\s]/,"-").toLowerCase()}function Bi(e){return["keydown","keyup"].includes(e)}function zi(e,t){let r=t.filter(o=>!["window","document","prevent","stop","once","capture"].includes(o));if(r.includes("debounce")){let o=r.indexOf("debounce");r.splice(o,et((r[o+1]||"invalid-wait").split("ms")[0])?2:1)}if(r.includes("throttle")){let o=r.indexOf("throttle");r.splice(o,et((r[o+1]||"invalid-wait").split("ms")[0])?2:1)}if(r.length===0||r.length===1&&_n(e.key).includes(r[0]))return!1;let i=["ctrl","shift","alt","meta","cmd","super"].filter(o=>r.includes(o));return r=r.filter(o=>!i.includes(o)),!(i.length>0&&i.filter(s=>((s==="cmd"||s==="super")&&(s="meta"),e[`${s}Key`])).length===i.length&&_n(e.key).includes(r[0]))}function _n(e){if(!e)return[];e=Fi(e);let t={ctrl:"control",slash:"/",space:" ",spacebar:" ",cmd:"meta",esc:"escape",up:"arrow-up",down:"arrow-down",left:"arrow-left",right:"arrow-right",period:".",equal:"=",minus:"-",underscore:"_"};return t[e]=e,Object.keys(t).map(r=>{if(t[r]===e)return r}).filter(r=>r)}d("model",(e,{modifiers:t,expression:r},{effect:n,cleanup:i})=>{let o=e;t.includes("parent")&&(o=e.parentNode);let s=x(o,r),a;typeof r=="string"?a=x(o,`${r} = __placeholder`):typeof r=="function"&&typeof r()=="string"?a=x(o,`${r()} = __placeholder`):a=()=>{};let c=()=>{let m;return s(w=>m=w),gn(m)?m.get():m},l=m=>{let w;s($=>w=$),gn(w)?w.set(m):a(()=>{},{scope:{__placeholder:m}})};typeof r=="string"&&e.type==="radio"&&h(()=>{e.hasAttribute("name")||e.setAttribute("name",r)});var u=e.tagName.toLowerCase()==="select"||["checkbox","radio"].includes(e.type)||t.includes("lazy")?"change":"input";let p=I?()=>{}:se(e,u,t,m=>{l(Ki(e,t,m,c()))});if(t.includes("fill")&&([void 0,null,""].includes(c())||e.type==="checkbox"&&Array.isArray(c()))&&e.dispatchEvent(new Event(u,{})),e._x_removeModelListeners||(e._x_removeModelListeners={}),e._x_removeModelListeners.default=p,i(()=>e._x_removeModelListeners.default()),e.form){let m=se(e.form,"reset",[],w=>{ne(()=>e._x_model&&e._x_model.set(e.value))});i(()=>m())}e._x_model={get(){return c()},set(m){l(m)}},e._x_forceModelUpdate=m=>{m===void 0&&typeof r=="string"&&r.match(/\./)&&(m=""),window.fromModel=!0,h(()=>ge(e,"value",m)),delete window.fromModel},n(()=>{let m=c();t.includes("unintrusive")&&document.activeElement.isSameNode(e)||e._x_forceModelUpdate(m)})});function Ki(e,t,r,n){return h(()=>{if(r instanceof CustomEvent&&r.detail!==void 0)return r.detail!==null&&r.detail!==void 0?r.detail:r.target.value;if(e.type==="checkbox")if(Array.isArray(n)){let i=null;return t.includes("number")?i=zt(r.target.value):t.includes("boolean")?i=xe(r.target.value):i=r.target.value,r.target.checked?n.concat([i]):n.filter(o=>!Hi(o,i))}else return r.target.checked;else return e.tagName.toLowerCase()==="select"&&e.multiple?t.includes("number")?Array.from(r.target.selectedOptions).map(i=>{let o=i.value||i.text;return zt(o)}):t.includes("boolean")?Array.from(r.target.selectedOptions).map(i=>{let o=i.value||i.text;return xe(o)}):Array.from(r.target.selectedOptions).map(i=>i.value||i.text):t.includes("number")?zt(r.target.value):t.includes("boolean")?xe(r.target.value):t.includes("trim")?r.target.value.trim():r.target.value})}function zt(e){let t=e?parseFloat(e):null;return Vi(t)?t:e}function Hi(e,t){return e==t}function Vi(e){return!Array.isArray(e)&&!isNaN(e)}function gn(e){return e!==null&&typeof e=="object"&&typeof e.get=="function"&&typeof e.set=="function"}d("cloak",e=>queueMicrotask(()=>h(()=>e.removeAttribute(C("cloak")))));Oe(()=>`[${C("init")}]`);d("init",D((e,{expression:t},{evaluate:r})=>typeof t=="string"?!!t.trim()&&r(t,{},!1):r(t,{},!1)));d("text",(e,{expression:t},{effect:r,evaluateLater:n})=>{let i=n(t);r(()=>{i(o=>{h(()=>{e.textContent=o})})})});d("html",(e,{expression:t},{effect:r,evaluateLater:n})=>{let i=n(t);r(()=>{i(o=>{h(()=>{e.innerHTML=o,e._x_ignoreSelf=!0,S(e),delete e._x_ignoreSelf})})})});re(Le(":",$e(C("bind:"))));var xn=(e,{value:t,modifiers:r,expression:n,original:i},{effect:o})=>{if(!t){let a={};zr(a),x(e,n)(l=>{Ct(e,l,i)},{scope:a});return}if(t==="key")return qi(e,n);if(e._x_inlineBindings&&e._x_inlineBindings[t]&&e._x_inlineBindings[t].extract)return;let s=x(e,n);o(()=>s(a=>{a===void 0&&typeof n=="string"&&n.match(/\./)&&(a=""),h(()=>ge(e,t,a,r))}))};xn.inline=(e,{value:t,modifiers:r,expression:n})=>{t&&(e._x_inlineBindings||(e._x_inlineBindings={}),e._x_inlineBindings[t]={expression:n,extract:!1})};d("bind",xn);function qi(e,t){e._x_keyExpression=t}Ae(()=>`[${C("data")}]`);d("data",(e,{expression:t},{cleanup:r})=>{if(Ui(e))return;t=t===""?"{}":t;let n={};de(n,e);let i={};Vr(i,n);let o=R(e,t,{scope:i});(o===void 0||o===!0)&&(o={}),de(o,e);let s=T(o);Ne(s);let a=P(e,s);s.init&&R(e,s.init),r(()=>{s.destroy&&R(e,s.destroy),a()})});z((e,t)=>{e._x_dataStack&&(t._x_dataStack=e._x_dataStack,t.setAttribute("data-has-alpine-state",!0))});function Ui(e){return I?Be?!0:e.hasAttribute("data-has-alpine-state"):!1}d("show",(e,{modifiers:t,expression:r},{effect:n})=>{let i=x(e,r);e._x_doHide||(e._x_doHide=()=>{h(()=>{e.style.setProperty("display","none",t.includes("important")?"important":void 0)})}),e._x_doShow||(e._x_doShow=()=>{h(()=>{e.style.length===1&&e.style.display==="none"?e.removeAttribute("style"):e.style.removeProperty("display")})});let o=()=>{e._x_doHide(),e._x_isShown=!1},s=()=>{e._x_doShow(),e._x_isShown=!0},a=()=>setTimeout(s),c=he(p=>p?s():o(),p=>{typeof e._x_toggleAndCascadeWithTransitions=="function"?e._x_toggleAndCascadeWithTransitions(e,p,s,o):p?a():o()}),l,u=!0;n(()=>i(p=>{!u&&p===l||(t.includes("immediate")&&(p?a():o()),c(p),l=p,u=!1)}))});d("for",(e,{expression:t},{effect:r,cleanup:n})=>{let i=Gi(t),o=x(e,i.items),s=x(e,e._x_keyExpression||"index");e._x_prevKeys=[],e._x_lookup={},r(()=>Wi(e,i,o,s)),n(()=>{Object.values(e._x_lookup).forEach(a=>a.remove()),delete e._x_prevKeys,delete e._x_lookup})});function Wi(e,t,r,n){let i=s=>typeof s=="object"&&!Array.isArray(s),o=e;r(s=>{Ji(s)&&s>=0&&(s=Array.from(Array(s).keys(),f=>f+1)),s===void 0&&(s=[]);let a=e._x_lookup,c=e._x_prevKeys,l=[],u=[];if(i(s))s=Object.entries(s).map(([f,g])=>{let b=yn(t,g,f,s);n(E=>u.push(E),{scope:{index:f,...b}}),l.push(b)});else for(let f=0;f<s.length;f++){let g=yn(t,s[f],f,s);n(b=>u.push(b),{scope:{index:f,...g}}),l.push(g)}let p=[],m=[],w=[],$=[];for(let f=0;f<c.length;f++){let g=c[f];u.indexOf(g)===-1&&w.push(g)}c=c.filter(f=>!w.includes(f));let Ee="template";for(let f=0;f<u.length;f++){let g=u[f],b=c.indexOf(g);if(b===-1)c.splice(f,0,g),p.push([Ee,f]);else if(b!==f){let E=c.splice(f,1)[0],A=c.splice(b-1,1)[0];c.splice(f,0,A),c.splice(b,0,E),m.push([E,A])}else $.push(g);Ee=g}for(let f=0;f<w.length;f++){let g=w[f];a[g]._x_effects&&a[g]._x_effects.forEach(ve),a[g].remove(),a[g]=null,delete a[g]}for(let f=0;f<m.length;f++){let[g,b]=m[f],E=a[g],A=a[b],Z=document.createElement("div");h(()=>{A||v('x-for ":key" is undefined or invalid',o),A.after(Z),E.after(A),A._x_currentIfEl&&A.after(A._x_currentIfEl),Z.before(E),E._x_currentIfEl&&E.after(E._x_currentIfEl),Z.remove()}),A._x_refreshXForScope(l[u.indexOf(b)])}for(let f=0;f<p.length;f++){let[g,b]=p[f],E=g==="template"?o:a[g];E._x_currentIfEl&&(E=E._x_currentIfEl);let A=l[b],Z=u[b],ae=document.importNode(o.content,!0).firstElementChild,Ht=T(A);P(ae,Ht,o),ae._x_refreshXForScope=wn=>{Object.entries(wn).forEach(([En,vn])=>{Ht[En]=vn})},h(()=>{E.after(ae),S(ae)}),typeof Z=="object"&&v("x-for key cannot be an object, it must be a string or an integer",o),a[Z]=ae}for(let f=0;f<$.length;f++)a[$[f]]._x_refreshXForScope(l[u.indexOf($[f])]);o._x_prevKeys=u})}function Gi(e){let t=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,r=/^\s*\(|\)\s*$/g,n=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/,i=e.match(n);if(!i)return;let o={};o.items=i[2].trim();let s=i[1].replace(r,"").trim(),a=s.match(t);return a?(o.item=s.replace(t,"").trim(),o.index=a[1].trim(),a[2]&&(o.collection=a[2].trim())):o.item=s,o}function yn(e,t,r,n){let i={};return/^\[.*\]$/.test(e.item)&&Array.isArray(t)?e.item.replace("[","").replace("]","").split(",").map(s=>s.trim()).forEach((s,a)=>{i[s]=t[a]}):/^\{.*\}$/.test(e.item)&&!Array.isArray(t)&&typeof t=="object"?e.item.replace("{","").replace("}","").split(",").map(s=>s.trim()).forEach(s=>{i[s]=t[s]}):i[e.item]=t,e.index&&(i[e.index]=r),e.collection&&(i[e.collection]=n),i}function Ji(e){return!Array.isArray(e)&&!isNaN(e)}function bn(){}bn.inline=(e,{expression:t},{cleanup:r})=>{let n=W(e);n._x_refs||(n._x_refs={}),n._x_refs[t]=e,r(()=>delete n._x_refs[t])};d("ref",bn);d("if",(e,{expression:t},{effect:r,cleanup:n})=>{e.tagName.toLowerCase()!=="template"&&v("x-if can only be used on a <template> tag",e);let i=x(e,t),o=()=>{if(e._x_currentIfEl)return e._x_currentIfEl;let a=e.content.cloneNode(!0).firstElementChild;return P(a,{},e),h(()=>{e.after(a),S(a)}),e._x_currentIfEl=a,e._x_undoIf=()=>{O(a,c=>{c._x_effects&&c._x_effects.forEach(ve)}),a.remove(),delete e._x_currentIfEl},a},s=()=>{e._x_undoIf&&(e._x_undoIf(),delete e._x_undoIf)};r(()=>i(a=>{a?o():s()})),n(()=>e._x_undoIf&&e._x_undoIf())});d("id",(e,{expression:t},{evaluate:r})=>{r(t).forEach(i=>dn(e,i))});z((e,t)=>{e._x_ids&&(t._x_ids=e._x_ids)});re(Le("@",$e(C("on:"))));d("on",D((e,{value:t,modifiers:r,expression:n},{cleanup:i})=>{let o=n?x(e,n):()=>{};e.tagName.toLowerCase()==="template"&&(e._x_forwardEvents||(e._x_forwardEvents=[]),e._x_forwardEvents.includes(t)||e._x_forwardEvents.push(t));let s=se(e,t,r,a=>{o(()=>{},{scope:{$event:a},params:[a]})});i(()=>s())}));tt("Collapse","collapse","collapse");tt("Intersect","intersect","intersect");tt("Focus","trap","focus");tt("Mask","mask","mask");function tt(e,t,r){d(t,n=>v(`You can't use [x-${t}] without first installing the "${e}" plugin here: https://alpinejs.dev/plugins/${r}`,n))}B.setEvaluator(xt);B.setReactivityEngine({reactive:Qe,effect:Zr,release:Qr,raw:_});var Kt=B;window.Alpine=Kt;queueMicrotask(()=>{Kt.start()});})(); |
M
tailwind.config.js
+19
-11
@@ -1,12 +1,16 @@
| 1 | 1 | /** @type {import('tailwindcss').Config} */ |
| 2 | 2 | module.exports = { |
| 3 | - content: ["./templates/**/*.html"], | |
| 3 | + content: [ | |
| 4 | + "./crates/rustwarden-ui/templates/**/*.html", | |
| 5 | + "./crates/rustwarden-fediverse-ui/templates/**/*.html", | |
| 6 | + "./crates/rustwarden-fediverse-guard/templates/**/*.html", | |
| 7 | + ], | |
| 4 | 8 | theme: { |
| 5 | 9 | extend: { |
| 6 | 10 | colors: { |
| 7 | 11 | 'rustwarden': { |
| 8 | - 'blue': '#3b82f6', | |
| 9 | - 'rust': '#f97316', | |
| 12 | + 'green': '#00ed64', | |
| 13 | + 'teal': '#001e2b', | |
| 10 | 14 | } |
| 11 | 15 | } |
| 12 | 16 | }, |
@@ -18,15 +22,19 @@ module.exports = {
| 18 | 22 | themes: [ |
| 19 | 23 | { |
| 20 | 24 | rustwarden: { |
| 21 | - "primary": "#3b82f6", | |
| 22 | - "secondary": "#1e293b", | |
| 23 | - "accent": "#10b981", | |
| 24 | - "neutral": "#f5f5f5", | |
| 25 | + "primary": "#00ed64", | |
| 26 | + "primary-content": "#001e2b", | |
| 27 | + "secondary": "#001e2b", | |
| 28 | + "accent": "#00684a", | |
| 29 | + "neutral": "#1c2d38", | |
| 25 | 30 | "base-100": "#ffffff", |
| 26 | - "info": "#3b82f6", | |
| 27 | - "success": "#10b981", | |
| 28 | - "warning": "#f59e0b", | |
| 29 | - "error": "#ef4444", | |
| 31 | + "base-200": "#f9fbfa", | |
| 32 | + "base-300": "#e1e5e8", | |
| 33 | + "base-content": "#001e2b", | |
| 34 | + "info": "#3d4f9f", | |
| 35 | + "success": "#00a35c", | |
| 36 | + "warning": "#fa6e39", | |
| 37 | + "error": "#e74856", | |
| 30 | 38 | }, |
| 31 | 39 | }, |
| 32 | 40 | "light", |
GitRust