From ea7041be28833d9f524699282724428e15e789ae Mon Sep 17 00:00:00 2001 From: Julien Nahum Date: Fri, 12 Jan 2024 15:43:28 +0100 Subject: [PATCH] Add custom domain support --- .../Middleware/CustomDomainRestriction.php | 6 ++- client/composables/useOpnApi.js | 27 ++++++++++-- client/lib/utils.js | 43 +++++++++++++++++- client/middleware/custom-domain.global.js | 44 +++++++++++++++---- client/server/plugins/embeddable.js | 2 - resources/js/middleware/custom-domains.js | 4 ++ 6 files changed, 108 insertions(+), 18 deletions(-) diff --git a/app/Http/Middleware/CustomDomainRestriction.php b/app/Http/Middleware/CustomDomainRestriction.php index 9553f27..3ff5b8b 100644 --- a/app/Http/Middleware/CustomDomainRestriction.php +++ b/app/Http/Middleware/CustomDomainRestriction.php @@ -11,7 +11,7 @@ use Illuminate\Database\Eloquent\Builder; class CustomDomainRestriction { - const CUSTOM_DOMAIN_HEADER = "User-Custom-Domain"; + const CUSTOM_DOMAIN_HEADER = "x-custom-domain"; /** * Handle an incoming request. @@ -27,7 +27,8 @@ class CustomDomainRestriction return response()->json([ 'success' => false, 'message' => 'Invalid domain', - ], 400); + 'error' => 'invalid_domain', + ], 401); } // Check if domain is different from current domain @@ -41,6 +42,7 @@ class CustomDomainRestriction return response()->json([ 'success' => false, 'message' => 'Unknown domain', + 'error' => 'invalid_domain', ], 400); } diff --git a/client/composables/useOpnApi.js b/client/composables/useOpnApi.js index 99d9a5f..e2c0ede 100644 --- a/client/composables/useOpnApi.js +++ b/client/composables/useOpnApi.js @@ -1,3 +1,4 @@ +import {getDomain, getHost, customDomainUsed} from "~/lib/utils.js"; function addAuthHeader(request, options) { 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) { const config = useRuntimeConfig() @@ -29,17 +38,27 @@ export function getOpnRequestsOptions(request, opts) { addAuthHeader(request, opts) addPasswordToFormRequest(request, opts) + addCustomDomainHeader(request, opts) return { baseURL: config.public.apiBase, onResponseError({response}) { const authStore = useAuthStore() + console.log(response) const {status} = response - if (status === 401 && authStore.check) { - console.log("Logging out due to 401") - authStore.logout() - useRouter().push({name: 'login'}) + 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") + authStore.logout() + useRouter().push({name: 'login'}) + } } if (status >= 500) { diff --git a/client/lib/utils.js b/client/lib/utils.js index bcb4bb0..b6914b8 100644 --- a/client/lib/utils.js +++ b/client/lib/utils.js @@ -1,4 +1,3 @@ - export const hash = (str, seed = 0) => { let h1 = 0xdeadbeef ^ seed, h2 = 0x41c6ce57 ^ seed; @@ -14,6 +13,15 @@ export const hash = (str, seed = 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 = '/') => { let baseUrl = useRuntimeConfig().public.appUrl if (!baseUrl) { @@ -31,3 +39,36 @@ export const appUrl = (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) +} diff --git a/client/middleware/custom-domain.global.js b/client/middleware/custom-domain.global.js index 7ec91b2..d5711e9 100644 --- a/client/middleware/custom-domain.global.js +++ b/client/middleware/custom-domain.global.js @@ -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) => { - if (opnformConfig.custom_domains_enabled && process.client) { - const isCustomDomain = getDomain(window.location.href) !== getDomain(opnformConfig.app_url) - if (isCustomDomain && !['forms.show_public'].includes(to.name)) { - // If route isn't a public form, redirect - return navigateTo({name: 'home',query: {utm_source: 'failed_custom_domain_redirect'}}); - } + if (process.client) return + + const config = useRuntimeConfig() + + 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() } }) diff --git a/client/server/plugins/embeddable.js b/client/server/plugins/embeddable.js index 55b026d..cdfe1f0 100644 --- a/client/server/plugins/embeddable.js +++ b/client/server/plugins/embeddable.js @@ -1,10 +1,8 @@ export default defineNitroPlugin(nitroApp => { nitroApp.hooks.hook('render:response', (response, { event }) => { const routePath = event.node?.req?.url || event.node?.req?.originalUrl - console.log(routePath, !routePath.startsWith('/forms/')) // const routePath= event.context.params._ if (routePath && !routePath.startsWith('/forms/')) { - console.log(response, event) // Only allow embedding of forms response.headers['X-Frame-Options'] = 'sameorigin' } diff --git a/resources/js/middleware/custom-domains.js b/resources/js/middleware/custom-domains.js index 2438c3c..f33f372 100644 --- a/resources/js/middleware/custom-domains.js +++ b/resources/js/middleware/custom-domains.js @@ -14,3 +14,7 @@ export default async (to, from, next) => { function getDomain (url) { return (new URL(url)).hostname } + +function isCustomDomain (url) { + return getDomain(url) !== getDomain(window.config.app_url) +}