Sifa professional network frontend (Next.js, React, TailwindCSS)
sifa.id/
1import { NextResponse, type NextRequest } from 'next/server';
2
3interface SessionResponse {
4 authenticated: boolean;
5 did: string;
6 handle: string;
7 displayName: string;
8}
9
10function handleAssetProtection(request: NextRequest): NextResponse | null {
11 const referer = request.headers.get('referer');
12 const host = request.headers.get('host') ?? '';
13
14 if (!referer) {
15 return null;
16 }
17
18 const isOwnSite = referer.includes(host) || referer.includes('sifa.id');
19
20 if (!isOwnSite) {
21 return new NextResponse(null, { status: 403 });
22 }
23
24 return null;
25}
26
27async function handleAdminProtection(request: NextRequest): Promise<NextResponse | null> {
28 const sessionCookie = request.cookies.get('session');
29
30 if (!sessionCookie?.value) {
31 return NextResponse.redirect(new URL('/login', request.url));
32 }
33
34 const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3100';
35
36 let session: SessionResponse;
37 try {
38 const response = await fetch(`${apiUrl}/api/auth/session`, {
39 headers: {
40 cookie: `session=${sessionCookie.value}`,
41 },
42 });
43
44 if (!response.ok) {
45 return NextResponse.redirect(new URL('/login', request.url));
46 }
47
48 session = (await response.json()) as SessionResponse;
49 } catch {
50 return NextResponse.redirect(new URL('/login', request.url));
51 }
52
53 if (!session.authenticated) {
54 return NextResponse.redirect(new URL('/login', request.url));
55 }
56
57 const adminDids = (process.env.ADMIN_DIDS ?? '')
58 .split(',')
59 .map((did) => did.trim())
60 .filter(Boolean);
61
62 if (!adminDids.includes(session.did)) {
63 return NextResponse.redirect(new URL('/', request.url));
64 }
65
66 return null;
67}
68
69export async function middleware(request: NextRequest): Promise<NextResponse> {
70 const { pathname } = request.nextUrl;
71
72 if (pathname.startsWith('/admin')) {
73 const adminResponse = await handleAdminProtection(request);
74 if (adminResponse) {
75 return adminResponse;
76 }
77 return NextResponse.next();
78 }
79
80 // Asset hotlink protection
81 const assetResponse = handleAssetProtection(request);
82 if (assetResponse) {
83 return assetResponse;
84 }
85
86 return NextResponse.next();
87}
88
89export const config = {
90 matcher: ['/admin/:path*', '/assets/:path*'],
91};