Add /spr/ route for soleprint index, fix sidebar system labels
This commit is contained in:
@@ -144,6 +144,9 @@ class GoogleOAuth:
|
||||
"name": userinfo.get("name"),
|
||||
"picture": userinfo.get("picture"),
|
||||
"hd": userinfo.get("hd"), # Hosted domain (Google Workspace)
|
||||
# Include tokens for storage
|
||||
"access_token": credentials.token,
|
||||
"refresh_token": credentials.refresh_token,
|
||||
}
|
||||
|
||||
def refresh_access_token(self, refresh_token: str) -> dict:
|
||||
|
||||
@@ -15,6 +15,7 @@ class AuthConfig(BaseModel):
|
||||
enabled: bool = False
|
||||
provider: str = "google" # Vein name to use for auth
|
||||
allowed_domains: list[str] = [] # Empty = allow any domain
|
||||
allowed_emails: list[str] = [] # Specific emails to allow
|
||||
session_secret: str = "" # Required if enabled, can be "ENV:VAR_NAME"
|
||||
session_timeout_hours: int = 24
|
||||
login_redirect: str = "/"
|
||||
|
||||
@@ -4,6 +4,7 @@ Authentication middleware for route protection.
|
||||
Generic middleware, provider-agnostic.
|
||||
"""
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
@@ -11,6 +12,10 @@ from starlette.responses import JSONResponse, RedirectResponse
|
||||
|
||||
from .config import AuthConfig
|
||||
|
||||
# Local dev bypass - set via environment variable only, can't be triggered remotely
|
||||
AUTH_BYPASS = os.environ.get("AUTH_BYPASS", "").lower() == "true"
|
||||
AUTH_BYPASS_USER = os.environ.get("AUTH_BYPASS_USER", "dev@local")
|
||||
|
||||
|
||||
class AuthMiddleware(BaseHTTPMiddleware):
|
||||
"""
|
||||
@@ -31,6 +36,15 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request, call_next):
|
||||
path = request.url.path
|
||||
|
||||
# Local dev bypass - auto-authenticate
|
||||
if AUTH_BYPASS:
|
||||
request.state.user = {
|
||||
"email": AUTH_BYPASS_USER,
|
||||
"name": "Dev User",
|
||||
"domain": "local",
|
||||
}
|
||||
return await call_next(request)
|
||||
|
||||
# Check if route is public
|
||||
if self._is_public(path):
|
||||
return await call_next(request)
|
||||
@@ -49,15 +63,20 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
||||
session.clear()
|
||||
return self._unauthorized(request, "Session expired")
|
||||
|
||||
# Check domain restriction
|
||||
# Check domain/email restriction
|
||||
user_domain = session.get("domain")
|
||||
if self.config.allowed_domains:
|
||||
if not user_domain or user_domain not in self.config.allowed_domains:
|
||||
session.clear()
|
||||
return self._unauthorized(
|
||||
request,
|
||||
f"Access restricted to: {', '.join(self.config.allowed_domains)}",
|
||||
)
|
||||
email_allowed = user_email in self.config.allowed_emails
|
||||
domain_allowed = user_domain and user_domain in self.config.allowed_domains
|
||||
no_restrictions = (
|
||||
not self.config.allowed_domains and not self.config.allowed_emails
|
||||
)
|
||||
|
||||
if not (email_allowed or domain_allowed or no_restrictions):
|
||||
session.clear()
|
||||
return self._unauthorized(
|
||||
request,
|
||||
f"Access restricted",
|
||||
)
|
||||
|
||||
# Attach user to request state for downstream use
|
||||
request.state.user = {
|
||||
|
||||
@@ -111,15 +111,18 @@ async def callback(
|
||||
except httpx.RequestError as e:
|
||||
raise HTTPException(500, f"Failed to contact provider: {e}")
|
||||
|
||||
# Verify domain if restricted
|
||||
# Verify domain/email restriction
|
||||
user_email = user_info.get("email")
|
||||
user_domain = user_info.get("hd")
|
||||
if auth_config.allowed_domains:
|
||||
if not user_domain or user_domain not in auth_config.allowed_domains:
|
||||
raise HTTPException(
|
||||
403,
|
||||
f"Access restricted to: {', '.join(auth_config.allowed_domains)}. "
|
||||
f"Your account is from: {user_domain or 'personal Gmail'}",
|
||||
)
|
||||
email_allowed = user_email in auth_config.allowed_emails
|
||||
domain_allowed = user_domain and user_domain in auth_config.allowed_domains
|
||||
no_restrictions = not auth_config.allowed_domains and not auth_config.allowed_emails
|
||||
|
||||
if not (email_allowed or domain_allowed or no_restrictions):
|
||||
raise HTTPException(
|
||||
403,
|
||||
f"Access restricted. Your account ({user_email}) is not authorized.",
|
||||
)
|
||||
|
||||
# Create session
|
||||
expires_at = datetime.now() + timedelta(hours=auth_config.session_timeout_hours)
|
||||
|
||||
@@ -92,18 +92,19 @@
|
||||
function renderSystemSection(systemKey, title) {
|
||||
const veins = config.veins || [];
|
||||
|
||||
// System icon - ALWAYS shown as non-clickable separator
|
||||
// System link with icon and label (same as soleprint)
|
||||
let html = `
|
||||
<div class="spr-sidebar-icon" title="${title}">
|
||||
<a href="${SPR_BASE}/${systemKey}" class="spr-sidebar-item" title="${title}">
|
||||
${icons[systemKey]}
|
||||
</div>
|
||||
<span class="label">${title}</span>
|
||||
<span class="tooltip">${title}</span>
|
||||
</a>
|
||||
`;
|
||||
|
||||
// Nested elements below icon - auth-gated
|
||||
// Nested elements below - auth-gated
|
||||
if (user) {
|
||||
// Logged in
|
||||
// Logged in - show vein links under artery
|
||||
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}">
|
||||
@@ -112,14 +113,6 @@
|
||||
</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
|
||||
@@ -159,7 +152,7 @@
|
||||
|
||||
<div class="spr-sidebar-divider"></div>
|
||||
|
||||
<a href="${SPR_BASE}/" class="spr-sidebar-item active" title="Soleprint">
|
||||
<a href="/spr/" class="spr-sidebar-item active" title="Soleprint">
|
||||
${icons.soleprint}
|
||||
<span class="label">Soleprint</span>
|
||||
<span class="tooltip">Soleprint</span>
|
||||
@@ -204,9 +197,9 @@
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
// Include current page as redirect target after login
|
||||
const redirectUrl = encodeURIComponent(window.location.href);
|
||||
const loginUrl = `${config.auth.login_url}?redirect=${redirectUrl}`;
|
||||
// Include current path as redirect target after login (relative, not full URL)
|
||||
const redirectPath = window.location.pathname + window.location.search;
|
||||
const loginUrl = `${config.auth.login_url}?redirect=${encodeURIComponent(redirectPath)}`;
|
||||
return `
|
||||
<div class="spr-sidebar-user">
|
||||
<a href="${loginUrl}" class="spr-sidebar-item" title="Login with Google">
|
||||
|
||||
Reference in New Issue
Block a user