import { Collection } from "@discordjs/collection"; 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"; import { signPermissions } from "../modules/permission-utils/signPermissions"; import { DecodedPermissionsBlock } from "../types/PermissionTypes"; import jwt from "jsonwebtoken"; import { permissionsPrivateKey } from "../constants"; export default class UserController { public id: string; public name: string | null; public login: string; public email: string; public image: string | null; public cwIdentifier: string | null; private _roles: Collection; private _permissions: string | null; 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.cwIdentifier = userdata.cwIdentifier ?? null; this.updatedAt = userdata.updatedAt; this.createdAt = userdata.createdAt; this._permissions = userdata.permissions ?? null; this._roles = (() => { let collection = new Collection(); 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.cwIdentifier = userdata.cwIdentifier ?? null; 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} - Object with an access token and a refresh token. */ public async createSession(): Promise { 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} - The updated user controller */ public async update(data: Partial>) { if (Object.keys(data).length == 0) throw new BodyError("Body cannot be empty."); const updatedUser = await prisma.user.update({ where: { id: this.id }, data, }); this._updateInternalValues(updatedUser); events.emit("user:updated", { user: this, updatedValues: data }); return this; } /** * Set Roles * * Replace the user's roles with the provided array of role identifiers (id or moniker). * Validates that each role exists before assigning. * * @param roleIdentifiers - Array of role ids or monikers to assign * @returns {Promise} - The updated user controller */ public async setRoles(roleIdentifiers: string[]): Promise { const resolvedRoles = await Promise.all( roleIdentifiers.map((identifier) => roles.fetch(identifier)), ); const updatedUser = await prisma.user.update({ where: { id: this.id }, data: { roles: { set: resolvedRoles.map((r) => ({ id: r.id })), }, }, include: { roles: true }, }); this._updateInternalValues(updatedUser); this._roles = new Collection(); updatedUser.roles.map((v: any) => this._roles.set(v.id, v)); for (const role of resolvedRoles) { events.emit("user:role:assigned", { user: this, role }); } return this; } /** * Set Permissions * * Replace the user's direct permissions with the provided array of permission strings. * Signs the permissions with the user issuer before storing. * * @param permissions - Array of permission node strings to assign * @returns {Promise} - The updated user controller */ public async setPermissions(permissions: string[]): Promise { const signed = signPermissions({ issuer: "user", subject: this.id, permissions, }); const updatedUser = await prisma.user.update({ where: { id: this.id }, data: { permissions: signed }, }); this._updateInternalValues(updatedUser); return this; } /** * Read Permissions * * Verifies and decodes the user's direct permissions JWT and returns the array of * permission node strings. Returns an empty array if the user has no direct permissions. * * @returns {string[]} The user's direct permission nodes */ public readPermissions(): string[] { if (!this._permissions) return []; const decoded = jwt.verify(this._permissions, permissionsPrivateKey, { algorithms: ["RS256"], issuer: "user", subject: this.id, }) as DecodedPermissionsBlock; return decoded.permissions; } /** * Read Role Permissions * * Verifies and decodes a role permissions JWT and returns the permission nodes. * Returns an empty array if verification fails. * * @param role - Role record containing the signed permissions token * @returns {string[]} The role permission nodes */ private _readRolePermissions(role: Role): string[] { try { const decoded = jwt.verify(role.permissions, permissionsPrivateKey, { algorithms: ["RS256"], issuer: "roles", subject: role.id, }) as DecodedPermissionsBlock; return decoded.permissions; } catch { return []; } } /** * Read All Permissions * * Aggregates the user's direct permissions and all permissions from their assigned roles * into a single deduplicated array. * * @returns {Promise} Combined array of all permission nodes */ public async readAllPermissions(): Promise { const directPermissions = this.readPermissions(); const rolePermissions = this._roles .map((role) => this._readRolePermissions(role)) .flatMap((permissions) => permissions); return [...new Set([...directPermissions, ...rolePermissions])]; } /** * 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>} A collection of all the roles a user has */ public async fetchRoles(): Promise> { const collection = new Collection(); 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 }, }, }, }); const resourceKeys: string[] = Object.keys(resources ?? {}) as string[]; const implicitPermissions = resources ? resourceKeys // @ts-ignore .filter((v) => resources[v].length > 0) .map( (v) => //@ts-ignore `resource.${v}.[${(resources![v] as { id: string }[]) .map((o) => o.id) .join(",")}].user.${this.id}.implicit`, ) : []; 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, permissions: opts?.safeReturn ? undefined : (() => { const directPermissions = this.readPermissions(); const rolePermissions = this._roles .map((role) => this._readRolePermissions(role)) .flatMap((permissions) => permissions); return [...new Set([...directPermissions, ...rolePermissions])]; })(), login: opts?.safeReturn ? undefined : this.login, email: opts?.safeReturn ? undefined : this.email, cwIdentifier: opts?.safeReturn ? undefined : this.cwIdentifier, image: this.image, createdAt: this.createdAt, updatedAt: this.updatedAt, }; } }