diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 16295b7..41fcfb9 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -47,19 +47,20 @@ jobs: cat .last-deployed-tag 2>/dev/null > .rollback-tag || true echo '${{ env.IMAGE_TAG }}' > .last-deployed-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 run: | - sleep 15 - HTTP_CODE=$(curl -sSL -o /dev/null -w "%{http_code}" --max-time 30 https://rspace.online/ 2>/dev/null || echo "000") + sleep 20 + 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 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") if [ -n "$ROLLBACK_TAG" ]; then 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" fi exit 1 diff --git a/server/index.ts b/server/index.ts index 3f50fee..5cbeeaf 100644 --- a/server/index.ts +++ b/server/index.ts @@ -2825,6 +2825,12 @@ app.get("/:space", (c) => { const space = c.req.param("space"); // Don't serve dashboard for static file paths 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())); }); @@ -3228,8 +3234,14 @@ const server = Bun.serve({ 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) { + const token = extractTokenFromRequest(req); + if (token) { + return Response.redirect(`${proto}//${url.host}/rspace`, 302); + } return new Response(renderSpaceDashboard(subdomain, getModuleInfoList()), { headers: { "Content-Type": "text/html" }, }); @@ -3385,24 +3397,28 @@ const server = Bun.serve({ 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) // Skip for known server paths (api, admin, etc.) - const serverPaths = new Set(["api", "admin", "admin-data", "admin-action", "modules", ".well-known"]); - if (!knownModuleIds.has(firstSegment) && !serverPaths.has(firstSegment) && pathSegments.length >= 2) { - 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 serverPaths = new Set(["api", "admin", "admin-data", "admin-action", "modules", ".well-known", "about", "create-space", "new", "discover"]); + if (!knownModuleIds.has(firstSegment) && !serverPaths.has(firstSegment)) { const space = firstSegment; - const rest = "/" + pathSegments.slice(1).join("/"); - return Response.redirect( - `${proto}//${space}.rspace.online${rest}${url.search}`, 301 - ); + if (pathSegments.length >= 2) { + 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 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); } } } diff --git a/server/landing.ts b/server/landing.ts index ce155cf..aa4e475 100644 --- a/server/landing.ts +++ b/server/landing.ts @@ -340,34 +340,9 @@ export function renderSpaceDashboard(space: string, modules: ModuleInfo[]): stri import '/shell.js'; document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON}); - // Check session in localStorage OR cross-subdomain cookie - function _hasSession() { - try { - 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') { + // Authenticated users are redirected server-side (no JS redirect needed). + // Non-demo spaces: redirect logged-out visitors to the main domain landing. + if ('${escapeAttr(space)}' !== 'demo') { var host = window.location.host.split(':')[0]; if (host.endsWith('.rspace.online') || host === 'rspace.online') { window.location.replace('https://rspace.online/');