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[] })[]; /** Cached result of JWT verification — avoids repeated RSA verify calls. */ private _cachedVerifiedPermissions: { permissions: string[] } | null = null; 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) { // Return cached result if the token hasn't changed if ( this._cachedVerifiedPermissions && permissionsToken === this._permissionsToken ) { return this._cachedVerifiedPermissions; } 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, ); } const result = perms as { permissions: string[] }; // Cache only if verifying the current token if (permissionsToken === this._permissionsToken) { this._cachedVerifiedPermissions = result; } return result; } /** * 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} - A collection of all the users that are assigned to this role */ public getUsers() { const collection = new Collection(); 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:set", { current: permissions, currentSigned: newPermissionsToken, previous: previous.permissions, previousSigned: this._permissionsToken, 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:added", { previous: previous.permissions, previousSigned: this._permissionsToken, added: permissions, currentSigned: newPermissionsToken, 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:removed", { previous: previous.permissions, previousSigned: this._permissionsToken, removed: permissions, currentSigned: newPermissionsToken, 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 && checkMoniker.moniker !== this.moniker) 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} 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; } }