Files
emilandClaude Sonnet 4.6 50b3c4198a feat: initial commit — backend API + student cabinet frontend
- Go backend: auth (JWT), points earn/spend, QR token generation,
  partners, admin grant/stats endpoints with chi router
- Next.js 14 frontend: login, student dashboard, transaction history,
  QR display, partners list
- PostgreSQL migrations (4 tables), Redis cache, Docker Compose
- CORS middleware, role-based route protection, Zustand auth store

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 10:03:27 +03:00

54 lines
1.8 KiB
TypeScript

import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
// Decodes a JWT payload without verifying the signature.
// Safe for middleware because we only need the role for redirects — the backend
// will reject any tampered token when the actual API call is made.
function getJwtPayload(token: string): { role?: string; exp?: number } | null {
try {
const part = token.split('.')[1];
if (!part) return null;
// Fix base64url → base64 padding
const padded = part.padEnd(part.length + ((4 - (part.length % 4)) % 4), '=');
const decoded = atob(padded.replace(/-/g, '+').replace(/_/g, '/'));
return JSON.parse(decoded) as { role?: string; exp?: number };
} catch {
return null;
}
}
// Maps URL path prefixes to the role required to access them.
const ROLE_REQUIREMENTS: [RegExp, string][] = [
[/^\/(dashboard|history|qr|partners)/, 'student'],
[/^\/scan/, 'partner'],
[/^\/admin/, 'admin'],
];
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const required = ROLE_REQUIREMENTS.find(([re]) => re.test(pathname))?.[1];
if (!required) return NextResponse.next();
const token = request.cookies.get('access_token')?.value;
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
const payload = getJwtPayload(token);
// Redirect if the token is missing, expired, or has the wrong role.
if (!payload || payload.role !== required) {
return NextResponse.redirect(new URL('/login', request.url));
}
if (payload.exp !== undefined && payload.exp * 1000 < Date.now()) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ['/(dashboard|history|qr|partners|scan|admin)(.*)'],
};