untested WIP
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user