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

137 lines
4.4 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 { TFunction } from "i18next";
import { cancelTenantSubscription } from "./.server/subscriptionService";
import EventsService from "~/modules/events/services/.server/EventsService";
import type { AccountDeletedDto } from "~/modules/events/dtos/AccountDeletedDto";
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,
request,
eventData,
authorizeDeletion,
t,
}: {
tenantId: string;
userId: string;
request?: Request;
eventData: AccountDeletedDto;
authorizeDeletion?: () => Promise<boolean>;
t?: TFunction;
}) {
await cancelAllTenantPlans({ t, tenantId, userId });
return EventsService.createTenantEventBeforeMutation(
{
request,
event: "account.deleted",
tenantId,
userId,
data: eventData,
},
async () => {
if (authorizeDeletion && !(await authorizeDeletion())) {
throw new Error("Tenant deletion ownership changed before commit");
}
return 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;
}