Fix space navigation redirects: server-side auth redirect + subdomain enforcement
Authenticated users visiting {space}.rspace.online/ now get a server-side
302 to /rspace instead of rendering the full dashboard then JS-redirecting
(eliminates flash of wrong header + 2-3 redirect chain → single redirect).
Bare domain rspace.online/{space} now 301-redirects to {space}.rspace.online/
so /{space}/ never appears in the URL bar.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
09ac17b332
commit
ddf5772025
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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/');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue