feat: add space subdomain routing and ownership support

- Traefik wildcard HostRegexp for <space>.r*.online subdomains
- Middleware subdomain extraction and path rewriting
- Provision endpoint with owner_did acknowledgement
- Registry enforces space ownership via EncryptID JWT

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-25 13:20:19 -08:00
parent 3eb1e9d41f
commit 6ad00b2dca
3 changed files with 51 additions and 2 deletions

View File

@ -16,7 +16,7 @@ services:
- RCART_INTERNAL_URL=${RCART_INTERNAL_URL:-http://rcart-online:3000}
labels:
- "traefik.enable=true"
- "traefik.http.routers.rtrips.rule=Host(`rtrips.online`) || Host(`www.rtrips.online`)"
- "traefik.http.routers.rtrips.rule=Host(`rtrips.online`) || Host(`www.rtrips.online`) || HostRegexp(`{subdomain:[a-z0-9-]+}.rtrips.online`)"
- "traefik.http.services.rtrips.loadbalancer.server.port=3000"
networks:
- traefik-public

View File

@ -4,6 +4,9 @@ import { NextResponse } from "next/server";
* Internal provision endpoint called by rSpace Registry when activating
* this app for a space. No auth required (only reachable from Docker network).
*
* Payload: { space, description, admin_email, public, owner_did }
* The owner_did identifies who registered the space via the registry.
*
* rtrips will associate trips with space slugs in future.
* Acknowledges provisioning for now.
*/
@ -13,5 +16,6 @@ export async function POST(request: Request) {
if (!space) {
return NextResponse.json({ error: "Missing space name" }, { status: 400 });
}
return NextResponse.json({ status: "ok", space, message: "rtrips space acknowledged" });
const ownerDid: string = body.owner_did || "";
return NextResponse.json({ status: "ok", space, owner_did: ownerDid, message: "rtrips space acknowledged" });
}

45
src/middleware.ts Normal file
View File

@ -0,0 +1,45 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
/**
* Middleware to handle subdomain-based space routing.
*
* Routes:
* - rtrips.online -> home/landing page
* - www.rtrips.online -> home/landing page
* - <space>.rtrips.online -> rewrite to /s/<space>
*
* Also handles localhost for development.
*/
export function middleware(request: NextRequest) {
const url = request.nextUrl.clone();
const hostname = request.headers.get('host') || '';
let subdomain: string | null = null;
// Match production: <space>.rtrips.online
const match = hostname.match(/^([a-z0-9][a-z0-9-]*[a-z0-9]|[a-z0-9])\.\w+\.online/);
if (match && match[1] !== 'www') {
subdomain = match[1];
} else if (hostname.includes('localhost')) {
// Development: <space>.localhost:port
const parts = hostname.split('.localhost')[0].split('.');
if (parts.length > 0 && parts[0] !== 'localhost') {
subdomain = parts[parts.length - 1];
}
}
// If we have a subdomain, rewrite root path to space page
if (subdomain && subdomain.length > 0 && url.pathname === '/') {
url.pathname = `/s/${subdomain}`;
return NextResponse.rewrite(url);
}
return NextResponse.next();
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\..*|api).*)',
],
};