[INIT] Initial Project Structure
This commit is contained in:
commit
0fc5f05b6a
105 changed files with 10448 additions and 0 deletions
32
lib/Cairo.ts
Normal file
32
lib/Cairo.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
// Import Core Modules
|
||||
import { INFO, OK, logInfo } from "@dunemask/vix/logging";
|
||||
import { Vixpress } from "@dunemask/vix";
|
||||
import { MappedRoute, routePreviews } from "@dunemask/vix/express";
|
||||
import figlet from "figlet";
|
||||
import AppRouter from "./vix/AppRouter.js";
|
||||
import PostgresService from "./database/PostgresService.js";
|
||||
import AppInitService from "./services/app-init.service.js";
|
||||
|
||||
export default class Cairo extends Vixpress {
|
||||
static PostgresService: string = "pg";
|
||||
static AppInitService: string = "app-init";
|
||||
constructor(port?: number) {
|
||||
super("Cairo", port ?? Number(process.env.CAIRO_DEV_PORT ?? 52000));
|
||||
this.setService(Cairo.PostgresService, PostgresService);
|
||||
this.setService(Cairo.AppInitService, AppInitService);
|
||||
this.setRouter(AppRouter);
|
||||
}
|
||||
|
||||
protected async preconfigure(): Promise<void> {
|
||||
logInfo(figlet.textSync(this.title, "Cosmike"));
|
||||
}
|
||||
|
||||
protected async onStart() {
|
||||
const previews = routePreviews(this.app, (r: MappedRoute, methodDisplay: string) => {
|
||||
const authSection = r.routeMetadata?.authType === "user" ? "🔒" : " ";
|
||||
return `${methodDisplay} ${authSection} ${r.path}`;
|
||||
});
|
||||
for (const p of previews) INFO("ROUTE", p);
|
||||
OK("SERVER", `${this.title} server running on ${this.port} 🚀`);
|
||||
}
|
||||
}
|
7
lib/app.ts
Normal file
7
lib/app.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import "dotenv/config";
|
||||
import "dotenv-expand/config";
|
||||
import Cairo from "./Cairo";
|
||||
import { assertRequired } from "./config";
|
||||
assertRequired();
|
||||
const cairo = new Cairo();
|
||||
await cairo.start();
|
49
lib/config.ts
Normal file
49
lib/config.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import DefaultRolePolicies from "./vix/AppPolicies";
|
||||
|
||||
const requiredEnvars: string[] = ["CAIRO_KEYPAIR_KEY"];
|
||||
|
||||
const encodedEnvar = (envar: string | undefined) => (!!envar ? Buffer.from(envar, "base64").toString("utf8") : envar);
|
||||
|
||||
export function assertRequired() {
|
||||
for (const e of requiredEnvars) if (!process.env[e]) throw Error(`Envar '${e}' is required!`);
|
||||
}
|
||||
|
||||
export default {
|
||||
Server: {
|
||||
basePath: "/cairo/",
|
||||
projectSlug: "$cairo",
|
||||
projectName: "$cairo",
|
||||
rootPassword: process.env.CAIRO_ROOT_PASSWORD,
|
||||
},
|
||||
RolePolicy: {
|
||||
Root: {
|
||||
id: "ck1ro7ekp000203zu5gn3d9cr",
|
||||
name: "Root",
|
||||
policies: DefaultRolePolicies.Root,
|
||||
},
|
||||
Admin: {
|
||||
id: "ck1ro7bm0000103z5h45sswqs",
|
||||
name: "Admin",
|
||||
policies: DefaultRolePolicies.Admin,
|
||||
},
|
||||
User: {
|
||||
id: "ck1ro7g3e000303z52ee63nqs",
|
||||
name: "User",
|
||||
policies: DefaultRolePolicies.User,
|
||||
},
|
||||
},
|
||||
SigningOptions: {
|
||||
HashRounds: 12,
|
||||
Version: "0.0.1-alpha",
|
||||
Issuer: encodedEnvar(process.env.CAIRO_HOSTNAME) ?? "https://cairo.dunemask.net",
|
||||
Keys: {
|
||||
KeyPair: encodedEnvar(process.env.CAIRO_KEYPAIR_KEY) ?? "keypair-key",
|
||||
},
|
||||
Subjects: {
|
||||
User: "user",
|
||||
Cargo: "cargo",
|
||||
Runner: "runner",
|
||||
Pod: "pod",
|
||||
},
|
||||
},
|
||||
};
|
54
lib/database/PostgresService.ts
Normal file
54
lib/database/PostgresService.ts
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { Prisma, PrismaClient } from "@prisma/client";
|
||||
import { VixpressService } from "@dunemask/vix";
|
||||
import { VERB } from "@dunemask/vix/logging";
|
||||
import UsersTableService from "./tables/UsersTableService.js";
|
||||
import RolePolicyTableService from "./tables/RolePolicyTableService.js";
|
||||
import ProjectTableService from "./tables/ProjectTableService.js";
|
||||
import KeyPairTableService from "./tables/KeyPairTableService.js";
|
||||
|
||||
export class DBPrismaClient extends PrismaClient {
|
||||
private async queryUniqueOrThrow<T = unknown>(data: unknown): Promise<T | undefined> {
|
||||
if (!Array.isArray(data)) throw Error("Returned non-array!");
|
||||
if (data.length > 1) throw Error("Non unique value found!");
|
||||
return data.length === 1 ? data[0] : undefined;
|
||||
}
|
||||
async $queryRawUnique<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Promise<T | undefined> {
|
||||
const data = await this.$queryRaw(query, ...values);
|
||||
return this.queryUniqueOrThrow<T>(data);
|
||||
}
|
||||
|
||||
async $queryRawUnsafeUnique<T = unknown>(query: string, ...values: any[]): Promise<T | undefined> {
|
||||
const data = await this.$queryRawUnsafe(query, ...values);
|
||||
return this.queryUniqueOrThrow<T>(data);
|
||||
}
|
||||
}
|
||||
|
||||
export default class PostgresService extends VixpressService {
|
||||
declare pg: DBPrismaClient;
|
||||
declare users: UsersTableService;
|
||||
declare rolePolicy: RolePolicyTableService;
|
||||
declare project: ProjectTableService;
|
||||
declare keypair: KeyPairTableService;
|
||||
|
||||
async configureService() {
|
||||
this.pg = new DBPrismaClient({
|
||||
errorFormat: "pretty",
|
||||
log: ["warn", "error", "info", { emit: "event", level: "query" }],
|
||||
});
|
||||
this.users = new UsersTableService(this.pg);
|
||||
this.rolePolicy = new RolePolicyTableService(this.pg);
|
||||
this.project = new ProjectTableService(this.pg);
|
||||
this.keypair = new KeyPairTableService(this.pg);
|
||||
}
|
||||
|
||||
async startService() {
|
||||
VERB("POSTGRES", "Connecting to postgres....");
|
||||
await this.pg.$connect();
|
||||
await this.project.$upsertDefaultProject();
|
||||
await this.keypair.$upsertDefaultKeyPairs();
|
||||
await this.rolePolicy.$upsertDefaultAuthorities();
|
||||
const user = await this.users.$upsertDefaultRootUser();
|
||||
if (!!user) VERB("APP INIT", `Created identity 'root' with password '${user.password}'!`);
|
||||
if (!!user) VERB("APP INIT", "This will not be shown again!");
|
||||
}
|
||||
}
|
9
lib/database/TableService.ts
Normal file
9
lib/database/TableService.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { DBPrismaClient } from "./PostgresService";
|
||||
|
||||
export default abstract class TableService {
|
||||
declare pg: DBPrismaClient;
|
||||
protected abstract table: string;
|
||||
constructor(pg: DBPrismaClient) {
|
||||
this.pg = pg;
|
||||
}
|
||||
}
|
60
lib/database/tables/KeyPairTableService.ts
Normal file
60
lib/database/tables/KeyPairTableService.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
import config from "@lib/config";
|
||||
import TableService from "../TableService";
|
||||
import { CKeyPairContract } from "@lib/contracts/keypair.contracts";
|
||||
import { KeyPairErrors, ProjectErrors } from "@lib/vix/ClientErrors";
|
||||
import { encrypt, generateKeypair } from "@lib/svc/crypt.service";
|
||||
import { KeyPair, KeyPairType } from "@prisma/client";
|
||||
|
||||
declare type Custom = "Custom"; // Make sure this matches KeyPairType.Custom;
|
||||
|
||||
export default class KeyPairTableService extends TableService {
|
||||
protected table = "KeyPair";
|
||||
async byId(keypairId: string) {
|
||||
const keypair = this.pg.keyPair.findUnique({ where: { id: keypairId } });
|
||||
if (!keypair) throw KeyPairErrors.NotFoundKeypair;
|
||||
}
|
||||
|
||||
async byUsage(projectIdentity: string, usage: Custom): Promise<KeyPair[]>;
|
||||
async byUsage(projectIdentity: string, usage: Exclude<KeyPairType, Custom>): Promise<KeyPair | null>;
|
||||
async byUsage(projectIdentity: string, usage: KeyPairType): Promise<KeyPair[] | KeyPair | null> {
|
||||
const projectOr = { OR: [{ id: projectIdentity }, { slug: projectIdentity }] };
|
||||
const projectInclude = { keyPairs: { where: { usage: KeyPairType.UserToken } } };
|
||||
const project = await this.pg.project.findFirst({ where: projectOr, include: projectInclude });
|
||||
if (!project) throw ProjectErrors.BadRequestProjectIncomplete;
|
||||
const keypairs = project.keyPairs;
|
||||
if (usage !== KeyPairType.Custom && keypairs.length > 1)
|
||||
throw new Error(`Multiple keypairs found for project ${projectIdentity} and usage ${usage}`);
|
||||
if (usage !== KeyPairType.Custom && keypairs.length === 0) return null;
|
||||
if (usage !== KeyPairType.Custom) return keypairs[0];
|
||||
return keypairs;
|
||||
}
|
||||
|
||||
async $upsertDefaultKeyPairs() {
|
||||
const projectSlug = config.Server.projectSlug;
|
||||
const cairoProject = await this.pg.project.findUnique({ where: { slug: projectSlug } });
|
||||
if (!cairoProject) throw new Error("Cairo Project Not Found!");
|
||||
const projectId = cairoProject.id;
|
||||
await this.upsertProjecttDefaultKeyPairs(projectId);
|
||||
}
|
||||
|
||||
async upsertProjecttDefaultKeyPairs(projectId: string) {
|
||||
const storeKeypair = this.create.bind(this);
|
||||
const keyTypes = Object.values(KeyPairType).filter((kp) => kp !== KeyPairType.Custom);
|
||||
await Promise.all(
|
||||
keyTypes.map(async (kp) => {
|
||||
const existingKp = await this.byUsage(projectId, kp);
|
||||
if (!!existingKp) return;
|
||||
const { publicKey, privateKey } = await generateKeypair();
|
||||
const [encryptedPrivateKey, encryptedPublicKey] = await Promise.all([
|
||||
encrypt(privateKey, config.SigningOptions.Keys.KeyPair),
|
||||
encrypt(publicKey, config.SigningOptions.Keys.KeyPair),
|
||||
]);
|
||||
return storeKeypair({ encryptedPrivateKey, encryptedPublicKey, projectId, usage: kp });
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async create(keypair: CKeyPairContract["Create"]) {
|
||||
return this.pg.keyPair.create({ data: keypair });
|
||||
}
|
||||
}
|
34
lib/database/tables/ProjectTableService.ts
Normal file
34
lib/database/tables/ProjectTableService.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import config from "@lib/config";
|
||||
import TableService from "../TableService";
|
||||
import { CProjectContract } from "@lib/types/ContractTypes";
|
||||
import { ProjectErrors } from "@lib/vix/ClientErrors";
|
||||
import { VERB } from "@dunemask/vix/logging";
|
||||
|
||||
export default class ProjectTableService extends TableService {
|
||||
protected table = "Project";
|
||||
async byId(projectId: string) {
|
||||
return this.pg.project.findUnique({ where: { id: projectId } });
|
||||
}
|
||||
|
||||
async bySlug(slug: string) {
|
||||
return this.pg.project.findUnique({ where: { slug } });
|
||||
}
|
||||
|
||||
async $upsertDefaultProject() {
|
||||
const { projectSlug: slug, projectName: name } = config.Server;
|
||||
const createOptions = { slug, name, parentProject: slug };
|
||||
const existingProject = await this.pg.project.findUnique({ where: { slug } });
|
||||
if (!!existingProject) return VERB("PROJECT", "Default project already exists!");
|
||||
VERB("PROJECT", "Default project not found! Creating now!");
|
||||
const proj = await this.pg.project.upsert({ where: { slug }, create: createOptions, update: createOptions });
|
||||
await this.pg.project.update({ where: { id: proj.id }, data: { parentProject: proj.id } }); // Use ProjectID instead of slug
|
||||
}
|
||||
|
||||
async create(project: CProjectContract["Create"] & { parentProject: string }) {
|
||||
const existingProject = await this.pg.project.findMany({ where: { id: project.slug } });
|
||||
if (existingProject.length > 1) throw ProjectErrors.BadRequestSlugInvalid;
|
||||
return this.pg.project.create({ data: project }).catch(() => {
|
||||
throw ProjectErrors.ConflictNonUnique;
|
||||
});
|
||||
}
|
||||
}
|
37
lib/database/tables/RolePolicyTableService.ts
Normal file
37
lib/database/tables/RolePolicyTableService.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import TableService from "../TableService";
|
||||
import { AuthorityType } from "@prisma/client";
|
||||
import { Policy, PolicyDefault } from "@lib/Policies";
|
||||
import config from "@lib/config";
|
||||
import { CRolePolicyContract } from "@lib/contracts/role-policy.contracts";
|
||||
|
||||
export default class RolePolicyTableService extends TableService {
|
||||
protected table = "RolePolicy";
|
||||
async byId(id: string) {
|
||||
return this.pg.rolePolicy.findUnique({ where: { id } });
|
||||
}
|
||||
|
||||
async $upsertDefaultAuthorities() {
|
||||
const projectSlug = config.Server.projectSlug;
|
||||
const cairoProject = await this.pg.project.findUnique({ where: { slug: projectSlug } });
|
||||
if (!cairoProject) throw new Error("Cairo Project Not Found!");
|
||||
const project = cairoProject.id;
|
||||
const $chk = ({ id, name, policies }: PolicyDefault) => this.$upsertDefaultsAuthority(project, name, id, policies);
|
||||
await Promise.all(Object.values(config.RolePolicy).map($chk));
|
||||
}
|
||||
|
||||
private async $upsertDefaultsAuthority(projectId: string, name: string, id: string, userPolicies: Policy[]) {
|
||||
const rootAuthority = config.RolePolicy.Root.id;
|
||||
const authorityType = id === rootAuthority ? AuthorityType.Root : AuthorityType.RolePolicy;
|
||||
const authority = id === rootAuthority ? name : rootAuthority; // Set Root Authority to root if root
|
||||
const policies = Policy.asStrings(userPolicies);
|
||||
return this.pg.rolePolicy.upsert({
|
||||
where: { id },
|
||||
create: { projectId, id, name, policies, authority, authorityType },
|
||||
update: { projectId, name, policies, authority, authorityType },
|
||||
});
|
||||
}
|
||||
|
||||
async create(rp: CRolePolicyContract["Create"]) {
|
||||
return this.pg.rolePolicy.create({ data: rp });
|
||||
}
|
||||
}
|
63
lib/database/tables/UsersTableService.ts
Normal file
63
lib/database/tables/UsersTableService.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
import config from "@lib/config";
|
||||
import TableService from "../TableService";
|
||||
import { CUserContract } from "@lib/types/ContractTypes";
|
||||
import { hashText } from "@lib/modules/auth/auth.service";
|
||||
import { KeyPairType } from "@prisma/client";
|
||||
import { UserErrors } from "@lib/vix/ClientErrors";
|
||||
|
||||
// prettier-ignore
|
||||
const generateBase64Password = (length: number = 32): string => Array.from(crypto.getRandomValues(new Uint8Array(length)), byte => 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'.charAt(byte % 64)).join('');
|
||||
|
||||
export default class UsersTableService extends TableService {
|
||||
protected table = "User";
|
||||
async byId(userId: string) {
|
||||
return this.pg.user.findUnique({ where: { id: userId }, include: { rolePolicy: true, project: true } });
|
||||
}
|
||||
|
||||
async byUsername(username: string, projectId: string) {
|
||||
return this.pg.user.findUnique({ where: { projectId_username: { projectId, username } } });
|
||||
}
|
||||
|
||||
async byEmail(email: string, projectId: string) {
|
||||
return this.pg.user.findUnique({ where: { projectId_email: { projectId, email } } });
|
||||
}
|
||||
|
||||
async $upsertDefaultRootUser() {
|
||||
const project = await this.pg.project.findUnique({ where: { slug: config.Server.projectSlug } });
|
||||
if (!project) throw new Error("Cairo Project Not Found!");
|
||||
const rolePolicyId = config.RolePolicy.Root.id;
|
||||
return this.$upsertRootUser(project.id, rolePolicyId);
|
||||
}
|
||||
|
||||
async $upsertRootUser(projectId: string, rolePolicyId: string) {
|
||||
const root = await this.pg.user.findUnique({ where: { projectId_username: { username: "root", projectId } } });
|
||||
if (!!root) return;
|
||||
const password = config.Server.rootPassword ?? generateBase64Password();
|
||||
const hash = await hashText(password);
|
||||
const user = await this.pg.user.create({ data: { projectId, username: "root", email: "root", hash, rolePolicyId } });
|
||||
return { ...user, password };
|
||||
}
|
||||
|
||||
async create(options: CUserContract["Create"]) {
|
||||
const { hash, projectId, rolePolicyId } = options;
|
||||
const username = options.username?.toLowerCase();
|
||||
const email = options.email?.toLowerCase() ?? undefined;
|
||||
const [existingUsername, existingEmail] = await Promise.all([
|
||||
this.byUsername(username, projectId),
|
||||
!!email ? this.byEmail(email, projectId) : undefined,
|
||||
]);
|
||||
if (!existingUsername || !existingEmail) throw UserErrors.ConflictIdentityTaken;
|
||||
const userData = { projectId, username, email, hash, rolePolicyId };
|
||||
return this.pg.user.create({ data: userData, include: { rolePolicy: true } });
|
||||
}
|
||||
|
||||
async byIdentity(projectIdentity: string, identity: string) {
|
||||
const username = identity.toLowerCase();
|
||||
const email = identity.toLowerCase();
|
||||
const OrUser = { OR: [{ username }, { email }] };
|
||||
const OrProject = { project: { OR: [{ id: projectIdentity }, { slug: projectIdentity }] } };
|
||||
const projectInclude = { include: { keyPairs: { where: { usage: KeyPairType.UserToken } } } };
|
||||
const AND = [OrUser, OrProject];
|
||||
return this.pg.user.findFirst({ where: { AND }, include: { rolePolicy: true, project: projectInclude } });
|
||||
}
|
||||
}
|
22
lib/middlewares/policy-guard.ts
Normal file
22
lib/middlewares/policy-guard.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { Request, Response, NextFunction, Router, Express } from "express";
|
||||
import userGuard from "./user-guard";
|
||||
import { MetadataRouter } from "@dunemask/vix/express";
|
||||
import { Policy } from "@lib/Policies";
|
||||
import { AuthErrors } from "@lib/vix/ClientErrors";
|
||||
import { UserRequest } from "@lib/types/ApiRequests";
|
||||
|
||||
export default function policyMiddlewareGuard(requiredPolicies: Policy[]) {
|
||||
const middlewares: MetadataRouter = Router({ mergeParams: true });
|
||||
|
||||
async function policyAuthMiddleware(req: Request, res: Response, next: NextFunction) {
|
||||
const { user, policies: userPolicies } = req as UserRequest;
|
||||
if (!user) throw AuthErrors.UnauthorizedRequest;
|
||||
if (!userPolicies) throw AuthErrors.UnauthorizedRequest;
|
||||
if (!Policy.multiAuthorizedTo(userPolicies, requiredPolicies)) throw AuthErrors.ForbiddenPermissions;
|
||||
if (!next) return res.sendStatus(200);
|
||||
next();
|
||||
}
|
||||
middlewares.routeMetadata = { authType: "policy" };
|
||||
middlewares.use([userGuard(), policyAuthMiddleware]);
|
||||
return middlewares;
|
||||
}
|
41
lib/middlewares/user-guard.ts
Normal file
41
lib/middlewares/user-guard.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
import { AuthorizedTokenRequest, MetadataRouter, tokenAuthMiddleware } from "@dunemask/vix/express";
|
||||
import Cairo from "@lib/Cairo";
|
||||
import { getUserTokenId } from "@lib/modules/auth/auth.service";
|
||||
import { Policy, PolicyComputeType } from "@lib/Policies";
|
||||
import { UserRequest } from "@lib/types/ApiRequests";
|
||||
import { Resource } from "@lib/vix/AppResources";
|
||||
import { AuthErrors, ProjectErrors } from "@lib/vix/ClientErrors";
|
||||
import { Request, Response, NextFunction, Router, Express } from "express";
|
||||
import expressBearerToken from "express-bearer-token";
|
||||
import type PostgresService from "@lib/database/PostgresService.js";
|
||||
import { KeyPairType, User } from "@prisma/client";
|
||||
|
||||
export default function userGuard() {
|
||||
const middlewares: MetadataRouter = Router({ mergeParams: true });
|
||||
async function userGuardMiddleware(req: Request, _res: Response, next: NextFunction) {
|
||||
const { token } = req as AuthorizedTokenRequest;
|
||||
if (!token) throw AuthErrors.UnauthorizedRequiredToken;
|
||||
|
||||
const PostgresService = req.app.get(Cairo.PostgresService) as PostgresService;
|
||||
const { project } = req.params;
|
||||
if (!project) throw AuthErrors.UnauthorizedRequiredProject;
|
||||
|
||||
const userKeypair = await PostgresService.keypair.byUsage(project, KeyPairType.UserToken);
|
||||
if (!userKeypair) throw ProjectErrors.BadRequestProjectIncomplete;
|
||||
|
||||
const id = await getUserTokenId(token, userKeypair.encryptedPublicKey);
|
||||
if (!id) throw AuthErrors.UnauthorizedRequest;
|
||||
const user = await PostgresService.users.byId(id);
|
||||
if (!user) throw AuthErrors.UnauthorizedRequiredUser;
|
||||
const policies = Policy.parseResourcePolicies<Resource>(user.rolePolicy.policies as PolicyComputeType);
|
||||
const projectData = { ...user.project };
|
||||
delete (user as Partial<typeof user>).project;
|
||||
(req as UserRequest).user = user;
|
||||
(req as UserRequest).policies = policies;
|
||||
(req as UserRequest).project = projectData;
|
||||
next();
|
||||
}
|
||||
middlewares.routeMetadata = { authType: "user" };
|
||||
middlewares.use([expressBearerToken(), userGuardMiddleware]);
|
||||
return middlewares;
|
||||
}
|
48
lib/modules/auth/auth.controller.ts
Normal file
48
lib/modules/auth/auth.controller.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { Request, Response, Express } from "express";
|
||||
import { VixpressController } from "@dunemask/vix";
|
||||
import Cairo from "@lib/Cairo";
|
||||
import type PostgresService from "@lib/database/PostgresService";
|
||||
import { CAuthContract, AuthContract } from "@lib/contracts/auth.contracts";
|
||||
import { ContractRouteContext } from "@dunemask/vix/express";
|
||||
import { AuthErrors, ProjectErrors } from "@lib/vix/ClientErrors";
|
||||
import { getUserToken, hashCompare } from "./auth.service";
|
||||
import { CDatabaseContract } from "@lib/contracts/database.contracts";
|
||||
import { ProjectContract } from "@lib/contracts/project.contracts";
|
||||
import { UserRequest } from "@lib/types/ApiRequests";
|
||||
import { ResourcePolicy } from "@dunemask/vix/util";
|
||||
|
||||
type LoginCRC = ContractRouteContext<{
|
||||
RequestBodyContract: typeof AuthContract.Login;
|
||||
RequestParamsContract: typeof ProjectContract.ProjectParams;
|
||||
}>;
|
||||
|
||||
export default class AuthController extends VixpressController {
|
||||
declare pg: PostgresService;
|
||||
constructor(app: Express) {
|
||||
super(app);
|
||||
this.pg = this.app.get(Cairo.PostgresService);
|
||||
}
|
||||
|
||||
verify = (_req: Request, res: Response) => res.sendStatus(200);
|
||||
|
||||
async login(crc: LoginCRC): Promise<CAuthContract["LoginCredentials"]> {
|
||||
const { identity, password } = crc.reqBody;
|
||||
const { project } = crc.reqParams;
|
||||
const user = await this.pg.users.byIdentity(project, identity);
|
||||
if (!user?.rolePolicy?.policies) throw AuthErrors.UnauthorizedRequest;
|
||||
const authorized = await hashCompare(password, user.hash);
|
||||
if (!authorized) throw AuthErrors.UnauthorizedRequest;
|
||||
const projectKeyPairs = user.project.keyPairs;
|
||||
if (projectKeyPairs.length !== 1) throw ProjectErrors.BadRequestProjectIncomplete;
|
||||
const token = await getUserToken(user.id, user.project.keyPairs[0].encryptedPrivateKey);
|
||||
const policies = user.rolePolicy.policies;
|
||||
const userData: CDatabaseContract["User"] = { username: user.username, rolePolicyId: user.rolePolicyId };
|
||||
return { token, user: userData, policies };
|
||||
}
|
||||
|
||||
async credentials(crc: ContractRouteContext): Promise<CAuthContract["Credentials"]> {
|
||||
const { user, policies } = crc.req as UserRequest;
|
||||
const userData: CDatabaseContract["User"] = { username: user.username, rolePolicyId: user.rolePolicyId };
|
||||
return { user: userData, policies: ResourcePolicy.asStrings(policies) };
|
||||
}
|
||||
}
|
26
lib/modules/auth/auth.router.ts
Normal file
26
lib/modules/auth/auth.router.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { VixpressRoute } from "@dunemask/vix";
|
||||
import { contract } from "@dunemask/vix/express";
|
||||
import RouteGuard from "@lib/vix/RouteGuard";
|
||||
import { Router } from "express";
|
||||
import AuthController from "./auth.controller";
|
||||
import { AuthContract } from "@lib/contracts/auth.contracts";
|
||||
import { ProjectContract } from "@lib/contracts/project.contracts";
|
||||
|
||||
export class AuthRoute extends VixpressRoute {
|
||||
async configureRoutes(router: Router) {
|
||||
const jsonOpts = { limit: "20mb" };
|
||||
const cBase = { json: jsonOpts, reqParams: ProjectContract.ProjectParams };
|
||||
// Controllers
|
||||
const authController = this.useController(AuthController);
|
||||
|
||||
// Configuration
|
||||
const loginCreds = { ...cBase, reqBody: AuthContract.Login, resBody: AuthContract.LoginCredentials };
|
||||
const credRes = { ...cBase, resBody: AuthContract.Credentials };
|
||||
// Middleware
|
||||
|
||||
// Routes
|
||||
router.get("/verify", RouteGuard.User, authController.verify);
|
||||
router.post("/login", contract(authController.login, loginCreds));
|
||||
router.get("/credentials", RouteGuard.User, contract(authController.credentials, credRes));
|
||||
}
|
||||
}
|
36
lib/modules/auth/auth.service.ts
Normal file
36
lib/modules/auth/auth.service.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import bcrypt from "bcrypt";
|
||||
import { signToken, verifyToken } from "@lib/svc/token.service";
|
||||
import config from "@lib/config";
|
||||
import { decrypt } from "@lib/svc/crypt.service";
|
||||
const { HashRounds } = config.SigningOptions;
|
||||
|
||||
export async function getUserToken(id: string, encryptedPrivateKey: string) {
|
||||
const privateKey = await decrypt(encryptedPrivateKey, config.SigningOptions.Keys.KeyPair);
|
||||
const tokenPayload = {
|
||||
iss: config.SigningOptions.Issuer,
|
||||
sub: [config.SigningOptions.Subjects.User],
|
||||
aud: [config.SigningOptions.Issuer],
|
||||
id,
|
||||
};
|
||||
return signToken(tokenPayload, privateKey);
|
||||
}
|
||||
|
||||
export async function userTokenLogin(token: string, encryptedPublicKey: string): Promise<boolean> {
|
||||
const publicKey = await decrypt(encryptedPublicKey, config.SigningOptions.Keys.KeyPair);
|
||||
return !!verifyToken(token, publicKey);
|
||||
}
|
||||
|
||||
export async function getUserTokenId(token: string, encryptedPublicKey: string): Promise<string | undefined> {
|
||||
const publicKey = await decrypt(encryptedPublicKey, config.SigningOptions.Keys.KeyPair);
|
||||
const tokenData = verifyToken(token, publicKey);
|
||||
if (!tokenData) return undefined;
|
||||
return tokenData.id;
|
||||
}
|
||||
|
||||
export async function hashText(password: string) {
|
||||
return bcrypt.hash(password, HashRounds);
|
||||
}
|
||||
|
||||
export async function hashCompare(password: string, hash: string) {
|
||||
return bcrypt.compare(password, hash);
|
||||
}
|
48
lib/modules/projects/project.controller.ts
Normal file
48
lib/modules/projects/project.controller.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { Express } from "express";
|
||||
import { VixpressController } from "@dunemask/vix";
|
||||
import Cairo from "@lib/Cairo";
|
||||
import type PostgresService from "@lib/database/PostgresService";
|
||||
import { ContractRouteContext } from "@dunemask/vix/express";
|
||||
import { ProjectErrors } from "@lib/vix/ClientErrors";
|
||||
import { CDatabaseContract } from "@lib/contracts/database.contracts";
|
||||
import { CProjectContract, ProjectContract } from "@lib/contracts/project.contracts";
|
||||
import { KeyPairType } from "@prisma/client";
|
||||
import { decrypt } from "@lib/svc/crypt.service";
|
||||
import config from "@lib/config";
|
||||
import { UserRequest } from "@lib/types/ApiRequests";
|
||||
import { PolicyString } from "@lib/Policies";
|
||||
import { Resource } from "@lib/vix/AppResources";
|
||||
|
||||
type CreateCRC = ContractRouteContext<{
|
||||
RequestBodyContract: typeof ProjectContract.Create;
|
||||
RequestParamsContract: typeof ProjectContract.ProjectParams;
|
||||
}>;
|
||||
|
||||
export default class ProjectController extends VixpressController {
|
||||
declare pg: PostgresService;
|
||||
constructor(app: Express) {
|
||||
super(app);
|
||||
this.pg = this.app.get(Cairo.PostgresService);
|
||||
}
|
||||
|
||||
async create(crc: CreateCRC): Promise<CProjectContract["CreateResponse"]> {
|
||||
const { project: parentProject } = crc.req as UserRequest;
|
||||
const proj = await this.pg.project.create({ ...crc.reqBody, parentProject: parentProject.id });
|
||||
const rolePolicy = await this.pg.rolePolicy.create({
|
||||
name: `${crc.reqBody.slug} Project Root`,
|
||||
authority: config.RolePolicy.Root.id,
|
||||
projectId: proj.id,
|
||||
policies: [`${Resource.CairoProjectRoot}.root`] as PolicyString[],
|
||||
});
|
||||
const [user] = await Promise.all([
|
||||
this.pg.users.$upsertRootUser(proj.id, rolePolicy.id),
|
||||
this.pg.keypair.upsertProjecttDefaultKeyPairs(proj.id),
|
||||
]);
|
||||
const kp = await this.pg.keypair.byUsage(proj.id, KeyPairType.UserToken);
|
||||
if (!kp) throw ProjectErrors.BadRequestProjectIncomplete;
|
||||
if (!user) throw ProjectErrors.UnexpectedRootUserError;
|
||||
const userData: CDatabaseContract["User"] = { username: user.username, rolePolicyId: user.rolePolicyId };
|
||||
const publicKey = await decrypt(kp.encryptedPublicKey, config.SigningOptions.Keys.KeyPair);
|
||||
return { user: userData, project: proj, publicKey };
|
||||
}
|
||||
}
|
22
lib/modules/projects/project.router.ts
Normal file
22
lib/modules/projects/project.router.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { VixpressRoute } from "@dunemask/vix";
|
||||
import { contract } from "@dunemask/vix/express";
|
||||
import { Router } from "express";
|
||||
import ProjectController from "./project.controller";
|
||||
import { ProjectContract } from "@lib/contracts/project.contracts";
|
||||
import RouteGuard from "@lib/vix/RouteGuard";
|
||||
|
||||
export class ProjectRoute extends VixpressRoute {
|
||||
async configureRoutes(router: Router) {
|
||||
const jsonOpts = { limit: "20mb" };
|
||||
const cBase = { json: jsonOpts, reqParams: ProjectContract.ProjectParams };
|
||||
// Controllers
|
||||
const projController = this.useController(ProjectController);
|
||||
|
||||
// Configuration
|
||||
const projCreate = { ...cBase, reqBody: ProjectContract.Create };
|
||||
// Middleware
|
||||
|
||||
// Routes
|
||||
router.post("/create", RouteGuard.MangeProjectsCreate, contract(projController.create, projCreate));
|
||||
}
|
||||
}
|
36
lib/modules/projects/project.service.ts
Normal file
36
lib/modules/projects/project.service.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import bcrypt from "bcrypt";
|
||||
import { signToken, verifyToken } from "@lib/svc/token.service";
|
||||
import config from "@lib/config";
|
||||
import { decrypt } from "@lib/svc/crypt.service";
|
||||
const { HashRounds } = config.SigningOptions;
|
||||
|
||||
export async function getUserToken(id: string, encryptedPrivateKey: string) {
|
||||
const privateKey = await decrypt(encryptedPrivateKey, config.SigningOptions.Keys.KeyPair);
|
||||
const tokenPayload = {
|
||||
iss: config.SigningOptions.Issuer,
|
||||
sub: [config.SigningOptions.Subjects.User],
|
||||
aud: [config.SigningOptions.Issuer],
|
||||
id,
|
||||
};
|
||||
return signToken(tokenPayload, privateKey);
|
||||
}
|
||||
|
||||
export async function userTokenLogin(token: string, encryptedPublicKey: string): Promise<boolean> {
|
||||
const publicKey = await decrypt(encryptedPublicKey, config.SigningOptions.Keys.KeyPair);
|
||||
return !!verifyToken(token, publicKey);
|
||||
}
|
||||
|
||||
export async function getUserTokenId(token: string, encryptedPublicKey: string): Promise<string | undefined> {
|
||||
const publicKey = await decrypt(encryptedPublicKey, config.SigningOptions.Keys.KeyPair);
|
||||
const tokenData = verifyToken(token, publicKey);
|
||||
if (!tokenData) return undefined;
|
||||
return tokenData.id;
|
||||
}
|
||||
|
||||
export async function hashText(password: string) {
|
||||
return bcrypt.hash(password, HashRounds);
|
||||
}
|
||||
|
||||
export async function hashCompare(password: string, hash: string) {
|
||||
return bcrypt.compare(password, hash);
|
||||
}
|
9
lib/services/app-init.service.ts
Normal file
9
lib/services/app-init.service.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { VixpressService } from "@dunemask/vix";
|
||||
import { OK, VERB } from "@dunemask/vix/logging";
|
||||
|
||||
export default class AppInitService extends VixpressService {
|
||||
async startService() {
|
||||
VERB("APP INIT", "Running init services....");
|
||||
OK("APP INIT", "Done!");
|
||||
}
|
||||
}
|
37
lib/services/crypt.service.ts
Normal file
37
lib/services/crypt.service.ts
Normal file
|
@ -0,0 +1,37 @@
|
|||
import crypto, { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
||||
|
||||
export async function generateKeypair() {
|
||||
return crypto.generateKeyPairSync("rsa", {
|
||||
modulusLength: 2048,
|
||||
publicKeyEncoding: { type: "pkcs1", format: "pem" },
|
||||
privateKeyEncoding: { type: "pkcs1", format: "pem" },
|
||||
});
|
||||
}
|
||||
|
||||
export async function encrypt(plaintext: string, hexKey: string): Promise<string> {
|
||||
const key = Buffer.from(hexKey, "hex");
|
||||
const algorithm = "aes-256-cbc"; // Encryption algorithm
|
||||
const iv = randomBytes(16); // Initialization vector
|
||||
|
||||
const cipher = createCipheriv(algorithm, key, iv);
|
||||
let encrypted = cipher.update(plaintext, "utf8", "hex");
|
||||
encrypted += cipher.final("hex");
|
||||
|
||||
// Combine IV and encrypted text
|
||||
return iv.toString("hex") + ":" + encrypted;
|
||||
}
|
||||
|
||||
// Decrypt function
|
||||
export async function decrypt(encryptedText: string, hexKey: string): Promise<string> {
|
||||
const key = Buffer.from(hexKey, "hex");
|
||||
const algorithm = "aes-256-cbc";
|
||||
const textParts = encryptedText.split(":");
|
||||
const iv = Buffer.from(textParts[0], "hex");
|
||||
const encrypted = textParts[1];
|
||||
|
||||
const decipher = createDecipheriv(algorithm, key, iv);
|
||||
let decrypted = decipher.update(encrypted, "hex", "utf8");
|
||||
decrypted += decipher.final("utf8");
|
||||
|
||||
return decrypted;
|
||||
}
|
15
lib/services/token.service.ts
Normal file
15
lib/services/token.service.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import jwt, { Secret, SignOptions } from "jsonwebtoken";
|
||||
|
||||
export function signToken(payload: object, signingKey: Secret, options: SignOptions = {}) {
|
||||
return jwt.sign(payload, signingKey, {
|
||||
...{ algorithm: "RS256" },
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function verifyToken(token: string, signingKey: Secret) {
|
||||
return jwtVerify(token, signingKey) ?? undefined;
|
||||
}
|
||||
|
||||
const jwtVerify = (token: string, key: Secret): any =>
|
||||
jwt.verify(token, key, (err: any, decoded: any) => (!err && decoded) || null);
|
9
lib/types/ApiRequests.ts
Normal file
9
lib/types/ApiRequests.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { Policy } from "@lib/Policies";
|
||||
import { AuthorizedTokenRequest } from "@dunemask/vix/express";
|
||||
import { Project, User } from "@prisma/client";
|
||||
|
||||
export interface UserRequest extends AuthorizedTokenRequest {
|
||||
user: User;
|
||||
policies: Policy[];
|
||||
project: Project;
|
||||
}
|
5
lib/types/ContractTypes.ts
Normal file
5
lib/types/ContractTypes.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export type { CAuthContract } from "./contracts/auth.contracts";
|
||||
export type { CDatabaseContract } from "./contracts/database.contracts";
|
||||
export type { CUserContract } from "./contracts/user.contracts";
|
||||
export type { CProjectContract } from "./contracts/project.contracts";
|
||||
export type { CKeyPairContract } from "./contracts/keypair.contracts";
|
40
lib/types/contracts/auth.contracts.ts
Normal file
40
lib/types/contracts/auth.contracts.ts
Normal file
|
@ -0,0 +1,40 @@
|
|||
import { ContractTypeDefinitions, defineContractExport, defineContracts } from "@dunemask/vix/util";
|
||||
import * as y from "yup";
|
||||
import { DatabaseContractRes } from "./database.contracts";
|
||||
|
||||
// ======================================= Re-used Contracts -======================================
|
||||
|
||||
const Credentials = y.object({
|
||||
user: DatabaseContractRes.User.required(),
|
||||
policies: y.array(y.string()).required(),
|
||||
});
|
||||
|
||||
// ====================================== Responses Contracts ======================================
|
||||
|
||||
export const AuthContractRes = defineContractExport("CAuthContractRes", {
|
||||
Credentials,
|
||||
LoginCredentials: y
|
||||
.object({
|
||||
token: y.string().required(),
|
||||
})
|
||||
.concat(Credentials),
|
||||
});
|
||||
|
||||
// ======================================= Request Contracts =======================================
|
||||
|
||||
export const AuthContractReq = defineContractExport("CAuthContractReq", {
|
||||
Login: y.object({
|
||||
identity: y.string().required(),
|
||||
password: y.string().required(),
|
||||
}),
|
||||
});
|
||||
|
||||
// ===================================== Combined Declarations =====================================
|
||||
|
||||
export const AuthContract = defineContractExport("CAuthContract", { ...AuthContractRes, ...AuthContractReq });
|
||||
|
||||
// ======================================= Type Declarations =======================================
|
||||
|
||||
export type CAuthContractRes = ContractTypeDefinitions<typeof AuthContractRes>;
|
||||
export type CAuthContractReq = ContractTypeDefinitions<typeof AuthContractReq>;
|
||||
export type CAuthContract = ContractTypeDefinitions<typeof AuthContract>;
|
35
lib/types/contracts/database.contracts.ts
Normal file
35
lib/types/contracts/database.contracts.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { ContractTypeDefinitions, defineContractExport, defineContracts } from "@dunemask/vix/util";
|
||||
import * as y from "yup";
|
||||
const antiRequired = y.string().test("insecure-exposure", "Insecure Exposure", (value) => !value);
|
||||
// ====================================== Reused Contracts ======================================
|
||||
|
||||
// ====================================== Response Contracts ======================================
|
||||
|
||||
export const DatabaseContractRes = defineContractExport("CDatabaseContractRes", {
|
||||
User: y.object({
|
||||
username: y.string().required(),
|
||||
email: y.string().nullable(),
|
||||
hash: antiRequired,
|
||||
rolePolicyId: y.string().required(),
|
||||
}),
|
||||
Project: y.object({
|
||||
id: y.string().required(),
|
||||
slug: y.string().required(),
|
||||
parentProject: y.string().required(),
|
||||
name: y.string().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
// ====================================== Request Contracts ======================================
|
||||
|
||||
export const DatabaseContractReq = defineContractExport("CDatabaseContractReq", {});
|
||||
|
||||
// ====================================== Combined Declarations ======================================
|
||||
|
||||
export const DatabaseContract = defineContractExport("CDatabaseContract", { ...DatabaseContractRes, ...DatabaseContractReq });
|
||||
|
||||
// ====================================== Type Declarations ======================================
|
||||
|
||||
export type CDatabaseContractRes = ContractTypeDefinitions<typeof DatabaseContractRes>;
|
||||
export type CDatabaseContractReq = ContractTypeDefinitions<typeof DatabaseContractReq>;
|
||||
export type CDatabaseContract = ContractTypeDefinitions<typeof DatabaseContract>;
|
31
lib/types/contracts/keypair.contracts.ts
Normal file
31
lib/types/contracts/keypair.contracts.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { ContractTypeDefinitions, defineContractExport, defineContracts } from "@dunemask/vix/util";
|
||||
import { KeyPairType } from "@prisma/client";
|
||||
import * as y from "yup";
|
||||
const antiRequired = y.string().test("insecure-exposure", "Insecure Exposure", (value) => !!value);
|
||||
// ====================================== Reused Contracts ======================================
|
||||
|
||||
// ====================================== Response Contracts ======================================
|
||||
|
||||
export const KeyPairContractRes = defineContractExport("CKeyPairContractRes", {});
|
||||
|
||||
// ====================================== Request Contracts ======================================
|
||||
|
||||
export const KeyPairContractReq = defineContractExport("CKeyPairContractReq", {
|
||||
Create: y.object({
|
||||
projectId: y.string().required(),
|
||||
usage: y.string().oneOf(Object.values(KeyPairType)).required(),
|
||||
encryptedPublicKey: y.string().required(),
|
||||
encryptedPrivateKey: y.string().required(),
|
||||
name: y.string().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
// ====================================== Combined Declarations ======================================
|
||||
|
||||
export const KeyPairContract = defineContractExport("CKeyPairContract", { ...KeyPairContractRes, ...KeyPairContractReq });
|
||||
|
||||
// ====================================== Type Declarations ======================================
|
||||
|
||||
export type CKeyPairContractRes = ContractTypeDefinitions<typeof KeyPairContractRes>;
|
||||
export type CKeyPairContractReq = ContractTypeDefinitions<typeof KeyPairContractReq>;
|
||||
export type CKeyPairContract = ContractTypeDefinitions<typeof KeyPairContract>;
|
36
lib/types/contracts/project.contracts.ts
Normal file
36
lib/types/contracts/project.contracts.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import { ContractTypeDefinitions, defineContractExport, defineContracts } from "@dunemask/vix/util";
|
||||
import * as y from "yup";
|
||||
import { DatabaseContract } from "./database.contracts";
|
||||
// ====================================== Reused Contracts ======================================
|
||||
|
||||
// ====================================== Response Contracts ======================================
|
||||
|
||||
export const ProjectContractRes = defineContractExport("CProjectContractRes", {
|
||||
CreateResponse: y.object({
|
||||
project: DatabaseContract.Project,
|
||||
user: DatabaseContract.User,
|
||||
publicKey: y.string().required(),
|
||||
}),
|
||||
});
|
||||
|
||||
// ====================================== Request Contracts ======================================
|
||||
|
||||
export const ProjectContractReq = defineContractExport("CProjectContractReq", {
|
||||
ProjectParams: y.object({
|
||||
project: y.string().required(),
|
||||
}),
|
||||
Create: y.object({
|
||||
slug: y.string().required(),
|
||||
name: y.string().nullable(),
|
||||
}),
|
||||
});
|
||||
|
||||
// ====================================== Combined Declarations ======================================
|
||||
|
||||
export const ProjectContract = defineContractExport("CProjectContract", { ...ProjectContractRes, ...ProjectContractReq });
|
||||
|
||||
// ====================================== Type Declarations ======================================
|
||||
|
||||
export type CProjectContractRes = ContractTypeDefinitions<typeof ProjectContractRes>;
|
||||
export type CProjectContractReq = ContractTypeDefinitions<typeof ProjectContractReq>;
|
||||
export type CProjectContract = ContractTypeDefinitions<typeof ProjectContract>;
|
34
lib/types/contracts/role-policy.contracts.ts
Normal file
34
lib/types/contracts/role-policy.contracts.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { ContractTypeDefinitions, defineContractExport, defineContracts } from "@dunemask/vix/util";
|
||||
import { AuthorityType } from "@prisma/client";
|
||||
import * as y from "yup";
|
||||
const antiRequired = y.string().test("insecure-exposure", "Insecure Exposure", (value) => !value);
|
||||
// ====================================== Reused Contracts ======================================
|
||||
|
||||
// ====================================== Response Contracts ======================================
|
||||
|
||||
export const RolePolicyContractRes = defineContractExport("CRolePolicyContractRes", {});
|
||||
|
||||
// ====================================== Request Contracts ======================================
|
||||
|
||||
export const RolePolicyContractReq = defineContractExport("CRolePolicyContractReq", {
|
||||
Create: y.object({
|
||||
authority: y.string().required(),
|
||||
authorityType: y.string().oneOf(Object.values(AuthorityType)),
|
||||
projectId: y.string().required(),
|
||||
name: y.string().required(),
|
||||
policies: y.array(y.string().required()).required(),
|
||||
}),
|
||||
});
|
||||
|
||||
// ====================================== Combined Declarations ======================================
|
||||
|
||||
export const RolePolicyContract = defineContractExport("CRolePolicyContract", {
|
||||
...RolePolicyContractRes,
|
||||
...RolePolicyContractReq,
|
||||
});
|
||||
|
||||
// ====================================== Type Declarations ======================================
|
||||
|
||||
export type CRolePolicyContractRes = ContractTypeDefinitions<typeof RolePolicyContractRes>;
|
||||
export type CRolePolicyContractReq = ContractTypeDefinitions<typeof RolePolicyContractReq>;
|
||||
export type CRolePolicyContract = ContractTypeDefinitions<typeof RolePolicyContract>;
|
29
lib/types/contracts/user.contracts.ts
Normal file
29
lib/types/contracts/user.contracts.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { ContractTypeDefinitions, defineContractExport } from "@dunemask/vix/util";
|
||||
import * as y from "yup";
|
||||
// ====================================== Reused Contracts ======================================
|
||||
|
||||
// ====================================== Response Contracts ======================================
|
||||
|
||||
export const UserContractRes = defineContractExport("CUserContractRes", {});
|
||||
|
||||
// ====================================== Request Contracts ======================================
|
||||
|
||||
export const UserContractReq = defineContractExport("CUserContractReq", {
|
||||
Create: y.object({
|
||||
projectId: y.string().required(),
|
||||
username: y.string().required(),
|
||||
email: y.string(),
|
||||
hash: y.string().required(),
|
||||
rolePolicyId: y.string().required(),
|
||||
}),
|
||||
});
|
||||
|
||||
// ====================================== Combined Declarations ======================================
|
||||
|
||||
export const UserContract = defineContractExport("CUserContract", { ...UserContractRes, ...UserContractReq });
|
||||
|
||||
// ====================================== Type Declarations ======================================
|
||||
|
||||
export type CUserContractRes = ContractTypeDefinitions<typeof UserContractRes>;
|
||||
export type CUserContractReq = ContractTypeDefinitions<typeof UserContractReq>;
|
||||
export type CUserContract = ContractTypeDefinitions<typeof UserContract>;
|
16
lib/util/mailing.ts
Normal file
16
lib/util/mailing.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import sgMail from "@sendgrid/mail";
|
||||
|
||||
const { CAIRO_BOT_EMAIL: botEmail, CAIRO_SENDGRID_KEY: sendgridApiKey } = process.env;
|
||||
|
||||
// Configure API Key
|
||||
sgMail.setApiKey(sendgridApiKey ?? "");
|
||||
if (!botEmail && !!sendgridApiKey) throw Error("Bot Email wasn't defined but API key was!");
|
||||
|
||||
const from = botEmail ?? "donotreply@dunemask.dev";
|
||||
const ignoreMessage = `If you did not sign up for a cairo account, please ignore this email!`;
|
||||
|
||||
export const sendMessage = (to: string, subject: string, text: string) =>
|
||||
sgMail.send({ from, to, subject, text: text + `\n${ignoreMessage}` });
|
||||
|
||||
export const sendHtml = (to: string, subject: string, html: string) =>
|
||||
sgMail.send({ from, to, subject, html: html + `<p>${ignoreMessage}</p>` });
|
15
lib/vix/AppGuards.ts
Normal file
15
lib/vix/AppGuards.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { IAMResource, ManagementResource } from "./AppResources";
|
||||
import { Policy, PolicyType } from "./AppPolicies";
|
||||
|
||||
function appPolicies(...userPolicies: PolicyType[] | PolicyType[][]) {
|
||||
const policies = userPolicies.length === 1 && Array.isArray(userPolicies[0]) ? userPolicies[0] : (userPolicies as PolicyType[]);
|
||||
const requiredPolicies = Policy.parseResourcePolicies(policies);
|
||||
return requiredPolicies;
|
||||
}
|
||||
|
||||
export default class AppGuard {
|
||||
static IAMRoot = appPolicies(`${IAMResource.Root}.root`);
|
||||
static IAMAuthenticated = appPolicies(Object.values(IAMResource).map((iam) => `${iam}.root`) as PolicyType[]);
|
||||
static ManageProjects = appPolicies(`${ManagementResource.ManageProject}.*`);
|
||||
static CreateProjects = appPolicies(`${ManagementResource.ManageProject}.create`);
|
||||
}
|
26
lib/vix/AppPolicies.ts
Normal file
26
lib/vix/AppPolicies.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { ResourcePolicy, ResourcePolicyComputeType, ResourcePolicyString, ResourcePolicyType } from "@dunemask/vix/util";
|
||||
import { IAMResource, ManagementResource, Resource } from "./AppResources";
|
||||
|
||||
export const Policy = ResourcePolicy<Resource>;
|
||||
export declare type Policy = ResourcePolicy<Resource>;
|
||||
export declare type PolicyType = ResourcePolicyType<Resource>;
|
||||
export declare type PolicyComputeType = ResourcePolicyComputeType<Resource>;
|
||||
export declare type PolicyString = ResourcePolicyString<Resource>;
|
||||
export declare type PolicyDefault = { id: string; name: string; policies: Policy[] };
|
||||
|
||||
export default class DefaultRolePolicies {
|
||||
static Root = $unsafeGetRootPolicy();
|
||||
static Admin = Policy.multiple<Resource>(
|
||||
`${IAMResource.Admin}.root`,
|
||||
`${ManagementResource.ManageProject}.root`,
|
||||
`${ManagementResource.ManageUser}.root`,
|
||||
);
|
||||
|
||||
static User = Policy.multiple<Resource>(`${IAMResource.User}.root`);
|
||||
}
|
||||
|
||||
function $unsafeGetRootPolicy(): Policy[] {
|
||||
const policies: PolicyString[] = [];
|
||||
for (const resource of Object.values(Resource)) policies.push(`${resource}.root`);
|
||||
return Policy.multiple<Resource>(...policies) as Policy[];
|
||||
}
|
20
lib/vix/AppResources.ts
Normal file
20
lib/vix/AppResources.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
export enum IAMResource {
|
||||
Root = "root",
|
||||
Admin = "admin",
|
||||
User = "user",
|
||||
CairoProjectRoot = "cairo-project-root",
|
||||
}
|
||||
|
||||
export enum ManagementResource {
|
||||
ManageAdmin = "manage-admin",
|
||||
ManageUser = "manage-user",
|
||||
ManageProject = "manage-project",
|
||||
}
|
||||
|
||||
export enum OtherResource {
|
||||
Random = "Random",
|
||||
}
|
||||
|
||||
type ResourceEnums<T extends Record<string, string>> = T[keyof T];
|
||||
export const Resource = { ...IAMResource, ...ManagementResource, ...OtherResource };
|
||||
export type Resource = ResourceEnums<typeof Resource>;
|
15
lib/vix/AppRouter.ts
Normal file
15
lib/vix/AppRouter.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import "express-async-errors";
|
||||
import config from "@lib/config";
|
||||
import { VixpressRouter } from "@dunemask/vix";
|
||||
import { AuthRoute } from "@lib/modules/auth/auth.router";
|
||||
import { ProjectRoute } from "@lib/modules/projects/project.router";
|
||||
|
||||
export default class AppRouter extends VixpressRouter {
|
||||
protected routerImportUrl = import.meta.url; // Used to configure the relative static route
|
||||
protected baseUrl = config.Server.basePath; // Path for static assets
|
||||
async configureRoutes() {
|
||||
// API Routes go here:
|
||||
await this.useRoute("/api/:project/auth", AuthRoute);
|
||||
await this.useRoute("/api/:project", ProjectRoute);
|
||||
}
|
||||
}
|
25
lib/vix/ClientErrors.ts
Normal file
25
lib/vix/ClientErrors.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { ClientError } from "@dunemask/vix/bridge";
|
||||
|
||||
export class AuthErrors {
|
||||
static readonly UnauthorizedRequiredProject = new ClientError(401, "Project required!");
|
||||
static readonly UnauthorizedRequiredToken = new ClientError(401, "Token required!");
|
||||
static readonly UnauthorizedRequiredUser = new ClientError(401, "User not set!");
|
||||
static readonly UnauthorizedRole = new ClientError(403, "Insufficient Privileges");
|
||||
static readonly UnauthorizedRequest = new ClientError(401, "Unauthorized!");
|
||||
static readonly ForbiddenPermissions = new ClientError(403, "Insufficient privileges!");
|
||||
}
|
||||
|
||||
export class UserErrors {
|
||||
static readonly ConflictIdentityTaken = new ClientError(409, "Identity taken!");
|
||||
}
|
||||
|
||||
export class ProjectErrors {
|
||||
static readonly BadRequestSlugInvalid = new ClientError(400, "Project slug invalid!");
|
||||
static readonly BadRequestProjectIncomplete = new ClientError(400, "Project incomplete!");
|
||||
static readonly UnexpectedRootUserError = new ClientError(500, "Error creating root user!");
|
||||
static readonly ConflictNonUnique = new ClientError(409, "Slug already taken!");
|
||||
}
|
||||
|
||||
export class KeyPairErrors {
|
||||
static readonly NotFoundKeypair = new ClientError(400, "Keypair not found!");
|
||||
}
|
9
lib/vix/RouteGuard.ts
Normal file
9
lib/vix/RouteGuard.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import policyMiddlewareGuard from "@lib/middlewares/policy-guard";
|
||||
import userGuard from "@lib/middlewares/user-guard";
|
||||
import AppGuard from "./AppGuards";
|
||||
|
||||
export default class RouteGuard {
|
||||
static User = userGuard();
|
||||
static ManageProjectsRead = policyMiddlewareGuard(AppGuard.ManageProjects);
|
||||
static MangeProjectsCreate = policyMiddlewareGuard(AppGuard.CreateProjects);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue