321 lines
11 KiB
TypeScript
321 lines
11 KiB
TypeScript
import { defaultBrandTokens } from "@toolctrl/brand";
|
|
import { loadRequiredWebConfig } from "@toolctrl/config";
|
|
import { Prisma } from "@toolctrl/db/prisma-browser";
|
|
import { getTranslations } from "~/locale/i18next.server";
|
|
import { getPublicAuthProviders } from "../auth/authProviders.server";
|
|
import { cachified, clearCacheKey } from "../cache.server";
|
|
import { db } from "../db.server";
|
|
import { defaultTheme } from "../theme/defaultThemes";
|
|
import { getBaseURL, getDomainName } from "../url.server";
|
|
import { PortalConfiguration, getAppPortalConfiguration } from "./appPortalConfiguration.db.server";
|
|
import { getWidgetsConfiguration, WidgetsConfiguration } from "./appWidgetsConfiguration.db.server";
|
|
|
|
export type AppConfiguration = {
|
|
app: {
|
|
name: string;
|
|
url: string;
|
|
domain: string;
|
|
theme: string;
|
|
company?: { name: string; address: string };
|
|
features: {
|
|
tenantHome: "/:tenant/" | "/";
|
|
tenantTypes: boolean;
|
|
tenantApiKeys?: boolean;
|
|
tenantEntityCustomization?: boolean;
|
|
crm: boolean;
|
|
tenantWorkflows: boolean;
|
|
tenantEmailMarketing: boolean;
|
|
tenantFeedback: boolean;
|
|
};
|
|
};
|
|
email: {
|
|
provider: "mailjet";
|
|
fromEmail: string;
|
|
fromName: string;
|
|
supportEmail: string;
|
|
};
|
|
auth: {
|
|
requireEmailVerification: boolean;
|
|
requireOrganization: boolean;
|
|
requireName: boolean;
|
|
slug: { require: boolean; type: "tenant" | "username" } | null;
|
|
captcha: {
|
|
enabled: boolean;
|
|
instanceUrl: string;
|
|
loginSiteKey: string;
|
|
};
|
|
authMethods: {
|
|
emailPassword: {
|
|
enabled: boolean;
|
|
};
|
|
magicLink: {
|
|
enabled: boolean;
|
|
};
|
|
};
|
|
};
|
|
analytics: {
|
|
enabled: boolean;
|
|
simpleAnalytics: boolean;
|
|
plausibleAnalytics: boolean;
|
|
// EU-Sterilisation (R-P1a): googleAnalyticsTrackingId entfernt. DB-Spalte bleibt vorerst
|
|
// verwaist (Drop per Merge-Migration, AD-21), wird aber nicht mehr gelesen/geschrieben.
|
|
ipLookup?: boolean;
|
|
};
|
|
subscription: {
|
|
required: boolean;
|
|
allowSubscribeBeforeSignUp: boolean;
|
|
allowSignUpBeforeSubscribe: boolean;
|
|
multiple: boolean;
|
|
};
|
|
cookies: {
|
|
enabled: boolean;
|
|
};
|
|
notifications: {
|
|
enabled: boolean;
|
|
};
|
|
onboarding: {
|
|
enabled: boolean;
|
|
};
|
|
featureFlags: {
|
|
enabled: boolean; // load all flags
|
|
};
|
|
metrics: {
|
|
enabled: boolean;
|
|
logToConsole: boolean;
|
|
saveToDatabase: boolean;
|
|
ignoreUrls: string[];
|
|
};
|
|
branding: {
|
|
logo?: string;
|
|
logoDarkMode?: string;
|
|
icon?: string;
|
|
iconDarkMode?: string;
|
|
favicon?: string;
|
|
};
|
|
reviews?: {
|
|
trustpilot?: { href: string; templateId: string; businessUnitId: string };
|
|
};
|
|
launches?: {
|
|
producthunt?: { title: string; url: string; postId: string };
|
|
};
|
|
portals: PortalConfiguration;
|
|
widgets: WidgetsConfiguration;
|
|
scripts: { head: string | null; body: string | null };
|
|
};
|
|
|
|
export async function getAppConfiguration({ request }: { request: Request }): Promise<AppConfiguration> {
|
|
const { t } = await getTranslations(request);
|
|
const webConfig = loadRequiredWebConfig();
|
|
const authProviders = getPublicAuthProviders({ request, webConfig });
|
|
const defaultEmailConfig: AppConfiguration["email"] = {
|
|
provider: "mailjet",
|
|
fromEmail: defaultBrandTokens.support.email,
|
|
fromName: defaultBrandTokens.support.fromName,
|
|
supportEmail: defaultBrandTokens.support.email,
|
|
};
|
|
const conf: AppConfiguration = {
|
|
app: {
|
|
name: webConfig.appName,
|
|
url: getBaseURL(request),
|
|
domain: getDomainName(request),
|
|
theme: defaultTheme,
|
|
company: undefined,
|
|
features: {
|
|
tenantHome: "/:tenant/",
|
|
tenantApiKeys: true,
|
|
tenantEntityCustomization: false,
|
|
crm: false,
|
|
tenantTypes: false,
|
|
tenantWorkflows: false,
|
|
tenantEmailMarketing: false,
|
|
tenantFeedback: true,
|
|
},
|
|
},
|
|
email: defaultEmailConfig,
|
|
auth: {
|
|
requireEmailVerification: false,
|
|
requireOrganization: false,
|
|
requireName: false,
|
|
slug: { require: false, type: "username" },
|
|
captcha: {
|
|
enabled: Boolean(webConfig.captcha.instanceUrl && webConfig.captcha.loginSiteKey),
|
|
instanceUrl: webConfig.captcha.instanceUrl ?? "",
|
|
loginSiteKey: webConfig.captcha.loginSiteKey ?? "",
|
|
},
|
|
authMethods: {
|
|
emailPassword: {
|
|
enabled: true,
|
|
},
|
|
magicLink: {
|
|
enabled: authProviders.magicLink.enabled,
|
|
},
|
|
},
|
|
},
|
|
analytics: {
|
|
enabled: false,
|
|
simpleAnalytics: false,
|
|
plausibleAnalytics: false,
|
|
ipLookup: false,
|
|
},
|
|
subscription: {
|
|
required: false,
|
|
allowSubscribeBeforeSignUp: true,
|
|
allowSignUpBeforeSubscribe: true,
|
|
// `multiple: false` = single-plan-per-tenant. When a user subscribes to a
|
|
// new plan while an old one is still active, the old one is auto-cancelled
|
|
// (see `addTenantProductsFromCheckoutSession` in pricingService). This is
|
|
// the standard SaaS upgrade/downgrade behaviour. Flip to `true` only if
|
|
// you want tenants to be able to hold multiple parallel subscriptions
|
|
// (e.g. add-on / marketplace pattern).
|
|
multiple: false,
|
|
},
|
|
cookies: {
|
|
enabled: false,
|
|
},
|
|
notifications: {
|
|
enabled: true,
|
|
},
|
|
onboarding: {
|
|
enabled: true,
|
|
},
|
|
featureFlags: {
|
|
enabled: true,
|
|
},
|
|
metrics: {
|
|
enabled: false,
|
|
logToConsole: false,
|
|
saveToDatabase: false,
|
|
ignoreUrls: ["/build", "/metrics"],
|
|
},
|
|
branding: {
|
|
logo: undefined,
|
|
logoDarkMode: undefined,
|
|
icon: undefined,
|
|
iconDarkMode: undefined,
|
|
favicon: undefined,
|
|
},
|
|
launches: undefined,
|
|
portals: getAppPortalConfiguration({ t }),
|
|
widgets: getWidgetsConfiguration({ t }),
|
|
scripts: { head: null, body: null },
|
|
};
|
|
|
|
const appConfiguration = await cachified({
|
|
key: `appConfiguration`,
|
|
ttl: 1000 * 60 * 60 * 24, // 1 day
|
|
getFreshValue: async () => db.appConfiguration.findFirst(),
|
|
});
|
|
if (!appConfiguration) {
|
|
return conf;
|
|
}
|
|
conf.app.name = appConfiguration?.name ?? "";
|
|
// conf.app.url = appConfiguration?.url ?? "";
|
|
conf.app.theme = appConfiguration?.theme ?? defaultTheme;
|
|
|
|
conf.email.provider = getTransactionalEmailProvider(appConfiguration?.emailProvider, defaultEmailConfig.provider);
|
|
conf.email.fromEmail = appConfiguration?.emailFromEmail ?? defaultEmailConfig.fromEmail;
|
|
conf.email.fromName = appConfiguration?.emailFromName ?? defaultEmailConfig.fromName;
|
|
conf.email.supportEmail = appConfiguration?.emailSupportEmail ?? defaultEmailConfig.supportEmail;
|
|
|
|
conf.auth.requireEmailVerification = appConfiguration?.authRequireEmailVerification;
|
|
conf.auth.requireOrganization = appConfiguration?.authRequireOrganization;
|
|
conf.auth.requireName = appConfiguration?.authRequireName;
|
|
// Captcha (mCaptcha) is env-driven (MCAPTCHA_*), not stored per-tenant in the DB.
|
|
// `conf.auth.captcha` is already populated from webConfig in the default builder.
|
|
|
|
conf.analytics.enabled = appConfiguration?.analyticsEnabled;
|
|
conf.analytics.simpleAnalytics = appConfiguration?.analyticsSimpleAnalytics;
|
|
conf.analytics.plausibleAnalytics = appConfiguration?.analyticsPlausibleAnalytics;
|
|
|
|
conf.subscription.required = appConfiguration?.subscriptionRequired;
|
|
conf.subscription.allowSubscribeBeforeSignUp = appConfiguration?.subscriptionAllowSubscribeBeforeSignUp;
|
|
conf.subscription.allowSignUpBeforeSubscribe = appConfiguration?.subscriptionAllowSignUpBeforeSubscribe;
|
|
|
|
conf.cookies.enabled = appConfiguration?.cookiesEnabled;
|
|
|
|
conf.metrics.enabled = appConfiguration?.metricsEnabled;
|
|
conf.metrics.logToConsole = appConfiguration?.metricsLogToConsole;
|
|
conf.metrics.saveToDatabase = appConfiguration?.metricsSaveToDatabase;
|
|
conf.metrics.ignoreUrls = appConfiguration?.metricsIgnoreUrls?.split(",") ?? [];
|
|
|
|
conf.branding.logo = appConfiguration?.brandingLogo ?? undefined;
|
|
conf.branding.logoDarkMode = appConfiguration?.brandingLogoDarkMode ?? undefined;
|
|
conf.branding.icon = appConfiguration?.brandingIcon ?? undefined;
|
|
conf.branding.iconDarkMode = appConfiguration?.brandingIconDarkMode ?? undefined;
|
|
conf.branding.favicon = appConfiguration?.brandingFavicon ?? undefined;
|
|
|
|
conf.scripts = {
|
|
head: appConfiguration?.headScripts || null,
|
|
body: appConfiguration?.bodyScripts || null,
|
|
};
|
|
|
|
return conf;
|
|
}
|
|
|
|
export async function getOrCreateAppConfiguration({ request }: { request: Request }) {
|
|
let settings = await db.appConfiguration.findFirst();
|
|
if (!settings) {
|
|
const conf = await getAppConfiguration({ request });
|
|
settings = await db.appConfiguration
|
|
.create({
|
|
data: {
|
|
name: conf.app.name,
|
|
url: conf.app.url,
|
|
authRequireEmailVerification: conf.auth.requireEmailVerification,
|
|
authRequireOrganization: conf.auth.requireOrganization,
|
|
authRequireName: conf.auth.requireName,
|
|
analyticsEnabled: conf.analytics.enabled,
|
|
analyticsSimpleAnalytics: conf.analytics.simpleAnalytics,
|
|
analyticsPlausibleAnalytics: conf.analytics.plausibleAnalytics,
|
|
subscriptionRequired: conf.subscription.required,
|
|
subscriptionAllowSubscribeBeforeSignUp: conf.subscription.allowSubscribeBeforeSignUp,
|
|
subscriptionAllowSignUpBeforeSubscribe: conf.subscription.allowSignUpBeforeSubscribe,
|
|
cookiesEnabled: conf.cookies.enabled,
|
|
metricsEnabled: conf.metrics.enabled,
|
|
metricsLogToConsole: conf.metrics.logToConsole,
|
|
metricsSaveToDatabase: conf.metrics.saveToDatabase,
|
|
metricsIgnoreUrls: conf.metrics.ignoreUrls.join(","),
|
|
brandingLogo: conf.branding.logo,
|
|
brandingLogoDarkMode: conf.branding.logoDarkMode,
|
|
brandingIcon: conf.branding.icon,
|
|
brandingIconDarkMode: conf.branding.iconDarkMode,
|
|
brandingFavicon: conf.branding.favicon,
|
|
},
|
|
})
|
|
.then((item) => {
|
|
clearCacheKey(`appConfiguration`);
|
|
return item;
|
|
});
|
|
}
|
|
return settings;
|
|
}
|
|
|
|
function getTransactionalEmailProvider(
|
|
provider: string | null | undefined,
|
|
fallback: AppConfiguration["email"]["provider"]
|
|
): AppConfiguration["email"]["provider"] {
|
|
if (provider === "mailjet") {
|
|
return provider;
|
|
}
|
|
|
|
return fallback;
|
|
}
|
|
|
|
export async function updateAppConfiguration(data: Prisma.AppConfigurationUpdateInput) {
|
|
return await db.appConfiguration
|
|
.updateMany({
|
|
data,
|
|
})
|
|
.then((item) => {
|
|
clearCacheKey(`appConfiguration`);
|
|
return item;
|
|
});
|
|
}
|
|
|
|
export async function deleteAppConfiguration() {
|
|
return await db.appConfiguration.deleteMany({ where: {} }).then((item) => {
|
|
clearCacheKey(`appConfiguration`);
|
|
return item;
|
|
});
|
|
}
|