d7b374f8ab
New features: - ActivityController and manager for CW sales activities (CRUD) - ForecastProductController for opportunity forecast/product lines - CW member cache with dual-layer (in-memory + Redis) resolution - Catalog category/subcategory/ecosystem taxonomy module - Quote statuses type definitions with CW mapping - User-defined fields (UDF) module with cache and event refresh - Company sites CW module with serialization - Procurement manager filters (category, ecosystem, manufacturer, price, stock) - Opportunity notes CRUD and product line management via CW API - Opportunity type definitions endpoint Updates: - OpportunityController: CW refresh, company hydration, activities, custom fields - UserController: cwIdentifier field for CW member linking - CatalogItemController: category/subcategory fields from CW - PermissionNodes: sales note/product CRUD nodes, subCategories, collectPermissions - API routes: procurement categories/filters, sales notes/products, opportunity types - Global events: UDF and member refresh intervals on startup Tests (414 passing): - ActivityController, ForecastProductController, OpportunityController unit tests - UserController cwIdentifier tests - catalogCategories, companySites, memberCache, procurement module tests - activityTypes, opportunityTypes, quoteStatuses type tests - permissionNodes subCategories and getAllPermissionNodes tests - Updated test setup with redis mock, API method mocks, and builder helpers
327 lines
10 KiB
TypeScript
327 lines
10 KiB
TypeScript
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<string, Role>;
|
|
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<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.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<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<Pick<User, "name" | "image">>) {
|
|
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<UserController>} - The updated user controller
|
|
*/
|
|
public async setRoles(roleIdentifiers: string[]): Promise<UserController> {
|
|
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<string, Role>();
|
|
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<UserController>} - The updated user controller
|
|
*/
|
|
public async setPermissions(permissions: string[]): Promise<UserController> {
|
|
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<string[]>} Combined array of all permission nodes
|
|
*/
|
|
public async readAllPermissions(): Promise<string[]> {
|
|
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<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 },
|
|
},
|
|
},
|
|
});
|
|
|
|
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,
|
|
};
|
|
}
|
|
}
|