Files
minecraft-account-manager/apps/web/src/lib/group-management.ts
T
dmg d4afb71798
CI / validate (push) Successful in 4m16s
Release / release (push) Failing after 9m14s
feat(admin): streamline group management
2026-08-02 11:06:55 -04:00

76 lines
2.7 KiB
TypeScript

const NAME_CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/;
const TEXT_CONTROL_CHARACTERS = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/;
export function validateGroupDetails(nameValue: unknown, descriptionValue: unknown) {
const name = String(nameValue ?? "").trim();
const description = String(descriptionValue ?? "").trim();
if (
name.length < 1 ||
name.length > 50 ||
NAME_CONTROL_CHARACTERS.test(name) ||
description.length > 500 ||
TEXT_CONTROL_CHARACTERS.test(description)
) return null;
return { name, description };
}
export function adminGroupReturnPath(
value: unknown,
result: "saved=group" | "error=invalid-group-assignment",
) {
const requested = String(value ?? "");
let pathname = "/admin/users";
const parameters = new URLSearchParams();
if (requested.startsWith("/")) {
const url = new URL(requested, "http://internal");
if (url.pathname === "/admin/users") {
const search = url.searchParams.get("q")?.trim().slice(0, 100);
if (search) parameters.set("q", search);
} else if (/^\/admin\/groups\/[0-9a-f-]{36}$/i.test(url.pathname)) {
pathname = url.pathname;
}
}
const [key, resultValue] = result.split("=", 2) as ["saved" | "error", string];
parameters.set(key, resultValue);
return `${pathname}?${parameters.toString()}`;
}
export function editableGroupName(currentName: string, isDefault: boolean, requestedName: string) {
return isDefault ? currentName : requestedName;
}
export function effectiveGroupMemberCount(
totalUsers: number,
assignedGroupIds: string[],
group: { id: string; isDefault: boolean },
) {
return group.isDefault
? Math.max(0, totalUsers - assignedGroupIds.length)
: assignedGroupIds.filter((groupId) => groupId === group.id).length;
}
export function isEffectiveGroupMember(
userId: string,
assignmentByUser: Record<string, string>,
group: { id: string; isDefault: boolean },
) {
return group.isDefault ? !assignmentByUser[userId] : assignmentByUser[userId] === group.id;
}
export function groupSlug(name: string, existingSlugs: Set<string>) {
const normalized = name
.normalize("NFKD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "") || "group";
const base = normalized.slice(0, 50).replace(/-+$/g, "") || "group";
if (!existingSlugs.has(base)) return base;
for (let suffix = 2; suffix < 10_000; suffix += 1) {
const suffixText = `-${suffix}`;
const candidate = `${base.slice(0, 50 - suffixText.length).replace(/-+$/g, "")}${suffixText}`;
if (!existingSlugs.has(candidate)) return candidate;
}
throw new Error("Could not generate a unique group slug");
}