9992105b49
Deploy Dashboard (Dev) / deploy-dev (push) Successful in 1m14s
- Add rbac.py with User model, LDAP group-to-role mapping, page/write dependencies - Proxy auth reads Remote-Groups header to resolve role from LDAP groups - JWT tokens carry role claim, /me returns role + allowed pages - Per-router page access control and viewer write protection - Terminal WebSocket RBAC check - Frontend filters sidebar links and routes by allowed pages - Admin-only Settings page and RBAC override endpoints - Mount rbac.json in docker-compose for page config persistence
144 lines
4.7 KiB
Svelte
144 lines
4.7 KiB
Svelte
<script>
|
|
import Sidebar from "./components/Sidebar.svelte";
|
|
import Dashboard from "./routes/Dashboard.svelte";
|
|
import Docker from "./routes/Docker.svelte";
|
|
import Gitea from "./routes/Gitea.svelte";
|
|
import Files from "./routes/Files.svelte";
|
|
import Terminal from "./routes/Terminal.svelte";
|
|
import OpenClaw from "./routes/OpenClaw.svelte";
|
|
import Settings from "./routes/Settings.svelte";
|
|
import ChatSummary from "./routes/ChatSummary.svelte";
|
|
import Security from "./routes/Security.svelte";
|
|
import Login from "./routes/Login.svelte";
|
|
import { onMount } from "svelte";
|
|
import { getToken, checkAuth, setToken, setRefreshToken, currentUser, setCurrentUser } from "./lib/api.js";
|
|
|
|
let page = $state("dashboard");
|
|
let dark = $state(false);
|
|
let sidebarOpen = $state(false);
|
|
let authorized = $state(false);
|
|
let loading = $state(true);
|
|
let userRole = $state("");
|
|
let allowedPages = $state([]);
|
|
|
|
async function fetchUserInfo() {
|
|
try {
|
|
const data = await checkAuth();
|
|
setCurrentUser(data);
|
|
userRole = data.role || "admin";
|
|
allowedPages = data.pages || "*";
|
|
} catch (e) {
|
|
console.error("Failed to fetch user info:", e);
|
|
}
|
|
}
|
|
|
|
function hasPageAccess(pageId) {
|
|
if (allowedPages === "*") return true;
|
|
return Array.isArray(allowedPages) && allowedPages.includes(pageId);
|
|
}
|
|
|
|
onMount(async () => {
|
|
// Theme init
|
|
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
|
dark = true;
|
|
document.documentElement.classList.add('dark');
|
|
} else {
|
|
dark = false;
|
|
document.documentElement.classList.remove('dark');
|
|
}
|
|
|
|
// Try proxy auth first (Authelia forward-auth sets Remote-User header)
|
|
try {
|
|
const proxyRes = await fetch("/api/auth/proxy");
|
|
if (proxyRes.ok) {
|
|
const data = await proxyRes.json();
|
|
setToken(data.access_token);
|
|
setRefreshToken(data.refresh_token);
|
|
authorized = true;
|
|
await fetchUserInfo();
|
|
loading = false;
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
// Proxy auth not available, fall through to regular auth
|
|
}
|
|
|
|
// Regular token auth check
|
|
const token = getToken();
|
|
if (!token) {
|
|
loading = false;
|
|
authorized = false;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await fetchUserInfo();
|
|
authorized = true;
|
|
} catch (e) {
|
|
console.error("Auth check failed:", e);
|
|
setToken("");
|
|
authorized = false;
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
});
|
|
|
|
function toggleTheme() {
|
|
dark = !dark;
|
|
if (dark) {
|
|
document.documentElement.classList.add("dark");
|
|
localStorage.theme = 'dark';
|
|
} else {
|
|
document.documentElement.classList.remove("dark");
|
|
localStorage.theme = 'light';
|
|
}
|
|
}
|
|
|
|
function logout() {
|
|
setToken("");
|
|
setRefreshToken("");
|
|
window.location.reload();
|
|
}
|
|
</script>
|
|
|
|
{#if loading}
|
|
<div class="flex h-screen items-center justify-center bg-surface-50 dark:bg-surface-950">
|
|
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
|
|
</div>
|
|
{:else if !authorized}
|
|
<Login />
|
|
{:else}
|
|
<div class="flex h-screen overflow-hidden bg-surface-50 {dark ? 'dark bg-surface-900' : ''}">
|
|
<Sidebar bind:page {dark} {toggleTheme} {logout} bind:sidebarOpen {allowedPages} {userRole} />
|
|
<main class="flex-1 overflow-auto">
|
|
<div class="max-w-[1400px] mx-auto p-6 lg:p-8">
|
|
<!-- Mobile hamburger -->
|
|
<button class="md:hidden mb-4 p-2 rounded-lg hover:bg-surface-200 {dark ? 'hover:bg-surface-700 text-surface-300' : 'text-surface-600'}" onclick={() => sidebarOpen = true} aria-label="Open menu">
|
|
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" /></svg>
|
|
</button>
|
|
{#if page === "dashboard" && hasPageAccess("dashboard")}
|
|
<Dashboard />
|
|
{:else if page === "docker" && hasPageAccess("docker")}
|
|
<Docker />
|
|
{:else if page === "gitea" && hasPageAccess("gitea")}
|
|
<Gitea />
|
|
{:else if page === "files" && hasPageAccess("files")}
|
|
<Files />
|
|
{:else if page === "terminal" && hasPageAccess("terminal")}
|
|
<Terminal />
|
|
{:else if page === "openclaw" && hasPageAccess("openclaw")}
|
|
<OpenClaw />
|
|
{:else if page === "chat-digest" && hasPageAccess("chat-digest")}
|
|
<ChatSummary />
|
|
{:else if page === "settings" && userRole === "admin"}
|
|
<Settings />
|
|
{:else if page === "security" && hasPageAccess("security")}
|
|
<Security />
|
|
{:else}
|
|
<Dashboard />
|
|
{/if}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
{/if}
|