Skip to content

Commit a63004b

Browse files
committed
Middleware review changes
Domain mappings: - use direct db calls with cachedFetcher to map domains Middleware: - pass main domain, custom domain, subName to the customDomainMiddleware; - retrieve main domain's hostname from env vars; - only get subName from domain mappings cache (domain is the key); - only check cache if we're not on the main domain; correct function declarations; - light cleanup Schema: - use Citext for domain names as they're case-insensitive
1 parent 2f7e852 commit a63004b

File tree

4 files changed

+68
-48
lines changed

4 files changed

+68
-48
lines changed

lib/domains.js

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { cachedFetcher } from '@/lib/fetch'
2+
import prisma from '@/api/models'
23

34
export const loggerInstance = process.env.CUSTOM_DOMAIN_LOGGER === 'true'
45
? {
@@ -16,21 +17,32 @@ export const loggerInstance = process.env.CUSTOM_DOMAIN_LOGGER === 'true'
1617

1718
export const domainLogger = () => loggerInstance
1819

19-
// fetch custom domain mappings from our API, caching it for 5 minutes
20+
// fetch custom domain mappings from database, caching it for 5 minutes
2021
export const getDomainMappingsCache = cachedFetcher(async function fetchDomainMappings () {
21-
const url = `${process.env.NEXT_PUBLIC_URL}/api/domains`
22-
domainLogger().log('fetching domain mappings from', url) // TEST
22+
domainLogger().log('fetching domain mappings from database') // TEST
2323
try {
24-
const response = await fetch(url)
25-
if (!response.ok) {
26-
domainLogger().error(`Cannot fetch domain mappings: ${response.status} ${response.statusText}`)
27-
return null
28-
}
24+
// fetch all VERIFIED custom domains from the database
25+
const domains = await prisma.customDomain.findMany({
26+
select: {
27+
domain: true,
28+
subName: true
29+
},
30+
where: {
31+
status: 'ACTIVE'
32+
}
33+
})
34+
35+
// map domains to a key-value pair
36+
const domainMappings = domains.reduce((acc, domain) => {
37+
acc[domain.domain.toLowerCase()] = {
38+
subName: domain.subName
39+
}
40+
return acc
41+
}, {})
2942

30-
const data = await response.json()
31-
return Object.keys(data).length > 0 ? data : null
43+
return domainMappings
3244
} catch (error) {
33-
domainLogger().error('Cannot fetch domain mappings:', error)
45+
domainLogger().error('cannot fetch domain mappings from db:', error)
3446
return null
3547
}
3648
}, {

middleware.js

Lines changed: 43 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,71 +12,73 @@ const SN_REFERRER = 'sn_referrer'
1212
const SN_REFERRER_NONCE = 'sn_referrer_nonce'
1313
// key for referred pages
1414
const SN_REFEREE_LANDING = 'sn_referee_landing'
15-
// rewrite to ~subname paths
15+
// rewrite to ~subName paths
1616
const TERRITORY_PATHS = ['/~', '/recent', '/random', '/top', '/post', '/edit']
1717

18-
// get a domain mapping from the cache
19-
export async function getDomainMapping (domain) {
20-
const domainMappings = await getDomainMappingsCache()
21-
return domainMappings?.[domain]
22-
}
23-
2418
// Redirects and rewrites for custom domains
25-
export async function customDomainMiddleware (request, referrerResp, domain) {
26-
const host = request.headers.get('host')
27-
const url = request.nextUrl.clone()
19+
async function customDomainMiddleware (req, referrerResp, mainDomain, domain, subName) {
20+
// clone the request url to build on top of it
21+
const url = req.nextUrl.clone()
22+
// get the pathname from the url
2823
const pathname = url.pathname
29-
const mainDomain = process.env.NEXT_PUBLIC_URL
24+
// get the query params from the url
25+
const query = url.searchParams
3026

3127
// TEST
32-
domainLogger().log('host', host)
33-
domainLogger().log('mainDomain', mainDomain)
28+
domainLogger().log('req.headers host', req.headers.get('host'))
29+
domainLogger().log('main domain', mainDomain)
30+
domainLogger().log('custom domain', domain)
31+
domainLogger().log('subName', subName)
3432
domainLogger().log('pathname', pathname)
35-
domainLogger().log('query', url.searchParams)
33+
domainLogger().log('query', query)
3634

37-
const requestHeaders = new Headers(request.headers)
38-
requestHeaders.set('x-stacker-news-subname', domain.subName)
35+
const requestHeaders = new Headers(req.headers)
36+
requestHeaders.set('x-stacker-news-subname', subName)
3937

4038
// Auth sync redirects with domain and optional callbackUrl and multiAuth params
4139
if (pathname === '/login' || pathname === '/signup') {
4240
const redirectUrl = new URL(pathname, mainDomain)
43-
redirectUrl.searchParams.set('domain', host)
44-
if (url.searchParams.get('callbackUrl')) {
45-
redirectUrl.searchParams.set('callbackUrl', url.searchParams.get('callbackUrl'))
41+
redirectUrl.searchParams.set('domain', domain)
42+
if (query.get('callbackUrl')) {
43+
redirectUrl.searchParams.set('callbackUrl', query.get('callbackUrl'))
4644
}
47-
if (url.searchParams.get('multiAuth')) {
48-
redirectUrl.searchParams.set('multiAuth', url.searchParams.get('multiAuth'))
45+
if (query.get('multiAuth')) {
46+
redirectUrl.searchParams.set('multiAuth', query.get('multiAuth'))
4947
}
5048
const redirectResp = NextResponse.redirect(redirectUrl, {
5149
headers: requestHeaders
5250
})
53-
return applyReferrerCookies(redirectResp, referrerResp) // apply referrer cookies to the redirect
51+
// apply referrer cookies to the redirect
52+
return applyReferrerCookies(redirectResp, referrerResp)
5453
}
5554

5655
// If trying to access a ~subname path, rewrite to /
57-
if (pathname.startsWith(`/~${domain.subName}`)) {
56+
if (pathname.startsWith(`/~${subName}`)) {
5857
// remove the territory prefix from the path
59-
const cleanPath = pathname.replace(`/~${domain.subName}`, '') || '/'
58+
const cleanPath = pathname.replace(`/~${subName}`, '') || '/'
6059
domainLogger().log('Redirecting to clean path:', cleanPath)
6160
const redirectResp = NextResponse.redirect(new URL(cleanPath + url.search, url.origin), {
6261
headers: requestHeaders
6362
})
64-
return applyReferrerCookies(redirectResp, referrerResp) // apply referrer cookies to the redirect
63+
// apply referrer cookies to the redirect
64+
return applyReferrerCookies(redirectResp, referrerResp)
6565
}
6666

6767
// If we're at the root or a territory path, rewrite to the territory path
6868
if (pathname === '/' || TERRITORY_PATHS.some(p => pathname.startsWith(p))) {
6969
const internalUrl = new URL(url)
70-
internalUrl.pathname = `/~${domain.subName}${pathname === '/' ? '' : pathname}`
70+
internalUrl.pathname = `/~${subName}${pathname === '/' ? '' : pathname}`
7171
domainLogger().log('Rewrite to:', internalUrl.pathname)
7272
// rewrite to the territory path
7373
const resp = NextResponse.rewrite(internalUrl, {
7474
headers: requestHeaders
7575
})
76-
return applyReferrerCookies(resp, referrerResp) // apply referrer cookies to the rewrite
76+
// apply referrer cookies to the rewrite
77+
return applyReferrerCookies(resp, referrerResp)
7778
}
7879

79-
return NextResponse.next({ // continue if we don't need to rewrite or redirect
80+
// continue if we don't need to rewrite or redirect
81+
return NextResponse.next({
8082
request: {
8183
headers: requestHeaders
8284
}
@@ -171,7 +173,7 @@ function referrerMiddleware (request) {
171173
return response
172174
}
173175

174-
export function applySecurityHeaders (resp) {
176+
function applySecurityHeaders (resp) {
175177
const isDev = process.env.NODE_ENV === 'development'
176178

177179
const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
@@ -227,12 +229,18 @@ export async function middleware (request) {
227229
}
228230

229231
// If we're on a custom domain, handle that next
230-
const host = request.headers.get('host')
231-
const isAllowedDomain = await getDomainMapping(host?.toLowerCase())
232-
if (isAllowedDomain) {
233-
domainLogger().log('detected allowed custom domain', isAllowedDomain)
234-
const customDomainResp = await customDomainMiddleware(request, referrerResp, isAllowedDomain)
235-
return applySecurityHeaders(customDomainResp)
232+
const domain = request.headers.get('x-forwarded-host') || request.headers.get('host')
233+
// get the main domain from the env vars
234+
const mainDomain = new URL(process.env.NEXT_PUBLIC_URL).host
235+
if (domain !== mainDomain) {
236+
// get the subName from the domain mappings cache
237+
const { subName } = await getDomainMappingsCache()?.[domain?.toLowerCase()]
238+
if (subName) {
239+
domainLogger().log('detected allowed custom domain for: ', subName)
240+
// handle the custom domain
241+
const customDomainResp = await customDomainMiddleware(request, referrerResp, mainDomain, domain, subName)
242+
return applySecurityHeaders(customDomainResp)
243+
}
236244
}
237245

238246
return applySecurityHeaders(referrerResp)

prisma/migrations/20250304121322_custom_domains/migration.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ CREATE TABLE "CustomDomain" (
33
"id" SERIAL NOT NULL,
44
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
55
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
6-
"domain" TEXT NOT NULL,
6+
"domain" CITEXT NOT NULL,
77
"subName" CITEXT NOT NULL,
88
"status" TEXT NOT NULL DEFAULT 'PENDING',
99
"failedAttempts" INTEGER NOT NULL DEFAULT 0,

prisma/schema.prisma

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1250,7 +1250,7 @@ model CustomDomain {
12501250
id Int @id @default(autoincrement())
12511251
createdAt DateTime @default(now()) @map("created_at")
12521252
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
1253-
domain String @unique
1253+
domain String @unique @db.Citext
12541254
subName String @unique @db.Citext
12551255
status String @default("PENDING")
12561256
failedAttempts Int @default(0)

0 commit comments

Comments
 (0)