untested WIP

This commit is contained in:
2026-01-24 16:59:50 -06:00
parent 935c7296f6
commit 4be36e6ca0
56 changed files with 8645 additions and 3 deletions
+171
View File
@@ -0,0 +1,171 @@
import { Collection } from "@discordjs/collection";
import GenericError from "../Errors/GenericError";
import RoleError from "../Errors/RoleError";
import { prisma } from "../constants";
import { RoleController } from "../controllers/RoleController";
import { signPermissions } from "../modules/permission-utils/signPermissions";
import cuid from "cuid";
import UserController from "../controllers/UserController";
import InsufficientPermission from "../Errors/InsufficientPermission";
import { z } from "zod";
export const roles = {
/**
* Create a role
*
* @param data - Data required to make a role
* @returns {RoleController} - The new role
*/
async create(data: {
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, "Cannot have a blank permission node."))
.optional(),
});
data = await schema.parseAsync(data);
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 id = cuid();
const newRole = await prisma.role.create({
data: {
id,
title: data.title,
moniker: data.moniker,
permissions: signPermissions({
issuer: "roles",
subject: id,
permissions: data.permissions ?? [],
}),
},
include: {
users: {
include: {
roles: true,
},
},
},
});
const controller = new RoleController(newRole);
return controller;
},
/**
* Fetch a role
*
* Fetch a role using either it's id or it's moniker.
*
* @param identifier - Role `id` or `moniker`
* @param identifier - Options for fetching a role.
* @returns {RoleController} - Role Controller
*/
async fetch(identifier:string, opt?: { requestingUser?: UserController }) {
const roleData = await prisma.role.findFirst({
where: { OR: [{ id: identifier }, { moniker: identifier }] },
include: {
users: {
include: {
roles: true,
},
},
},
});
if (!roleData)
throw new GenericError({
name: "UnknownRole",
message: "Unknown role...",
status: 404,
});
if (
opt?.requestingUser &&
!(await opt.requestingUser.hasPermission(
this._buildPermissionNode(roleData.id, "read")
))
)
throw new InsufficientPermission(
"You do not have permission to access this role."
);
const controller = new RoleController(roleData);
return controller;
},
/**
* Fetch all roles
*
* This will give you all of the roles and their respective controllers
*
* @param opt - Options for fetching all roles.
* @returns {Collection<string, RoleController>} A collection of all the roles and their id's
*/
async fetchAllRoles(opt?: { requestingUser?: UserController }) {
let collection = new Collection<string, RoleController>();
const roles = await prisma.role.findMany({
include: { users: { include: { roles: true } } },
});
roles. map((v:any) => collection.set(v.id, new RoleController(v)));
if (opt?.requestingUser) {
const permittedRoles = await Promise.all(
collection.map(async (v) =>
(await opt.requestingUser?.hasPermission(
this._buildPermissionNode(v.id, "read")
))
? v.id
: null
)
);
collection = collection.filter((v) =>
permittedRoles.filter((x) => x !== null).includes(v.id)
);
}
return collection;
},
/**
* Build Permissino Node
*
* Build a role centric permission node with a role id and permission scope.
*
* **FORMAT**: `roles.{id}.{scope}
*
* @param id - Role ID
* @param scope - Scope of the permission node
* @returns {string} Constructed Permission Node
*/
_buildPermissionNode(id: string, scope: "read" | "write") {
return ["roles", id, scope].join(".");
},
};
/**
* @TODO automatic string transformation for monikers to be all lower case with no spaces and only underscores.
*
* @TODO create and inheritance and ordering system @see https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations
*
* @TODO go through and make sure all the zod schemas have mins, maxes, etc.
*
* @TODO Limit those who can give out the `*` permission to those with the `*` permission.
* - Maybe also limit the use of `*` all together?
*/
+159
View File
@@ -0,0 +1,159 @@
import {
prisma,
refreshTokenDuration,
sessionDuration,
accessTokenDuration,
accessTokenPrivateKey,
refreshTokenPrivateKey,
} from "../constants";
import UserController from "../controllers/UserController";
import {
SessionController,
DecodedSession,
SessionPayloadObject,
SessionTokensObject,
} from "../controllers/SessionController";
import jwt from "jsonwebtoken";
import SessionError from "../Errors/SessionError";
import SessionTokenError from "../Errors/SessionTokenError";
import ExpiredAccessTokenError from "../Errors/ExpiredAccessTokenError";
import ExpiredRefreshTokenError from "../Errors/ExpiredRefreshTokenError";
import { events } from "../modules/globalEvents";
export interface SessionCreationData {
user: UserController;
}
export const sessions = {
/**
* Create a session
*
* This will create a session instance in the databse that will be linked to both the session token
* and the refresh token.
*
* @param data - The params needed to create the session tokens
* @returns {Promise<SessionTokensObject>} Session Token and the Refresh Token
*/
async create(data: SessionCreationData): Promise<SessionTokensObject> {
const session = await prisma.session.create({
data: {
expires: new Date(Date.now() + sessionDuration),
userId: data.user.id,
},
});
const controller = new SessionController(session);
// Trigger Global Event
events.emit("session:created", {
user: data.user,
session: controller,
});
let tokens: SessionTokensObject = await controller.generateTokens();
return tokens;
},
/**
* Fetch a session
*
* This method is designed to be as versitile as possible, if you are looking for a session, use this method with
* your choice of `accessToken`, `refreshToken`, `id`, or `sessionKey`. The identifier value is a partial type.
*
* @param identifier - An object allowing you to put either session token, sessionKey, or id in to fetch the desired session.
* @returns {Promise<SessionController>} The controller for the desired session.
*/
async fetch(
identifier: Partial<{
refreshToken: string;
accessToken: string;
id: string;
sessionKey: string;
}>
) {
if (identifier.refreshToken || identifier.accessToken) {
const decodedJWT = identifier.refreshToken
? ((await new Promise((res, rej) =>
jwt.verify(
identifier.refreshToken!,
refreshTokenPrivateKey,
{
algorithms: ["RS256"],
},
async (err, decode) => {
if (
err &&
(err.name == "TokenExpiredError" ||
err.message == "invalid signature")
) {
let sessionDat = await prisma.session.findFirst({
where: {
sessionKey: (
jwt.decode(identifier.refreshToken!) as DecodedSession
).sessionKey,
},
});
if (!sessionDat)
return rej(new SessionError("Invalid session."));
let session = new SessionController(sessionDat);
await session.terminate();
if (err.message == "invalid signature")
return rej(new SessionError("Invalid session."));
return rej(new ExpiredRefreshTokenError("It epired."));
}
if (err) return rej(err);
return res(decode as DecodedSession);
}
)
)) as DecodedSession)
: ((await new Promise((res, rej) =>
jwt.verify(
identifier.accessToken!,
accessTokenPrivateKey,
{
algorithms: ["RS256"],
},
(err, decode) => {
if (err && err.name == "TokenExpiredError")
return rej(new ExpiredAccessTokenError());
if (err) return rej(err);
return res(decode as DecodedSession);
}
)
)) as DecodedSession);
const sessionData = await prisma.session.findFirst({
where: { sessionKey: decodedJWT.sessionKey },
});
if (!sessionData) throw new SessionError("Invalid Session");
if (identifier.accessToken && decodedJWT.exp > Date.now())
throw new ExpiredAccessTokenError();
if (identifier.refreshToken && decodedJWT.exp > Date.now()) {
let sess = new SessionController(sessionData);
await sess.terminate();
throw new SessionError("Invalid Session...", "Expired Refresh Token");
}
return new SessionController(sessionData);
}
const sessionData = await prisma.session.findFirst({
where: { OR: [{ sessionKey: identifier.sessionKey, id: identifier.id }] },
});
if (!sessionData) throw new SessionError("Invalid Session");
return new SessionController(sessionData);
},
};
/**
* @TODO As a consequence of the above, need to setup pgBoss for cron and event loop.
*/
+118
View File
@@ -0,0 +1,118 @@
import { User } from "../../generated/prisma/client";
import { prisma } from "../constants";
import { SessionTokensObject } from "../controllers/SessionController";
import UserController from "../controllers/UserController";
export const users = {
/**
* Authenticate The User
*
* If the user has already been registered, this function will supply them with a session id, otherwise
* this method will create an account, then create a session id.
*
* @summary It creates a user if one doesn't exist and will supply a session id
*
* @async
* @param ghCode - The code supplied in the callback url of a GitHub oAuth transaction
*/
/* async authenticate(ghCode: string): Promise<SessionTokensObject> {
const token = await ghApp.oauth.createToken({ code: ghCode }).catch((e) => {
throw new AuthenticationError("Invalid OAuth code...");
});
const userOK = await ghApp.oauth.getUserOctokit({
token: token.authentication.token,
});
const ghUser = await userOK.request("GET /user");
let user =
(await this.fetchUser({ userId: ghUser.data.id })) ??
(await this.createUser(token.authentication.token));
const tokens = await sessions.create({ user });
events.emit("user:authenticated", { user, tokens });
return tokens;
}, */
/**
* Check to see if the user exists
*
* @param partial - A partial object that you can feed any value from the database into.
* @returns {Promise<boolean>} Does the user exist?
*/
async userExists(partial: Partial<User>): Promise<boolean> {
const match = await prisma.user.findFirst({ where: partial });
return !!match;
},
/**
* Fetch a user
*
* This method takes in a unique identifier for the user you are trying to fetch, and returns
* the controller for the user if the user exists. If the user doesn't exist, or no identifier
* was provided, this will reutrn `null`.
*
* @param identifier - A partial identifier for a user (e.g. id, email, userID, etc.)
* @returns {Promise<UserController>} The controller for the user
*/
async fetchUser(
identifier: Partial<{
id: string;
email: string;
login: string;
userId: number;
}>
) {
if (Object.keys(identifier).length == 0) return null;
const userData = await prisma.user.findFirst({
where: {
//@ts-ignore
OR: Object.keys(identifier).map((v) => ({ [v]: identifier[v] })),
},
include: { roles: true },
});
if (!userData) return null;
return new UserController(userData);
},
/**
* Create a new user
*
* This method will poll GitHub and get all the information on the user to then create the
* record in our database. On top of that it also pushes it into the user cache.
*
* @param token - The Github token provided by the auth method
* @returns {Promise<UserController>} The new user controller for the user
*/
async createUser(token: string): Promise<UserController> {
const ghUser = await (
await ghApp.oauth.getUserOctokit({ token })
).request("GET /user");
const emails = await (
await ghApp.oauth.getUserOctokit({ token })
).request("GET /user/emails");
const newUser = await prisma.user.create({
data: {
userId: ghUser.data.id,
email: emails.data[0].email,
image: ghUser.data.avatar_url,
name: ghUser.data.name,
login: ghUser.data.login,
token,
},
include: { roles: true },
});
let controller = new UserController(newUser);
events.emit("user:created", controller);
return controller;
},
};
/**
* @TODO Figure out default permissions
*/