110 lines
4.1 KiB
TypeScript
110 lines
4.1 KiB
TypeScript
import { NextResponse } from 'next/server'
|
|
import type { NextRequest } from 'next/server'
|
|
|
|
const PUBLIC_PREFIXES = [
|
|
'/login',
|
|
'/api/auth',
|
|
'/api/trpc/stellen.listPublic',
|
|
'/api/setup',
|
|
'/registrierung',
|
|
'/impressum',
|
|
'/datenschutz',
|
|
]
|
|
const PUBLIC_EXACT_PATHS = ['/']
|
|
|
|
// Reserved subdomains that shouldn't be treated as tenant slugs
|
|
const RESERVED_SUBDOMAINS = [
|
|
'www', 'app', 'admin', 'localhost', 'superadmin', 'api',
|
|
'logo.png', 'favicon.ico', 'robots.txt', 'sitemap.xml',
|
|
'apple-touch-icon', 'android-chrome', 'manifest'
|
|
]
|
|
|
|
export function middleware(request: NextRequest) {
|
|
const url = request.nextUrl
|
|
const pathname = url.pathname
|
|
|
|
// 1. Subdomain Extraction
|
|
const hostname = request.headers.get('host') || ''
|
|
const domainParts = hostname.split(':')[0].split('.')
|
|
let slug = null
|
|
|
|
// For localhost: tischler.localhost -> parts: ['tischler', 'localhost']
|
|
// For production: tischler.innungsapp.de -> parts: ['tischler', 'innungsapp', 'de']
|
|
if (
|
|
domainParts.length > 2 ||
|
|
(domainParts.length === 2 && domainParts[1] === 'localhost')
|
|
) {
|
|
const potentialSlug = domainParts[0]
|
|
if (!RESERVED_SUBDOMAINS.includes(potentialSlug)) {
|
|
slug = potentialSlug
|
|
}
|
|
}
|
|
|
|
// Allow static files from /public
|
|
const isStaticFile = pathname.includes('.') && !pathname.startsWith('/api')
|
|
const isPublic =
|
|
isStaticFile ||
|
|
PUBLIC_EXACT_PATHS.includes(pathname) ||
|
|
PUBLIC_PREFIXES.some((p) => pathname.startsWith(p))
|
|
|
|
// 2. Auth Check
|
|
const sessionToken =
|
|
request.cookies.get('better-auth.session_token') ??
|
|
request.cookies.get('__Secure-better-auth.session_token')
|
|
|
|
if (!isPublic && !sessionToken) {
|
|
const loginUrl = new URL('/login', request.url)
|
|
loginUrl.searchParams.set('callbackUrl', pathname)
|
|
return NextResponse.redirect(loginUrl)
|
|
}
|
|
|
|
// 3. Subdomain Redirection / Rewrite
|
|
if (slug) {
|
|
// Paths that should not be rewritten into the slug folder
|
|
// because they are shared across the entire app
|
|
const SHARED_PATHS = ['/login', '/api', '/superadmin', '/registrierung', '/impressum', '/datenschutz', '/passwort-aendern']
|
|
const isSharedPath = SHARED_PATHS.some((p) => pathname.startsWith(p)) ||
|
|
pathname.startsWith('/_next') ||
|
|
/\.(png|jpg|jpeg|gif|svg|webp|ico|txt|xml)$/i.test(pathname)
|
|
|
|
if (!isSharedPath && !pathname.startsWith(`/${slug}`)) {
|
|
const rewriteUrl = request.nextUrl.clone()
|
|
rewriteUrl.pathname = `/${slug}${pathname === '/' ? '' : pathname}`
|
|
return NextResponse.rewrite(rewriteUrl)
|
|
}
|
|
} else {
|
|
// Check if the user is trying to access a path that starts with a potential slug
|
|
// but they are on the root domain.
|
|
// Example: localhost/tischler/... should redirect to tischler.localhost/...
|
|
const pathParts = pathname.split('/')
|
|
if (pathParts.length > 1) {
|
|
const potentialSlug = pathParts[1]
|
|
// Check if it's a known non-reserved path but could be an organization slug
|
|
// We don't want to redirect /login, /api, etc.
|
|
const SHARED_PATHS = ['login', 'api', 'superadmin', 'dashboard', 'registrierung', 'impressum', 'datenschutz', '_next', 'uploads', 'favicon.ico', 'passwort-aendern']
|
|
const isStaticAsset = /\.(png|jpg|jpeg|gif|svg|webp|ico|txt|xml)$/i.test(potentialSlug)
|
|
if (potentialSlug && !SHARED_PATHS.includes(potentialSlug) && !isStaticAsset) {
|
|
// This looks like a tenant path being accessed from the root domain.
|
|
// Redirect to subdomain.
|
|
const baseHost = hostname.split('.').slice(-2).join('.') // Simplistic, assumes domain.tld or localhost
|
|
// For localhost it's special
|
|
const isLocalhost = hostname.includes('localhost')
|
|
const newHost = isLocalhost
|
|
? `${potentialSlug}.localhost${hostname.includes(':') ? `:${hostname.split(':')[1]}` : ''}`
|
|
: `${potentialSlug}.${baseHost}`
|
|
|
|
const remainingPath = '/' + pathParts.slice(2).join('/')
|
|
return NextResponse.redirect(new URL(remainingPath, `${url.protocol}//${newHost}`))
|
|
}
|
|
}
|
|
}
|
|
|
|
return NextResponse.next()
|
|
}
|
|
|
|
export const config = {
|
|
matcher: [
|
|
'/((?!_next/static|_next/image|favicon.ico|uploads).*)',
|
|
],
|
|
}
|