diff --git a/app/Controllers/Http/ServiceController.ts b/app/Controllers/Http/ServiceController.ts index e0b93e9..9556001 100644 --- a/app/Controllers/Http/ServiceController.ts +++ b/app/Controllers/Http/ServiceController.ts @@ -1,7 +1,7 @@ import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'; import { schema } from '@ioc:Adonis/Core/Validator'; import Service from 'App/Models/Service'; -import { url } from 'Config/app'; +import { getAppUrl } from 'Config/app'; import { v4 as uuid } from 'uuid'; import * as fs from 'fs-extra'; import path from 'node:path'; @@ -90,6 +90,7 @@ export default class ServiceController { const { id } = user; const services = await loadUserServices(user.id); + const appUrl = getAppUrl(request.hostname()); // Convert to array with all data Franz wants // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -101,7 +102,7 @@ export default class ServiceController { // eslint-disable-next-line unicorn/no-null let iconUrl: string | null = null; if (settings.iconId) { - iconUrl = `${url}/v1/icon/${settings.iconId}`; + iconUrl = `${appUrl}/v1/icon/${settings.iconId}`; } return { @@ -162,6 +163,7 @@ export default class ServiceController { } const { id } = params; + const appUrl = getAppUrl(request.hostname()); const service = await Service.query() .where('serviceId', id) .where('userId', user.id) @@ -224,7 +226,7 @@ export default class ServiceController { id, name: service.name, ...newSettings, - iconUrl: `${url}/v1/icon/${newSettings.iconId}`, + iconUrl: `${appUrl}/v1/icon/${newSettings.iconId}`, userId: user.id, }, status: ['updated'], @@ -272,7 +274,7 @@ export default class ServiceController { id, name: serviceUpdated.name, ...settings, - iconUrl: `${url}/v1/icon/${settings.iconId}`, + iconUrl: `${appUrl}/v1/icon/${settings.iconId}`, userId: user.id, }, status: ['updated'], @@ -289,6 +291,7 @@ export default class ServiceController { } const data = request.all(); + const appUrl = getAppUrl(request.hostname()); for (const service of Object.keys(data)) { // Get current settings from db @@ -326,7 +329,7 @@ export default class ServiceController { // eslint-disable-next-line unicorn/no-null let iconUrl: string | null = null; if (settings.iconId) { - iconUrl = `${url}/v1/icon/${settings.iconId}`; + iconUrl = `${appUrl}/v1/icon/${settings.iconId}`; } return { diff --git a/app/Models/User.ts b/app/Models/User.ts index b501dd6..5b06a77 100644 --- a/app/Models/User.ts +++ b/app/Models/User.ts @@ -15,7 +15,7 @@ import Token from './Token'; import Workspace from './Workspace'; import Service from './Service'; import Mail from '@ioc:Adonis/Addons/Mail'; -import { url } from 'Config/app'; +import { url } from 'Config/app'; // Primary URL for emails import { mailFrom } from 'Config/dashboard'; export default class User extends BaseModel { diff --git a/config/app.ts b/config/app.ts index fb3c0be..c41db44 100644 --- a/config/app.ts +++ b/config/app.ts @@ -27,7 +27,49 @@ import { ValidatorConfig } from '@ioc:Adonis/Core/Validator'; */ export const appKey: string = Env.get('APP_KEY'); -export const url: string = Env.get('APP_URL'); +// Parse APP_URL(S) - supports both single URL and comma-separated multiple URLs +function parseAppUrls(): string[] { + const appUrls = Env.get('APP_URLS'); + const appUrl = Env.get('APP_URL'); + + if (appUrls) { + return appUrls.split(',').map((u: string) => u.trim()).filter(Boolean); + } + + if (appUrl) { + return [appUrl]; + } + + throw new Error('Either APP_URL or APP_URLS must be defined'); +} + +export const urls: string[] = parseAppUrls(); +export const url: string = urls[0]; // Primary URL for backward compatibility + +/** + * Get the appropriate app URL based on the request hostname. + * Falls back to the primary URL if no match or no request provided. + * + * @param requestHostname - Optional hostname from the incoming request + * @returns The matching URL or primary URL + */ +export function getAppUrl(requestHostname?: string): string { + if (!requestHostname) { + return url; + } + + // Try to find a URL that matches the request hostname + const matchingUrl = urls.find(u => { + try { + const urlObj = new URL(u); + return urlObj.hostname === requestHostname; + } catch { + return false; + } + }); + + return matchingUrl || url; +} // TODO: this is parsed as string to be coherent with the previous version of the code we add (before migrating to AdonisJS 5) export const isRegistrationEnabled: string = Env.get('IS_REGISTRATION_ENABLED'); diff --git a/env.ts b/env.ts index 1f163b0..b9db27a 100644 --- a/env.ts +++ b/env.ts @@ -20,6 +20,8 @@ export default Env.rules({ APP_KEY: Env.schema.string(), APP_NAME: Env.schema.string(), + APP_URL: Env.schema.string.optional(), + APP_URLS: Env.schema.string.optional(), NODE_ENV: Env.schema.enum(['development', 'production', 'test'] as const), });