Merge branch 'dev'
CI/CD / deploy (push) Waiting to run Details

This commit is contained in:
Jeff Emmett 2026-04-01 14:13:34 -07:00
commit ba5f3bfe3d
3 changed files with 40 additions and 48 deletions

View File

@ -47,19 +47,20 @@ jobs:
cat .last-deployed-tag 2>/dev/null > .rollback-tag || true cat .last-deployed-tag 2>/dev/null > .rollback-tag || true
echo '${{ env.IMAGE_TAG }}' > .last-deployed-tag echo '${{ env.IMAGE_TAG }}' > .last-deployed-tag
docker pull ${{ env.IMAGE }}:${{ env.IMAGE_TAG }} docker pull ${{ env.IMAGE }}:${{ env.IMAGE_TAG }}
IMAGE_TAG=${{ env.IMAGE_TAG }} docker compose up -d --no-build IMAGE_TAG=${{ env.IMAGE_TAG }} docker compose up -d --no-build rspace
" "
- name: Smoke test - name: Smoke test
run: | run: |
sleep 15 sleep 20
HTTP_CODE=$(curl -sSL -o /dev/null -w "%{http_code}" --max-time 30 https://rspace.online/ 2>/dev/null || echo "000") HTTP_CODE=$(ssh -o StrictHostKeyChecking=no -i ~/.ssh/deploy_key root@${{ secrets.DEPLOY_HOST }} \
"curl -sSL -o /dev/null -w '%{http_code}' --max-time 30 https://rspace.online/ 2>/dev/null || echo 000")
if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 400 ]; then if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 400 ]; then
echo "Smoke test failed (HTTP $HTTP_CODE) — rolling back" echo "Smoke test failed (HTTP $HTTP_CODE) — rolling back"
ROLLBACK_TAG=$(ssh -o StrictHostKeyChecking=no -i ~/.ssh/deploy_key root@${{ secrets.DEPLOY_HOST }} "cat /opt/websites/rspace-online/.rollback-tag 2>/dev/null") ROLLBACK_TAG=$(ssh -o StrictHostKeyChecking=no -i ~/.ssh/deploy_key root@${{ secrets.DEPLOY_HOST }} "cat /opt/websites/rspace-online/.rollback-tag 2>/dev/null")
if [ -n "$ROLLBACK_TAG" ]; then if [ -n "$ROLLBACK_TAG" ]; then
ssh -o StrictHostKeyChecking=no -i ~/.ssh/deploy_key root@${{ secrets.DEPLOY_HOST }} \ ssh -o StrictHostKeyChecking=no -i ~/.ssh/deploy_key root@${{ secrets.DEPLOY_HOST }} \
"cd /opt/websites/rspace-online && IMAGE_TAG=$ROLLBACK_TAG docker compose up -d --no-build" "cd /opt/websites/rspace-online && IMAGE_TAG=$ROLLBACK_TAG docker compose up -d --no-build rspace"
echo "Rolled back to $ROLLBACK_TAG" echo "Rolled back to $ROLLBACK_TAG"
fi fi
exit 1 exit 1

View File

@ -2825,6 +2825,12 @@ app.get("/:space", (c) => {
const space = c.req.param("space"); const space = c.req.param("space");
// Don't serve dashboard for static file paths // Don't serve dashboard for static file paths
if (space.includes(".")) return c.notFound(); if (space.includes(".")) return c.notFound();
// On production bare domain, this route is unreachable (caught by subdomain redirect above).
// On localhost/dev: redirect authenticated users to canvas, show dashboard for guests.
const token = extractToken(c.req.raw.headers);
if (token) {
return c.redirect(`/${space}/rspace`, 302);
}
return c.html(renderSpaceDashboard(space, getModuleInfoList())); return c.html(renderSpaceDashboard(space, getModuleInfoList()));
}); });
@ -3228,8 +3234,14 @@ const server = Bun.serve<WSData>({
const pathSegments = url.pathname.split("/").filter(Boolean); const pathSegments = url.pathname.split("/").filter(Boolean);
// Root: show space dashboard // Root: redirect authenticated users straight to rSpace canvas
// (avoids rendering space dashboard just to JS-redirect, eliminating
// the flash of different header + 2-3 redirect chain)
if (pathSegments.length === 0) { if (pathSegments.length === 0) {
const token = extractTokenFromRequest(req);
if (token) {
return Response.redirect(`${proto}//${url.host}/rspace`, 302);
}
return new Response(renderSpaceDashboard(subdomain, getModuleInfoList()), { return new Response(renderSpaceDashboard(subdomain, getModuleInfoList()), {
headers: { "Content-Type": "text/html" }, headers: { "Content-Type": "text/html" },
}); });
@ -3385,24 +3397,28 @@ const server = Bun.serve<WSData>({
return app.fetch(rewrittenReq); return app.fetch(rewrittenReq);
} }
// rspace.online/{space}/{...} → handle space-prefixed paths // rspace.online/{space} or rspace.online/{space}/{...} → redirect to subdomain
// (space is not a module ID — it's a space slug) // (space is not a module ID — it's a space slug)
// Skip for known server paths (api, admin, etc.) // Skip for known server paths (api, admin, etc.)
const serverPaths = new Set(["api", "admin", "admin-data", "admin-action", "modules", ".well-known"]); const serverPaths = new Set(["api", "admin", "admin-data", "admin-action", "modules", ".well-known", "about", "create-space", "new", "discover"]);
if (!knownModuleIds.has(firstSegment) && !serverPaths.has(firstSegment) && pathSegments.length >= 2) { if (!knownModuleIds.has(firstSegment) && !serverPaths.has(firstSegment)) {
const secondSeg = pathSegments[1]?.toLowerCase();
const isApiCall = secondSeg === "api" || pathSegments.some((s, i) => i >= 1 && s === "api");
if (isApiCall) {
// API calls: rewrite internally (avoid redirect + mixed-content)
const rewrittenUrl = new URL(url.pathname + url.search, `http://localhost:${PORT}`);
return app.fetch(new Request(rewrittenUrl, req));
}
// Page navigation: redirect to canonical subdomain URL
const space = firstSegment; const space = firstSegment;
const rest = "/" + pathSegments.slice(1).join("/"); if (pathSegments.length >= 2) {
return Response.redirect( const secondSeg = pathSegments[1]?.toLowerCase();
`${proto}//${space}.rspace.online${rest}${url.search}`, 301 const isApiCall = secondSeg === "api" || pathSegments.some((s, i) => i >= 1 && s === "api");
); if (isApiCall) {
// API calls: rewrite internally (avoid redirect + mixed-content)
const rewrittenUrl = new URL(url.pathname + url.search, `http://localhost:${PORT}`);
return app.fetch(new Request(rewrittenUrl, req));
}
// Page navigation: redirect to canonical subdomain URL
const rest = "/" + pathSegments.slice(1).join("/");
return Response.redirect(
`${proto}//${space}.rspace.online${rest}${url.search}`, 301
);
}
// Single segment: rspace.online/{space} → {space}.rspace.online/
return Response.redirect(`${proto}//${space}.rspace.online/${url.search}`, 301);
} }
} }
} }

View File

@ -340,34 +340,9 @@ export function renderSpaceDashboard(space: string, modules: ModuleInfo[]): stri
import '/shell.js'; import '/shell.js';
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON}); document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
// Check session in localStorage OR cross-subdomain cookie // Authenticated users are redirected server-side (no JS redirect needed).
function _hasSession() { // Non-demo spaces: redirect logged-out visitors to the main domain landing.
try { if ('${escapeAttr(space)}' !== 'demo') {
var raw = localStorage.getItem('encryptid_session');
if (raw && JSON.parse(raw)?.accessToken) return true;
} catch(e) {}
try {
var m = document.cookie.match(/(?:^|; )eid_token=([^;]*)/);
if (!m) return false;
var tok = decodeURIComponent(m[1]);
var parts = tok.split('.');
if (parts.length < 2) return false;
var b64 = parts[1].replace(/-/g,'+').replace(/_/g,'/');
var pad = '='.repeat((4 - b64.length % 4) % 4);
var payload = JSON.parse(atob(b64 + pad));
return payload.exp && Math.floor(Date.now()/1000) < payload.exp;
} catch(e) { return false; }
}
// Logged-in users: redirect to rSpace canvas instead of showing the grid
// Non-demo spaces: redirect logged-out visitors to the main domain landing
var loggedIn = _hasSession();
if (loggedIn) {
var dest = window.__rspaceNavUrl
? window.__rspaceNavUrl('${escapeAttr(space)}', 'rspace')
: '/${escapeAttr(space)}/rspace';
window.location.replace(dest);
} else if ('${escapeAttr(space)}' !== 'demo') {
var host = window.location.host.split(':')[0]; var host = window.location.host.split(':')[0];
if (host.endsWith('.rspace.online') || host === 'rspace.online') { if (host.endsWith('.rspace.online') || host === 'rspace.online') {
window.location.replace('https://rspace.online/'); window.location.replace('https://rspace.online/');