Add custom domain support

This commit is contained in:
Julien Nahum 2024-01-12 15:43:28 +01:00
parent a0513c4458
commit ea7041be28
6 changed files with 108 additions and 18 deletions

View File

@ -11,7 +11,7 @@ use Illuminate\Database\Eloquent\Builder;
class CustomDomainRestriction class CustomDomainRestriction
{ {
const CUSTOM_DOMAIN_HEADER = "User-Custom-Domain"; const CUSTOM_DOMAIN_HEADER = "x-custom-domain";
/** /**
* Handle an incoming request. * Handle an incoming request.
@ -27,7 +27,8 @@ class CustomDomainRestriction
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => 'Invalid domain', 'message' => 'Invalid domain',
], 400); 'error' => 'invalid_domain',
], 401);
} }
// Check if domain is different from current domain // Check if domain is different from current domain
@ -41,6 +42,7 @@ class CustomDomainRestriction
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => 'Unknown domain', 'message' => 'Unknown domain',
'error' => 'invalid_domain',
], 400); ], 400);
} }

View File

@ -1,3 +1,4 @@
import {getDomain, getHost, customDomainUsed} from "~/lib/utils.js";
function addAuthHeader(request, options) { function addAuthHeader(request, options) {
const authStore = useAuthStore() const authStore = useAuthStore()
@ -17,6 +18,14 @@ function addPasswordToFormRequest(request, options) {
} }
} }
/**
* Add custom domain header if custom domain is used
*/
function addCustomDomainHeader(request, options) {
if (!customDomainUsed()) return
options.headers['x-custom-domain'] = getDomain(getHost())
}
export function getOpnRequestsOptions(request, opts) { export function getOpnRequestsOptions(request, opts) {
const config = useRuntimeConfig() const config = useRuntimeConfig()
@ -29,18 +38,28 @@ export function getOpnRequestsOptions(request, opts) {
addAuthHeader(request, opts) addAuthHeader(request, opts)
addPasswordToFormRequest(request, opts) addPasswordToFormRequest(request, opts)
addCustomDomainHeader(request, opts)
return { return {
baseURL: config.public.apiBase, baseURL: config.public.apiBase,
onResponseError({response}) { onResponseError({response}) {
const authStore = useAuthStore() const authStore = useAuthStore()
console.log(response)
const {status} = response const {status} = response
if (status === 401 && authStore.check) { if (status === 401) {
if (response.body.error && response.body.error === 'invalid_domain' && process.client) {
// If invalid domain, redirect to main domain
window.location.href = config.public.appUrl + '?utm_source=failed_custom_domain_redirect'
return
}
if (authStore.check) {
console.log("Logging out due to 401") console.log("Logging out due to 401")
authStore.logout() authStore.logout()
useRouter().push({name: 'login'}) useRouter().push({name: 'login'})
} }
}
if (status >= 500) { if (status >= 500) {
console.error('Request error', status) console.error('Request error', status)

43
client/lib/utils.js vendored
View File

@ -1,4 +1,3 @@
export const hash = (str, seed = 0) => { export const hash = (str, seed = 0) => {
let h1 = 0xdeadbeef ^ seed, let h1 = 0xdeadbeef ^ seed,
h2 = 0x41c6ce57 ^ seed; h2 = 0x41c6ce57 ^ seed;
@ -14,6 +13,15 @@ export const hash = (str, seed = 0) => {
return 4294967296 * (2097151 & h2) + (h1 >>> 0); return 4294967296 * (2097151 & h2) + (h1 >>> 0);
} }
/*
* Url and domain related utils
*/
/**
* Returns the appUrl with the given path appended.
* @param path
* @returns {string}
*/
export const appUrl = (path = '/') => { export const appUrl = (path = '/') => {
let baseUrl = useRuntimeConfig().public.appUrl let baseUrl = useRuntimeConfig().public.appUrl
if (!baseUrl) { if (!baseUrl) {
@ -31,3 +39,36 @@ export const appUrl = (path = '/') => {
return baseUrl + path return baseUrl + path
} }
/**
* SSR compatible function to get current host
* @param path
* @returns {string}
*/
export const getHost = function () {
if (process.server) {
return useNuxtApp().ssrContext?.event.context.siteConfigNitroOrigin || useNuxtApp().ssrContext?.event.node.req.headers.host
} else {
return window.location.host
}
}
/**
* Extract domain from url
* @param url
* @returns {*}
*/
export const getDomain = function (url) {
return (new URL(url)).hostname
}
/**
* Returns true if the app is running on a custom domain, false otherwise.
* @returns {boolean}
*/
export const customDomainUsed = function() {
const config = useRuntimeConfig()
const appUrl = config.public.appUrl
return getDomain(getHost()) !== getDomain(appUrl)
}

View File

@ -1,16 +1,42 @@
import opnformConfig from "~/opnform.config.js"; import {customDomainUsed, getDomain, getHost} from "~/lib/utils.js";
function getDomain (url) { /**
return (new URL(url)).hostname * Added by Caddy when proxying to the app
* @type {string}
*/
const customDomainHeaderName = 'CUSTOM_DOMAIN_HEADER'
/**
* List of routes that can be used with a custom domain
* @type {string[]}
*/
const customDomainAllowedRoutes = ['forms-slug']
function redirectToMainDomain() {
return navigateTo(useRuntimeConfig().public.appUrl + '?utm_source=failed_custom_domain_redirect', { redirectCode: 301, external: true })
} }
export default defineNuxtRouteMiddleware((to, from) => { export default defineNuxtRouteMiddleware((to, from) => {
if (opnformConfig.custom_domains_enabled && process.client) { if (process.client) return
const isCustomDomain = getDomain(window.location.href) !== getDomain(opnformConfig.app_url)
if (isCustomDomain && !['forms.show_public'].includes(to.name)) { const config = useRuntimeConfig()
// If route isn't a public form, redirect
return navigateTo({name: 'home',query: {utm_source: 'failed_custom_domain_redirect'}}); if (!customDomainUsed()) return
const customDomainHeaderValue = useRequestHeaders()[customDomainHeaderName]
if (!customDomainHeaderValue || customDomainHeaderValue !== getDomain(getHost())) {
// If custom domain header doesn't match, redirect
return redirectToMainDomain()
} }
if (!config.public.customDomainsEnabled) {
// If custom domain not allowed, redirect
return redirectToMainDomain()
}
if (!customDomainAllowedRoutes.includes(to.name)) {
// Custom domain only allowed for form url
return redirectToMainDomain()
} }
}) })

View File

@ -1,10 +1,8 @@
export default defineNitroPlugin(nitroApp => { export default defineNitroPlugin(nitroApp => {
nitroApp.hooks.hook('render:response', (response, { event }) => { nitroApp.hooks.hook('render:response', (response, { event }) => {
const routePath = event.node?.req?.url || event.node?.req?.originalUrl const routePath = event.node?.req?.url || event.node?.req?.originalUrl
console.log(routePath, !routePath.startsWith('/forms/'))
// const routePath= event.context.params._ // const routePath= event.context.params._
if (routePath && !routePath.startsWith('/forms/')) { if (routePath && !routePath.startsWith('/forms/')) {
console.log(response, event)
// Only allow embedding of forms // Only allow embedding of forms
response.headers['X-Frame-Options'] = 'sameorigin' response.headers['X-Frame-Options'] = 'sameorigin'
} }

View File

@ -14,3 +14,7 @@ export default async (to, from, next) => {
function getDomain (url) { function getDomain (url) {
return (new URL(url)).hostname return (new URL(url)).hostname
} }
function isCustomDomain (url) {
return getDomain(url) !== getDomain(window.config.app_url)
}