feat: productionize learning engine and classroom

This commit is contained in:
inman
2026-08-16 21:44:52 +08:00
parent 2d04197f3f
commit ba35adfbfa
124 changed files with 9071 additions and 796 deletions

View File

@@ -7,15 +7,90 @@ import {
import { verifyAccessTokenWithWebCrypto } from '@/lib/server/access-token-edge';
import { ACCESS_CODE_COOKIE_NAME } from '@/lib/server/access-token-policy';
const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
const OPS_ADMIN_USERNAME = 'jiaoyuop';
function configuredHttpOrigin(name: string): string | null {
const raw = process.env[name]?.trim();
if (!raw) return null;
try {
const url = new URL(raw);
if (
(url.protocol !== 'https:' && url.protocol !== 'http:') ||
url.username ||
url.password ||
url.search ||
url.hash ||
(url.pathname !== '' && url.pathname !== '/')
) {
return null;
}
return url.origin;
} catch {
return null;
}
}
function opsError(status: number, errorCode: string, error: string) {
return NextResponse.json({ success: false, errorCode, error }, { status });
}
async function authorizeOpsApi(request: NextRequest): Promise<NextResponse> {
const worksOrigin = configuredHttpOrigin('WORKS_SQUARE_API_BASE_URL');
const publicOrigin = configuredHttpOrigin('OPS_PUBLIC_ORIGIN');
if (!worksOrigin || !publicOrigin) {
return opsError(503, 'INTERNAL_ERROR', 'Operations identity is not configured');
}
if (!SAFE_METHODS.has(request.method.toUpperCase())) {
const origin = request.headers.get('origin');
try {
if (!origin || new URL(origin).origin !== publicOrigin) {
return opsError(403, 'FORBIDDEN', 'Cross-origin operations request denied');
}
} catch {
return opsError(403, 'FORBIDDEN', 'Cross-origin operations request denied');
}
}
const authorization = request.headers.get('authorization')?.trim() ?? '';
if (!/^Bearer\s+\S+$/.test(authorization) || authorization.length > 8192) {
return opsError(401, 'INVALID_CREDENTIALS', 'Works administrator session required');
}
try {
const response = await fetch(`${worksOrigin}/api/auth/me`, {
method: 'GET',
headers: { Authorization: authorization, Accept: 'application/json' },
redirect: 'error',
cache: 'no-store',
signal: AbortSignal.timeout(5000),
});
if (!response.ok) {
return opsError(401, 'INVALID_CREDENTIALS', 'Works administrator session is invalid');
}
const identity = (await response.json()) as { username?: unknown; site_role?: unknown };
if (identity.username !== OPS_ADMIN_USERNAME || identity.site_role !== 'admin') {
return opsError(403, 'FORBIDDEN', 'Works administrator role required');
}
return NextResponse.next();
} catch {
return opsError(503, 'INTERNAL_ERROR', 'Works identity service is unavailable');
}
}
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const configuredBasePath = process.env.OPENMAIC_BASE_PATH?.replace(/\/+$/, '') || '';
const boundaryPathname =
configuredBasePath &&
(pathname === configuredBasePath || pathname.startsWith(`${configuredBasePath}/`))
? pathname.slice(configuredBasePath.length) || '/'
: pathname;
const role = resolveDeploymentRole(
process.env.OPENMAIC_DEPLOYMENT_ROLE ?? process.env.NEXT_PUBLIC_OPENMAIC_DEPLOYMENT_ROLE,
process.env.NODE_ENV,
);
if (shouldReturnNotFoundAtServerBoundary(role, pathname, request.method)) {
if (isApiPath(pathname)) {
if (shouldReturnNotFoundAtServerBoundary(role, boundaryPathname, request.method)) {
if (isApiPath(boundaryPathname)) {
return NextResponse.json(
{ success: false, errorCode: 'NOT_FOUND', error: 'Not found' },
{ status: 404 },
@@ -32,16 +107,16 @@ export async function middleware(request: NextRequest) {
}
const isOpsPath =
pathname === '/courses' ||
pathname.startsWith('/courses/') ||
pathname === '/publish' ||
pathname === '/api/courses' ||
pathname.startsWith('/api/courses/') ||
pathname === '/api/ops' ||
pathname.startsWith('/api/ops/');
boundaryPathname === '/courses' ||
boundaryPathname.startsWith('/courses/') ||
boundaryPathname === '/publish' ||
boundaryPathname === '/api/courses' ||
boundaryPathname.startsWith('/api/courses/') ||
boundaryPathname === '/api/ops' ||
boundaryPathname.startsWith('/api/ops/');
if (isOpsPath && !hasDeploymentCapability(role, 'manage_courses')) {
if (pathname.startsWith('/api/')) {
if (boundaryPathname.startsWith('/api/')) {
return NextResponse.json(
{
success: false,
@@ -61,13 +136,31 @@ export async function middleware(request: NextRequest) {
return NextResponse.next();
}
const isHealthPath =
boundaryPathname === '/api/health' ||
boundaryPathname === '/api/runtime/v1/health/live' ||
boundaryPathname === '/api/runtime/v1/health/ready';
if (role === 'ops') {
if (isHealthPath) return NextResponse.next();
if (isApiPath(boundaryPathname)) return authorizeOpsApi(request);
// Operations pages are authenticated shells. Browser API calls carry the
// Works Bearer and are introspected above; ACCESS_CODE is never consulted.
return NextResponse.next();
}
const accessCode = process.env.ACCESS_CODE;
if (isOpsPath && process.env.NODE_ENV === 'production' && !accessCode) {
const worksIdentityConfigured = Boolean(process.env.WORKS_SQUARE_API_BASE_URL?.trim());
if (isOpsPath && worksIdentityConfigured) {
// Pages are public shells; every data route performs server-side Works
// introspection. Bearer sessions are not cookies and must reach the route.
return NextResponse.next();
}
if (isOpsPath && process.env.NODE_ENV === 'production') {
return NextResponse.json(
{
success: false,
errorCode: 'INTERNAL_ERROR',
error: 'Operations deployment is not configured with ACCESS_CODE',
error: 'Operations deployment is not configured with Works identity',
},
{ status: 503 },
);
@@ -78,7 +171,10 @@ export async function middleware(request: NextRequest) {
}
// Whitelist: access-code endpoints, health check
if (pathname.startsWith('/api/access-code/') || pathname === '/api/health') {
if (
boundaryPathname.startsWith('/api/access-code/') ||
isHealthPath
) {
return NextResponse.next();
}
@@ -89,7 +185,7 @@ export async function middleware(request: NextRequest) {
}
// API requests without valid cookie → 401
if (pathname.startsWith('/api/')) {
if (boundaryPathname.startsWith('/api/')) {
return NextResponse.json(
{ success: false, errorCode: 'INVALID_REQUEST', error: 'Access code required' },
{ status: 401 },