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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
import {
|
||||
prisma,
|
||||
refreshTokenDuration,
|
||||
accessTokenDuration,
|
||||
accessTokenPrivateKey,
|
||||
refreshTokenPrivateKey,
|
||||
} from "../constants";
|
||||
import SessionTokenError from "../Errors/SessionTokenError";
|
||||
import { events } from "../modules/globalEvents";
|
||||
import UserController from "./UserController";
|
||||
import { users } from "../managers/users";
|
||||
import { Session } from "../../generated/prisma/client";
|
||||
|
||||
export interface SessionPayloadObject {
|
||||
userID: string;
|
||||
sessionKey: string;
|
||||
}
|
||||
|
||||
export interface SessionTokensObject {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
export type DecodedSession = SessionPayloadObject & {
|
||||
iat: number;
|
||||
exp: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Session Controller
|
||||
*
|
||||
* This class is for create a controller that can manage, generate and refresh tokens, self terminate and self delete
|
||||
* all sessions. This also allows you to access all data about a session.
|
||||
*/
|
||||
export class SessionController {
|
||||
public readonly id: string;
|
||||
public readonly sessionKey: string;
|
||||
public readonly userId: string;
|
||||
public readonly expires: Date;
|
||||
public refreshedAt: Date | null;
|
||||
public invalidatedAt: Date | null;
|
||||
|
||||
public terminated: boolean = false;
|
||||
|
||||
private _refreshTokenGenrated: boolean;
|
||||
|
||||
constructor(sessionData: Session) {
|
||||
this.id = sessionData.id;
|
||||
this.sessionKey = sessionData.sessionKey;
|
||||
this.userId = sessionData.userId;
|
||||
this.expires = sessionData.expires;
|
||||
this.refreshedAt = sessionData.refreshedAt;
|
||||
this.invalidatedAt = sessionData.invalidatedAt;
|
||||
|
||||
this._refreshTokenGenrated = sessionData.refreshTokenGenerated;
|
||||
}
|
||||
|
||||
/** @ignore */
|
||||
private _generateAccessToken() {
|
||||
const payload: SessionPayloadObject = {
|
||||
sessionKey: this.sessionKey,
|
||||
userID: this.userId,
|
||||
};
|
||||
|
||||
return jwt.sign(payload, accessTokenPrivateKey, {
|
||||
algorithm: "RS256",
|
||||
expiresIn: accessTokenDuration,
|
||||
});
|
||||
}
|
||||
|
||||
/** @ignore */
|
||||
private _generateRefreshToken() {
|
||||
const payload: SessionPayloadObject = {
|
||||
sessionKey: this.sessionKey,
|
||||
userID: this.userId,
|
||||
};
|
||||
|
||||
return jwt.sign(payload, refreshTokenPrivateKey, {
|
||||
algorithm: "RS256",
|
||||
expiresIn: refreshTokenDuration,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the Session
|
||||
*
|
||||
* The purpose for this function is if you wanted to be able to listen for somebody using an invalid session,
|
||||
* you just have to invalidate it with this function and go from there.
|
||||
*
|
||||
* @returns {Promise<void>} - nothing
|
||||
*/
|
||||
public async invalidate() {
|
||||
const invalidationDate = new Date();
|
||||
|
||||
if (this.invalidatedAt)
|
||||
throw new Error("Session has already been invalidated.");
|
||||
|
||||
await prisma.session.update({
|
||||
data: { invalidatedAt: invalidationDate },
|
||||
where: { id: this.id },
|
||||
});
|
||||
this.invalidatedAt = invalidationDate;
|
||||
|
||||
events.emit("session:invalidated", this);
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Terminate the session
|
||||
*
|
||||
* Terminating the session will immediately delete the session making it impossible for it to be referenced again.
|
||||
*
|
||||
* @returns {Promise<void>} - nothing
|
||||
*/
|
||||
public async terminate() {
|
||||
await prisma.session.delete({ where: { id: this.id } });
|
||||
events.emit("session:terminated", this);
|
||||
this.terminated = true;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Tokens
|
||||
*
|
||||
* **NOTE**: This method can only be ran once per session.
|
||||
*
|
||||
* Running this function will allow you to generate an accessToken and a refreshToken. Each token
|
||||
* will be generated with their own respective private keys.
|
||||
*
|
||||
*
|
||||
* @returns {Promise<SessionTokensObject>} An object containing the `accessToken` and `refreshToken`
|
||||
*/
|
||||
public async generateTokens(): Promise<SessionTokensObject> {
|
||||
if (this._refreshTokenGenrated)
|
||||
throw new Error("Tokens have alredy been generated for this session.");
|
||||
|
||||
const accessToken = this._generateAccessToken();
|
||||
const refreshToken = this._generateRefreshToken();
|
||||
|
||||
const newRefreshDate = new Date();
|
||||
await prisma.session.update({
|
||||
data: { refreshTokenGenerated: true, refreshedAt: newRefreshDate },
|
||||
where: { id: this.id },
|
||||
});
|
||||
this._refreshTokenGenrated = true;
|
||||
this.refreshedAt = newRefreshDate;
|
||||
|
||||
let tokens = { accessToken, refreshToken };
|
||||
|
||||
events.emit("session:tokens_generated", { session: this, tokens });
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the session
|
||||
*
|
||||
* Refreshing the session will generate a new accessToken for the user to authenticate their requests with.
|
||||
*
|
||||
* **NOTE**: Best practice when implementing token refreshing into the UI is that if for any reason this method
|
||||
* throws an error, imediately purge the existing tokens and have the user login again. This way you don't hold
|
||||
* them up any longer than necessary.
|
||||
*
|
||||
* @param refreshToken - The refresh token provided at session generation
|
||||
* @returns {Promise<string>} The new access token.
|
||||
*/
|
||||
public refresh(refreshToken: string): Promise<string> {
|
||||
return new Promise(async (res, rej) => {
|
||||
if (this.expires.getTime() <= Date.now()) {
|
||||
await this.terminate();
|
||||
throw new SessionTokenError("Session has Expired.");
|
||||
}
|
||||
jwt.verify(
|
||||
refreshToken,
|
||||
refreshTokenPrivateKey,
|
||||
{
|
||||
algorithms: ["RS256"],
|
||||
},
|
||||
async (err, decode) => {
|
||||
if (err) {
|
||||
if (
|
||||
err.name == "TokenExpiredError" ||
|
||||
err.message == "invalid signature"
|
||||
)
|
||||
this.terminate();
|
||||
rej(err);
|
||||
}
|
||||
const data: DecodedSession = decode as DecodedSession;
|
||||
|
||||
if (
|
||||
data.sessionKey !== this.sessionKey ||
|
||||
data.userID !== this.userId
|
||||
)
|
||||
rej(
|
||||
new SessionTokenError(
|
||||
"Refresh token does not match this session."
|
||||
)
|
||||
);
|
||||
|
||||
await prisma.session.update({
|
||||
data: { refreshedAt: new Date() },
|
||||
where: { id: this.id },
|
||||
});
|
||||
|
||||
const newToken = this._generateAccessToken();
|
||||
events.emit("session:token_refresh", {
|
||||
session: this,
|
||||
tokens: { accessToken: newToken, refreshToken },
|
||||
});
|
||||
|
||||
return res(newToken);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Session User
|
||||
*
|
||||
* Fetch the user controller of the user that created the session.
|
||||
*
|
||||
* @returns {Promise<UserController>} The user that created this session.
|
||||
*/
|
||||
public async fetchUser(): Promise<UserController> {
|
||||
return (await users.fetchUser({ id: this.userId }))!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import { Collection } from "@discordjs/collection";
|
||||
import { z } from "zod";
|
||||
import { Role } from "../../generated/prisma/client";
|
||||
import { User } from "../../generated/prisma/browser";
|
||||
import { SessionTokensObject } from "./SessionController";
|
||||
import { sessions } from "../managers/sessions";
|
||||
import BodyError from "../Errors/BodyError";
|
||||
import { prisma } from "../constants";
|
||||
import { events } from "../modules/globalEvents";
|
||||
import { RoleController } from "./RoleController";
|
||||
import { roles } from "../managers/roles";
|
||||
|
||||
|
||||
|
||||
export default class UserController {
|
||||
public id: string;
|
||||
public name: string | null;
|
||||
public login: string;
|
||||
public email: string;
|
||||
public image: string | null;
|
||||
|
||||
private _roles: Collection<string, Role>;
|
||||
|
||||
public createdAt: Date;
|
||||
public updatedAt: Date;
|
||||
constructor(userdata: User & { roles: Role[] }) {
|
||||
this.id = userdata.id;
|
||||
this.name = userdata.name;
|
||||
this.login = userdata.login;
|
||||
this.email = userdata.email;
|
||||
this.image = userdata.image;
|
||||
this.updatedAt = userdata.updatedAt;
|
||||
this.createdAt = userdata.createdAt;
|
||||
|
||||
this._roles = (() => {
|
||||
let collection = new Collection<string, Role>();
|
||||
userdata.roles.map((v: any) => collection.set(v.id, v));
|
||||
|
||||
return collection;
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the internal values
|
||||
*
|
||||
* This is an internal method used to update all the internal values when we query the database. This way
|
||||
* everything stays upto date even when we pass around the user controller.
|
||||
*
|
||||
* @param userdata - User object from Prisma
|
||||
*/
|
||||
private _updateInternalValues(userdata: User) {
|
||||
this.id = userdata.id;
|
||||
this.name = userdata.name;
|
||||
this.login = userdata.login;
|
||||
this.email = userdata.email;
|
||||
this.image = userdata.image;
|
||||
this.updatedAt = userdata.updatedAt;
|
||||
this.createdAt = userdata.createdAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Session
|
||||
*
|
||||
* This will create a session in the database that is linked to the user and will then create a pair of access and refresh
|
||||
* tokens to provide to the user such that they can authorized their api requests.
|
||||
*
|
||||
* @returns {Promise<SessionTokensObject>} - Object with an access token and a refresh token.
|
||||
*/
|
||||
public async createSession(): Promise<SessionTokensObject> {
|
||||
return sessions.create({ user: this });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user
|
||||
*
|
||||
* Take in a partial of the user data and validate it then updated it if it passes validation and return
|
||||
* the updated `UserController` object.
|
||||
*
|
||||
* @param data - A partial of the user data
|
||||
* @returns {Promise<UserController>} - The updated user controller
|
||||
*/
|
||||
public async update(data: Partial<User>) {
|
||||
// Parsed Data With Schema
|
||||
const pData = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
image: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.parse(data);
|
||||
|
||||
if (Object.keys(data).length == 0)
|
||||
throw new BodyError("Body cannot be empty.");
|
||||
|
||||
const updatedUser = await prisma.user.update({
|
||||
where: { id: this.id },
|
||||
data: pData,
|
||||
});
|
||||
|
||||
this._updateInternalValues(updatedUser);
|
||||
events.emit("user:updated", { user: this, updatedValues: data });
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Roles
|
||||
*
|
||||
* This method will fetch all of the roles that a user belongs to and will return each of their controllers in a collection
|
||||
* of role id's and RoleControllers.
|
||||
*
|
||||
* @returns {Promise<Collection<string, RoleController>>} A collection of all the roles a user has
|
||||
*/
|
||||
public async fetchRoles(): Promise<Collection<string, RoleController>> {
|
||||
const collection = new Collection<string, RoleController>();
|
||||
|
||||
await Promise.all(
|
||||
this._roles.map(async (v) =>
|
||||
collection.set(v.id, await roles.fetch(v.id))
|
||||
)
|
||||
);
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check Permission
|
||||
*
|
||||
* Check if this user has this specific permission. This method will not only check explicit permissions defined in
|
||||
* the database under users and roles, but will also generate implicit permissions for resources that the user has
|
||||
* access to but doesn't specifically have defined under any given permissions object.
|
||||
*
|
||||
* @param permission - The permission to check for
|
||||
* @returns {boolean} Does this user have the specified permission
|
||||
*/
|
||||
public async hasPermission(permission: string) {
|
||||
let resources = await prisma.user.findFirst({
|
||||
where: { id: this.id },
|
||||
select: {
|
||||
sessions: {
|
||||
select: { id: true },
|
||||
},
|
||||
apiKeys: {
|
||||
select: { id: true },
|
||||
},
|
||||
projects: {
|
||||
select: { id: true },
|
||||
},
|
||||
services: {
|
||||
select: { id: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const implicitPermissions = Object.keys(resources ?? {})
|
||||
.filter((v) => resources![v].length > 0)
|
||||
.map(
|
||||
(v) =>
|
||||
`resource.${v}.[${(resources![v] as { id: string }[])
|
||||
.map((o) => o.id)
|
||||
.join(",")}].user.${this.id}.implicit`
|
||||
);
|
||||
|
||||
console.log(implicitPermissions);
|
||||
|
||||
let checks = [
|
||||
(await this.fetchRoles()).map((v) => v.checkPermission(permission)),
|
||||
].flatMap((v) => v);
|
||||
|
||||
return checks.includes(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* To JSON
|
||||
*
|
||||
* Create an object that can be safely returned to the user of an api request such that when you
|
||||
* need to return data to the end user, you don't accidently return data that could be harmful
|
||||
* if leaked.
|
||||
*
|
||||
* Options:
|
||||
* - Safe return is to return only data that is considered "safe", and not detrimental to pass around
|
||||
*
|
||||
* @param opts - Options to change the output
|
||||
* @returns - An object that is JSON friendly
|
||||
*/
|
||||
public toJson(opts?: { safeReturn: boolean }) {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
roles: opts?.safeReturn
|
||||
? undefined
|
||||
: this._roles.size > 0
|
||||
? this._roles.map((v) => v.moniker)
|
||||
: undefined,
|
||||
login: opts?.safeReturn ? undefined : this.login,
|
||||
email: opts?.safeReturn ? undefined : this.email,
|
||||
image: this.image,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user