homogeineze sidebar conf
This commit is contained in:
@@ -11,5 +11,6 @@
|
|||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"enabled": false
|
"enabled": false
|
||||||
}
|
},
|
||||||
|
"veins": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -135,9 +135,25 @@ async def oauth_callback(
|
|||||||
if not code:
|
if not code:
|
||||||
raise HTTPException(400, "Missing authorization code")
|
raise HTTPException(400, "Missing authorization code")
|
||||||
|
|
||||||
|
# Extract redirect URL from state if present
|
||||||
|
# Format: "csrf_state|redirect_url" OR just "redirect_url" if no csrf state
|
||||||
|
redirect_url = None
|
||||||
|
if state:
|
||||||
|
if "|" in state:
|
||||||
|
parts = state.split("|", 1)
|
||||||
|
redirect_url = parts[1] if len(parts) > 1 else None
|
||||||
|
elif state.startswith("http"):
|
||||||
|
# No csrf state, just the redirect URL
|
||||||
|
redirect_url = state
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tokens = oauth_client.exchange_code_for_tokens(code)
|
tokens = oauth_client.exchange_code_for_tokens(code)
|
||||||
token_storage.save_tokens(DEFAULT_USER_ID, tokens)
|
token_storage.save_tokens(DEFAULT_USER_ID, tokens)
|
||||||
|
|
||||||
|
# Redirect back to original page if specified
|
||||||
|
if redirect_url:
|
||||||
|
return RedirectResponse(url=redirect_url)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"message": "Successfully authenticated with Google",
|
"message": "Successfully authenticated with Google",
|
||||||
@@ -164,6 +180,32 @@ async def get_userinfo(
|
|||||||
raise HTTPException(400, f"Failed to get user info: {e}")
|
raise HTTPException(400, f"Failed to get user info: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/oauth/status")
|
||||||
|
async def oauth_status():
|
||||||
|
"""Check if user is authenticated (for sidebar)."""
|
||||||
|
try:
|
||||||
|
tokens = token_storage.load_tokens(DEFAULT_USER_ID)
|
||||||
|
if not tokens:
|
||||||
|
return {"authenticated": False}
|
||||||
|
|
||||||
|
# Check if expired
|
||||||
|
if token_storage.is_expired(tokens):
|
||||||
|
if "refresh_token" not in tokens:
|
||||||
|
return {"authenticated": False}
|
||||||
|
try:
|
||||||
|
new_tokens = oauth_client.refresh_access_token(tokens["refresh_token"])
|
||||||
|
token_storage.save_tokens(DEFAULT_USER_ID, new_tokens)
|
||||||
|
except Exception:
|
||||||
|
return {"authenticated": False}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"authenticated": True,
|
||||||
|
"user": {"email": f"{DEFAULT_USER_ID}@authenticated"},
|
||||||
|
}
|
||||||
|
except Exception:
|
||||||
|
return {"authenticated": False}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/oauth/logout")
|
@router.get("/oauth/logout")
|
||||||
async def logout():
|
async def logout():
|
||||||
"""Clear stored tokens."""
|
"""Clear stored tokens."""
|
||||||
|
|||||||
@@ -215,56 +215,12 @@
|
|||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
{% if managed %}
|
||||||
|
<link rel="stylesheet" href="/sidebar.css">
|
||||||
|
<script src="/sidebar.js"></script>
|
||||||
|
{% endif %}
|
||||||
</head>
|
</head>
|
||||||
<body{% if managed %} class="has-sidebar"{% endif %}>
|
<body{% if managed %} class="has-sidebar"{% endif %}>
|
||||||
{% if managed %}
|
|
||||||
<nav class="sidebar">
|
|
||||||
{% if managed_url %}
|
|
||||||
<a href="{{ managed_url }}" class="sidebar-item" title="{{ managed.name }}">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<rect x="3" y="3" width="18" height="18" rx="2"/>
|
|
||||||
<path d="M9 3v18M3 9h6"/>
|
|
||||||
</svg>
|
|
||||||
<span class="tooltip">{{ managed.name }}</span>
|
|
||||||
</a>
|
|
||||||
<div class="sidebar-divider"></div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<a href="/" class="sidebar-item active" title="Soleprint">
|
|
||||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
|
||||||
<ellipse cx="8" cy="10" rx="3" ry="5" transform="rotate(-10 8 10)"/>
|
|
||||||
<ellipse cx="8" cy="17" rx="2.5" ry="3" transform="rotate(-10 8 17)"/>
|
|
||||||
<ellipse cx="16" cy="8" rx="3" ry="5" transform="rotate(10 16 8)"/>
|
|
||||||
<ellipse cx="16" cy="15" rx="2.5" ry="3" transform="rotate(10 16 15)"/>
|
|
||||||
</svg>
|
|
||||||
<span class="tooltip">Soleprint</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="sidebar-divider"></div>
|
|
||||||
|
|
||||||
<div class="sidebar-icon" title="Artery">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<path d="M12 2v8M12 10L6 18M12 10l6 8"/>
|
|
||||||
<circle cx="12" cy="10" r="2"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sidebar-icon" title="Atlas">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<circle cx="12" cy="12" r="8"/>
|
|
||||||
<path d="M12 4v16M4 12h16"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sidebar-icon" title="Station">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<rect x="4" y="6" width="16" height="12" rx="1"/>
|
|
||||||
<circle cx="9" cy="11" r="2"/>
|
|
||||||
<circle cx="15" cy="11" r="2"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<!-- Two shoe prints walking -->
|
<!-- Two shoe prints walking -->
|
||||||
|
|||||||
@@ -553,20 +553,16 @@ def sidebar_config(request: Request):
|
|||||||
host_no_port = host.split(":")[0]
|
host_no_port = host.split(":")[0]
|
||||||
scheme = request.headers.get("x-forwarded-proto", "http")
|
scheme = request.headers.get("x-forwarded-proto", "http")
|
||||||
|
|
||||||
# Soleprint tools are at /spr/* when accessed via <room>.spr.<domain>
|
# Soleprint tools are at root (no prefix needed)
|
||||||
soleprint_base = "/spr"
|
soleprint_base = ""
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"room": managed.get("name", "standalone"),
|
"room": managed.get("name", "standalone"),
|
||||||
"soleprint_base": soleprint_base,
|
"soleprint_base": soleprint_base,
|
||||||
"auth_enabled": auth.get("enabled", False),
|
"auth_enabled": auth.get("enabled", False),
|
||||||
"tools": {
|
"veins": config.get("veins", []),
|
||||||
"artery": f"{soleprint_base}/artery",
|
|
||||||
"atlas": f"{soleprint_base}/atlas",
|
|
||||||
"station": f"{soleprint_base}/station",
|
|
||||||
},
|
|
||||||
"auth": {
|
"auth": {
|
||||||
"login_url": f"{soleprint_base}/artery/google/oauth/login",
|
"login_url": f"{soleprint_base}/artery/google/oauth/start",
|
||||||
"status_url": f"{soleprint_base}/artery/google/oauth/status",
|
"status_url": f"{soleprint_base}/artery/google/oauth/status",
|
||||||
"logout_url": f"{soleprint_base}/artery/google/oauth/logout",
|
"logout_url": f"{soleprint_base}/artery/google/oauth/logout",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -147,6 +147,63 @@ body.spr-sidebar-active.spr-sidebar-expanded {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Vein items under artery icon */
|
||||||
|
.spr-vein-item {
|
||||||
|
width: 36px;
|
||||||
|
height: 28px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
margin-left: 8px;
|
||||||
|
background: #252525;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spr-vein-item .label {
|
||||||
|
display: block !important;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#spr-sidebar.expanded .spr-vein-item {
|
||||||
|
margin-left: 20px;
|
||||||
|
width: calc(100% - 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* System links (atlas, station when logged in) */
|
||||||
|
.spr-system-link {
|
||||||
|
height: 28px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Locked state (not logged in) */
|
||||||
|
.spr-sidebar-locked {
|
||||||
|
width: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #555;
|
||||||
|
font-size: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spr-sidebar-locked .lock-text {
|
||||||
|
writing-mode: vertical-rl;
|
||||||
|
text-orientation: mixed;
|
||||||
|
transform: rotate(180deg);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#spr-sidebar.expanded .spr-sidebar-locked {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#spr-sidebar.expanded .spr-sidebar-locked .lock-text {
|
||||||
|
writing-mode: horizontal-tb;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* User/Auth Section */
|
/* User/Auth Section */
|
||||||
.spr-sidebar-user {
|
.spr-sidebar-user {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
|
|||||||
@@ -4,7 +4,8 @@
|
|||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const SPR_BASE = "/spr";
|
// Soleprint routes are at root (no prefix)
|
||||||
|
const SPR_BASE = "";
|
||||||
|
|
||||||
let config = null;
|
let config = null;
|
||||||
let user = null;
|
let user = null;
|
||||||
@@ -56,7 +57,7 @@
|
|||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
try {
|
try {
|
||||||
// Load config
|
// Load config using dynamic base URL
|
||||||
const response = await fetch(`${SPR_BASE}/api/sidebar/config`);
|
const response = await fetch(`${SPR_BASE}/api/sidebar/config`);
|
||||||
config = await response.json();
|
config = await response.json();
|
||||||
|
|
||||||
@@ -88,6 +89,50 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderSystemSection(systemKey, title) {
|
||||||
|
const veins = config.veins || [];
|
||||||
|
|
||||||
|
// System icon - ALWAYS shown as non-clickable separator
|
||||||
|
let html = `
|
||||||
|
<div class="spr-sidebar-icon" title="${title}">
|
||||||
|
${icons[systemKey]}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Nested elements below icon - auth-gated
|
||||||
|
if (user) {
|
||||||
|
// Logged in
|
||||||
|
if (systemKey === "artery") {
|
||||||
|
// Show vein links under artery
|
||||||
|
for (const vein of veins) {
|
||||||
|
html += `
|
||||||
|
<a href="${SPR_BASE}/artery/${vein}" class="spr-sidebar-item spr-vein-item" title="${vein}">
|
||||||
|
<span class="label">${vein}</span>
|
||||||
|
<span class="tooltip">${vein}</span>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Atlas/Station: clickable link to main page
|
||||||
|
html += `
|
||||||
|
<a href="${SPR_BASE}/${systemKey}" class="spr-sidebar-item spr-system-link" title="${title}">
|
||||||
|
<span class="label">${title}</span>
|
||||||
|
<span class="tooltip">${title}</span>
|
||||||
|
</a>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
} else if (config.auth_enabled) {
|
||||||
|
// Not logged in: show "login to access" below each system icon
|
||||||
|
html += `
|
||||||
|
<div class="spr-sidebar-locked">
|
||||||
|
<span class="lock-text">login to access</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
function createSidebar() {
|
function createSidebar() {
|
||||||
// Add body class
|
// Add body class
|
||||||
document.body.classList.add("spr-sidebar-active");
|
document.body.classList.add("spr-sidebar-active");
|
||||||
@@ -122,17 +167,9 @@
|
|||||||
|
|
||||||
<div class="spr-sidebar-divider"></div>
|
<div class="spr-sidebar-divider"></div>
|
||||||
|
|
||||||
<div class="spr-sidebar-icon" title="Artery">
|
${renderSystemSection("artery", "Artery")}
|
||||||
${icons.artery}
|
${renderSystemSection("atlas", "Atlas")}
|
||||||
</div>
|
${renderSystemSection("station", "Station")}
|
||||||
|
|
||||||
<div class="spr-sidebar-icon" title="Atlas">
|
|
||||||
${icons.atlas}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="spr-sidebar-icon" title="Station">
|
|
||||||
${icons.station}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="spr-sidebar-spacer"></div>
|
<div class="spr-sidebar-spacer"></div>
|
||||||
|
|
||||||
@@ -167,9 +204,12 @@
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
|
// Include current page as redirect target after login
|
||||||
|
const redirectUrl = encodeURIComponent(window.location.href);
|
||||||
|
const loginUrl = `${config.auth.login_url}?redirect=${redirectUrl}`;
|
||||||
return `
|
return `
|
||||||
<div class="spr-sidebar-user">
|
<div class="spr-sidebar-user">
|
||||||
<a href="${config.auth.login_url}" class="spr-sidebar-item" title="Login with Google">
|
<a href="${loginUrl}" class="spr-sidebar-item" title="Login with Google">
|
||||||
${icons.google}
|
${icons.google}
|
||||||
<span class="label">Login</span>
|
<span class="label">Login</span>
|
||||||
<span class="tooltip">Login</span>
|
<span class="tooltip">Login</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user