untested WIP
This commit is contained in:
@@ -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?
|
||||
*/
|
||||
@@ -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.
|
||||
*/
|
||||
@@ -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
|
||||
*/
|
||||
Reference in New Issue
Block a user