112 lines
4.0 KiB
TypeScript
112 lines
4.0 KiB
TypeScript
import UrlUtils from "../app/UrlUtils";
|
|
import { reservedTenantChildRouteSlugs, reservedTenantRouteSlugs } from "../app/PublicRouteSegments";
|
|
import { db } from "../db.server";
|
|
import { deleteTenant } from "../db/tenants.db.server";
|
|
import { getTenantSubscription } from "../db/tenantSubscriptions.db.server";
|
|
import { deleteStripeCustomer } from "../stripe.server";
|
|
import { TFunction } from "i18next";
|
|
import { cancelTenantSubscription } from "./.server/subscriptionService";
|
|
|
|
export function normalizeTenantSlug(slug: string) {
|
|
return UrlUtils.slugify(slug).toLowerCase();
|
|
}
|
|
|
|
export const reservedTenantSlugs = new Set(reservedTenantRouteSlugs.map((slug) => normalizeTenantSlug(slug)));
|
|
export const reservedTenantChildSlugs = new Set(reservedTenantChildRouteSlugs.map((slug) => normalizeTenantSlug(slug)));
|
|
|
|
export function isReservedTenantSlug(slug: string) {
|
|
return reservedTenantSlugs.has(normalizeTenantSlug(slug));
|
|
}
|
|
|
|
export function isReservedTenantChildRouteSlug(slug: string) {
|
|
return reservedTenantChildSlugs.has(normalizeTenantSlug(slug));
|
|
}
|
|
|
|
// Mindestlaenge eines Tenant-Slugs. Ein-Zeichen-Slugs kollidieren mit reservierten
|
|
// Kind-Routen (z.B. "/g") und sind als Workspace-URL ohnehin ungeeignet.
|
|
export const MIN_TENANT_SLUG_LENGTH = 2;
|
|
|
|
// Fallback-Basis, falls ein Firmenname keinen verwertbaren Slug ergibt (reine
|
|
// Sonderzeichen -> slugify liefert ""). Verhindert leere/numerische Slugs ("", "1").
|
|
const FALLBACK_TENANT_SLUG_BASE = "workspace";
|
|
|
|
function tenantSlugBaseFromName(name: string) {
|
|
const base = normalizeTenantSlug(name ?? "");
|
|
return base.length > 0 ? base : FALLBACK_TENANT_SLUG_BASE;
|
|
}
|
|
|
|
export type TenantSlugValidation = { ok: true; slug: string } | { ok: false; reason: "empty" | "too-short" | "reserved" };
|
|
|
|
// Synchrone Validierung eines vom Nutzer GEWAEHLTEN Slugs (Signup-Formular): liefert
|
|
// einen typisierten Grund fuer Klartext-Fehlermeldungen, statt den Wunsch still
|
|
// umzuschreiben. Die DB-Eindeutigkeitspruefung bleibt in getAvailableTenantSlug.
|
|
export function validateDesiredTenantSlug(input: string): TenantSlugValidation {
|
|
const slug = normalizeTenantSlug(input ?? "");
|
|
if (slug.length === 0) {
|
|
return { ok: false, reason: "empty" };
|
|
}
|
|
if (slug.length < MIN_TENANT_SLUG_LENGTH) {
|
|
return { ok: false, reason: "too-short" };
|
|
}
|
|
if (isReservedTenantSlug(slug)) {
|
|
return { ok: false, reason: "reserved" };
|
|
}
|
|
return { ok: true, slug };
|
|
}
|
|
|
|
export async function deleteAndCancelTenant({ tenantId, userId, t }: { tenantId: string; userId: string; t?: TFunction }) {
|
|
const tenantSubscription = await getTenantSubscription(tenantId);
|
|
await cancelAllTenantPlans({ t, tenantId, userId });
|
|
if (tenantSubscription?.stripeCustomerId) {
|
|
await deleteStripeCustomer(tenantSubscription?.stripeCustomerId);
|
|
}
|
|
return await deleteTenant(tenantId);
|
|
}
|
|
|
|
export async function cancelAllTenantPlans({ tenantId, userId, t }: { tenantId: string; userId: string; t?: TFunction }) {
|
|
const tenantSubscription = await getTenantSubscription(tenantId);
|
|
if (tenantSubscription?.products) {
|
|
await Promise.all(
|
|
tenantSubscription.products.map(async (tenantSubscriptionProduct) => {
|
|
await cancelTenantSubscription(tenantSubscriptionProduct.id, {
|
|
tenantId,
|
|
userId,
|
|
t,
|
|
});
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function getAvailableTenantSlug({ name, slug }: { name: string; slug?: string }) {
|
|
const base = tenantSlugBaseFromName(name);
|
|
slug = slug === undefined ? base : normalizeTenantSlug(slug);
|
|
// Degenerierter Wunsch-Slug (leer nach Normalisierung) faellt auf die Namens-Basis zurueck.
|
|
if (slug.length === 0) {
|
|
slug = base;
|
|
}
|
|
let tries = 1;
|
|
do {
|
|
const existingSlug = await getExistingSlug(slug);
|
|
if (existingSlug) {
|
|
slug = base + tries.toString();
|
|
tries++;
|
|
} else {
|
|
break;
|
|
}
|
|
} while (true);
|
|
return slug;
|
|
}
|
|
|
|
export async function getExistingSlug(slug: string) {
|
|
if (isReservedTenantSlug(slug)) {
|
|
return true;
|
|
}
|
|
const existing = await db.tenant.count({
|
|
where: {
|
|
slug,
|
|
},
|
|
});
|
|
return existing > 0;
|
|
}
|