models/Group.ts
9.9 KB · sha256:f7e33d256ed0d5e54e5753ea8d7c6288e24b6c6747727986002c079725cad675
import "reflect-metadata";
import { Schema } from "mongoose";
import { field, model, getModel, unique, type Doc } from "../db/odm";
import {
Node,
type NodeContext,
inferNodeType,
nodeKey,
parseAffix,
parseWeight,
isActive,
sameContext,
inheritedGroupName,
} from "./Node";
const NodeSchema = new Schema<Node>(
{
key: { type: String, required: true },
value: { type: Boolean, default: true },
type: {
type: String,
enum: ["permission", "inheritance", "prefix", "suffix", "weight", "meta"],
default: "permission",
},
context: { type: Schema.Types.Mixed, default: () => ({}) },
expiry: { type: Number, default: null },
priority: { type: Number, default: 0 },
},
{ _id: false },
);
@model("Group")
export class Group {
@unique()
@field({ type: String, required: true, lowercase: true, trim: true })
name!: string;
@field({ type: String, default: "" })
displayName!: string;
@field({ type: Number, default: 0, index: true })
weight!: number;
@field({ type: Boolean, default: false, index: true })
isDefault!: boolean;
@field({ type: [NodeSchema], default: () => [] })
nodes!: Node[];
}
export type GroupDoc = Doc<Group>;
const GroupModel = getModel(Group);
export function findGroup(name: string): Promise<GroupDoc | null> {
return GroupModel.findOne({ name: name.toLowerCase() }).exec();
}
export function findGroupById(id: string): Promise<GroupDoc | null> {
return GroupModel.findById(id).exec();
}
export function listGroups(): Promise<GroupDoc[]> {
return GroupModel.find().sort({ weight: -1, name: 1 }).exec();
}
export function listGroupNames(): Promise<string[]> {
return GroupModel.find()
.select("name")
.sort({ name: 1 })
.lean()
.exec()
.then((rows) => rows.map((r) => r.name));
}
export function getDefaultGroup(): Promise<GroupDoc | null> {
return GroupModel.findOne({ isDefault: true }).exec();
}
export function groupExists(name: string): Promise<boolean> {
return GroupModel.exists({ name: name.toLowerCase() }).then(Boolean);
}
export async function getParents(name: string): Promise<string[]> {
const g = await findGroup(name);
if (!g) return [];
return g.nodes
.filter((n) => n.type === "inheritance" && n.value && isActive(n))
.map((n) => inheritedGroupName(n.key))
.filter((x): x is string => x != null);
}
export async function resolveGroupPermissions(
name: string,
ctx: NodeContext = {},
): Promise<Map<string, boolean>> {
const out = new Map<string, boolean>();
const seen = new Set<string>();
async function walk(gname: string, depth: number): Promise<void> {
const key = gname.toLowerCase();
if (seen.has(key) || depth > 32) return;
seen.add(key);
const g = await findGroup(key);
if (!g) return;
for (const parent of g.nodes) {
if (parent.type === "inheritance" && parent.value && isActive(parent)) {
const pn = inheritedGroupName(parent.key);
if (pn) await walk(pn, depth + 1);
}
}
for (const n of g.nodes) {
if (n.type !== "permission" || !isActive(n)) continue;
if (!contextOk(n.context, ctx)) continue;
out.set(n.key, n.value);
}
}
await walk(name, 0);
return out;
}
function contextOk(nodeCtx: NodeContext = {}, queryCtx: NodeContext = {}) {
if (nodeCtx.server && nodeCtx.server !== queryCtx.server) return false;
if (nodeCtx.world && nodeCtx.world !== queryCtx.world) return false;
return true;
}
export function getGroupPrefix(g: GroupDoc): string | null {
return topAffix(g.nodes, "prefix");
}
export function getGroupSuffix(g: GroupDoc): string | null {
return topAffix(g.nodes, "suffix");
}
function topAffix(nodes: Node[], type: "prefix" | "suffix"): string | null {
let best: { priority: number; text: string } | null = null;
for (const n of nodes) {
if (n.type !== type || !n.value || !isActive(n)) continue;
const parsed = parseAffix(n.key);
if (!parsed) continue;
if (!best || parsed.priority > best.priority) best = parsed;
}
return best?.text ?? null;
}
export async function createGroup(
name: string,
opts: { displayName?: string; weight?: number } = {},
): Promise<GroupDoc> {
const existing = await findGroup(name);
if (existing) throw new Error(`Group '${name}' already exists`);
return GroupModel.create({
name: name.toLowerCase(),
displayName: opts.displayName ?? name,
weight: opts.weight ?? 0,
nodes: [],
});
}
/**
* Create `target` as a copy of `source`. Copies every node (permissions,
* parents, prefix/suffix, meta) verbatim, then optionally overrides
* weight + displayName.
*/
export async function cloneGroup(
source: string,
target: string,
opts: { displayName?: string; weight?: number } = {},
): Promise<GroupDoc> {
const src = await findGroup(source);
if (!src) throw new Error(`source group '${source}' not found`);
if (await findGroup(target))
throw new Error(`target group '${target}' already exists`);
return GroupModel.create({
name: target.toLowerCase(),
displayName: opts.displayName ?? target,
weight: opts.weight ?? src.weight,
nodes: src.nodes.map((n) => ({
key: n.key,
value: n.value,
type: n.type,
context: n.context,
expiry: n.expiry,
priority: n.priority,
})),
});
}
export async function deleteGroup(name: string): Promise<boolean> {
const res = await GroupModel.deleteOne({ name: name.toLowerCase() }).exec();
if (res.deletedCount) {
await GroupModel.updateMany(
{},
{ $pull: { nodes: { key: nodeKey.group(name) } } },
).exec();
}
return res.deletedCount > 0;
}
export async function addNode(
name: string,
key: string,
opts: {
value?: boolean;
context?: NodeContext;
expiry?: number | null;
priority?: number;
} = {},
): Promise<GroupDoc | null> {
const g = await findGroup(name);
if (!g) return null;
const type = inferNodeType(key);
const context = opts.context ?? {};
const existing = g.nodes.find(
(n) => n.key === key && sameContext(n.context, context),
);
if (existing) {
existing.value = opts.value ?? true;
existing.expiry = opts.expiry ?? existing.expiry ?? null;
existing.priority = opts.priority ?? existing.priority;
} else {
g.nodes.push({
key,
value: opts.value ?? true,
type,
context,
expiry: opts.expiry ?? null,
priority: opts.priority ?? 0,
} as Node);
}
if (type === "weight") {
const w = parseWeight(key);
if (w != null) g.weight = w;
}
await g.save();
return g;
}
export async function removeNode(
name: string,
key: string,
context: NodeContext = {},
): Promise<GroupDoc | null> {
const g = await findGroup(name);
if (!g) return null;
g.nodes = g.nodes.filter(
(n) => !(n.key === key && sameContext(n.context, context)),
);
await g.save();
return g;
}
export const addPermission = (name: string, perm: string, ctx?: NodeContext) =>
addNode(name, perm, { value: true, context: ctx });
export const denyPermission = (name: string, perm: string, ctx?: NodeContext) =>
addNode(name, perm, { value: false, context: ctx });
export const removePermission = (
name: string,
perm: string,
ctx?: NodeContext,
) => removeNode(name, perm, ctx);
export const addParent = (name: string, parent: string) =>
addNode(name, nodeKey.group(parent));
export const removeParent = (name: string, parent: string) =>
removeNode(name, nodeKey.group(parent));
export async function setParent(
name: string,
parent: string | "none",
): Promise<GroupDoc | null> {
const g = await findGroup(name);
if (!g) return null;
g.nodes = g.nodes.filter((n) => n.type !== "inheritance");
if (parent !== "none") {
g.nodes.push({
key: nodeKey.group(parent),
value: true,
type: "inheritance",
context: {},
expiry: null,
priority: 0,
} as Node);
}
await g.save();
return g;
}
export async function setPrefix(
name: string,
text: string,
priority = 100,
): Promise<GroupDoc | null> {
const g = await findGroup(name);
if (!g) return null;
g.nodes = g.nodes.filter((n) => n.type !== "prefix");
if (text) await pushAffix(g, "prefix", text, priority);
else await g.save();
return g;
}
export async function setSuffix(
name: string,
text: string,
priority = 100,
): Promise<GroupDoc | null> {
const g = await findGroup(name);
if (!g) return null;
g.nodes = g.nodes.filter((n) => n.type !== "suffix");
if (text) await pushAffix(g, "suffix", text, priority);
else await g.save();
return g;
}
async function pushAffix(
g: GroupDoc,
type: "prefix" | "suffix",
text: string,
priority: number,
) {
const key =
type === "prefix"
? nodeKey.prefix(priority, text)
: nodeKey.suffix(priority, text);
g.nodes.push({
key,
value: true,
type,
context: {},
expiry: null,
priority,
} as Node);
await g.save();
}
export async function setWeight(
name: string,
weight: number,
): Promise<GroupDoc | null> {
const g = await findGroup(name);
if (!g) return null;
g.weight = weight;
g.nodes = g.nodes.filter((n) => n.type !== "weight");
g.nodes.push({
key: nodeKey.weight(weight),
value: true,
type: "weight",
context: {},
expiry: null,
priority: 0,
} as Node);
await g.save();
return g;
}
export async function setDefaultGroup(name: string): Promise<GroupDoc | null> {
await GroupModel.updateMany({ isDefault: true }, { isDefault: false }).exec();
return GroupModel.findOneAndUpdate(
{ name: name.toLowerCase() },
{ isDefault: true },
{ new: true },
).exec();
}
export { GroupModel, NodeSchema };