diff --git a/cfg/sample/config.json b/cfg/sample/config.json index bd11c42..2442333 100644 --- a/cfg/sample/config.json +++ b/cfg/sample/config.json @@ -1,16 +1,143 @@ { - "managed": { - "name": "sample", - "repos": { - "frontend": "/path/to/your/frontend/repo", - "backend": "/path/to/your/backend/repo" - } - }, "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": { - "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" + } + } + } } diff --git a/cfg/sample/sample/index.html b/cfg/sample/sample/index.html index 90bf11d..5531a5b 100644 --- a/cfg/sample/sample/index.html +++ b/cfg/sample/sample/index.html @@ -1,35 +1,102 @@ - + - - - - Sample App - - - -
-

Sample App

-

This is a sample managed room for Soleprint.

-

Replace this with your actual frontend.

-
- + + + + Sample - Public Demo + + + +
+

Sample

+

Public demo - open to any Gmail account

+ Public Demo + +
+

Soleprint Managed Room

+ +
+
+ diff --git a/cfg/sample/soleprint/.env b/cfg/sample/soleprint/.env index 2e18aad..e9cbd4e 100644 --- a/cfg/sample/soleprint/.env +++ b/cfg/sample/soleprint/.env @@ -1,7 +1,6 @@ # ============================================================================= -# SOLEPRINT - Sample Room Configuration +# Sample Soleprint Configuration # ============================================================================= -# Copy this to cfg//soleprint/.env and customize # ============================================================================= # DEPLOYMENT @@ -9,32 +8,24 @@ DEPLOYMENT_NAME=sample_spr # ============================================================================= -# NETWORK (unique per room to allow concurrent operation) +# 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_SECRET= -GOOGLE_REDIRECT_URI=http://sample.spr.local.ar/spr/artery/google/oauth/callback +GOOGLE_CLIENT_ID=1076380473867-k6gvdg8etujj2e51bqejve78ft99hnqd.apps.googleusercontent.com +GOOGLE_CLIENT_SECRET=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 +GOOGLE_REDIRECT_URI=http://sample.spr.local.ar/artery/google/oauth/callback # ============================================================================= # AUTH # ============================================================================= -AUTH_SESSION_SECRET=change-this-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 +AUTH_BYPASS=true +AUTH_SESSION_SECRET=sample-dev-secret-change-in-production diff --git a/soleprint/artery/veins/google/core/oauth.py b/soleprint/artery/veins/google/core/oauth.py index e64d0cf..17ab99e 100644 --- a/soleprint/artery/veins/google/core/oauth.py +++ b/soleprint/artery/veins/google/core/oauth.py @@ -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: diff --git a/soleprint/common/auth/config.py b/soleprint/common/auth/config.py index 08f97ef..3a7863d 100644 --- a/soleprint/common/auth/config.py +++ b/soleprint/common/auth/config.py @@ -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 = "/" diff --git a/soleprint/common/auth/middleware.py b/soleprint/common/auth/middleware.py index 17a3fa1..094dc41 100644 --- a/soleprint/common/auth/middleware.py +++ b/soleprint/common/auth/middleware.py @@ -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 = { diff --git a/soleprint/common/auth/routes.py b/soleprint/common/auth/routes.py index 6ca9a87..7f50a1f 100644 --- a/soleprint/common/auth/routes.py +++ b/soleprint/common/auth/routes.py @@ -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) diff --git a/soleprint/station/tools/sbwrapper/sidebar.js b/soleprint/station/tools/sbwrapper/sidebar.js index 94482ba..2f7e64f 100755 --- a/soleprint/station/tools/sbwrapper/sidebar.js +++ b/soleprint/station/tools/sbwrapper/sidebar.js @@ -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 = ` -
+ ${icons[systemKey]} -
+ ${title} + ${title} + `; - // 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 += ` @@ -112,14 +113,6 @@ `; } - } else { - // Atlas/Station: clickable link to main page - html += ` - - ${title} - ${title} - - `; } } else if (config.auth_enabled) { // Not logged in: show "login to access" below each system icon @@ -159,7 +152,7 @@
- + ${icons.soleprint} Soleprint Soleprint @@ -204,9 +197,9 @@ `; } 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 `