Add /spr/ route for soleprint index, fix sidebar system labels

This commit is contained in:
buenosairesam
2026-01-27 07:07:41 -03:00
parent 5603979d5c
commit ed1c8f6c96
8 changed files with 300 additions and 96 deletions

View File

@@ -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"
}
}
}
} }

View File

@@ -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>
</body> </div>
</body>
</html> </html>

View File

@@ -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

View File

@@ -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:

View File

@@ -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 = "/"

View File

@@ -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

View File

@@ -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

View File

@@ -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">