Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/utils/services/tenantService.ts
T

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