Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/utils/db/appConfiguration.db.server.ts
T

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;
});
}