import { escapeHtml, toJsLiteral } from '../pwa/source-literals'
import { AccessRulesSettings } from './settings'
/**
* The country rule, decided at the edge before a page is served: the
* visitor's country from the platform (`request.geo`, else the
* `x-vercel-ip-country` header) against the listed countries. A blocked
* visitor is served the project's own Unavailable page in place of what was
* asked for (a plain notice, HTTP 451, when the project has none).
* API routes are never blocked — the site's own server segments call them —
* nor files (robots.txt, the manifest, images: the Unavailable page's own
* assets among them).
*/
/** The plain notice, for a project whose Unavailable page is gone. */
const UNAVAILABLE_MESSAGE = 'This site is not available in your country.'
/** The `` of the plain notice. */
const buildUnavailablePage = (message: string): string => {
const safe = escapeHtml(message)
return [
'
',
'',
'',
`${safe}`,
'',
`${safe}
`,
].join('')
}
/**
* The helpers and `__tqGeoResponse(request)`: the response a blocked visitor
* gets, `null` for everyone else. Expects `NextResponse` in scope.
*/
export const buildGeoMiddlewareSegment = (
settings: AccessRulesSettings,
unavailableRoute: string | null
): string => {
const geo = {
mode: settings.geoMode,
countries: settings.geoCountries,
unknown: settings.geoUnknownCountry,
unavailableRoute,
}
return `// Age gate & geo block: the country rule.
var __tqGeo = ${toJsLiteral(geo)};
var __tqGeoUnavailablePage = ${toJsLiteral(buildUnavailablePage(UNAVAILABLE_MESSAGE))};
function __tqGeoCountry(request) {
var country = (request.geo && request.geo.country) || request.headers.get('x-vercel-ip-country') || '';
return String(country).toUpperCase();
}
function __tqGeoBlocked(request) {
if (__tqGeo.mode === 'off' || __tqGeo.countries.length === 0) return false;
var country = __tqGeoCountry(request);
if (!country) return __tqGeo.unknown === 'block';
var listed = __tqGeo.countries.indexOf(country) >= 0;
return __tqGeo.mode === 'block' ? listed : !listed;
}
function __tqGeoResponse(request) {
var pathname = request.nextUrl.pathname;
if (pathname === '/api' || pathname.indexOf('/api/') === 0 || pathname.indexOf('/_next/') === 0) {
return null;
}
// A file (its last segment has an extension), never a page.
if (/\\.[a-z0-9]{1,8}$/i.test(pathname)) return null;
if (!__tqGeoBlocked(request)) return null;
if (__tqGeo.unavailableRoute) {
// The project's own Unavailable page, served in place of what was asked for.
var route = __tqGeo.unavailableRoute;
if (pathname === route || pathname.indexOf(route + '/') === 0) return null;
var unavailable = request.nextUrl.clone();
unavailable.pathname = route;
unavailable.search = '';
var rewritten = NextResponse.rewrite(unavailable);
rewritten.headers.set('x-robots-tag', 'noindex');
rewritten.headers.set('cache-control', 'no-store');
return rewritten;
}
return new NextResponse(__tqGeoUnavailablePage, {
status: 451,
headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' },
});
}
`
}
const DEFAULT_EXPORT = 'export default middleware;'
/**
* The authentication middleware with the country rule in front of it: the
* segment is appended, and the default export wrapped, so the file keeps
* every auth helper untouched. Null when the file has no default export to
* wrap (then the rule is left out rather than a broken middleware written).
*/
export const wrapMiddlewareWithGeo = (content: string, segment: string): string | null => {
if (!content.includes(DEFAULT_EXPORT) || content.includes('__tqGeoResponse')) {
return null
}
return content.replace(
DEFAULT_EXPORT,
`${segment}
function __tqWithGeo(next) {
return function (request) {
return __tqGeoResponse(request) || next(request);
};
}
export default __tqWithGeo(middleware);`
)
}
/** A middleware of the rule's own, for a project without the authentication one. */
export const buildStandaloneGeoMiddleware = (
segment: string
): string => `import { NextResponse } from 'next/server';
${segment}
function middleware(request) {
return __tqGeoResponse(request) || NextResponse.next();
}
export default middleware;
export const config = {
// '/' on its own, because Next.js does not run the second matcher on the root path.
matcher: ['/', '/((?!api/|_next/static|_next/image|favicon\\\\.ico).*)'],
};
`