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:
parent
bb7bdc0b5b
commit
cf00a77da2
|
|
@ -23,7 +23,7 @@ services:
|
||||||
- /tmp
|
- /tmp
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=true"
|
- "traefik.enable=true"
|
||||||
- "traefik.http.routers.rcal.rule=Host(`rcal.jeffemmett.com`) || Host(`rcal.online`) || Host(`www.rcal.online`) || Host(`booking.xhiva.art`)"
|
- "traefik.http.routers.rcal.rule=Host(`rcal.jeffemmett.com`) || Host(`rcal.online`) || Host(`www.rcal.online`) || Host(`booking.xhiva.art`) || HostRegexp(`{subdomain:[a-z0-9-]+}.rcal.online`)"
|
||||||
- "traefik.http.routers.rcal.entrypoints=web"
|
- "traefik.http.routers.rcal.entrypoints=web"
|
||||||
- "traefik.http.services.rcal.loadbalancer.server.port=3000"
|
- "traefik.http.services.rcal.loadbalancer.server.port=3000"
|
||||||
networks:
|
networks:
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ import { NextResponse } from "next/server";
|
||||||
* Internal provision endpoint — called by rSpace Registry when activating
|
* Internal provision endpoint — called by rSpace Registry when activating
|
||||||
* this app for a space. No auth required (only reachable from Docker network).
|
* 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.
|
||||||
|
*
|
||||||
* rcal will associate calendars with space slugs in future.
|
* rcal will associate calendars with space slugs in future.
|
||||||
* Acknowledges provisioning for now.
|
* Acknowledges provisioning for now.
|
||||||
*/
|
*/
|
||||||
|
|
@ -13,5 +16,6 @@ export async function POST(request: Request) {
|
||||||
if (!space) {
|
if (!space) {
|
||||||
return NextResponse.json({ error: "Missing space name" }, { status: 400 });
|
return NextResponse.json({ error: "Missing space name" }, { status: 400 });
|
||||||
}
|
}
|
||||||
return NextResponse.json({ status: "ok", space, message: "rcal space acknowledged" });
|
const ownerDid: string = body.owner_did || "";
|
||||||
|
return NextResponse.json({ status: "ok", space, owner_did: ownerDid, message: "rcal space acknowledged" });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import type { NextRequest } from 'next/server';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware to handle subdomain-based space routing.
|
||||||
|
*
|
||||||
|
* Routes:
|
||||||
|
* - rcal.online -> home/landing page
|
||||||
|
* - www.rcal.online -> home/landing page
|
||||||
|
* - <space>.rcal.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>.rcal.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).*)',
|
||||||
|
],
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue