fix: close issue #2 about account missing page

This commit is contained in:
2026-08-24 10:09:51 +02:00
parent 93a59d1deb
commit 105580313f
9 changed files with 784 additions and 167 deletions
+198 -158
View File
@@ -1,6 +1,44 @@
{
"$schema": "https://inlang.com/schema/inlang-message-format",
"account": "Account",
"account_2fa_description": "Require a time-based code from your authenticator app at sign-in.",
"account_2fa_disable": "Disable two-factor",
"account_2fa_disabled": "Two-factor authentication is off.",
"account_2fa_disabled_done": "Two-factor authentication disabled.",
"account_2fa_enable": "Set up two-factor",
"account_2fa_enabled": "Two-factor authentication is on.",
"account_2fa_enforced": "Two-factor authentication is enforced by the administrator and cannot be disabled.",
"account_2fa_title": "Two-factor authentication",
"account_current_password": "Current password",
"account_description": "Your profile, sign-in methods and active sessions.",
"account_email_unverified": "Not verified",
"account_email_verified": "Verified",
"account_link": "Connect",
"account_linked_on": "Connected {date}",
"account_member_since": "Member since {date}",
"account_no_sessions": "No active sessions.",
"account_password_changed": "Password updated.",
"account_password_description": "Change the password you use to sign in.",
"account_password_set_description": "You sign in with a provider only. Set a password to also sign in with your username.",
"account_password_set_title": "Set a password",
"account_password_title": "Password",
"account_profile_description": "How you appear in the dashboard.",
"account_profile_title": "Profile",
"account_provider_credential": "Email + password",
"account_providers_description": "Sign-in providers connected to this account.",
"account_providers_title": "Connected accounts",
"account_resend_verification": "Resend verification email",
"account_revoke": "Revoke",
"account_send_reset_link": "Email me a reset link",
"account_session_current": "This device",
"account_session_expires": "Expires",
"account_session_revoked": "Session revoked.",
"account_sessions_description": "Devices currently signed in to your account.",
"account_sessions_title": "Active sessions",
"account_unlink": "Disconnect",
"account_unlink_last_hint": "You cannot disconnect your only sign-in method.",
"account_unlinked": "Provider disconnected.",
"account_verification_sent": "Verification email sent.",
"admin_config_auth_description": "Sign-in, registration and second-factor policy.",
"admin_config_auth_title": "Authentication",
"admin_config_description": "Runtime configuration for the dashboard. Stored in the database; falls back to environment values.",
@@ -55,8 +93,8 @@
"create_account": "Create account",
"dashboard": "Dashboard",
"dashboard_architecture": "Architecture",
"dashboard_auto_refresh": "Auto-refresh",
"dashboard_ascending": "Ascending",
"dashboard_auto_refresh": "Auto-refresh",
"dashboard_clock": "Clock",
"dashboard_col_actions": "Actions",
"dashboard_col_free": "Free",
@@ -132,6 +170,122 @@
"dashboard_used_percent": "{used}% used",
"dashboard_utc": "UTC",
"delete": "Delete",
"docs_arch_bounds_desc": "The central Web UI and the individual agent instances interact over standard HTTPS connections using token-based authentication.",
"docs_arch_bounds_title": "Technical boundaries",
"docs_arch_card_agent_desc": "Lightweight daemon compiled in Go",
"docs_arch_card_agent_item1": "Runs as root with PAM authentication + RBAC",
"docs_arch_card_agent_item2": "Direct communication with systemd & packages",
"docs_arch_card_agent_item3": "SQLite-backed local audit trail",
"docs_arch_card_agent_title": "Nadir Agent (Backend)",
"docs_arch_card_flow_desc": "Bearer token auth",
"docs_arch_card_flow_title": "HTTPS / API Call",
"docs_arch_card_webui_desc": "SvelteKit 2 + Svelte 5 console application",
"docs_arch_card_webui_item1": "SQLite Database for machine lists and profiles",
"docs_arch_card_webui_item2": "Better Auth session and security layers",
"docs_arch_card_webui_item3": "Stateless deployment in Docker",
"docs_arch_card_webui_title": "Nadir Web UI (Frontend)",
"docs_arch_daemon_desc": "The agent daemon runs on each host machine as a systemd service. It is designed to be fully self-contained and does not require external databases, runtimes, or interpreters.",
"docs_arch_daemon_desc2": "Instead of implementing custom system logic, the Go daemon acts as an API proxy over the host's native utilities:",
"docs_arch_daemon_item1": "<strong>Systemd</strong> controls service operations via direct interactions with systemd units.",
"docs_arch_daemon_item2": "<strong>PAM (Pluggable Authentication Modules)</strong> handles credentials and account validations.",
"docs_arch_daemon_item3": "<strong>OS Utilities</strong> like <code>useradd</code>, <code>userdel</code>, <code>groupadd</code>, <code>groupdel</code>, <code>hostnamectl</code>, and filesystem tools perform account, host, and storage changes.",
"docs_arch_daemon_item4": "<strong>Package Managers</strong> (APT on Debian/Ubuntu, DNF on RHEL/Fedora, Pacman on Arch) perform software package updates and upgrades, streaming outputs live using Server-Sent Events (SSE).",
"docs_arch_daemon_title": "Self-contained Go agent",
"docs_arch_desc": "Nadir is engineered for high performance, isolation, and security. Learn about the technical implementation details that drive the Web UI and the Agent.",
"docs_arch_minisign_desc": "To ensure agent updates are secure, the updater implements strict cryptographic signature verification:",
"docs_arch_minisign_item1": "The agent queries the configured Gitea release API to fetch the latest tagged release.",
"docs_arch_minisign_item2": "It downloads both the checksum list file (<code>sha256sums.txt</code>) and its signature file (<code>sha256sums.txt.minisig</code>).",
"docs_arch_minisign_item3": "It verifies the signature file against the hardcoded Minisign public key (<code>minisign.pub</code>) compiled into the binary. This trust anchor cannot be changed without rebuilding the binary.",
"docs_arch_minisign_item4": "If the signature checks out, the agent downloads the new binary, verifies its SHA-256 hash against the verified checksum file, atomically replaces the running binary at <code>/usr/local/bin/nadir</code>, and restarts the systemd unit.",
"docs_arch_minisign_title": "Secure auto-updates via Minisign",
"docs_arch_pam_desc": "To authenticate human logins, the agent delegates password checks to PAM (<code>pam_unix</code>). Instead of using default system login services, the agent installs a dedicated, minimal PAM service file at <code>/etc/pam.d/nadir</code>:",
"docs_arch_pam_desc2": "This specific setup prevents performance issues common with stock login services. For instance, on laptops, the default <code>system-auth</code> pulls in <code>pam_fprintd.so</code> which blocks password verification for up to 30 seconds while waiting for fingerprint swipes. A dedicated PAM service keeps login validation times down to milliseconds.",
"docs_arch_pam_title": "Dedicated PAM integration",
"docs_arch_title": "Architecture",
"docs_go_to_dashboard": "Go to Dashboard",
"docs_install_agent_desc": "Install the <code>nadir-agent</code> backend daemon on any Linux machine you want to manage. Run the bootstrap script on the host:",
"docs_install_agent_title": "2. Bootstrap agent nodes",
"docs_install_desc": "Deploy Nadir in under two minutes. Run the central Web UI as a stateless Docker container and bootstrap agent nodes on target Linux hosts.",
"docs_install_disclaimer_desc": "Piping untrusted scripts directly from the internet into a root shell is extremely dangerous. You should <strong>always inspect and read the source code</strong> of any script before running it. Review the installer code served by this instance at <a href=\"https://{nadirHost}/install.sh\" target=\"_blank\" class=\"underline text-primary hover:text-primary/80 font-medium\">/install.sh</a> or download it to read first: <code class=\"bg-muted px-1.5 py-0.5 rounded text-foreground font-mono text-[10px]\">curl -fsSL https://{nadirHost}/install.sh</code>.",
"docs_install_disclaimer_title": "Disclaimer: Inspect Before Execution",
"docs_install_link_desc": "When the installer finishes, it generates a bearer token (starting with <code>nad_</code>) for dashboard authentication. Copy this token, log into the Web UI, click <strong>Add Server</strong>, enter the IP address and port, paste the token, and save.",
"docs_install_link_manual": "To manually add additional authentication tokens on a host, run:",
"docs_install_link_title": "3. Link nodes to dashboard",
"docs_install_privilege_desc": "The bootstrap script must run as root (via <code>sudo sh</code>). The agent requires root privileges because it executes PAM authorization, checks systemd state, mounts filesystems, and interacts with <code>/etc/shadow</code> and <code>/etc/hosts</code> directly.",
"docs_install_privilege_title": "Security & Privileges",
"docs_install_task_1": "Detects the system processor architecture (<code>amd64</code> or <code>arm64</code>).",
"docs_install_task_2": "Queries the configured Gitea repository API for the latest tagged binary release.",
"docs_install_task_3": "Downloads the release asset and verifies its SHA-256 signature against the published checksum file.",
"docs_install_task_4": "Installs the <code>nadir</code> executable to <code>/usr/local/bin/nadir</code>.",
"docs_install_task_5": "Provisions the dedicated PAM service <code>/etc/pam.d/nadir</code> to enable password validation.",
"docs_install_task_6": "Generates persistent self-signed TLS certificates under <code>/var/lib/nadir/tls</code> (if TLS is enabled).",
"docs_install_task_7": "Creates and starts a systemd service unit (<code>nadir.service</code>) that starts on boot.",
"docs_install_tasks_intro": "The installer script performs the following tasks automatically:",
"docs_install_title": "Installation",
"docs_install_webui_desc": "The central dashboard operates as a stateless web server that registers and coordinates multiple node instances. Use Docker to spin it up in seconds:",
"docs_install_webui_run": "Once the container is running, navigate to <code>http://localhost:3000</code> to register your administrator account.",
"docs_install_webui_title": "1. Deploy the Web UI",
"docs_intro_desc": "Welcome to the Nadir documentation. Nadir is a lightweight, modular Linux system-administration suite that provides modern, self-hosted web management panels for your servers.",
"docs_intro_how_desc": "Nadir is divided into two distinct parts: a central <strong>Web UI (Frontend)</strong> and a per-host <strong>Agent (Backend)</strong>. The frontend does not talk to your operating system directly; instead, it registers multiple agent nodes using secure API tokens and makes server-to-server HTTP queries to manage your systems.",
"docs_intro_how_title": "How it works",
"docs_intro_mod_audit_desc": "Read-only, tamper-evident SQLite-backed audit log capturing who executed what, when, and the result.",
"docs_intro_mod_audit_title": "Audit Trail",
"docs_intro_mod_networking_desc": "Manage network interfaces, routing tables, and DNS settings. Supports temporary configuration and safety auto-rollback.",
"docs_intro_mod_services_desc": "List and inspect systemd units. Start, stop, restart, enable, or disable units, and stream journald logs live over SSE.",
"docs_intro_mod_storage_desc": "Monitor storage devices, partition layouts, and active mounts. View and modify /etc/fstab configurations.",
"docs_intro_mod_system_desc": "Dashboard overview (OS, kernel, CPU, memory, load, uptime, temperatures), hostname configuration, and date/time/NTP settings.",
"docs_intro_mod_users_desc": "Manage local PAM/Unix user accounts and group assignments. Run useradd/groupadd utilities natively.",
"docs_intro_modules_desc": "Functionality is organized into isolated modules. Each module manages a slice of system configuration and defines its own role-based access control (RBAC) permission vocabulary.",
"docs_intro_modules_title": "Core modules",
"docs_intro_ready_cta": "Get started",
"docs_intro_ready_desc": "Proceed to the installation guide to spin up the Web UI and deploy the backend agent.",
"docs_intro_ready_title": "Ready to deploy?",
"docs_intro_title": "Introduction",
"docs_limits_credits_desc": "Nadir's branding assets and icons are based on open-source packages:",
"docs_limits_credits_item1": "<strong>Favicon:</strong> Recolored version of the <a class=\"text-primary hover:underline\" href=\"https://lucide.dev/icons/orbit\" target=\"_blank\" rel=\"noreferrer\">Orbit icon</a> from the Lucide project.",
"docs_limits_credits_item2": "<strong>Icons:</strong> Visual markers across pages are provided by the <a class=\"text-primary hover:underline\" href=\"https://lucide.dev\" target=\"_blank\" rel=\"noreferrer\">Lucide icons library</a>, licensed under the ISC License.",
"docs_limits_credits_title": "Credits & assets",
"docs_limits_desc": "Review the current architectural limits of the agent and the open-source licensing terms of the Nadir project.",
"docs_limits_license_desc": "Nadir is free, open-source software published under the terms of the MIT License.",
"docs_limits_license_mit_body1": "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:",
"docs_limits_license_mit_body2": "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.",
"docs_limits_license_mit_body3": "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.",
"docs_limits_license_mit_copyright": "Copyright (c) 2026 Urania",
"docs_limits_license_mit_title": "MIT License",
"docs_limits_license_title": "Licensing",
"docs_limits_sys_desc": "Because Nadir works directly with core operating system abstractions rather than virtualization layers, it is bound by the following environmental requirements:",
"docs_limits_sys_item1": "<strong>Linux Only:</strong> The agent relies heavily on standard Linux utilities (<code>useradd</code>, <code>groupadd</code>, <code>shutdown</code>), kernel namespaces, and file systems under <code>/sys</code>, <code>/proc</code>, and <code>/etc</code>. It cannot run on macOS, Windows, or BSD distributions.",
"docs_limits_sys_item2": "<strong>Systemd Dependency:</strong> The service management, boot status, and journal logging modules interact directly with <code>systemd</code> and <code>journald</code>. Runtimes using other init systems (like sysvinit, OpenRC, or Runit) are not supported.",
"docs_limits_sys_item3": "<strong>Root Privileges Required:</strong> The agent must execute as <code>root</code>. The PAM authenticator needs read access to <code>/etc/shadow</code>, and managing hardware settings (hostname, network interfaces, disk mounts) is locked behind root level syscalls by the kernel.",
"docs_limits_sys_item4": "<strong>Co-hosting CORS constraints:</strong> Running the Web UI cross-origin relative to target agents triggers browser CORS preflight checks. If you host the frontend on a different origin, you must utilize server-to-server dashboard API queries (default behavior of SvelteKit server load functions) or configure a reverse proxy to terminate both under a shared domain.",
"docs_limits_sys_title": "System limitations",
"docs_limits_title": "Limitations & License",
"docs_nav_architecture": "Architecture",
"docs_nav_installation": "Installation",
"docs_nav_intro": "Introduction",
"docs_nav_limitations": "Limitations & License",
"docs_nav_security": "Security & VPN",
"docs_security_acl_desc": "If you deploy over a private overlay network, restrict access so that only the central Web UI server is permitted to talk to the agent ports (tcp/9999):",
"docs_security_acl_iptables": "<strong>Standard iptables:</strong> Reject all connections on the WireGuard interface except from the proxy peer:",
"docs_security_acl_netbird": "<strong>Netbird:</strong> Setup an Access Control Policy permitting group <code>nadir-ui</code> to group <code>nadir-agent</code> on port <code>9999</code>, denying other peers.",
"docs_security_acl_tailscale": "<strong>Tailscale:</strong> Add an ACL rule restricting target ports to the proxy tag:",
"docs_security_acl_title": "Access control list (ACL) rules",
"docs_security_deploy_coloc_desc": "Install both the <strong>Nadir Web UI</strong> and the <strong>nadir-agent</strong> on the exact same virtual machine or server. Configure the agent to bind to <code>127.0.0.1:9999</code> and let the Web UI container access the local agent directly. This keeps the agent API completely unexposed to any external network interface.",
"docs_security_deploy_coloc_title": "A. Co-Location (Loopback)",
"docs_security_deploy_desc": "Choose one of these recommended deployment strategies to isolate your system administration ports:",
"docs_security_deploy_title": "Suggested deployments",
"docs_security_deploy_vpn_desc": "For managing multiple servers, configure an overlay VPN (such as <strong>Tailscale</strong>, <strong>Netbird</strong>, or <strong>WireGuard</strong>). Bind the <code>nadir-agent</code> instance only to the host's private overlay IP (e.g. <code>100.64.x.x</code>). This guarantees that only authenticated VPN peers can see the administration port.",
"docs_security_deploy_vpn_title": "B. Private Overlay VPN (Recommended)",
"docs_security_desc": "Because Nadir executes system commands as root, maintaining strict network boundaries is the most critical element of a secure deployment.",
"docs_security_title": "Security & VPN",
"docs_security_tls_desc": "API keys, tokens, and session cookies must always travel over encrypted HTTPS connections. Secure the connection using one of three modes:",
"docs_security_tls_direct_desc": "Direct TLS termination (<code>tls_cert</code> + <code>tls_key</code>): Specify paths to a valid PEM certificate and key file in <code>config.yaml</code> to have the agent server terminate TLS directly.",
"docs_security_tls_proxy_desc": "Behind a reverse proxy (<code>trust_proxy: true</code>): A proxy like Traefik terminates TLS at the edge and forwards plaintext HTTP to the agent over a secure local network. In this setup, make sure the proxy overrides client-supplied <code>X-Forwarded-*</code> headers, and set <code>secure_tls: true</code> to ensure the session cookie keeps its Secure attribute.",
"docs_security_tls_self_desc": "Self-signed certificate fallback (development only): If no proxy or certificates are configured, the agent generates an in-memory self-signed certificate valid for <code>localhost</code> for one year. This triggers browser security warnings and should never be used in production.",
"docs_security_tls_title": "TLS connection modes",
"docs_security_warning_desc": "An instance of <code>nadir-agent</code> running on a host holds root privileges. Exposing the agent port directly to the public internet is extremely dangerous and strongly discouraged. By default, the agent binds to localhost to ensure it remains local-only.",
"docs_security_warning_title": "Warning: Root Access Risks",
"docs_title": "Docs",
"download": "Download",
"edit": "Edit",
"email": "Email",
@@ -176,9 +330,9 @@
"errors_non_empty": "This field is required",
"errors_not_found": "This item has not been found",
"errors_password_too_short": "Password must be at least {min} characters",
"errors_port_invalid": "Enter a port between 1 and 65535",
"errors_password_weak": "Use upper- and lower-case letters and at least one number.",
"errors_passwords_no_match": "Passwords do not match",
"errors_port_invalid": "Enter a port between 1 and 65535",
"errors_unauthenticated": "Unauthenticated",
"errors_username_too_short": "Username must be at least {min} characters",
"errors_wrong_credentials": "Wrong credentials, try again.",
@@ -197,7 +351,6 @@
"groups_add_member_title": "Add member to {name}",
"groups_col_gid": "GID",
"groups_col_members": "Members",
"groups_gid_optional": "GID (optional)",
"groups_create_description": "Adds a Unix group via groupadd.",
"groups_create_field_system": "System group",
"groups_create_title": "Create group",
@@ -207,6 +360,7 @@
"groups_deleted": "Group deleted",
"groups_filter_show_system": "Show system groups",
"groups_filter_system_gid_hint": "(gid < 1000)",
"groups_gid_optional": "GID (optional)",
"groups_member_added": "Added {username}",
"groups_member_removed": "Removed {username}",
"groups_members_description": "Supplementary members are managed via <code>usermod -G</code> on each user. Primary-group members (users whose primary gid is {gid}) appear below but cannot be removed from here.",
@@ -214,19 +368,26 @@
"groups_nav_description": "Unix groups from /etc/group on this machine.",
"groups_nav_title": "Groups",
"groups_no_results": "No groups found.",
"groups_remove_member_aria": "Remove {name}",
"groups_no_supplementary_members": "No supplementary members.",
"groups_not_found": "Group not found: {name}",
"groups_primary_empty": "None.",
"groups_primary_label": "Primary ({count})",
"groups_remove_member_aria": "Remove {name}",
"groups_search_placeholder": "Search name or gid…",
"groups_supplementary_label": "Supplementary ({count})",
"home": "Home",
"invalid_reset_link": "This link is invalid or has expired.",
"landing_arch_backend_desc": "The per-host service daemon. Lightweight binary compiled in Go. Runs as root under PAM authentication, providing local system access.",
"landing_arch_backend_title": "Nadir Agent",
"landing_arch_desc": "Nadir separates your presentation layer from target host runtimes. The frontend operates as a stateless console, and the lightweight Go agent manages your systems directly.",
"landing_arch_frontend_desc": "The central operations dashboard. Served as a stateless Docker container. Sign in, manage server associations, and monitor all nodes.",
"landing_arch_frontend_title": "Nadir Web UI",
"landing_arch_title": "Dual architecture, simple deployment",
"landing_cta_desc": "Deploy your own instance and manage your servers from any device.",
"landing_cta_title": "Ready to take control?",
"landing_features_dashboard": "Live performance metrics",
"landing_features_dashboard_desc": "Inspect real-time CPU performance heatmaps, memory consumption metrics, network bandwidth usage, and storage capacity graphs.",
"landing_features_desc": "No complex configuration files or rigid wrappers. Nadir exposes standard system abstractions directly, letting you perform everyday administration actions natively.",
"landing_features_networking": "Network administration",
"landing_features_networking_desc": "Configure network settings dynamically. Review interfaces, inspect routing tables, modify local hosts files, and configure system DNS resolution.",
"landing_features_services": "Systemd service control",
@@ -236,7 +397,6 @@
"landing_features_terminal": "Interactive terminal",
"landing_features_terminal_desc": "Open secure terminal windows connected directly to target hosts. Features terminal multiplexing, size auto-fitting, and persistent sessions.",
"landing_features_title": "Complete server control",
"landing_features_desc": "No complex configuration files or rigid wrappers. Nadir exposes standard system abstractions directly, letting you perform everyday administration actions natively.",
"landing_features_users": "User and group management",
"landing_features_users_desc": "Manage system user accounts and group assignments. Edit user privileges, view memberships, and authenticate via secure PAM rules.",
"landing_footer_docs": "Documentation",
@@ -244,42 +404,35 @@
"landing_footer_subtitle": "Self-hosted, open-source server management.",
"landing_hero_cta_github": "View source",
"landing_hero_cta_start": "Get started",
"landing_hero_title": "Modern server administration, unified.",
"landing_hero_tagline": "An open-source console and lightweight agent built to inspect, manage, and secure your Linux hosts from any browser. Simple architecture, complete control.",
"landing_arch_title": "Dual architecture, simple deployment",
"landing_arch_desc": "Nadir separates your presentation layer from target host runtimes. The frontend operates as a stateless console, and the lightweight Go agent manages your systems directly.",
"landing_arch_frontend_title": "Nadir Web UI",
"landing_arch_frontend_desc": "The central operations dashboard. Served as a stateless Docker container. Sign in, manage server associations, and monitor all nodes.",
"landing_arch_backend_title": "Nadir Agent",
"landing_arch_backend_desc": "The per-host service daemon. Lightweight binary compiled in Go. Runs as root under PAM authentication, providing local system access.",
"landing_how_title": "Deploy in under two minutes",
"landing_hero_title": "Modern server administration, unified.",
"landing_how_desc": "Follow these simple steps to run the Web UI and link your first target host.",
"landing_how_step1_title": "Spin up the Web UI",
"landing_how_step1_desc": "Deploy the stateless web dashboard using Docker. This interface will coordinate your registered agents. Access the web setup at port 3000 to configure your dashboard admin account.",
"landing_how_step2_title": "Bootstrap the target server",
"landing_how_step1_title": "Spin up the Web UI",
"landing_how_step2_desc": "Run the installation script on the host machine you want to manage. The script automatically detects the host architecture (amd64/arm64), fetches the latest tagged binary release from Gitea, verifies SHA-256 integrity, configures a systemd unit, and installs a PAM configuration file.",
"landing_security_note": "Security note:",
"landing_security_note_text": "The bootstrap installer requires root privileges. It generates a self-signed TLS certificate automatically if secure TLS is configured.",
"landing_security_architecture_title": "Deployment security and architecture",
"landing_security_architecture_desc": "Because the nadir-agent operates with root privileges, it should never be exposed directly to the public internet. By default, it binds to localhost. It is designed to be co-located on the same server as the Web UI communicating over loopback, or securely accessed across nodes over a private VPN (such as WireGuard, Tailscale, or Netbird). While agent installations support generating self-signed TLS certificates automatically, operating within a trusted private network boundary remains the recommended deployment model.",
"landing_how_step3_title": "Link in the dashboard",
"landing_how_step2_title": "Bootstrap the target server",
"landing_how_step3_desc": "When the agent installation completes, it outputs a dashboard association token. Copy this Bearer token, log into the Web UI dashboard, click \"Add Machine,\" enter the IP/address and token, and start administering your server.",
"landing_how_step3_title": "Link in the dashboard",
"landing_how_title": "Deploy in under two minutes",
"landing_screenshot_dashboard": "Machine dashboard with real-time performance metrics, CPU heatmap, memory usage, and network activity.",
"landing_screenshot_hero_label": "Dashboard overview with real-time metrics",
"landing_screenshot_machines": "Multi-server overview showing all connected machines with health status.",
"landing_screenshot_services": "Service management table with start, stop, and restart controls for systemd units.",
"landing_screenshots_title": "See it in action",
"landing_security_architecture_desc": "Because the nadir-agent operates with root privileges, it should never be exposed directly to the public internet. By default, it binds to localhost. It is designed to be co-located on the same server as the Web UI communicating over loopback, or securely accessed across nodes over a private VPN (such as WireGuard, Tailscale, or Netbird). While agent installations support generating self-signed TLS certificates automatically, operating within a trusted private network boundary remains the recommended deployment model.",
"landing_security_architecture_title": "Deployment security and architecture",
"landing_security_note": "Security note:",
"landing_security_note_text": "The bootstrap installer requires root privileges. It generates a self-signed TLS certificate automatically if secure TLS is configured.",
"landing_start_code": "bun install && cp .env.example .env && bun run db:push && bun run dev",
"landing_tech_linux": "Linux",
"landing_tech_opensource": "Open Source",
"landing_tech_selfhosted": "Self-Hosted",
"landing_start_desc": "Prerequisites: Bun and a reachable nadir-agent instance.",
"landing_start_title": "Get started in minutes",
"landing_stats_linux": "Linux-native",
"landing_stats_opensource": "Open source",
"landing_stats_setup": "One-command setup",
"landing_tech_linux": "Linux",
"landing_tech_opensource": "Open Source",
"landing_tech_selfhosted": "Self-Hosted",
"login": "Login",
"more": "More",
"login_social_description": "You have to login to use this platform. Use your favorite social or your credentials",
"login_with": "Login with <span class=capitalize>{social}</span>",
"logout": "Logout",
@@ -316,6 +469,7 @@
"machine_token_keep": "Leave blank to keep the current token.",
"machine_token_placeholder": "Agent bearer token",
"manual_entry_key": "Can't scan? Enter this key in your authenticator app manually:",
"more": "More",
"name": "Name",
"nav_admin": "Admin",
"nav_admin_config": "Config",
@@ -475,6 +629,7 @@
"saved": "Saved",
"scan_qr": "Add this key to your authenticator app, then enter the generated code below.",
"send_reset_link": "Send reset link",
"seo_desc_account": "Manage your profile, sign-in methods and active sessions.",
"seo_desc_admin_config": "Application-wide configuration settings.",
"seo_desc_admin_users": "Manage user accounts, roles and access.",
"seo_desc_auth_2fa": "Verify your identity with a two-factor authentication code.",
@@ -509,6 +664,7 @@
"seo_desc_users_detail": "View and manage a PAM/Unix user account.",
"seo_desc_users_groups": "Unix groups from /etc/group on this machine.",
"seo_desc_users_groups_detail": "View and manage a Unix group.",
"seo_title_account": "Account",
"seo_title_admin_config": "Config",
"seo_title_admin_users": "Users",
"seo_title_auth_2fa": "Two-factor authentication",
@@ -622,6 +778,14 @@
"storage_mounts_search_placeholder": "Search by device, mount point or type…",
"storage_no_fstab": "No fstab entries.",
"storage_no_mounts": "No mounts found.",
"syslog_alert": "1 alert",
"syslog_crit": "2 crit",
"syslog_debug": "7 debug",
"syslog_emerg": "0 emerg",
"syslog_err": "3 err",
"syslog_info": "6 info",
"syslog_notice": "5 notice",
"syslog_warning": "4 warning",
"system_hostname_current": "Current hostname",
"system_hostname_invalid": "Hostname is invalid",
"system_locale_generate": "Generate new locale",
@@ -641,6 +805,10 @@
"system_locale_search_keymap_placeholder": "Search keymap…",
"system_locale_search_locale_placeholder": "Search locale…",
"system_locale_x11": "X11 layout",
"system_nadir_modules": "Registered Modules",
"system_nadir_no_permissions": "No permissions defined",
"system_nadir_permissions": "Resolved Permissions",
"system_nadir_username": "Authenticated User",
"system_power_confirm_description": "The machine will be unreachable while it shuts down. This cannot be undone from here.",
"system_power_confirm_poweroff_title": "Power off this machine?",
"system_power_confirm_reboot_title": "Reboot this machine?",
@@ -660,28 +828,19 @@
"system_time_ntp_synced": "Synchronized",
"system_time_search_timezone_placeholder": "Search timezone…",
"system_time_timezone": "Timezone",
"syslog_alert": "1 alert",
"syslog_crit": "2 crit",
"syslog_debug": "7 debug",
"syslog_emerg": "0 emerg",
"syslog_err": "3 err",
"syslog_info": "6 info",
"syslog_notice": "5 notice",
"syslog_warning": "4 warning",
"terms_notice": "By clicking continue, you agree to our <a class='link' href={terms}>Terms of Service</a> and <a class='link' href={privacy}>Privacy Policy</a>.",
"terminal_auth_private_key": "Private Key",
"terminal_connect": "Connect",
"terminal_connect_desc": "Connect to this host via SSH.",
"terminal_connecting": "Connecting…",
"terminal_connecting_desc": "Opening SSH session to {credential} on port {port}",
"terminal_credential_cleared": "Saved credentials cleared",
"terminal_credential_forgotten": "Forgot saved credentials for {username}",
"terminal_credential_load_failed": "Could not load saved credentials",
"terminal_credential_saved_toast": "Credentials saved for {username}",
"terminal_connect": "Connect",
"terminal_connect_desc": "Connect to this host via SSH.",
"terminal_discard": "Discard",
"terminal_download_history": "Download history",
"terminal_forget": "Forget",
"terminal_history_truncated": "Output truncated, last 2 MB shown",
"terminal_connecting": "Connecting…",
"terminal_connecting_desc": "Opening SSH session to {credential} on port {port}",
"terminal_loaded_from_saved": "Loaded saved credentials",
"terminal_password_placeholder": "Enter password",
"terminal_port": "Port",
@@ -693,6 +852,7 @@
"terminal_session_ended": "Session ended",
"terminal_ssh_failed": "SSH connection failed",
"terminal_use_saved": "Use saved",
"terms_notice": "By clicking continue, you agree to our <a class='link' href={terms}>Terms of Service</a> and <a class='link' href={privacy}>Privacy Policy</a>.",
"theme": "Theme",
"theme_dark": "Dark",
"theme_light": "Light",
@@ -798,125 +958,5 @@
"verification_sent": "We sent a verification link to {email}. Click it to activate your account.",
"verify": "Verify",
"verify_your_email": "You need to first verify your email address",
"welcome_back": "Welcome back",
"system_nadir_username": "Authenticated User",
"system_nadir_permissions": "Resolved Permissions",
"system_nadir_modules": "Registered Modules",
"system_nadir_no_permissions": "No permissions defined",
"docs_title": "Docs",
"docs_go_to_dashboard": "Go to Dashboard",
"docs_nav_intro": "Introduction",
"docs_nav_installation": "Installation",
"docs_nav_architecture": "Architecture",
"docs_nav_security": "Security & VPN",
"docs_nav_limitations": "Limitations & License",
"docs_intro_title": "Introduction",
"docs_intro_desc": "Welcome to the Nadir documentation. Nadir is a lightweight, modular Linux system-administration suite that provides modern, self-hosted web management panels for your servers.",
"docs_intro_how_title": "How it works",
"docs_intro_how_desc": "Nadir is divided into two distinct parts: a central <strong>Web UI (Frontend)</strong> and a per-host <strong>Agent (Backend)</strong>. The frontend does not talk to your operating system directly; instead, it registers multiple agent nodes using secure API tokens and makes server-to-server HTTP queries to manage your systems.",
"docs_intro_modules_title": "Core modules",
"docs_intro_modules_desc": "Functionality is organized into isolated modules. Each module manages a slice of system configuration and defines its own role-based access control (RBAC) permission vocabulary.",
"docs_intro_mod_system_desc": "Dashboard overview (OS, kernel, CPU, memory, load, uptime, temperatures), hostname configuration, and date/time/NTP settings.",
"docs_intro_mod_services_desc": "List and inspect systemd units. Start, stop, restart, enable, or disable units, and stream journald logs live over SSE.",
"docs_intro_mod_users_desc": "Manage local PAM/Unix user accounts and group assignments. Run useradd/groupadd utilities natively.",
"docs_intro_mod_storage_desc": "Monitor storage devices, partition layouts, and active mounts. View and modify /etc/fstab configurations.",
"docs_intro_mod_networking_desc": "Manage network interfaces, routing tables, and DNS settings. Supports temporary configuration and safety auto-rollback.",
"docs_intro_mod_audit_desc": "Read-only, tamper-evident SQLite-backed audit log capturing who executed what, when, and the result.",
"docs_intro_mod_audit_title": "Audit Trail",
"docs_intro_ready_title": "Ready to deploy?",
"docs_intro_ready_desc": "Proceed to the installation guide to spin up the Web UI and deploy the backend agent.",
"docs_intro_ready_cta": "Get started",
"docs_install_title": "Installation",
"docs_install_desc": "Deploy Nadir in under two minutes. Run the central Web UI as a stateless Docker container and bootstrap agent nodes on target Linux hosts.",
"docs_install_webui_title": "1. Deploy the Web UI",
"docs_install_webui_desc": "The central dashboard operates as a stateless web server that registers and coordinates multiple node instances. Use Docker to spin it up in seconds:",
"docs_install_webui_run": "Once the container is running, navigate to <code>http://localhost:3000</code> to register your administrator account.",
"docs_install_agent_title": "2. Bootstrap agent nodes",
"docs_install_agent_desc": "Install the <code>nadir-agent</code> backend daemon on any Linux machine you want to manage. Run the bootstrap script on the host:",
"docs_install_disclaimer_title": "Disclaimer: Inspect Before Execution",
"docs_install_disclaimer_desc": "Piping untrusted scripts directly from the internet into a root shell is extremely dangerous. You should <strong>always inspect and read the source code</strong> of any script before running it. Review the installer code served by this instance at <a href=\"https://{nadirHost}/install.sh\" target=\"_blank\" class=\"underline text-primary hover:text-primary/80 font-medium\">/install.sh</a> or download it to read first: <code class=\"bg-muted px-1.5 py-0.5 rounded text-foreground font-mono text-[10px]\">curl -fsSL https://{nadirHost}/install.sh</code>.",
"docs_install_privilege_title": "Security & Privileges",
"docs_install_privilege_desc": "The bootstrap script must run as root (via <code>sudo sh</code>). The agent requires root privileges because it executes PAM authorization, checks systemd state, mounts filesystems, and interacts with <code>/etc/shadow</code> and <code>/etc/hosts</code> directly.",
"docs_install_tasks_intro": "The installer script performs the following tasks automatically:",
"docs_install_task_1": "Detects the system processor architecture (<code>amd64</code> or <code>arm64</code>).",
"docs_install_task_2": "Queries the configured Gitea repository API for the latest tagged binary release.",
"docs_install_task_3": "Downloads the release asset and verifies its SHA-256 signature against the published checksum file.",
"docs_install_task_4": "Installs the <code>nadir</code> executable to <code>/usr/local/bin/nadir</code>.",
"docs_install_task_5": "Provisions the dedicated PAM service <code>/etc/pam.d/nadir</code> to enable password validation.",
"docs_install_task_6": "Generates persistent self-signed TLS certificates under <code>/var/lib/nadir/tls</code> (if TLS is enabled).",
"docs_install_task_7": "Creates and starts a systemd service unit (<code>nadir.service</code>) that starts on boot.",
"docs_install_link_title": "3. Link nodes to dashboard",
"docs_install_link_desc": "When the installer finishes, it generates a bearer token (starting with <code>nad_</code>) for dashboard authentication. Copy this token, log into the Web UI, click <strong>Add Server</strong>, enter the IP address and port, paste the token, and save.",
"docs_install_link_manual": "To manually add additional authentication tokens on a host, run:",
"docs_arch_title": "Architecture",
"docs_arch_desc": "Nadir is engineered for high performance, isolation, and security. Learn about the technical implementation details that drive the Web UI and the Agent.",
"docs_arch_bounds_title": "Technical boundaries",
"docs_arch_bounds_desc": "The central Web UI and the individual agent instances interact over standard HTTPS connections using token-based authentication.",
"docs_arch_card_webui_title": "Nadir Web UI (Frontend)",
"docs_arch_card_webui_desc": "SvelteKit 2 + Svelte 5 console application",
"docs_arch_card_webui_item1": "SQLite Database for machine lists and profiles",
"docs_arch_card_webui_item2": "Better Auth session and security layers",
"docs_arch_card_webui_item3": "Stateless deployment in Docker",
"docs_arch_card_flow_title": "HTTPS / API Call",
"docs_arch_card_flow_desc": "Bearer token auth",
"docs_arch_card_agent_title": "Nadir Agent (Backend)",
"docs_arch_card_agent_desc": "Lightweight daemon compiled in Go",
"docs_arch_card_agent_item1": "Runs as root with PAM authentication + RBAC",
"docs_arch_card_agent_item2": "Direct communication with systemd & packages",
"docs_arch_card_agent_item3": "SQLite-backed local audit trail",
"docs_arch_daemon_title": "Self-contained Go agent",
"docs_arch_daemon_desc": "The agent daemon runs on each host machine as a systemd service. It is designed to be fully self-contained and does not require external databases, runtimes, or interpreters.",
"docs_arch_daemon_desc2": "Instead of implementing custom system logic, the Go daemon acts as an API proxy over the host's native utilities:",
"docs_arch_daemon_item1": "<strong>Systemd</strong> controls service operations via direct interactions with systemd units.",
"docs_arch_daemon_item2": "<strong>PAM (Pluggable Authentication Modules)</strong> handles credentials and account validations.",
"docs_arch_daemon_item3": "<strong>OS Utilities</strong> like <code>useradd</code>, <code>userdel</code>, <code>groupadd</code>, <code>groupdel</code>, <code>hostnamectl</code>, and filesystem tools perform account, host, and storage changes.",
"docs_arch_daemon_item4": "<strong>Package Managers</strong> (APT on Debian/Ubuntu, DNF on RHEL/Fedora, Pacman on Arch) perform software package updates and upgrades, streaming outputs live using Server-Sent Events (SSE).",
"docs_arch_pam_title": "Dedicated PAM integration",
"docs_arch_pam_desc": "To authenticate human logins, the agent delegates password checks to PAM (<code>pam_unix</code>). Instead of using default system login services, the agent installs a dedicated, minimal PAM service file at <code>/etc/pam.d/nadir</code>:",
"docs_arch_pam_desc2": "This specific setup prevents performance issues common with stock login services. For instance, on laptops, the default <code>system-auth</code> pulls in <code>pam_fprintd.so</code> which blocks password verification for up to 30 seconds while waiting for fingerprint swipes. A dedicated PAM service keeps login validation times down to milliseconds.",
"docs_arch_minisign_title": "Secure auto-updates via Minisign",
"docs_arch_minisign_desc": "To ensure agent updates are secure, the updater implements strict cryptographic signature verification:",
"docs_arch_minisign_item1": "The agent queries the configured Gitea release API to fetch the latest tagged release.",
"docs_arch_minisign_item2": "It downloads both the checksum list file (<code>sha256sums.txt</code>) and its signature file (<code>sha256sums.txt.minisig</code>).",
"docs_arch_minisign_item3": "It verifies the signature file against the hardcoded Minisign public key (<code>minisign.pub</code>) compiled into the binary. This trust anchor cannot be changed without rebuilding the binary.",
"docs_arch_minisign_item4": "If the signature checks out, the agent downloads the new binary, verifies its SHA-256 hash against the verified checksum file, atomically replaces the running binary at <code>/usr/local/bin/nadir</code>, and restarts the systemd unit.",
"docs_security_title": "Security & VPN",
"docs_security_desc": "Because Nadir executes system commands as root, maintaining strict network boundaries is the most critical element of a secure deployment.",
"docs_security_warning_title": "Warning: Root Access Risks",
"docs_security_warning_desc": "An instance of <code>nadir-agent</code> running on a host holds root privileges. Exposing the agent port directly to the public internet is extremely dangerous and strongly discouraged. By default, the agent binds to localhost to ensure it remains local-only.",
"docs_security_deploy_title": "Suggested deployments",
"docs_security_deploy_desc": "Choose one of these recommended deployment strategies to isolate your system administration ports:",
"docs_security_deploy_coloc_title": "A. Co-Location (Loopback)",
"docs_security_deploy_coloc_desc": "Install both the <strong>Nadir Web UI</strong> and the <strong>nadir-agent</strong> on the exact same virtual machine or server. Configure the agent to bind to <code>127.0.0.1:9999</code> and let the Web UI container access the local agent directly. This keeps the agent API completely unexposed to any external network interface.",
"docs_security_deploy_vpn_title": "B. Private Overlay VPN (Recommended)",
"docs_security_deploy_vpn_desc": "For managing multiple servers, configure an overlay VPN (such as <strong>Tailscale</strong>, <strong>Netbird</strong>, or <strong>WireGuard</strong>). Bind the <code>nadir-agent</code> instance only to the host's private overlay IP (e.g. <code>100.64.x.x</code>). This guarantees that only authenticated VPN peers can see the administration port.",
"docs_security_acl_title": "Access control list (ACL) rules",
"docs_security_acl_desc": "If you deploy over a private overlay network, restrict access so that only the central Web UI server is permitted to talk to the agent ports (tcp/9999):",
"docs_security_acl_tailscale": "<strong>Tailscale:</strong> Add an ACL rule restricting target ports to the proxy tag:",
"docs_security_acl_netbird": "<strong>Netbird:</strong> Setup an Access Control Policy permitting group <code>nadir-ui</code> to group <code>nadir-agent</code> on port <code>9999</code>, denying other peers.",
"docs_security_acl_iptables": "<strong>Standard iptables:</strong> Reject all connections on the WireGuard interface except from the proxy peer:",
"docs_security_tls_title": "TLS connection modes",
"docs_security_tls_desc": "API keys, tokens, and session cookies must always travel over encrypted HTTPS connections. Secure the connection using one of three modes:",
"docs_security_tls_proxy_desc": "Behind a reverse proxy (<code>trust_proxy: true</code>): A proxy like Traefik terminates TLS at the edge and forwards plaintext HTTP to the agent over a secure local network. In this setup, make sure the proxy overrides client-supplied <code>X-Forwarded-*</code> headers, and set <code>secure_tls: true</code> to ensure the session cookie keeps its Secure attribute.",
"docs_security_tls_direct_desc": "Direct TLS termination (<code>tls_cert</code> + <code>tls_key</code>): Specify paths to a valid PEM certificate and key file in <code>config.yaml</code> to have the agent server terminate TLS directly.",
"docs_security_tls_self_desc": "Self-signed certificate fallback (development only): If no proxy or certificates are configured, the agent generates an in-memory self-signed certificate valid for <code>localhost</code> for one year. This triggers browser security warnings and should never be used in production.",
"docs_limits_title": "Limitations & License",
"docs_limits_desc": "Review the current architectural limits of the agent and the open-source licensing terms of the Nadir project.",
"docs_limits_sys_title": "System limitations",
"docs_limits_sys_desc": "Because Nadir works directly with core operating system abstractions rather than virtualization layers, it is bound by the following environmental requirements:",
"docs_limits_sys_item1": "<strong>Linux Only:</strong> The agent relies heavily on standard Linux utilities (<code>useradd</code>, <code>groupadd</code>, <code>shutdown</code>), kernel namespaces, and file systems under <code>/sys</code>, <code>/proc</code>, and <code>/etc</code>. It cannot run on macOS, Windows, or BSD distributions.",
"docs_limits_sys_item2": "<strong>Systemd Dependency:</strong> The service management, boot status, and journal logging modules interact directly with <code>systemd</code> and <code>journald</code>. Runtimes using other init systems (like sysvinit, OpenRC, or Runit) are not supported.",
"docs_limits_sys_item3": "<strong>Root Privileges Required:</strong> The agent must execute as <code>root</code>. The PAM authenticator needs read access to <code>/etc/shadow</code>, and managing hardware settings (hostname, network interfaces, disk mounts) is locked behind root level syscalls by the kernel.",
"docs_limits_sys_item4": "<strong>Co-hosting CORS constraints:</strong> Running the Web UI cross-origin relative to target agents triggers browser CORS preflight checks. If you host the frontend on a different origin, you must utilize server-to-server dashboard API queries (default behavior of SvelteKit server load functions) or configure a reverse proxy to terminate both under a shared domain.",
"docs_limits_license_title": "Licensing",
"docs_limits_license_desc": "Nadir is free, open-source software published under the terms of the MIT License.",
"docs_limits_license_mit_title": "MIT License",
"docs_limits_license_mit_copyright": "Copyright (c) 2026 Urania",
"docs_limits_license_mit_body1": "Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:",
"docs_limits_license_mit_body2": "The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.",
"docs_limits_license_mit_body3": "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.",
"docs_limits_credits_title": "Credits & assets",
"docs_limits_credits_desc": "Nadir's branding assets and icons are based on open-source packages:",
"docs_limits_credits_item1": "<strong>Favicon:</strong> Recolored version of the <a class=\"text-primary hover:underline\" href=\"https://lucide.dev/icons/orbit\" target=\"_blank\" rel=\"noreferrer\">Orbit icon</a> from the Lucide project.",
"docs_limits_credits_item2": "<strong>Icons:</strong> Visual markers across pages are provided by the <a class=\"text-primary hover:underline\" href=\"https://lucide.dev\" target=\"_blank\" rel=\"noreferrer\">Lucide icons library</a>, licensed under the ISC License."
"welcome_back": "Welcome back"
}
+17
View File
@@ -91,3 +91,20 @@ export const inviteUserSchema = v.object({
role,
username: v.optional(v.string(), '')
});
export const updateProfileSchema = v.object({
name: v.pipe(v.string(m.errors_non_empty()), v.nonEmpty(m.errors_non_empty())),
username
});
export const changePasswordSchema = v.pipe(
v.object({ _confirm: confirm, _current: v.optional(v.string(), ''), newPassword }),
v.forward(
v.partialCheck(
[['newPassword'], ['_confirm']],
(input) => input.newPassword === input._confirm,
m.errors_passwords_no_match()
),
['_confirm']
)
);
+3 -3
View File
@@ -108,7 +108,9 @@ function build() {
// ponytail: single-slot cache, rebuilt on config edits via invalidateAuth().
// In-flight rate-limit counters and 2FA flow state reset on invalidation.
let cached: ReturnType<typeof build> | null = null;
let cached: null | ReturnType<typeof build> = null;
export type Auth = ReturnType<typeof build>;
export function getAuth(): ReturnType<typeof build> {
return (cached ??= build());
@@ -117,5 +119,3 @@ export function getAuth(): ReturnType<typeof build> {
export function invalidateAuth(): void {
cached = null;
}
export type Auth = ReturnType<typeof build>;
@@ -8,6 +8,7 @@
const LABELS: Record<string, () => string> = {
'/': m.home,
account: m.account,
admin: m.nav_admin,
config: m.nav_admin_config,
configure: m.networking_configure,
@@ -3,6 +3,7 @@
import ChevronsUpDownIcon from '@lucide/svelte/icons/chevrons-up-down';
import LogOutIcon from '@lucide/svelte/icons/log-out';
import { invalidateAll } from '$app/navigation';
import { resolve } from '$app/paths';
import { getAuthClient } from '$lib/auth/client';
import * as Avatar from '$lib/components/ui/avatar/index.js';
import * as DropdownMenu from '$lib/components/ui/dropdown-menu/index.js';
@@ -61,8 +62,12 @@
<DropdownMenu.Separator />
<DropdownMenu.Group>
<DropdownMenu.Item>
<BadgeCheckIcon />
{m.account()}
{#snippet child({ props })}
<a href={resolve('/account')} {...props}>
<BadgeCheckIcon />
{m.account()}
</a>
{/snippet}
</DropdownMenu.Item>
</DropdownMenu.Group>
<DropdownMenu.Separator />
+157
View File
@@ -0,0 +1,157 @@
import { error, redirect } from '@sveltejs/kit';
import { command, form, getRequestEvent, query } from '$app/server';
import { v } from '$lib';
import { changePasswordSchema, updateProfileSchema } from '$lib/auth/schemas';
import { getAuth, oauthConfig } from '$lib/auth/server';
import { m } from '$lib/paraglide/messages';
import { getConfig } from '$lib/server/config';
import { extractErrorMessage } from '$lib/utils';
function ctx() {
const { locals, request } = getRequestEvent();
if (!locals.user) redirect(307, '/auth/sign-in');
return { headers: request.headers, session: locals.session, user: locals.user };
}
const fail = (e: unknown) => {
console.log(e);
error(400, { message: extractErrorMessage(e) ?? m.errors_generic() });
};
export const getAccount = query(async () => {
const { headers, session, user } = ctx();
const cfg = getConfig();
const auth = getAuth();
const accounts = await auth.api.listUserAccounts({ headers });
const sessions = await auth.api.listSessions({ headers });
const linked = new Set(accounts.map((a) => a.providerId));
const configured = [
cfg.FACEBOOK_CLIENT_ID ? 'facebook' : undefined,
cfg.GITHUB_CLIENT_ID ? 'github' : undefined,
cfg.GOOGLE_CLIENT_ID ? 'google' : undefined
].filter((p): p is string => Boolean(p));
return {
accounts: accounts.map((a) => ({
accountId: a.accountId,
createdAt: a.createdAt,
id: a.id,
providerId: a.providerId
})),
hasPassword: linked.has('credential'),
// Providers that are configured but not linked yet.
linkable: {
generic: oauthConfig.map((o) => o.providerId).filter((p) => !linked.has(p)),
social: configured.filter((p) => !linked.has(p))
},
sessions: sessions.map((s) => ({
createdAt: s.createdAt,
current: s.token === session?.token,
expiresAt: s.expiresAt,
id: s.id,
ipAddress: s.ipAddress ?? '',
token: s.token,
userAgent: s.userAgent ?? ''
})),
twoFactorRequired: cfg.ENABLE_2FA ?? false,
user: {
createdAt: user.createdAt,
email: user.email,
emailVerified: user.emailVerified,
name: user.name,
twoFactorEnabled: user.twoFactorEnabled ?? false,
username: user.username ?? ''
}
};
});
export const updateProfile = form(updateProfileSchema, async ({ name, username }) => {
const { headers } = ctx();
try {
await getAuth().api.updateUser({
body: { displayUsername: username, name, username },
headers
});
} catch (e) {
fail(e);
}
await getAccount().refresh();
});
// Same form for both cases: with a credential account it's a change (current password
// required), without one it's the initial password for an OAuth-only user.
export const changePassword = form(changePasswordSchema, async ({ _current, newPassword }) => {
const { headers } = ctx();
const auth = getAuth();
const accounts = await auth.api.listUserAccounts({ headers });
try {
if (accounts.some((a) => a.providerId === 'credential')) {
if (!_current) error(400, { message: m.errors_non_empty() });
await auth.api.changePassword({
body: { currentPassword: _current, newPassword, revokeOtherSessions: true },
headers
});
} else {
await auth.api.setPassword({ body: { newPassword }, headers });
}
} catch (e) {
fail(e);
}
await getAccount().refresh();
});
export const sendResetLink = command(async () => {
const { user } = ctx();
try {
await getAuth().api.requestPasswordReset({
body: { email: user.email, redirectTo: getConfig().ORIGIN + '/auth/reset-password' }
});
} catch (e) {
fail(e);
}
});
export const resendVerification = command(async () => {
const { user } = ctx();
try {
await getAuth().api.sendVerificationEmail({
body: { callbackURL: '/account', email: user.email }
});
} catch (e) {
fail(e);
}
});
export const disableTwoFactor = command(v.string(), async (password) => {
const { headers } = ctx();
if (getConfig().ENABLE_2FA) error(403, { message: m.account_2fa_enforced() });
try {
await getAuth().api.disableTwoFactor({ body: { password }, headers });
} catch (e) {
fail(e);
}
await getAccount().refresh();
});
export const unlinkAccount = command(
v.object({ accountId: v.string(), providerId: v.string() }),
async ({ accountId, providerId }) => {
const { headers } = ctx();
try {
await getAuth().api.unlinkAccount({ body: { accountId, providerId }, headers });
} catch (e) {
fail(e);
}
await getAccount().refresh();
}
);
export const revokeSession = command(v.string(), async (token) => {
const { headers } = ctx();
try {
await getAuth().api.revokeSession({ body: { token }, headers });
} catch (e) {
fail(e);
}
await getAccount().refresh();
});
+7 -3
View File
@@ -8,9 +8,13 @@ import type { MailPayload } from './schemas';
export async function sendMail(data: MailPayload) {
const cfg = getConfig();
if (!cfg.SMTP_HOST) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { html: _html, ...log } = data;
console.log('\n%s\n\n', log.plainText);
// ponytail: no SMTP configured, dump the mail to stdout instead of dropping it
console.log(
'\n[email] no SMTP_HOST — not sent\nTo: %s\nSubject: %s\n\n%s\n',
data.to,
data.subject,
data.plainText
);
return;
}
+4 -1
View File
@@ -17,7 +17,10 @@
let { children } = $props();
const user = $derived(getUser());
const showSidebar = (cU: null | User) =>
cU && (page.url.pathname.startsWith('/dashboard') || page.url.pathname.startsWith('/admin'));
cU &&
(page.url.pathname.startsWith('/dashboard') ||
page.url.pathname.startsWith('/admin') ||
page.url.pathname.startsWith('/account'));
class TerminalState {
open = $state(false);
+390
View File
@@ -0,0 +1,390 @@
<script lang="ts">
import KeyRoundIcon from '@lucide/svelte/icons/key-round';
import LinkIcon from '@lucide/svelte/icons/link';
import MailCheckIcon from '@lucide/svelte/icons/mail-check';
import MonitorIcon from '@lucide/svelte/icons/monitor';
import ShieldCheckIcon from '@lucide/svelte/icons/shield-check';
import { resolve } from '$app/paths';
import { env } from '$env/dynamic/public';
import { getAuthClient } from '$lib/auth/client';
import { changePasswordSchema, updateProfileSchema } from '$lib/auth/schemas';
import PageMeta from '$lib/components/seo/page-meta.svelte';
import { Badge } from '$lib/components/ui/badge';
import { Button } from '$lib/components/ui/button';
import * as Card from '$lib/components/ui/card';
import * as Dialog from '$lib/components/ui/dialog';
import * as Field from '$lib/components/ui/field';
import { Input } from '$lib/components/ui/input';
import * as Table from '$lib/components/ui/table';
import { m } from '$lib/paraglide/messages';
import {
changePassword,
disableTwoFactor,
getAccount,
resendVerification,
revokeSession,
sendResetLink,
unlinkAccount,
updateProfile
} from '$lib/remotes/account.remote';
import { extractErrorMessage } from '$lib/utils';
import { toast } from 'svelte-sonner';
const authClient = getAuthClient();
const id = $props.id();
const account = $derived(await getAccount());
let disableOpen = $state(false);
let disablePassword = $state('');
let busy = $state(false);
const fmt = (d: Date | string) => new Date(d).toLocaleString();
const providerLabel = (p: string) =>
p === 'credential' ? m.account_provider_credential() : p.charAt(0).toUpperCase() + p.slice(1);
async function run(fn: () => Promise<unknown>, done: string) {
busy = true;
try {
await fn();
toast.success(done);
} catch (err) {
toast.error(extractErrorMessage(err) ?? m.errors_generic());
} finally {
busy = false;
}
}
async function link(provider: string, generic: boolean) {
const callbackURL = new URL('/account', env.PUBLIC_ORIGIN).href;
const { error } = generic
? await authClient.oauth2.link({ callbackURL, providerId: provider })
: await authClient.linkSocial({
callbackURL,
provider: provider as 'facebook' | 'github' | 'google'
});
if (error) toast.error(error.message || m.errors_generic());
}
</script>
<PageMeta title={m.seo_title_account()} description={m.seo_desc_account()} noIndex />
<div class="mx-auto flex w-full max-w-4xl flex-col gap-4 p-4">
<div class="flex flex-col gap-0.5">
<h1 class="text-2xl font-semibold tracking-tight">{m.account()}</h1>
<p class="text-muted-foreground text-sm">{m.account_description()}</p>
</div>
<!-- Profile + email -->
<Card.Root>
<Card.Header>
<Card.Title>{m.account_profile_title()}</Card.Title>
<Card.Description>
{m.account_profile_description()} · {m.account_member_since({
date: fmt(account.user.createdAt)
})}
</Card.Description>
</Card.Header>
<Card.Content>
<form
oninput={() => updateProfile.validate()}
{...updateProfile.preflight(updateProfileSchema).enhance(async ({ submit }) => {
try {
await submit();
toast.success(m.saved());
} catch (err) {
toast.error(extractErrorMessage(err) ?? m.errors_generic());
}
})}
>
<Field.Group>
<Field.Field>
<Field.Label for="name-{id}">{m.name()}</Field.Label>
<Input
id="name-{id}"
{...updateProfile.fields.name.as('text')}
value={account.user.name}
/>
{#each updateProfile.fields.name.issues() as issue, i (`${issue}-${i}`)}
<Field.Error>{issue.message}</Field.Error>
{/each}
</Field.Field>
<Field.Field>
<Field.Label for="username-{id}">{m.username()}</Field.Label>
<Input
id="username-{id}"
autocomplete="username"
{...updateProfile.fields.username.as('text')}
value={account.user.username}
/>
{#each updateProfile.fields.username.issues() as issue, i (`${issue}-${i}`)}
<Field.Error>{issue.message}</Field.Error>
{/each}
</Field.Field>
<Field.Field>
<Field.Label>{m.email()}</Field.Label>
<div class="flex flex-wrap items-center gap-2">
<span class="text-sm">{account.user.email}</span>
{#if account.user.emailVerified}
<Badge variant="secondary"
><MailCheckIcon class="size-3" />{m.account_email_verified()}</Badge
>
{:else}
<Badge variant="destructive">{m.account_email_unverified()}</Badge>
<Button
type="button"
variant="outline"
size="sm"
disabled={busy}
onclick={() => run(() => resendVerification(), m.account_verification_sent())}
>
{m.account_resend_verification()}
</Button>
{/if}
</div>
</Field.Field>
<Field.Field>
<Button type="submit" disabled={!!updateProfile.pending}>{m.save()}</Button>
</Field.Field>
</Field.Group>
</form>
</Card.Content>
</Card.Root>
<!-- Password -->
<Card.Root>
<Card.Header>
<Card.Title class="flex items-center gap-2">
<KeyRoundIcon class="size-4" />
{account.hasPassword ? m.account_password_title() : m.account_password_set_title()}
</Card.Title>
<Card.Description>
{account.hasPassword
? m.account_password_description()
: m.account_password_set_description()}
</Card.Description>
</Card.Header>
<Card.Content>
<form
oninput={() => changePassword.validate()}
{...changePassword.preflight(changePasswordSchema).enhance(async ({ submit }) => {
try {
await submit();
toast.success(m.account_password_changed());
} catch (err) {
toast.error(extractErrorMessage(err) ?? m.errors_generic());
}
})}
>
<Field.Group>
{#if account.hasPassword}
<Field.Field>
<Field.Label for="current-{id}">{m.account_current_password()}</Field.Label>
<Input
id="current-{id}"
autocomplete="current-password"
{...changePassword.fields._current.as('password')}
required
/>
</Field.Field>
{/if}
<Field.Field>
<Field.Label for="new-{id}">{m.new_password()}</Field.Label>
<Input
id="new-{id}"
autocomplete="new-password"
{...changePassword.fields.newPassword.as('password')}
required
/>
<Field.Description>{m.password_hint()}</Field.Description>
{#each changePassword.fields.newPassword.issues() as issue, i (`${issue}-${i}`)}
<Field.Error>{issue.message}</Field.Error>
{/each}
</Field.Field>
<Field.Field>
<Field.Label for="confirm-{id}">{m.confirm_password()}</Field.Label>
<Input
id="confirm-{id}"
autocomplete="new-password"
{...changePassword.fields._confirm.as('password')}
required
/>
{#each changePassword.fields._confirm.issues() as issue, i (`${issue}-${i}`)}
<Field.Error>{issue.message}</Field.Error>
{/each}
</Field.Field>
<Field.Field orientation="horizontal">
<Button type="submit" disabled={!!changePassword.pending}>{m.save()}</Button>
<Button
type="button"
variant="outline"
disabled={busy}
onclick={() => run(() => sendResetLink(), m.reset_link_sent())}
>
{m.account_send_reset_link()}
</Button>
</Field.Field>
</Field.Group>
</form>
</Card.Content>
</Card.Root>
<!-- Two-factor -->
<Card.Root>
<Card.Header>
<Card.Title class="flex items-center gap-2">
<ShieldCheckIcon class="size-4" />
{m.account_2fa_title()}
</Card.Title>
<Card.Description>{m.account_2fa_description()}</Card.Description>
</Card.Header>
<Card.Content class="flex flex-wrap items-center gap-3">
<Badge variant={account.user.twoFactorEnabled ? 'secondary' : 'outline'}>
{account.user.twoFactorEnabled ? m.account_2fa_enabled() : m.account_2fa_disabled()}
</Badge>
{#if !account.user.twoFactorEnabled}
<Button href={resolve('/auth/setup-2fa')} variant="outline" size="sm">
{m.account_2fa_enable()}
</Button>
{:else if account.twoFactorRequired}
<span class="text-muted-foreground text-sm">{m.account_2fa_enforced()}</span>
{:else}
<Button variant="outline" size="sm" onclick={() => (disableOpen = true)}>
{m.account_2fa_disable()}
</Button>
{/if}
</Card.Content>
</Card.Root>
<!-- Connected accounts -->
<Card.Root>
<Card.Header>
<Card.Title class="flex items-center gap-2">
<LinkIcon class="size-4" />
{m.account_providers_title()}
</Card.Title>
<Card.Description>{m.account_providers_description()}</Card.Description>
</Card.Header>
<Card.Content class="flex flex-col gap-2">
{#each account.accounts as acc (acc.id)}
<div class="flex flex-wrap items-center gap-2 rounded-md border p-3">
<span class="font-medium">{providerLabel(acc.providerId)}</span>
<span class="text-muted-foreground text-sm">
{m.account_linked_on({ date: fmt(acc.createdAt) })}
</span>
<Button
class="ms-auto"
variant="outline"
size="sm"
disabled={busy || account.accounts.length < 2}
title={account.accounts.length < 2 ? m.account_unlink_last_hint() : undefined}
onclick={() =>
run(
() => unlinkAccount({ accountId: acc.accountId, providerId: acc.providerId }),
m.account_unlinked()
)}
>
{m.account_unlink()}
</Button>
</div>
{/each}
{#if account.linkable.social.length || account.linkable.generic.length}
<div class="flex flex-wrap gap-2 pt-2">
{#each account.linkable.social as provider (provider)}
<Button variant="outline" size="sm" onclick={() => link(provider, false)}>
{m.account_link()} · {providerLabel(provider)}
</Button>
{/each}
{#each account.linkable.generic as provider (provider)}
<Button variant="outline" size="sm" onclick={() => link(provider, true)}>
{m.account_link()} · {providerLabel(provider)}
</Button>
{/each}
</div>
{/if}
</Card.Content>
</Card.Root>
<!-- Sessions -->
<Card.Root>
<Card.Header>
<Card.Title class="flex items-center gap-2">
<MonitorIcon class="size-4" />
{m.account_sessions_title()}
</Card.Title>
<Card.Description>{m.account_sessions_description()}</Card.Description>
</Card.Header>
<Card.Content>
<Table.Root>
<Table.Header>
<Table.Row>
<Table.Head>{m.account_sessions_title()}</Table.Head>
<Table.Head class="hidden md:table-cell">IP</Table.Head>
<Table.Head class="hidden md:table-cell">{m.account_session_expires()}</Table.Head>
<Table.Head></Table.Head>
</Table.Row>
</Table.Header>
<Table.Body>
{#each account.sessions as s (s.id)}
<Table.Row>
<Table.Cell class="max-w-[24ch] truncate" title={s.userAgent}>
{s.userAgent || '—'}
{#if s.current}<Badge variant="secondary">{m.account_session_current()}</Badge>{/if}
</Table.Cell>
<Table.Cell class="hidden md:table-cell">{s.ipAddress || '—'}</Table.Cell>
<Table.Cell class="hidden md:table-cell">{fmt(s.expiresAt)}</Table.Cell>
<Table.Cell class="text-end">
{#if !s.current}
<Button
variant="ghost"
size="sm"
disabled={busy}
onclick={() => run(() => revokeSession(s.token), m.account_session_revoked())}
>
{m.account_revoke()}
</Button>
{/if}
</Table.Cell>
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={4} class="text-muted-foreground text-center">
{m.account_no_sessions()}
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</Card.Content>
</Card.Root>
</div>
<Dialog.Root bind:open={disableOpen}>
<Dialog.Content>
<Dialog.Header>
<Dialog.Title>{m.account_2fa_disable()}</Dialog.Title>
<Dialog.Description>{m.enter_password_to_continue()}</Dialog.Description>
</Dialog.Header>
<Field.Field>
<Field.Label for="disable-2fa-{id}">{m.password()}</Field.Label>
<Input
id="disable-2fa-{id}"
type="password"
autocomplete="current-password"
bind:value={disablePassword}
/>
</Field.Field>
<Dialog.Footer>
<Button variant="outline" onclick={() => (disableOpen = false)}>{m.cancel()}</Button>
<Button
disabled={busy || !disablePassword}
onclick={async () => {
await run(() => disableTwoFactor(disablePassword), m.account_2fa_disabled_done());
disablePassword = '';
disableOpen = false;
}}
>
{m.account_2fa_disable()}
</Button>
</Dialog.Footer>
</Dialog.Content>
</Dialog.Root>