untested WIP

This commit is contained in:
2026-01-24 16:59:50 -06:00
parent 935c7296f6
commit 4be36e6ca0
56 changed files with 8645 additions and 3 deletions
+338
View File
@@ -0,0 +1,338 @@
import UserController from "./UserController";
import { Collection } from "@discordjs/collection";
import jwt, { JsonWebTokenError } from "jsonwebtoken";
import { permissionsPrivateKey, prisma } from "../constants";
import PermissionsVerificationError from "../Errors/PermissionsVerificationError";
import { mergeArrays } from "../modules/tools/mergeArrays";
import { signPermissions } from "../modules/permission-utils/signPermissions";
import { DecodedPermissionsBlock } from "../types/PermissionTypes";
import { permissionValidator } from "../modules/permission-utils/permissionValidator";
import { z } from "zod";
import { roles } from "../managers/roles";
import GenericError from "../Errors/GenericError";
import RoleError from "../Errors/RoleError";
import { Role, User } from "../../generated/prisma/client";
import { events } from "../modules/globalEvents";
/**
* Roles
*
* Roles are for adding onto a users permissions. They are not for defining a default users permissions.
*
* Roles have two forms of identifiers, titles and monikers. The title is something that can be capitalized and publicly displayed,
* so if you make modifications, and you need to be able to see how they were able to do that without having explicit permission to
* make the modifications, the title is what would be shown to you. The moniker is the human readable identifier. So in an api
* response where you need to get the roles of a user, instead of being given a bunch of id's, you will be given a bunch of monikers
* which are easy to read and easy to identify.
*/
export class RoleController {
public readonly id: string;
public title: string;
public moniker: string; // e.g. admin, super_admin, moderator
private _permissionsToken: string;
private _users: (User & { roles: Role[] })[];
public readonly createdAt: Date;
public updatedAt: Date;
public deleted: boolean = false;
constructor(roledata: Role & { users: (User & { roles: Role[] })[] }) {
this.id = roledata.id;
this.title = roledata.title;
this.moniker = roledata.moniker;
this._permissionsToken = roledata.permissions;
this._users = roledata.users;
this.createdAt = roledata.createdAt;
this.updatedAt = roledata.updatedAt;
}
private _signPermissions = signPermissions;
/**
* Verify Permissions
*
* This method is vital to maintining the security of this system. This method is here to ensure
* that the permissions object is authentic and signed by our system.
*
* @param permissionsToken - Signed permissions JWT
* @returns - Verified object with permissions in it.
*/
private _verifyPermissions(permissionsToken: string) {
let perms: DecodedPermissionsBlock;
try {
perms = jwt.verify(permissionsToken, permissionsPrivateKey, {
algorithms: ["RS256"],
issuer: "roles",
subject: this.id,
}) as DecodedPermissionsBlock;
} catch (err) {
events.emit("role:permissions:verification_error", {
currentSigned: this._permissionsToken,
attemptedVerification: permissionsToken,
err: err as Error,
role: this,
});
throw new PermissionsVerificationError(
`Unable to verify permissions for role '${this.title}, it is recommended that you override and rewrite these permissions immediately.`,
(err as Error).message
);
}
return perms as { permissions: string[] };
}
/**
* Get Users
*
* This will get all the users that have this role and return it as a collection dictionary where the key is
* the `id` of the user and the value is the users `UserController`.
*
* @returns {Collection<string, UserController>} - A collection of all the users that are assigned to this role
*/
public getUsers() {
const collection = new Collection<string, UserController>();
this._users.map((v) => collection.set(v.id, new UserController(v)));
return collection;
}
/**
* Check Permission
*
* Check to see if a role has a specified set of permissions.
*
* @param permission - The permission to check for
* @returns {boolean} Does this role have the specified permission
*/
public checkPermission(permission: string): boolean {
const permissions = this._verifyPermissions(this._permissionsToken);
return permissionValidator(permission, permissions.permissions);
}
/**
* Set permissions
*
* This will remove all existing permissions and replate them with the permissions defined in the params
* of this method.
*
* @param permissions - Array of all Permissions
* @returns {void}
*/
public async setPermissions(...permissions: string[]) {
/*
Make sure the current permissions are verified before updating them again. Basically speaking if the permissions
are tampered with in any way, this bricks the further modification of the permissions
*/
const previous = this._verifyPermissions(this._permissionsToken);
const newPermissionsToken = this._signPermissions({
issuer: "roles",
subject: this.id,
permissions,
});
const newRaw = await prisma.role.update({
where: { id: this.id },
data: { permissions: newPermissionsToken },
});
events.emit("role:permissions:updated", {
current: permissions,
currentSigned: newPermissionsToken,
previous: previous.permissions,
previousSigned: this._permissionsToken,
action: "set",
role: this,
});
this._permissionsToken = newPermissionsToken;
this.updatedAt = newRaw.updatedAt;
return;
}
/**
* Add Permissions
*
* This will take the permissions provided in the params of the method and combine them into the pre-existing
* permissions of this role.
*
* @param permissions - Array of added Permissions
* @returns {void}
*/
public async addPermissions(...permissions: string[]) {
/*
Make sure the current permissions are verified before updating them again. Basically speaking if the permissions
are tampered with in any way, this bricks the further modification of the permissions
*/
const previous = this._verifyPermissions(this._permissionsToken);
const newPermissionsToken = this._signPermissions({
issuer: "roles",
subject: this.id,
permissions: mergeArrays(previous.permissions, permissions),
});
const newRaw = await prisma.role.update({
where: { id: this.id },
data: { permissions: newPermissionsToken },
});
events.emit("role:permissions:updated", {
current: permissions,
currentSigned: newPermissionsToken,
previous: previous.permissions,
previousSigned: this._permissionsToken,
action: "added",
role: this,
});
this._permissionsToken = newPermissionsToken;
this.updatedAt = newRaw.updatedAt;
return;
}
/**
* Remove Permissions
*
* This will take the permissions provided in the params of the method and remove them from the exisitng
* permissions object for this role.
*
* @param permissions - Array of removed Permissions
* @returns {void}
*/
public async removePermissions(...permissions: string[]) {
/*
Make sure the current permissions are verified before updating them again. Basically speaking if the permissions
are tampered with in any way, this bricks the further modification of the permissions
*/
const previous = this._verifyPermissions(this._permissionsToken);
const newPermissionsToken = this._signPermissions({
issuer: "roles",
subject: this.id,
permissions: previous.permissions.filter((v) => !permissions.includes(v)),
});
const newRaw = await prisma.role.update({
where: { id: this.id },
data: { permissions: newPermissionsToken },
});
events.emit("role:permissions:updated", {
current: permissions,
currentSigned: newPermissionsToken,
previous: previous.permissions,
previousSigned: this._permissionsToken,
action: "removed",
role: this,
});
this._permissionsToken = newPermissionsToken;
this.updatedAt = newRaw.updatedAt;
return;
}
/**
* Get Current Permissions
*
* Get all of the permissions for this role.
* @returns {string[]} - Existing permissions in an array
*/
public getPermissions() {
const permissions = this._verifyPermissions(this._permissionsToken!);
return permissions;
}
public async update(
data: Partial<{
title: string;
moniker: string;
permissions: string[];
}>
) {
const schema = z
.object({
title: z.string().min(1, "Title cannot be empty."),
moniker: z.string().min(1, "Moniker cannot be empty."),
permissions: z.array(
z.string().min(1, "Permission node cannot be empty")
),
})
.partial()
.strict();
data = schema.parse(data);
if (data.moniker) {
const checkMoniker = await prisma.role.findFirst({
where: { moniker: data.moniker },
});
if (checkMoniker)
throw new RoleError(
"Moniker is already taken.",
"Another role with this moniker already exists in the databse."
);
}
const updatedRole = await prisma.role.update({
where: { id: this.id },
data: {
...data,
permissions: undefined,
},
});
if (data.permissions) await this.setPermissions(...data.permissions);
events.emit("role:updated", { role: this, updateData: data });
this.title = data.title ?? this.title;
this.moniker = data.moniker ?? this.moniker;
return this;
}
/**
* Delete Role
*
* @returns {Promise<RoleController>} The Remains of a deleted role.
*/
public async delete() {
const deletedData = await prisma.role.delete({ where: { id: this.id } });
this.deleted = true;
events.emit("role:deleted", this);
return this;
}
/**
* To JSON
*
* Create a JSON object that can be used in things like API responses.
*
* @param opts - Optional values to include in the response.
* @returns - JSON-Friendly object.
*/
public toJson(opts?: { viewUsers?: boolean; viewPermissions?: boolean }) {
let object = {
id: this.id,
title: this.title,
moniker: this.moniker,
permissions: opts?.viewPermissions
? this._verifyPermissions(this._permissionsToken).permissions
: undefined,
users: opts?.viewUsers
? this._users.map((v) => ({
id: v.id,
name: v.name,
login: v.login,
roles: v.roles.map((r:any) => r.id),
}))
: undefined,
createdAt: this.createdAt,
updatedAt: this.updatedAt,
};
return object;
}
}