diff --git a/app/api/internal/provision/route.ts b/app/api/internal/provision/route.ts new file mode 100644 index 0000000..e6ae7ee --- /dev/null +++ b/app/api/internal/provision/route.ts @@ -0,0 +1,21 @@ +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. + * + * rfunds has no server-side database — spaces are managed client-side. + * This endpoint acknowledges the provisioning request. + */ +export async function POST(request: Request) { + const body = await request.json(); + const space: string = body.space?.trim(); + if (!space) { + return NextResponse.json({ error: "Missing space name" }, { status: 400 }); + } + const ownerDid: string = body.owner_did || ""; + return NextResponse.json({ status: "ok", space, owner_did: ownerDid, message: "rfunds space acknowledged" }); +} diff --git a/docker-compose.yml b/docker-compose.yml index 366c6cf..2bdf312 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: restart: unless-stopped labels: - "traefik.enable=true" - - "traefik.http.routers.rfunds.rule=Host(`rfunds.online`) || Host(`www.rfunds.online`)" + - "traefik.http.routers.rfunds.rule=Host(`rfunds.online`) || Host(`www.rfunds.online`) || HostRegexp(`{subdomain:[a-z0-9-]+}.rfunds.online`)" - "traefik.http.services.rfunds.loadbalancer.server.port=3000" healthcheck: test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3000/"] diff --git a/middleware.ts b/middleware.ts index 8c3ddbd..83127f1 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,23 +1,55 @@ import { NextResponse } from 'next/server' import type { NextRequest } from 'next/server' +import { isDemoRequest } from '@encryptid/sdk/server/nextjs' /** - * Middleware to protect /space routes. + * Middleware to handle subdomain-based space routing and protect /space routes. * - * Client-side auth enforcement: the space page itself checks auth state via - * Zustand store. This middleware adds a cookie-based check for server-rendered - * requests — if no encryptid_token cookie is present on /space, redirect to - * the home page with a login hint. + * Subdomain routing: + * - rfunds.online -> home/landing page + * - www.rfunds.online -> home/landing page + * - .rfunds.online -> rewrite to /s/ * - * Note: Since rfunds uses client-side Zustand persistence (localStorage), - * the primary auth gate is in the SpacePage component itself. This middleware - * serves as an additional layer for direct URL access. + * Auth protection: + * - /space routes require auth (cookie or Bearer token) + * - Demo spaces (ENCRYPTID_DEMO_SPACES env var) bypass auth + * + * Also handles localhost for development. */ export function middleware(request: NextRequest) { + const url = request.nextUrl.clone() + const hostname = request.headers.get('host') || '' const { pathname } = request.nextUrl + // --- Subdomain routing --- + let subdomain: string | null = null + + // Match production: .rfunds.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: .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 && pathname === '/') { + url.pathname = `/s/${subdomain}` + return NextResponse.rewrite(url) + } + + // --- Auth protection for /space routes --- // Only protect /space routes (not /tbff which is a public demo) if (pathname.startsWith('/space')) { + // Demo spaces get anonymous access — SDK provides synthetic claims + if (isDemoRequest(request)) { + return NextResponse.next() + } + // Check for auth token in cookie (set by client after login) const token = request.cookies.get('encryptid_token')?.value @@ -26,10 +58,6 @@ export function middleware(request: NextRequest) { const bearerToken = authHeader?.startsWith('Bearer ') ? authHeader.slice(7) : null if (!token && !bearerToken) { - // No auth — redirect to home with login hint - // The client-side auth store is the primary gate, but this catches - // direct navigation before hydration - const url = request.nextUrl.clone() url.pathname = '/' url.searchParams.set('login', 'required') url.searchParams.set('return', pathname) @@ -41,5 +69,7 @@ export function middleware(request: NextRequest) { } export const config = { - matcher: ['/space/:path*'], + matcher: [ + '/((?!_next/static|_next/image|favicon.ico|.*\\..*|api).*)', + ], }