Leon4gr45/builder
0
1/**2 * Next.js Middleware3 *4 * Handles authentication and routing for Server mode.5 * In Browser mode, server-only routes are blocked.6 * In Server mode, all data routes require authentication.7 *8 * Workspace routing:9 * - /w/[workspaceId]/* pages require auth10 * - /api/w/[workspaceId]/* routes require auth11 * - /api/server-generate/* routes require auth12 * - Legacy /admin/{view} paths redirect to /w/{defaultWorkspaceId}/{view}13 */14 15import { NextResponse } from 'next/server';16import type { NextRequest } from 'next/server';17import { verifySession, maybeRefreshSession, SESSION_COOKIE_NAME, SESSION_DURATION } from '@/lib/auth/session';18import type { SessionData } from '@/lib/auth/session';19 20async function nextWithRefreshedSession(session: SessionData): Promise<NextResponse> {21 const response = NextResponse.next();22 const refreshed = await maybeRefreshSession(session);23 if (refreshed) {24 response.cookies.set(SESSION_COOKIE_NAME, refreshed, {25 httpOnly: true,26 secure: process.env.SECURE_COOKIES !== 'false' && process.env.NODE_ENV === 'production',27 sameSite: 'lax',28 maxAge: SESSION_DURATION / 1000,29 path: '/',30 });31 }32 return response;33}34 35// Views that have moved from /admin/{view} to /w/{workspaceId}/{view}36const WORKSPACE_VIEWS = ['projects', 'dashboard', 'deployments', 'settings', 'skills', 'templates', 'docs'];37 38function loginRedirect(request: NextRequest): NextResponse {39 const gatewayUrl = process.env.NEXT_PUBLIC_GATEWAY_URL;40 if (gatewayUrl) return NextResponse.redirect(gatewayUrl + '/login');41 return NextResponse.redirect(new URL('/admin/login', request.url));42}43 44export async function middleware(request: NextRequest) {45 const isServerMode = process.env.NEXT_PUBLIC_SERVER_MODE === 'true';46 const { pathname } = request.nextUrl;47 const isDesktop = process.env.OSW_DESKTOP === 'true';48 49 // Desktop app: skip auth but still handle workspace routing50 if (isDesktop) {51 // Legacy redirect: /admin/{view} -> /w/{workspaceId}/{view}52 if (pathname.startsWith('/admin')) {53 for (const view of WORKSPACE_VIEWS) {54 if (pathname === `/admin/${view}` || pathname.startsWith(`/admin/${view}/`)) {55 const workspaceId = request.cookies.get('osw_workspace')?.value;56 if (workspaceId) {57 const newPath = pathname.replace(`/admin/${view}`, `/w/${workspaceId}/${view}`);58 return NextResponse.redirect(new URL(newPath + request.nextUrl.search, request.url));59 }60 // No workspace cookie yet — redirect to root to trigger bootstrap61 return NextResponse.redirect(new URL('/', request.url));62 }63 }64 }65 return NextResponse.next();66 }67 68 // ============================================69 // Workspace page routes: /w/[workspaceId]/*70 // ============================================71 if (pathname.startsWith('/w/')) {72 if (!isServerMode) {73 return NextResponse.redirect(new URL('/', request.url));74 }75 76 const token = request.cookies.get('osw_session')?.value;77 if (!token) {78 const response = loginRedirect(request);79 // Clear stale workspace cookie80 response.cookies.delete('osw_workspace');81 return response;82 }83 84 const session = await verifySession(token);85 if (!session) {86 const response = loginRedirect(request);87 // Clear stale cookies88 response.cookies.delete('osw_session');89 response.cookies.delete('osw_workspace');90 return response;91 }92 93 return nextWithRefreshedSession(session);94 }95 96 // ============================================97 // Server-generate API routes: /api/server-generate/*98 // ============================================99 if (pathname.startsWith('/api/server-generate')) {100 if (!isServerMode) {101 return NextResponse.json({ error: 'Not available in Browser mode' }, { status: 404 });102 }103 104 const token = request.cookies.get('osw_session')?.value;105 if (!token) {106 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });107 }108 const session = await verifySession(token);109 if (!session) {110 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });111 }112 113 return nextWithRefreshedSession(session);114 }115 116 // ============================================117 // Workspace API routes: /api/w/[workspaceId]/*118 // ============================================119 if (pathname.startsWith('/api/w/')) {120 if (!isServerMode) {121 return NextResponse.json({ error: 'Not available in Browser mode' }, { status: 404 });122 }123 124 const token = request.cookies.get('osw_session')?.value;125 if (!token) {126 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });127 }128 const session = await verifySession(token);129 if (!session) {130 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });131 }132 133 return nextWithRefreshedSession(session);134 }135 136 // ============================================137 // Admin API routes: /api/admin/*138 // ============================================139 if (pathname.startsWith('/api/admin')) {140 if (!isServerMode) {141 return NextResponse.json({ error: 'Not available in Browser mode' }, { status: 404 });142 }143 // Defense-in-depth: verify session for admin API routes144 const token = request.cookies.get('osw_session')?.value;145 const apiKey = request.headers.get('x-instance-api-key');146 if (!token && !apiKey) {147 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });148 }149 if (token) {150 const session = await verifySession(token);151 if (!session) {152 return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });153 }154 return nextWithRefreshedSession(session);155 }156 return NextResponse.next();157 }158 159 // ============================================160 // Admin pages: /admin/*161 // ============================================162 if (pathname.startsWith('/admin')) {163 if (!isServerMode) {164 return NextResponse.redirect(new URL('/', request.url));165 }166 167 // When managed by an external auth provider, redirect login/register there168 const gatewayUrl = process.env.NEXT_PUBLIC_GATEWAY_URL;169 if (gatewayUrl && (pathname === '/admin/login' || pathname === '/admin/register')) {170 return NextResponse.redirect(gatewayUrl + '/login');171 }172 173 // Allow login and register pages without auth174 // (Register API enforces REGISTRATION_MODE + zero-users check server-side)175 if (pathname === '/admin/login' || pathname === '/admin/register') {176 return NextResponse.next();177 }178 179 const token = request.cookies.get('osw_session')?.value;180 if (!token) return loginRedirect(request);181 182 const session = await verifySession(token);183 if (!session) return loginRedirect(request);184 185 // Only admins can access user/workspace management186 if (!session.isAdmin && (pathname.startsWith('/admin/users') || pathname.startsWith('/admin/workspaces'))) {187 // Redirect non-admin to their default workspace188 const workspaceId = request.cookies.get('osw_workspace')?.value;189 if (workspaceId) {190 return NextResponse.redirect(new URL(`/w/${workspaceId}/projects`, request.url));191 }192 return loginRedirect(request);193 }194 195 // Legacy redirect: /admin/{view} -> /w/{workspaceId}/{view}196 for (const view of WORKSPACE_VIEWS) {197 if (pathname === `/admin/${view}` || pathname.startsWith(`/admin/${view}/`)) {198 const workspaceId = request.cookies.get('osw_workspace')?.value;199 if (workspaceId) {200 const newPath = pathname.replace(`/admin/${view}`, `/w/${workspaceId}/${view}`);201 return NextResponse.redirect(new URL(newPath + request.nextUrl.search, request.url));202 }203 // No workspace cookie — redirect to login204 return loginRedirect(request);205 }206 }207 208 // /admin root redirect209 if (pathname === '/admin' || pathname === '/admin/') {210 const workspaceId = request.cookies.get('osw_workspace')?.value;211 if (workspaceId) {212 return NextResponse.redirect(new URL(`/w/${workspaceId}/projects`, request.url));213 }214 return loginRedirect(request);215 }216 217 return nextWithRefreshedSession(session);218 }219 220 return NextResponse.next();221}222 223export const config = {224 matcher: [225 '/((?!_next/static|_next/image|favicon.ico|deployments/|health|api-docs|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',226 ],227};228 