Files
Lageplan/src/middleware.ts
Pepe Ziberi 6aa70e1504
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 31m39s
feat(share): Nur-Ansicht-Teilen-Link (live, optional PIN) + QR (v1.6.9)
Neuer öffentlicher Teilen-Link für die aktuelle Lage — ohne Login nur zum
Ansehen, live aktualisiert. Optional mit 4–8-stelliger PIN geschützt.

- DB: Project.shareToken (unique) / sharePin (bcrypt) / shareEnabled + Migration
- API: POST/GET /api/projects/[id]/share (Verwaltung, Mandanten-Check, VIEWER blockiert),
  GET/POST /api/share/[token] (öffentlich, PIN-Prüfung, Rate-Limit gegen Brute-Force)
- Öffentliche Seite /view/[token]: PIN-Gate, read-only MapView, Socket-Live + 30s-Poll
- Teilen-Dialog im Werkzeuge-/Mobil-Menü: Link kopieren, QR-Code, PIN setzen/entfernen,
  Teilen beenden (widerruft den Link)
- Middleware: /api/share/ öffentlich freigeschaltet

Hinweis: hochgeladene Custom-Symbole zeigen im anonymen Viewer den Platzhalter
(Icon-Endpunkte bleiben auth-geschützt); eingebaute taktische Zeichen erscheinen normal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 23:30:47 +02:00

113 lines
3.4 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server'
import { jwtVerify } from 'jose'
import { JWT_SECRET } from './lib/jwt-secret'
// Routes that require authentication
const PROTECTED_ROUTES = ['/app', '/settings', '/admin']
// Routes that should redirect to /app if already logged in
const AUTH_ROUTES = ['/login', '/register']
// API routes that are public (no auth needed)
const PUBLIC_API_PREFIXES = [
'/api/auth/login',
'/api/auth/register',
'/api/auth/forgot-password',
'/api/auth/reset-password',
'/api/auth/verify-email',
'/api/auth/resend-verification',
'/api/auth/logout',
'/api/contact',
'/api/demo',
'/api/donate',
'/api/rapports/',
'/api/tenants/by-slug/',
'/api/share/',
]
export async function middleware(req: NextRequest) {
const { pathname } = req.nextUrl
const token = req.cookies.get('auth-token')?.value
// Verify token if present
let user: any = null
if (token) {
try {
const { payload } = await jwtVerify(token, JWT_SECRET)
user = payload.user
} catch {
// Invalid/expired token — clear it
const response = NextResponse.redirect(new URL('/login', req.url))
response.cookies.delete('auth-token')
// Only redirect if accessing protected routes
if (PROTECTED_ROUTES.some(r => pathname.startsWith(r))) {
return response
}
}
}
// Protected routes: redirect to login if not authenticated
if (PROTECTED_ROUTES.some(r => pathname.startsWith(r))) {
if (!user) {
const loginUrl = new URL('/login', req.url)
loginUrl.searchParams.set('redirect', pathname)
return NextResponse.redirect(loginUrl)
}
// Admin routes: only SERVER_ADMIN and TENANT_ADMIN
if (pathname.startsWith('/admin') && user.role !== 'SERVER_ADMIN' && user.role !== 'TENANT_ADMIN') {
return NextResponse.redirect(new URL('/app', req.url))
}
}
// Auth routes: redirect to /app if already logged in
if (AUTH_ROUTES.some(r => pathname.startsWith(r))) {
if (user) {
return NextResponse.redirect(new URL('/app', req.url))
}
}
// API routes: check auth for non-public endpoints
if (pathname.startsWith('/api/') && !PUBLIC_API_PREFIXES.some(p => pathname.startsWith(p))) {
if (!user) {
// Allow /api/auth/me to return null (used for auth check)
if (pathname === '/api/auth/me') {
return NextResponse.next()
}
// Allow /api/icons GET (public for symbol loading)
if (pathname === '/api/icons' && req.method === 'GET') {
return NextResponse.next()
}
return NextResponse.json({ error: 'Nicht autorisiert' }, { status: 401 })
}
}
// Security: block common attack paths
if (
pathname.includes('..') ||
pathname.includes('.env') ||
pathname.includes('wp-admin') ||
pathname.includes('wp-login') ||
pathname.includes('.php') ||
pathname.includes('xmlrpc') ||
pathname.match(/\.(sql|bak|config|log|ini)$/i)
) {
return new NextResponse(null, { status: 404 })
}
return NextResponse.next()
}
export const config = {
matcher: [
/*
* Match all request paths except:
* - _next/static (static files)
* - _next/image (image optimization)
* - favicon.ico, sitemap.xml, robots.txt
* - public files (images, sw.js, etc.)
*/
'/((?!_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt|icons/|sw.js|manifest.json|opengraph-image).*)',
],
}