Add /spr/ route for soleprint index, fix sidebar system labels
This commit is contained in:
@@ -1,16 +1,143 @@
|
|||||||
{
|
{
|
||||||
"managed": {
|
|
||||||
"name": "sample",
|
|
||||||
"repos": {
|
|
||||||
"frontend": "/path/to/your/frontend/repo",
|
|
||||||
"backend": "/path/to/your/backend/repo"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"framework": {
|
"framework": {
|
||||||
"name": "soleprint"
|
"name": "soleprint",
|
||||||
|
"slug": "soleprint",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Development workflow and documentation system",
|
||||||
|
"tagline": "Mapping development footprints",
|
||||||
|
"icon": "",
|
||||||
|
"hub_port": 12030
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"enabled": false
|
"enabled": true,
|
||||||
|
"provider": "google",
|
||||||
|
"allowed_domains": [],
|
||||||
|
"allowed_emails": [],
|
||||||
|
"session_secret": "ENV:AUTH_SESSION_SECRET"
|
||||||
},
|
},
|
||||||
"veins": []
|
"veins": ["google"],
|
||||||
|
"managed": {
|
||||||
|
"name": "sample"
|
||||||
|
},
|
||||||
|
"systems": [
|
||||||
|
{
|
||||||
|
"key": "data_flow",
|
||||||
|
"name": "artery",
|
||||||
|
"slug": "artery",
|
||||||
|
"title": "Artery",
|
||||||
|
"tagline": "Todo lo vital",
|
||||||
|
"icon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "documentation",
|
||||||
|
"name": "atlas",
|
||||||
|
"slug": "atlas",
|
||||||
|
"title": "Atlas",
|
||||||
|
"tagline": "Documentacion accionable",
|
||||||
|
"icon": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "execution",
|
||||||
|
"name": "station",
|
||||||
|
"slug": "station",
|
||||||
|
"title": "Station",
|
||||||
|
"tagline": "Monitores, Entornos y Herramientas",
|
||||||
|
"icon": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"components": {
|
||||||
|
"shared": {
|
||||||
|
"config": {
|
||||||
|
"name": "room",
|
||||||
|
"title": "Room",
|
||||||
|
"description": "Runtime environment configuration",
|
||||||
|
"plural": "rooms"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"name": "depot",
|
||||||
|
"title": "Depot",
|
||||||
|
"description": "Data storage / provisions",
|
||||||
|
"plural": "depots"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data_flow": {
|
||||||
|
"connector": {
|
||||||
|
"name": "vein",
|
||||||
|
"title": "Vein",
|
||||||
|
"description": "Stateless API connector",
|
||||||
|
"plural": "veins"
|
||||||
|
},
|
||||||
|
"mock": {
|
||||||
|
"name": "shunt",
|
||||||
|
"title": "Shunt",
|
||||||
|
"description": "Fake connector for testing",
|
||||||
|
"plural": "shunts"
|
||||||
|
},
|
||||||
|
"composed": {
|
||||||
|
"name": "pulse",
|
||||||
|
"title": "Pulse",
|
||||||
|
"description": "Composed data flow",
|
||||||
|
"plural": "pulses",
|
||||||
|
"formula": "Vein + Room + Depot"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"name": "plexus",
|
||||||
|
"title": "Plexus",
|
||||||
|
"description": "Full app with backend, frontend and DB",
|
||||||
|
"plural": "plexus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"documentation": {
|
||||||
|
"pattern": {
|
||||||
|
"name": "template",
|
||||||
|
"title": "Template",
|
||||||
|
"description": "Documentation pattern",
|
||||||
|
"plural": "templates"
|
||||||
|
},
|
||||||
|
"library": {
|
||||||
|
"name": "book",
|
||||||
|
"title": "Book",
|
||||||
|
"description": "Documentation library"
|
||||||
|
},
|
||||||
|
"composed": {
|
||||||
|
"name": "book",
|
||||||
|
"title": "Book",
|
||||||
|
"description": "Composed documentation",
|
||||||
|
"plural": "books",
|
||||||
|
"formula": "Template + Depot"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"execution": {
|
||||||
|
"utility": {
|
||||||
|
"name": "tool",
|
||||||
|
"title": "Tool",
|
||||||
|
"description": "Execution utility",
|
||||||
|
"plural": "tools"
|
||||||
|
},
|
||||||
|
"watcher": {
|
||||||
|
"name": "monitor",
|
||||||
|
"title": "Monitor",
|
||||||
|
"description": "Service monitor",
|
||||||
|
"plural": "monitors"
|
||||||
|
},
|
||||||
|
"container": {
|
||||||
|
"name": "cabinet",
|
||||||
|
"title": "Cabinet",
|
||||||
|
"description": "Tool container",
|
||||||
|
"plural": "cabinets"
|
||||||
|
},
|
||||||
|
"workspace": {
|
||||||
|
"name": "desk",
|
||||||
|
"title": "Desk",
|
||||||
|
"description": "Execution workspace"
|
||||||
|
},
|
||||||
|
"composed": {
|
||||||
|
"name": "desk",
|
||||||
|
"title": "Desk",
|
||||||
|
"description": "Composed execution bundle",
|
||||||
|
"plural": "desks",
|
||||||
|
"formula": "Cabinet + Room + Depots"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,102 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Sample App</title>
|
<title>Sample - Public Demo</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
* {
|
||||||
font-family: system-ui, -apple-system, sans-serif;
|
box-sizing: border-box;
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background: #f5f5f5;
|
padding: 0;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family:
|
||||||
|
system-ui,
|
||||||
|
-apple-system,
|
||||||
|
sans-serif;
|
||||||
|
background: linear-gradient(135deg, #1e3a5f 0%, #0d1b2a 100%);
|
||||||
|
color: #e5e5e5;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.container {
|
.container {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
background: white;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
||||||
}
|
}
|
||||||
h1 { color: #333; }
|
h1 {
|
||||||
p { color: #666; }
|
font-size: 3rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
background: linear-gradient(135deg, #3a86ff, #8338ec);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #a3a3a3;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.status {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: rgba(58, 134, 255, 0.2);
|
||||||
|
border: 1px solid #3a86ff;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #3a86ff;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
margin-top: 3rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-radius: 8px;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
.info h3 {
|
||||||
|
color: #8338ec;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.info ul {
|
||||||
|
list-style: none;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.info li {
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
.info li:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #3a86ff;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>Sample App</h1>
|
<h1>Sample</h1>
|
||||||
<p>This is a sample managed room for Soleprint.</p>
|
<p>Public demo - open to any Gmail account</p>
|
||||||
<p>Replace this with your actual frontend.</p>
|
<span class="status">Public Demo</span>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
<h3>Soleprint Managed Room</h3>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
With sidebar:
|
||||||
|
<a href="https://sample.spr.mcrn.ar"
|
||||||
|
>sample.spr.mcrn.ar</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Standalone:
|
||||||
|
<a href="https://sample.mcrn.ar">sample.mcrn.ar</a>
|
||||||
|
</li>
|
||||||
|
<li>Login with any Google account</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# SOLEPRINT - Sample Room Configuration
|
# Sample Soleprint Configuration
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Copy this to cfg/<your-room>/soleprint/.env and customize
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# DEPLOYMENT
|
# DEPLOYMENT
|
||||||
@@ -9,32 +8,24 @@
|
|||||||
DEPLOYMENT_NAME=sample_spr
|
DEPLOYMENT_NAME=sample_spr
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# NETWORK (unique per room to allow concurrent operation)
|
# NETWORK
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
NETWORK_NAME=sample_network
|
NETWORK_NAME=sample_network
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# PORTS (choose unique ports for each room)
|
# PORTS (unique per room)
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
SOLEPRINT_PORT=12020
|
SOLEPRINT_PORT=12030
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# GOOGLE OAUTH (optional - for auth)
|
# GOOGLE OAUTH
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
GOOGLE_CLIENT_ID=
|
GOOGLE_CLIENT_ID=1076380473867-k6gvdg8etujj2e51bqejve78ft99hnqd.apps.googleusercontent.com
|
||||||
GOOGLE_CLIENT_SECRET=
|
GOOGLE_CLIENT_SECRET=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
|
||||||
GOOGLE_REDIRECT_URI=http://sample.spr.local.ar/spr/artery/google/oauth/callback
|
GOOGLE_REDIRECT_URI=http://sample.spr.local.ar/artery/google/oauth/callback
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
# AUTH
|
# AUTH
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
AUTH_SESSION_SECRET=change-this-in-production
|
AUTH_BYPASS=true
|
||||||
|
AUTH_SESSION_SECRET=sample-dev-secret-change-in-production
|
||||||
# =============================================================================
|
|
||||||
# DATABASE (optional - if your app uses a database)
|
|
||||||
# =============================================================================
|
|
||||||
DB_HOST=sample_db
|
|
||||||
DB_PORT=5432
|
|
||||||
DB_NAME=sampledb
|
|
||||||
DB_USER=postgres
|
|
||||||
DB_PASSWORD=localdev123
|
|
||||||
|
|||||||
@@ -144,6 +144,9 @@ class GoogleOAuth:
|
|||||||
"name": userinfo.get("name"),
|
"name": userinfo.get("name"),
|
||||||
"picture": userinfo.get("picture"),
|
"picture": userinfo.get("picture"),
|
||||||
"hd": userinfo.get("hd"), # Hosted domain (Google Workspace)
|
"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:
|
def refresh_access_token(self, refresh_token: str) -> dict:
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class AuthConfig(BaseModel):
|
|||||||
enabled: bool = False
|
enabled: bool = False
|
||||||
provider: str = "google" # Vein name to use for auth
|
provider: str = "google" # Vein name to use for auth
|
||||||
allowed_domains: list[str] = [] # Empty = allow any domain
|
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_secret: str = "" # Required if enabled, can be "ENV:VAR_NAME"
|
||||||
session_timeout_hours: int = 24
|
session_timeout_hours: int = 24
|
||||||
login_redirect: str = "/"
|
login_redirect: str = "/"
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Authentication middleware for route protection.
|
|||||||
Generic middleware, provider-agnostic.
|
Generic middleware, provider-agnostic.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from starlette.middleware.base import BaseHTTPMiddleware
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
@@ -11,6 +12,10 @@ from starlette.responses import JSONResponse, RedirectResponse
|
|||||||
|
|
||||||
from .config import AuthConfig
|
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):
|
class AuthMiddleware(BaseHTTPMiddleware):
|
||||||
"""
|
"""
|
||||||
@@ -31,6 +36,15 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|||||||
async def dispatch(self, request, call_next):
|
async def dispatch(self, request, call_next):
|
||||||
path = request.url.path
|
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
|
# Check if route is public
|
||||||
if self._is_public(path):
|
if self._is_public(path):
|
||||||
return await call_next(request)
|
return await call_next(request)
|
||||||
@@ -49,14 +63,19 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
|||||||
session.clear()
|
session.clear()
|
||||||
return self._unauthorized(request, "Session expired")
|
return self._unauthorized(request, "Session expired")
|
||||||
|
|
||||||
# Check domain restriction
|
# Check domain/email restriction
|
||||||
user_domain = session.get("domain")
|
user_domain = session.get("domain")
|
||||||
if self.config.allowed_domains:
|
email_allowed = user_email in self.config.allowed_emails
|
||||||
if not user_domain or user_domain not in self.config.allowed_domains:
|
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()
|
session.clear()
|
||||||
return self._unauthorized(
|
return self._unauthorized(
|
||||||
request,
|
request,
|
||||||
f"Access restricted to: {', '.join(self.config.allowed_domains)}",
|
f"Access restricted",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Attach user to request state for downstream use
|
# Attach user to request state for downstream use
|
||||||
|
|||||||
@@ -111,14 +111,17 @@ async def callback(
|
|||||||
except httpx.RequestError as e:
|
except httpx.RequestError as e:
|
||||||
raise HTTPException(500, f"Failed to contact provider: {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")
|
user_domain = user_info.get("hd")
|
||||||
if auth_config.allowed_domains:
|
email_allowed = user_email in auth_config.allowed_emails
|
||||||
if not user_domain or user_domain not in auth_config.allowed_domains:
|
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(
|
raise HTTPException(
|
||||||
403,
|
403,
|
||||||
f"Access restricted to: {', '.join(auth_config.allowed_domains)}. "
|
f"Access restricted. Your account ({user_email}) is not authorized.",
|
||||||
f"Your account is from: {user_domain or 'personal Gmail'}",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Create session
|
# Create session
|
||||||
|
|||||||
@@ -92,18 +92,19 @@
|
|||||||
function renderSystemSection(systemKey, title) {
|
function renderSystemSection(systemKey, title) {
|
||||||
const veins = config.veins || [];
|
const veins = config.veins || [];
|
||||||
|
|
||||||
// System icon - ALWAYS shown as non-clickable separator
|
// System link with icon and label (same as soleprint)
|
||||||
let html = `
|
let html = `
|
||||||
<div class="spr-sidebar-icon" title="${title}">
|
<a href="${SPR_BASE}/${systemKey}" class="spr-sidebar-item" title="${title}">
|
||||||
${icons[systemKey]}
|
${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) {
|
if (user) {
|
||||||
// Logged in
|
// Logged in - show vein links under artery
|
||||||
if (systemKey === "artery") {
|
if (systemKey === "artery") {
|
||||||
// Show vein links under artery
|
|
||||||
for (const vein of veins) {
|
for (const vein of veins) {
|
||||||
html += `
|
html += `
|
||||||
<a href="${SPR_BASE}/artery/${vein}" class="spr-sidebar-item spr-vein-item" title="${vein}">
|
<a href="${SPR_BASE}/artery/${vein}" class="spr-sidebar-item spr-vein-item" title="${vein}">
|
||||||
@@ -112,14 +113,6 @@
|
|||||||
</a>
|
</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) {
|
} else if (config.auth_enabled) {
|
||||||
// Not logged in: show "login to access" below each system icon
|
// Not logged in: show "login to access" below each system icon
|
||||||
@@ -159,7 +152,7 @@
|
|||||||
|
|
||||||
<div class="spr-sidebar-divider"></div>
|
<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}
|
${icons.soleprint}
|
||||||
<span class="label">Soleprint</span>
|
<span class="label">Soleprint</span>
|
||||||
<span class="tooltip">Soleprint</span>
|
<span class="tooltip">Soleprint</span>
|
||||||
@@ -204,9 +197,9 @@
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
// Include current page as redirect target after login
|
// Include current path as redirect target after login (relative, not full URL)
|
||||||
const redirectUrl = encodeURIComponent(window.location.href);
|
const redirectPath = window.location.pathname + window.location.search;
|
||||||
const loginUrl = `${config.auth.login_url}?redirect=${redirectUrl}`;
|
const loginUrl = `${config.auth.login_url}?redirect=${encodeURIComponent(redirectPath)}`;
|
||||||
return `
|
return `
|
||||||
<div class="spr-sidebar-user">
|
<div class="spr-sidebar-user">
|
||||||
<a href="${loginUrl}" class="spr-sidebar-item" title="Login with Google">
|
<a href="${loginUrl}" class="spr-sidebar-item" title="Login with Google">
|
||||||
|
|||||||
Reference in New Issue
Block a user