File size: 8,408 Bytes
94193b5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86c76ea
94193b5
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
/**
 * Next.js Middleware
 *
 * Handles authentication and routing for Server mode.
 * In Browser mode, server-only routes are blocked.
 * In Server mode, all data routes require authentication.
 *
 * Workspace routing:
 * - /w/[workspaceId]/* pages require auth
 * - /api/w/[workspaceId]/* routes require auth
 * - /api/server-generate/* routes require auth
 * - Legacy /admin/{view} paths redirect to /w/{defaultWorkspaceId}/{view}
 */

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { verifySession, maybeRefreshSession, SESSION_COOKIE_NAME, SESSION_DURATION } from '@/lib/auth/session';
import type { SessionData } from '@/lib/auth/session';

async function nextWithRefreshedSession(session: SessionData): Promise<NextResponse> {
  const response = NextResponse.next();
  const refreshed = await maybeRefreshSession(session);
  if (refreshed) {
    response.cookies.set(SESSION_COOKIE_NAME, refreshed, {
      httpOnly: true,
      secure: process.env.SECURE_COOKIES !== 'false' && process.env.NODE_ENV === 'production',
      sameSite: 'lax',
      maxAge: SESSION_DURATION / 1000,
      path: '/',
    });
  }
  return response;
}

// Views that have moved from /admin/{view} to /w/{workspaceId}/{view}
const WORKSPACE_VIEWS = ['projects', 'dashboard', 'deployments', 'settings', 'skills', 'templates', 'docs'];

function loginRedirect(request: NextRequest): NextResponse {
  const gatewayUrl = process.env.NEXT_PUBLIC_GATEWAY_URL;
  if (gatewayUrl) return NextResponse.redirect(gatewayUrl + '/login');
  return NextResponse.redirect(new URL('/admin/login', request.url));
}

export async function middleware(request: NextRequest) {
  const isServerMode = process.env.NEXT_PUBLIC_SERVER_MODE === 'true';
  const { pathname } = request.nextUrl;
  const isDesktop = process.env.OSW_DESKTOP === 'true';

  // Desktop app: skip auth but still handle workspace routing
  if (isDesktop) {
    // Legacy redirect: /admin/{view} -> /w/{workspaceId}/{view}
    if (pathname.startsWith('/admin')) {
      for (const view of WORKSPACE_VIEWS) {
        if (pathname === `/admin/${view}` || pathname.startsWith(`/admin/${view}/`)) {
          const workspaceId = request.cookies.get('osw_workspace')?.value;
          if (workspaceId) {
            const newPath = pathname.replace(`/admin/${view}`, `/w/${workspaceId}/${view}`);
            return NextResponse.redirect(new URL(newPath + request.nextUrl.search, request.url));
          }
          // No workspace cookie yet — redirect to root to trigger bootstrap
          return NextResponse.redirect(new URL('/', request.url));
        }
      }
    }
    return NextResponse.next();
  }

  // ============================================
  // Workspace page routes: /w/[workspaceId]/*
  // ============================================
  if (pathname.startsWith('/w/')) {
    if (!isServerMode) {
      return NextResponse.redirect(new URL('/', request.url));
    }

    const token = request.cookies.get('osw_session')?.value;
    if (!token) {
      const response = loginRedirect(request);
      // Clear stale workspace cookie
      response.cookies.delete('osw_workspace');
      return response;
    }

    const session = await verifySession(token);
    if (!session) {
      const response = loginRedirect(request);
      // Clear stale cookies
      response.cookies.delete('osw_session');
      response.cookies.delete('osw_workspace');
      return response;
    }

    return nextWithRefreshedSession(session);
  }

  // ============================================
  // Server-generate API routes: /api/server-generate/*
  // ============================================
  if (pathname.startsWith('/api/server-generate')) {
    if (!isServerMode) {
      return NextResponse.json({ error: 'Not available in Browser mode' }, { status: 404 });
    }

    const token = request.cookies.get('osw_session')?.value;
    if (!token) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    }
    const session = await verifySession(token);
    if (!session) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    }

    return nextWithRefreshedSession(session);
  }

  // ============================================
  // Workspace API routes: /api/w/[workspaceId]/*
  // ============================================
  if (pathname.startsWith('/api/w/')) {
    if (!isServerMode) {
      return NextResponse.json({ error: 'Not available in Browser mode' }, { status: 404 });
    }

    const token = request.cookies.get('osw_session')?.value;
    if (!token) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    }
    const session = await verifySession(token);
    if (!session) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    }

    return nextWithRefreshedSession(session);
  }

  // ============================================
  // Admin API routes: /api/admin/*
  // ============================================
  if (pathname.startsWith('/api/admin')) {
    if (!isServerMode) {
      return NextResponse.json({ error: 'Not available in Browser mode' }, { status: 404 });
    }
    // Defense-in-depth: verify session for admin API routes
    const token = request.cookies.get('osw_session')?.value;
    const apiKey = request.headers.get('x-instance-api-key');
    if (!token && !apiKey) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
    }
    if (token) {
      const session = await verifySession(token);
      if (!session) {
        return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
      }
      return nextWithRefreshedSession(session);
    }
    return NextResponse.next();
  }

  // ============================================
  // Admin pages: /admin/*
  // ============================================
  if (pathname.startsWith('/admin')) {
    if (!isServerMode) {
      return NextResponse.redirect(new URL('/', request.url));
    }

    // When managed by an external auth provider, redirect login/register there
    const gatewayUrl = process.env.NEXT_PUBLIC_GATEWAY_URL;
    if (gatewayUrl && (pathname === '/admin/login' || pathname === '/admin/register')) {
      return NextResponse.redirect(gatewayUrl + '/login');
    }

    // Allow login and register pages without auth
    // (Register API enforces REGISTRATION_MODE + zero-users check server-side)
    if (pathname === '/admin/login' || pathname === '/admin/register') {
      return NextResponse.next();
    }

    const token = request.cookies.get('osw_session')?.value;
    if (!token) return loginRedirect(request);

    const session = await verifySession(token);
    if (!session) return loginRedirect(request);

    // Only admins can access user/workspace management
    if (!session.isAdmin && (pathname.startsWith('/admin/users') || pathname.startsWith('/admin/workspaces'))) {
      // Redirect non-admin to their default workspace
      const workspaceId = request.cookies.get('osw_workspace')?.value;
      if (workspaceId) {
        return NextResponse.redirect(new URL(`/w/${workspaceId}/projects`, request.url));
      }
      return loginRedirect(request);
    }

    // Legacy redirect: /admin/{view} -> /w/{workspaceId}/{view}
    for (const view of WORKSPACE_VIEWS) {
      if (pathname === `/admin/${view}` || pathname.startsWith(`/admin/${view}/`)) {
        const workspaceId = request.cookies.get('osw_workspace')?.value;
        if (workspaceId) {
          const newPath = pathname.replace(`/admin/${view}`, `/w/${workspaceId}/${view}`);
          return NextResponse.redirect(new URL(newPath + request.nextUrl.search, request.url));
        }
        // No workspace cookie — redirect to login
        return loginRedirect(request);
      }
    }

    // /admin root redirect
    if (pathname === '/admin' || pathname === '/admin/') {
      const workspaceId = request.cookies.get('osw_workspace')?.value;
      if (workspaceId) {
        return NextResponse.redirect(new URL(`/w/${workspaceId}/projects`, request.url));
      }
      return loginRedirect(request);
    }

    return nextWithRefreshedSession(session);
  }

  return NextResponse.next();
}

export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|deployments/|health|api-docs|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
};