CREDENTIAL TYPE MANAGEMENT WORKS
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { credentialTypes } from "../../managers/credentialTypes";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
import { z } from "zod";
|
||||
|
||||
/* /v1/credential-type */
|
||||
export default createRoute(
|
||||
"post",
|
||||
["/"],
|
||||
|
||||
async (c) => {
|
||||
const body = await c.req.json();
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
permissionScope: z.string().min(1, "Permission scope is required"),
|
||||
icon: z.string().optional(),
|
||||
fields: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
required: z.boolean(),
|
||||
secure: z.boolean(),
|
||||
valueType: z.enum(["plain_text", "password"]),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const data = schema.parse(body);
|
||||
|
||||
const credentialType = await credentialTypes.create(data as any);
|
||||
|
||||
const response = apiResponse.created(
|
||||
"Credential Type Created Successfully!",
|
||||
credentialType.toJson(),
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["credential_type.create"] }),
|
||||
);
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { credentialTypes } from "../../managers/credentialTypes";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
|
||||
/* /v1/credential-type/:id */
|
||||
export default createRoute(
|
||||
"delete",
|
||||
["/:id"],
|
||||
|
||||
async (c) => {
|
||||
await credentialTypes.delete(c.req.param("id"));
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Credential Type Deleted Successfully!",
|
||||
null,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["credential_type.delete"] }),
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { credentialTypes } from "../../managers/credentialTypes";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
|
||||
/* /v1/credential-type/:identifier */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/:identifier"],
|
||||
|
||||
async (c) => {
|
||||
const credentialType = await credentialTypes.fetch(
|
||||
c.req.param("identifier"),
|
||||
);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Credential Type Fetched Successfully!",
|
||||
credentialType.toJson({ includeCredentialCount: true }),
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["credential_type.fetch"] }),
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { credentialTypes } from "../../managers/credentialTypes";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
|
||||
/* /v1/credential-type */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/"],
|
||||
|
||||
async (c) => {
|
||||
const allCredentialTypes = await credentialTypes.fetchAll();
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Credential Types Fetched Successfully!",
|
||||
allCredentialTypes.map((ct) =>
|
||||
ct.toJson({ includeCredentialCount: true }),
|
||||
),
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["credential_type.fetch.many"] }),
|
||||
);
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { credentialTypes } from "../../managers/credentialTypes";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
|
||||
/* /v1/credential-type/:id/credentials */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/:id/credentials"],
|
||||
|
||||
async (c) => {
|
||||
const credentialType = await credentialTypes.fetch(c.req.param("id"));
|
||||
const credentials = await credentialType.fetchCredentials();
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Credentials Fetched Successfully!",
|
||||
credentials.map((cred) => cred.toJson()),
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({
|
||||
permissions: ["credential_type.fetch", "credential.fetch.many"],
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,15 @@
|
||||
import { default as fetch } from "./fetch";
|
||||
import { default as fetchAll } from "./fetchAll";
|
||||
import { default as create } from "./create";
|
||||
import { default as update } from "./update";
|
||||
import { default as deleteCredentialType } from "./delete";
|
||||
import { default as fetchCredentials } from "./fetchCredentials";
|
||||
|
||||
export {
|
||||
fetch,
|
||||
fetchAll,
|
||||
create,
|
||||
update,
|
||||
deleteCredentialType as delete,
|
||||
fetchCredentials,
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { credentialTypes } from "../../managers/credentialTypes";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
import { z } from "zod";
|
||||
|
||||
/* /v1/credential-type/:id */
|
||||
export default createRoute(
|
||||
"patch",
|
||||
["/:id"],
|
||||
|
||||
async (c) => {
|
||||
const body = await c.req.json();
|
||||
const credentialType = await credentialTypes.fetch(c.req.param("id"));
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().optional(),
|
||||
permissionScope: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
fields: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
required: z.boolean(),
|
||||
secure: z.boolean(),
|
||||
valueType: z.enum(["plain_text", "password"]),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const data = schema.parse(body);
|
||||
|
||||
await credentialType.update(data as any);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Credential Type Updated Successfully!",
|
||||
credentialType.toJson(),
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["credential_type.update"] }),
|
||||
);
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { credentials } from "../../managers/credentials";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
import { z } from "zod";
|
||||
|
||||
/* /v1/credential/create */
|
||||
export default createRoute(
|
||||
"post",
|
||||
["/credentials"],
|
||||
|
||||
async (c) => {
|
||||
const body = await c.req.json();
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().min(1, "Name is required"),
|
||||
typeId: z.string().min(1, "Type ID is required"),
|
||||
companyId: z.string().min(1, "Company ID is required"),
|
||||
fields: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
fieldId: z.string(),
|
||||
value: z.string(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const data = schema.parse(body);
|
||||
|
||||
const credential = await credentials.create(data);
|
||||
|
||||
const response = apiResponse.created(
|
||||
"Credential Created Successfully!",
|
||||
credential.toJson(),
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["credential.create"] }),
|
||||
);
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { credentials } from "../../managers/credentials";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
|
||||
/* /v1/credential/:id */
|
||||
export default createRoute(
|
||||
"delete",
|
||||
["/credentials/:id"],
|
||||
|
||||
async (c) => {
|
||||
await credentials.delete(c.req.param("id"));
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Credential Deleted Successfully!",
|
||||
null,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["credential.delete"] }),
|
||||
);
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { credentials } from "../../managers/credentials";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
|
||||
/* /v1/credential/:id */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/credentials/:id"],
|
||||
|
||||
async (c) => {
|
||||
const credential = await credentials.fetch(c.req.param("id"));
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Credential Fetched Successfully!",
|
||||
credential.toJson(),
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["credential.fetch"] }),
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { credentials } from "../../managers/credentials";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
|
||||
/* /v1/credential/company/:companyId */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/credentials/company/:companyId"],
|
||||
|
||||
async (c) => {
|
||||
const companyCredentials = await credentials.fetchByCompany(
|
||||
c.req.param("companyId"),
|
||||
);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Company Credentials Fetched Successfully!",
|
||||
companyCredentials.map((cred) => cred.toJson()),
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["credential.fetch.many"] }),
|
||||
);
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { credentials } from "../../managers/credentials";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
|
||||
/* /v1/credential/:id/fields */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/credentials/:id/fields"],
|
||||
|
||||
async (c) => {
|
||||
const credential = await credentials.fetch(c.req.param("id"));
|
||||
const fields = await credential.fetchAllFieldValues();
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Credential Fields Fetched Successfully!",
|
||||
fields,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({
|
||||
permissions: ["credential.fetch", "credential.fields.fetch"],
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,19 @@
|
||||
import { default as fetch } from "./fetch";
|
||||
import { default as fetchByCompany } from "./fetchByCompany";
|
||||
import { default as create } from "./create";
|
||||
import { default as update } from "./update";
|
||||
import { default as updateFields } from "./updateFields";
|
||||
import { default as fetchFields } from "./fetchFields";
|
||||
import { default as readSecureValues } from "./readSecureValues";
|
||||
import { default as deleteCredential } from "./delete";
|
||||
|
||||
export {
|
||||
fetch,
|
||||
fetchByCompany,
|
||||
create,
|
||||
update,
|
||||
updateFields,
|
||||
fetchFields,
|
||||
readSecureValues,
|
||||
deleteCredential as delete,
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { credentials } from "../../managers/credentials";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
|
||||
/* /v1/credential/:id/secure-values */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/credentials/:id/secure-values"],
|
||||
|
||||
async (c) => {
|
||||
const credential = await credentials.fetch(c.req.param("id"));
|
||||
const secureValues = await credential.readAllSecureValues();
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Secure Values Fetched Successfully!",
|
||||
secureValues,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({
|
||||
permissions: ["credential.fetch", "credential.secure_values.read"],
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { credentials } from "../../managers/credentials";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
import { z } from "zod";
|
||||
|
||||
/* /v1/credential/:id */
|
||||
export default createRoute(
|
||||
"patch",
|
||||
["/credentials/:id"],
|
||||
|
||||
async (c) => {
|
||||
const body = await c.req.json();
|
||||
const credential = await credentials.fetch(c.req.param("id"));
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().optional(),
|
||||
});
|
||||
|
||||
const data = schema.parse(body);
|
||||
|
||||
await credential.update(data);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Credential Updated Successfully!",
|
||||
credential.toJson(),
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["credential.update"] }),
|
||||
);
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { credentials } from "../../managers/credentials";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
import { z } from "zod";
|
||||
|
||||
/* /v1/credential/:id/fields */
|
||||
export default createRoute(
|
||||
"put",
|
||||
["/credentials/:id/fields"],
|
||||
|
||||
async (c) => {
|
||||
const body = await c.req.json();
|
||||
const credential = await credentials.fetch(c.req.param("id"));
|
||||
|
||||
const schema = z.object({
|
||||
fields: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
fieldId: z.string(),
|
||||
value: z.string(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const data = schema.parse(body);
|
||||
|
||||
await credential.validateAndUpdateFields(data.fields);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Credential Fields Updated Successfully!",
|
||||
credential.toJson(),
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({
|
||||
permissions: ["credential.update", "credential.fields.update"],
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Hono } from "hono";
|
||||
import * as credentialRoutes from "../credentials";
|
||||
|
||||
const credentialRouter = new Hono();
|
||||
Object.values(credentialRoutes).map((r) => credentialRouter.route("/", r));
|
||||
|
||||
export default credentialRouter;
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Hono } from "hono";
|
||||
import * as credentialTypeRoutes from "../credential-types";
|
||||
|
||||
const credentialTypeRouter = new Hono();
|
||||
Object.values(credentialTypeRoutes).map((r) =>
|
||||
credentialTypeRouter.route("/", r),
|
||||
);
|
||||
|
||||
export default credentialTypeRouter;
|
||||
@@ -50,6 +50,8 @@ v1.route("/teapot", teapot);
|
||||
v1.route("/auth", require("./routers/authRouter").default);
|
||||
v1.route("/user", require("./routers/user").default);
|
||||
v1.route("/company", require("./routers/companyRouter").default);
|
||||
v1.route("/credential", require("./routers/credentialRouter").default);
|
||||
v1.route("/credential-type", require("./routers/credentialTypeRouter").default);
|
||||
app.route("/v1", v1);
|
||||
|
||||
export default app;
|
||||
|
||||
+9
-11
@@ -23,18 +23,16 @@ export const sessionDuration = 30 * 24 * 60 * 60000;
|
||||
export const accessTokenDuration = "10min";
|
||||
export const refreshTokenDuration = "30d";
|
||||
|
||||
export const accessTokenPrivateKey = readFileSync(
|
||||
`${import.meta.dir}/../.accessToken.key`,
|
||||
export const accessTokenPrivateKey =
|
||||
readFileSync(`.accessToken.key`).toString();
|
||||
export const refreshTokenPrivateKey =
|
||||
readFileSync(`.refreshToken.key`).toString();
|
||||
export const permissionsPrivateKey = readFileSync(`.permissions.key`);
|
||||
export const secureValuesPrivateKey =
|
||||
readFileSync(`.secureValues.key`).toString();
|
||||
export const secureValuesPublicKey = readFileSync(
|
||||
`public-keys/.secureValues.pub`,
|
||||
).toString();
|
||||
export const refreshTokenPrivateKey = readFileSync(
|
||||
`${import.meta.dir}/../.refreshToken.key`,
|
||||
).toString();
|
||||
export const permissionsPrivateKey = readFileSync(
|
||||
`${import.meta.dir}/../.permissions.key`,
|
||||
);
|
||||
export const apiKeyTokenPrivateKey = readFileSync(
|
||||
`${import.meta.dir}/../.apiKeyToken.key`,
|
||||
);
|
||||
|
||||
// Microsoft Auth Constants
|
||||
const msalConfig: msal.Configuration = {
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
import { z } from "zod";
|
||||
import {
|
||||
Credential,
|
||||
CredentialType,
|
||||
Company,
|
||||
SecureValue,
|
||||
} from "../../generated/prisma/client";
|
||||
import { prisma } from "../constants";
|
||||
import { fieldValidator } from "../modules/credentials/fieldValidator";
|
||||
import {
|
||||
CredentialField,
|
||||
CredentialTypeField,
|
||||
} from "../modules/credentials/credentialTypeDefs";
|
||||
import { generateSecureValue } from "../modules/credentials/generateSecureValue";
|
||||
import { readSecureValue } from "../modules/credentials/readSecureValue";
|
||||
import GenericError from "../Errors/GenericError";
|
||||
|
||||
/**
|
||||
* Credential Controller
|
||||
*
|
||||
* This class manages credential data, including field validation,
|
||||
* secure value storage/retrieval, and credential updates.
|
||||
*/
|
||||
export class CredentialController {
|
||||
public readonly id: string;
|
||||
public name: string;
|
||||
public readonly typeId: string;
|
||||
public readonly companyId: string;
|
||||
public fields: any;
|
||||
|
||||
private _type: CredentialType;
|
||||
private _company: Company;
|
||||
private _secureValues: SecureValue[];
|
||||
|
||||
public readonly createdAt: Date;
|
||||
public updatedAt: Date;
|
||||
|
||||
constructor(
|
||||
credentialData: Credential & {
|
||||
type: CredentialType;
|
||||
company: Company;
|
||||
securevalues: SecureValue[];
|
||||
},
|
||||
) {
|
||||
this.id = credentialData.id;
|
||||
this.name = credentialData.name;
|
||||
this.typeId = credentialData.typeId;
|
||||
this.companyId = credentialData.companyId;
|
||||
this.fields = credentialData.fields;
|
||||
this._type = credentialData.type;
|
||||
this._company = credentialData.company;
|
||||
this._secureValues = credentialData.securevalues;
|
||||
this.createdAt = credentialData.createdAt;
|
||||
this.updatedAt = credentialData.updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Internal Values
|
||||
*
|
||||
* Internal method to update all internal values when we query the database.
|
||||
* This keeps everything up-to-date even when we pass around the credential controller.
|
||||
*
|
||||
* @param credentialData - Credential object from Prisma
|
||||
*/
|
||||
private _updateInternalValues(credentialData: Credential) {
|
||||
this.name = credentialData.name;
|
||||
this.fields = credentialData.fields;
|
||||
this.updatedAt = credentialData.updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and Update Fields
|
||||
*
|
||||
* This method validates the submitted fields against the credential type's
|
||||
* acceptable fields, then updates the credential in the database.
|
||||
* Secure fields are encrypted and stored in the SecureValue table.
|
||||
*
|
||||
* @param fields - The fields to validate and update
|
||||
* @returns {Promise<CredentialController>} - The updated credential controller
|
||||
*/
|
||||
async validateAndUpdateFields(
|
||||
fields: CredentialField[],
|
||||
): Promise<CredentialController> {
|
||||
// Get acceptable fields from the credential type
|
||||
const acceptableFields = this._type.fields as any as CredentialTypeField[];
|
||||
// Validate the fields
|
||||
const validatedFields = await fieldValidator(fields, acceptableFields);
|
||||
|
||||
// Separate secure and non-secure fields
|
||||
const secureFields = validatedFields.filter((f) => f.secure);
|
||||
const nonSecureFields = validatedFields.filter((f) => !f.secure);
|
||||
|
||||
// Process secure fields - encrypt and store in SecureValue table
|
||||
await Promise.all(
|
||||
secureFields.map(async (field) => {
|
||||
const { encrypted, hash } = generateSecureValue(field.value);
|
||||
|
||||
// Check if a secure value already exists for this field
|
||||
const existingSecureValue = await prisma.secureValue.findFirst({
|
||||
where: {
|
||||
credentialId: this.id,
|
||||
name: field.fieldId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingSecureValue) {
|
||||
// Update existing secure value
|
||||
await prisma.secureValue.update({
|
||||
where: { id: existingSecureValue.id },
|
||||
data: {
|
||||
content: encrypted,
|
||||
hash,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Create new secure value
|
||||
await prisma.secureValue.create({
|
||||
data: {
|
||||
name: field.fieldId,
|
||||
content: encrypted,
|
||||
hash,
|
||||
credentialId: this.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
// Build fields object for non-secure fields
|
||||
const fieldsObject: Record<string, any> = {};
|
||||
nonSecureFields.forEach((field) => {
|
||||
fieldsObject[field.fieldId] = field.value;
|
||||
});
|
||||
|
||||
// Update the credential with non-secure fields
|
||||
const updatedCredential = await prisma.credential.update({
|
||||
where: { id: this.id },
|
||||
data: {
|
||||
fields: fieldsObject,
|
||||
},
|
||||
});
|
||||
|
||||
this._updateInternalValues(updatedCredential);
|
||||
|
||||
// Refresh secure values
|
||||
const secureValues = await prisma.secureValue.findMany({
|
||||
where: { credentialId: this.id },
|
||||
});
|
||||
this._secureValues = secureValues;
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch All Field Values
|
||||
*
|
||||
* Returns all field values (both secure and non-secure) for this credential.
|
||||
* Secure field values are NOT decrypted - use `readSecureFieldValue` for that.
|
||||
*
|
||||
* @returns {Promise<CredentialField[]>} - Array of all fields with their encrypted values
|
||||
*/
|
||||
async fetchAllFieldValues(): Promise<CredentialField[]> {
|
||||
const fields: CredentialField[] = [];
|
||||
|
||||
// Add non-secure fields from the fields JSON
|
||||
const nonSecureFields = this.fields as Record<string, any>;
|
||||
Object.entries(nonSecureFields || {}).forEach(([fieldId, value]) => {
|
||||
fields.push({
|
||||
id: `${this.id}-${fieldId}`, // Generate a consistent ID
|
||||
fieldId,
|
||||
value: value as string,
|
||||
});
|
||||
});
|
||||
|
||||
// Add secure fields from SecureValue table (encrypted)
|
||||
this._secureValues.forEach((secureValue) => {
|
||||
fields.push({
|
||||
id: secureValue.id,
|
||||
fieldId: secureValue.name,
|
||||
value: secureValue.content, // Encrypted value
|
||||
});
|
||||
});
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read Secure Field Value
|
||||
*
|
||||
* Decrypts and returns the value of a specific secure field.
|
||||
*
|
||||
* @param fieldId - The field ID to read
|
||||
* @returns {Promise<string>} - The decrypted field value
|
||||
*/
|
||||
async readSecureFieldValue(fieldId: string): Promise<string> {
|
||||
const secureValue = this._secureValues.find((sv) => sv.name === fieldId);
|
||||
|
||||
if (!secureValue) {
|
||||
throw new GenericError({
|
||||
message: `Secure field not found: ${fieldId}`,
|
||||
name: "SecureFieldNotFound",
|
||||
cause: `No secure value exists for field '${fieldId}' in credential '${this.id}'`,
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
// Decrypt the value
|
||||
const decryptedValue = readSecureValue(
|
||||
secureValue.content,
|
||||
secureValue.hash,
|
||||
);
|
||||
|
||||
return decryptedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read All Secure Values
|
||||
*
|
||||
* Decrypts and returns all secure field values for this credential.
|
||||
*
|
||||
* @returns {Promise<Record<string, string>>} - Object mapping field IDs to decrypted values
|
||||
*/
|
||||
async readAllSecureValues(): Promise<Record<string, string>> {
|
||||
const secureValues: Record<string, string> = {};
|
||||
|
||||
await Promise.all(
|
||||
this._secureValues.map(async (secureValue) => {
|
||||
const decryptedValue = readSecureValue(
|
||||
secureValue.content,
|
||||
secureValue.hash,
|
||||
);
|
||||
secureValues[secureValue.name] = decryptedValue;
|
||||
}),
|
||||
);
|
||||
|
||||
return secureValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Credential
|
||||
*
|
||||
* Update the credential name or other basic properties.
|
||||
*
|
||||
* @param data - Partial credential data to update
|
||||
* @returns {Promise<CredentialController>} - The updated credential controller
|
||||
*/
|
||||
async update(
|
||||
data: Partial<Pick<Credential, "name">>,
|
||||
): Promise<CredentialController> {
|
||||
const pData = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
})
|
||||
.strict()
|
||||
.parse(data);
|
||||
|
||||
const updatedCredential = await prisma.credential.update({
|
||||
where: { id: this.id },
|
||||
data: pData,
|
||||
});
|
||||
|
||||
this._updateInternalValues(updatedCredential);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Credential Type
|
||||
*
|
||||
* Returns the credential type information.
|
||||
*
|
||||
* @returns {CredentialType} - The credential type
|
||||
*/
|
||||
getType(): CredentialType {
|
||||
return this._type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Company
|
||||
*
|
||||
* Returns the company this credential belongs to.
|
||||
*
|
||||
* @returns {Company} - The company
|
||||
*/
|
||||
getCompany(): Company {
|
||||
return this._company;
|
||||
}
|
||||
|
||||
/**
|
||||
* To JSON
|
||||
*
|
||||
* Create an object that can be safely returned to the user.
|
||||
* Secure values are not included by default.
|
||||
*
|
||||
* @param opts - Options to change the output
|
||||
* @returns - An object that is JSON friendly
|
||||
*/
|
||||
toJson(opts?: { includeSecureValues?: boolean }) {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
typeId: this.typeId,
|
||||
companyId: this.companyId,
|
||||
fields: this.fields,
|
||||
type: {
|
||||
id: this._type.id,
|
||||
name: this._type.name,
|
||||
fields: this._type.fields,
|
||||
permissionScope: this._type.permissionScope,
|
||||
},
|
||||
company: {
|
||||
id: this._company.id,
|
||||
name: this._company.name,
|
||||
},
|
||||
secureFieldIds: opts?.includeSecureValues
|
||||
? this._secureValues.map((sv) => sv.name)
|
||||
: undefined,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
import { z } from "zod";
|
||||
import { CredentialType, Credential } from "../../generated/prisma/client";
|
||||
import { prisma } from "../constants";
|
||||
import { CredentialTypeField } from "../modules/credentials/credentialTypeDefs";
|
||||
import { CredentialController } from "./CredentialController";
|
||||
|
||||
/**
|
||||
* Credential Type Controller
|
||||
*
|
||||
* This class manages credential type data, including field definitions,
|
||||
* permission scopes, and associated credentials.
|
||||
*/
|
||||
export class CredentialTypeController {
|
||||
public readonly id: string;
|
||||
public name: string;
|
||||
public permissionScope: string;
|
||||
public icon: string | null;
|
||||
public fields: CredentialTypeField[];
|
||||
|
||||
private _credentials: Credential[];
|
||||
|
||||
public readonly createdAt: Date;
|
||||
public updatedAt: Date;
|
||||
|
||||
constructor(
|
||||
credentialTypeData: CredentialType & {
|
||||
credentials: Credential[];
|
||||
},
|
||||
) {
|
||||
this.id = credentialTypeData.id;
|
||||
this.name = credentialTypeData.name;
|
||||
this.permissionScope = credentialTypeData.permissionScope;
|
||||
this.icon = credentialTypeData.icon;
|
||||
this.fields = credentialTypeData.fields! as any as CredentialTypeField[];
|
||||
this._credentials = credentialTypeData.credentials;
|
||||
this.createdAt = credentialTypeData.createdAt;
|
||||
this.updatedAt = credentialTypeData.updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Internal Values
|
||||
*
|
||||
* Internal method to update all internal values when we query the database.
|
||||
* This keeps everything up-to-date even when we pass around the credential type controller.
|
||||
*
|
||||
* @param credentialTypeData - CredentialType object from Prisma
|
||||
*/
|
||||
private _updateInternalValues(credentialTypeData: CredentialType) {
|
||||
this.name = credentialTypeData.name;
|
||||
this.permissionScope = credentialTypeData.permissionScope;
|
||||
this.icon = credentialTypeData.icon;
|
||||
this.fields = credentialTypeData.fields! as any as CredentialTypeField[];
|
||||
this.updatedAt = credentialTypeData.updatedAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Credential Type
|
||||
*
|
||||
* Update the credential type's name, permission scope, icon, or fields.
|
||||
*
|
||||
* @param data - Partial credential type data to update
|
||||
* @returns {Promise<CredentialTypeController>} - The updated credential type controller
|
||||
*/
|
||||
async update(
|
||||
data: Partial<
|
||||
Pick<CredentialType, "name" | "permissionScope" | "icon"> & {
|
||||
fields: CredentialTypeField[];
|
||||
}
|
||||
>,
|
||||
): Promise<CredentialTypeController> {
|
||||
const pData = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
permissionScope: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
fields: z.array(z.any()).optional(),
|
||||
})
|
||||
.strict()
|
||||
.parse(data);
|
||||
|
||||
const updatedCredentialType = await prisma.credentialType.update({
|
||||
where: { id: this.id },
|
||||
data: pData,
|
||||
});
|
||||
|
||||
this._updateInternalValues(updatedCredentialType);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Field Definition
|
||||
*
|
||||
* Get the definition for a specific field by its ID.
|
||||
*
|
||||
* @param fieldId - The field ID to look up
|
||||
* @returns {CredentialTypeField | undefined} - The field definition or undefined
|
||||
*/
|
||||
getFieldDefinition(fieldId: string): CredentialTypeField | undefined {
|
||||
return this.fields.find((f) => f.id === fieldId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Required Fields
|
||||
*
|
||||
* Returns all fields that are marked as required.
|
||||
*
|
||||
* @returns {CredentialTypeField[]} - Array of required fields
|
||||
*/
|
||||
getRequiredFields(): CredentialTypeField[] {
|
||||
return this.fields.filter((f) => f.required);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Secure Fields
|
||||
*
|
||||
* Returns all fields that should be stored securely (encrypted).
|
||||
*
|
||||
* @returns {CredentialTypeField[]} - Array of secure fields
|
||||
*/
|
||||
getSecureFields(): CredentialTypeField[] {
|
||||
return this.fields.filter((f) => f.secure);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Credentials
|
||||
*
|
||||
* Fetch all credentials that use this credential type.
|
||||
*
|
||||
* @returns {Promise<CredentialController[]>} - Array of credential controllers
|
||||
*/
|
||||
async fetchCredentials(): Promise<CredentialController[]> {
|
||||
const credentials = await prisma.credential.findMany({
|
||||
where: { typeId: this.id },
|
||||
include: {
|
||||
type: true,
|
||||
company: true,
|
||||
securevalues: true,
|
||||
},
|
||||
});
|
||||
|
||||
return credentials.map((cred) => new CredentialController(cred));
|
||||
}
|
||||
|
||||
/**
|
||||
* Count Credentials
|
||||
*
|
||||
* Count how many credentials use this credential type.
|
||||
*
|
||||
* @returns {number} - Number of credentials using this type
|
||||
*/
|
||||
countCredentials(): number {
|
||||
return this._credentials.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* To JSON
|
||||
*
|
||||
* Create an object that can be safely returned to the user.
|
||||
*
|
||||
* @param opts - Options to change the output
|
||||
* @returns - An object that is JSON friendly
|
||||
*/
|
||||
toJson(opts?: { includeCredentialCount?: boolean }) {
|
||||
return {
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
permissionScope: this.permissionScope,
|
||||
icon: this.icon,
|
||||
fields: this.fields,
|
||||
credentialCount: opts?.includeCredentialCount
|
||||
? this._credentials.length
|
||||
: undefined,
|
||||
createdAt: this.createdAt,
|
||||
updatedAt: this.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { prisma } from "../constants";
|
||||
import { CredentialTypeController } from "../controllers/CredentialTypeController";
|
||||
import { CredentialTypeField } from "../modules/credentials/credentialTypeDefs";
|
||||
import GenericError from "../Errors/GenericError";
|
||||
|
||||
export const credentialTypes = {
|
||||
/**
|
||||
* Fetch Credential Type
|
||||
*
|
||||
* Fetch a credential type by its ID or name and return a CredentialTypeController instance.
|
||||
*
|
||||
* @param identifier - The credential type ID or name to fetch
|
||||
* @returns {Promise<CredentialTypeController>} - The credential type controller
|
||||
*/
|
||||
async fetch(identifier: string): Promise<CredentialTypeController> {
|
||||
const credentialType = await prisma.credentialType.findFirst({
|
||||
where: {
|
||||
OR: [{ id: identifier }, { name: identifier }],
|
||||
},
|
||||
include: {
|
||||
credentials: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!credentialType) {
|
||||
throw new GenericError({
|
||||
message: "Credential type not found",
|
||||
name: "CredentialTypeNotFound",
|
||||
cause: `No credential type exists with identifier '${identifier}'`,
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
return new CredentialTypeController(credentialType);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch All Credential Types
|
||||
*
|
||||
* Fetch all credential types in the system.
|
||||
*
|
||||
* @returns {Promise<CredentialTypeController[]>} - Array of credential type controllers
|
||||
*/
|
||||
async fetchAll(): Promise<CredentialTypeController[]> {
|
||||
const credentialTypesList = await prisma.credentialType.findMany({
|
||||
include: {
|
||||
credentials: true,
|
||||
},
|
||||
});
|
||||
|
||||
return credentialTypesList.map((ct) => new CredentialTypeController(ct));
|
||||
},
|
||||
|
||||
/**
|
||||
* Create Credential Type
|
||||
*
|
||||
* Create a new credential type with its field definitions.
|
||||
*
|
||||
* @param data - The credential type data to create
|
||||
* @returns {Promise<CredentialTypeController>} - The created credential type controller
|
||||
*/
|
||||
async create(data: {
|
||||
name: string;
|
||||
permissionScope: string;
|
||||
fields: CredentialTypeField[];
|
||||
icon?: string;
|
||||
}): Promise<CredentialTypeController> {
|
||||
// Check if a credential type with this name already exists
|
||||
const existing = await prisma.credentialType.findFirst({
|
||||
where: { name: data.name },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
throw new GenericError({
|
||||
message: "Credential type name already exists",
|
||||
name: "CredentialTypeAlreadyExists",
|
||||
cause: `A credential type with name '${data.name}' already exists`,
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
const credentialType = await prisma.credentialType.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
permissionScope: data.permissionScope,
|
||||
fields: data.fields as any,
|
||||
icon: data.icon,
|
||||
},
|
||||
include: {
|
||||
credentials: true,
|
||||
},
|
||||
});
|
||||
|
||||
return new CredentialTypeController(credentialType);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete Credential Type
|
||||
*
|
||||
* Delete a credential type by its ID.
|
||||
* This will cascade delete all credentials of this type.
|
||||
*
|
||||
* @param id - The credential type ID to delete
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async delete(id: string): Promise<void> {
|
||||
await prisma.credentialType.delete({
|
||||
where: { id },
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,158 @@
|
||||
import { prisma } from "../constants";
|
||||
import { CredentialController } from "../controllers/CredentialController";
|
||||
import { fieldValidator } from "../modules/credentials/fieldValidator";
|
||||
import {
|
||||
CredentialField,
|
||||
CredentialTypeField,
|
||||
} from "../modules/credentials/credentialTypeDefs";
|
||||
import { generateSecureValue } from "../modules/credentials/generateSecureValue";
|
||||
import GenericError from "../Errors/GenericError";
|
||||
|
||||
export const credentials = {
|
||||
/**
|
||||
* Fetch Credential
|
||||
*
|
||||
* Fetch a credential by its ID and return a CredentialController instance.
|
||||
*
|
||||
* @param id - The credential ID to fetch
|
||||
* @returns {Promise<CredentialController>} - The credential controller
|
||||
*/
|
||||
async fetch(id: string): Promise<CredentialController> {
|
||||
const credential = await prisma.credential.findFirst({
|
||||
where: { id },
|
||||
include: {
|
||||
type: true,
|
||||
company: true,
|
||||
securevalues: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!credential) {
|
||||
throw new GenericError({
|
||||
message: "Credential not found",
|
||||
name: "CredentialNotFound",
|
||||
cause: `No credential exists with ID '${id}'`,
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
return new CredentialController(credential);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch Credentials by Company
|
||||
*
|
||||
* Fetch all credentials associated with a specific company.
|
||||
*
|
||||
* @param companyId - The company ID to fetch credentials for
|
||||
* @returns {Promise<CredentialController[]>} - Array of credential controllers
|
||||
*/
|
||||
async fetchByCompany(companyId: string): Promise<CredentialController[]> {
|
||||
const credentialsList = await prisma.credential.findMany({
|
||||
where: { companyId },
|
||||
include: {
|
||||
type: true,
|
||||
company: true,
|
||||
securevalues: true,
|
||||
},
|
||||
});
|
||||
|
||||
return credentialsList.map((cred) => new CredentialController(cred));
|
||||
},
|
||||
|
||||
/**
|
||||
* Create Credential
|
||||
*
|
||||
* Create a new credential with validated fields.
|
||||
* This method processes all incoming field values, validating them against
|
||||
* the credential type, encrypting secure fields, and inserting everything
|
||||
* into the database atomically.
|
||||
*
|
||||
* @param data - The credential data to create
|
||||
* @returns {Promise<CredentialController>} - The created credential controller
|
||||
*/
|
||||
async create(data: {
|
||||
name: string;
|
||||
typeId: string;
|
||||
companyId: string;
|
||||
fields: CredentialField[];
|
||||
}): Promise<CredentialController> {
|
||||
// Fetch the credential type to get acceptable fields
|
||||
const credentialType = await prisma.credentialType.findFirst({
|
||||
where: { id: data.typeId },
|
||||
});
|
||||
|
||||
if (!credentialType) {
|
||||
throw new GenericError({
|
||||
message: "Credential type not found",
|
||||
name: "CredentialTypeNotFound",
|
||||
cause: `No credential type exists with ID '${data.typeId}'`,
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
// Validate the fields against acceptable fields
|
||||
const acceptableFields = JSON.parse(credentialType.fields! as string).map(
|
||||
(f: { id: string; name: string; secure: boolean }) => ({
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
secure: f.secure,
|
||||
}),
|
||||
) as CredentialTypeField[];
|
||||
const validatedFields = await fieldValidator(data.fields, acceptableFields);
|
||||
|
||||
// Separate secure and non-secure fields
|
||||
const secureFields = validatedFields.filter((f) => f.secure);
|
||||
const nonSecureFields = validatedFields.filter((f) => !f.secure);
|
||||
|
||||
// Build fields object for non-secure fields
|
||||
const fieldsObject: Record<string, any> = {};
|
||||
nonSecureFields.forEach((field) => {
|
||||
fieldsObject[field.fieldId] = field.value;
|
||||
});
|
||||
|
||||
// Encrypt secure field values
|
||||
const secureValueData = secureFields.map((field) => {
|
||||
const { encrypted, hash } = generateSecureValue(field.value);
|
||||
return {
|
||||
name: field.fieldId,
|
||||
content: encrypted,
|
||||
hash,
|
||||
};
|
||||
});
|
||||
|
||||
// Create credential and all secure values in a transaction
|
||||
const credential = await prisma.credential.create({
|
||||
data: {
|
||||
name: data.name,
|
||||
typeId: data.typeId,
|
||||
companyId: data.companyId,
|
||||
fields: fieldsObject,
|
||||
securevalues: {
|
||||
create: secureValueData,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
type: true,
|
||||
company: true,
|
||||
securevalues: true,
|
||||
},
|
||||
});
|
||||
|
||||
return new CredentialController(credential);
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete Credential
|
||||
*
|
||||
* Delete a credential by its ID.
|
||||
*
|
||||
* @param id - The credential ID to delete
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async delete(id: string): Promise<void> {
|
||||
await prisma.credential.delete({
|
||||
where: { id },
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
export enum ValueType {
|
||||
PLAIN_TEXT = "plain_text",
|
||||
PASSWORD = "password",
|
||||
}
|
||||
|
||||
export interface CredentialTypeField {
|
||||
id: string; // I.e. "clientId", "clientSecret", etc.
|
||||
name: string; // I.e. "Client ID", "Client Secret", etc.
|
||||
required: boolean;
|
||||
secure: boolean; // Whether this field should be stored encrypted in the database
|
||||
valueType: ValueType; // For future extensibility, currently all fields are strings
|
||||
}
|
||||
|
||||
export interface CredentialField {
|
||||
id: string; // CUID
|
||||
fieldId: string; // I.e. "clientId", "clientSecret", etc.
|
||||
value: string; // Encrypted value stored in the database
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { Collection } from "@discordjs/collection";
|
||||
import { CredentialField, CredentialTypeField } from "./credentialTypeDefs";
|
||||
import GenericError from "../../Errors/GenericError";
|
||||
|
||||
/**
|
||||
* Field Validator
|
||||
*
|
||||
* This method will take a record of the fields being submitted and compare them against a record of the acceptable fields
|
||||
* for a credential type. If any of the submitted fields do not match an acceptable field, an error will be thrown.
|
||||
*
|
||||
* If all the credentials pass, it will return a processed version of the submitted fields including fields that need to be
|
||||
* stored securely (encrypted) and fields that do not.
|
||||
*
|
||||
* @param fields - The fields in object form that are being submitted.
|
||||
* @param acceptableFields - The acceptable field to be compared against.
|
||||
*/
|
||||
export const fieldValidator = async (
|
||||
fields: CredentialField[],
|
||||
acceptableFields: CredentialTypeField[],
|
||||
): Promise<
|
||||
{
|
||||
id: string;
|
||||
fieldId: string;
|
||||
value: string;
|
||||
secure: boolean;
|
||||
}[]
|
||||
> => {
|
||||
const afCollection = new Collection(acceptableFields.map((f) => [f.id, f]));
|
||||
|
||||
await Promise.all(
|
||||
fields.map(async (field) => {
|
||||
const matchingField = afCollection.get(field.fieldId);
|
||||
if (!matchingField) {
|
||||
throw new GenericError({
|
||||
message: `Invalid field ID: ${field.fieldId}`,
|
||||
name: "InvalidCredentialField",
|
||||
cause: `No acceptable field with ID '${field.fieldId}' found.`,
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}),
|
||||
);
|
||||
|
||||
return fields.map((field) => {
|
||||
const matchingField = afCollection.get(field.fieldId)!;
|
||||
|
||||
return {
|
||||
id: field.id,
|
||||
fieldId: field.fieldId,
|
||||
value: field.value,
|
||||
secure: matchingField.secure,
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import Password from "../tools/Password";
|
||||
import crypto from "crypto";
|
||||
import { secureValuesPublicKey } from "../../constants";
|
||||
|
||||
export const generateSecureValue = (content: string) => {
|
||||
// Generate a hash of the content
|
||||
const hash = Password.hash(content);
|
||||
|
||||
// Encrypt the content using the .secureValues.pub public key
|
||||
const encrypted = crypto.publicEncrypt(
|
||||
{
|
||||
key: secureValuesPublicKey,
|
||||
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
|
||||
oaepHash: "sha256",
|
||||
},
|
||||
Buffer.from(content, "utf-8"),
|
||||
);
|
||||
|
||||
// Return the encrypted content and the hash for storage
|
||||
return {
|
||||
encrypted: encrypted.toString("base64"),
|
||||
hash,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import Password from "../tools/Password";
|
||||
import crypto from "crypto";
|
||||
import { secureValuesPrivateKey } from "../../constants";
|
||||
|
||||
export const readSecureValue = (
|
||||
encryptedContent: string,
|
||||
hash?: string,
|
||||
): string => {
|
||||
// Decrypt the content using the .secureValues.key private key
|
||||
const decrypted = crypto.privateDecrypt(
|
||||
{
|
||||
key: secureValuesPrivateKey,
|
||||
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
|
||||
oaepHash: "sha256",
|
||||
},
|
||||
Buffer.from(encryptedContent, "base64"),
|
||||
);
|
||||
|
||||
const content = decrypted.toString("utf-8");
|
||||
|
||||
// Optionally validate the hash if provided
|
||||
if (hash) {
|
||||
const isValid = Password.validate(content, hash);
|
||||
if (!isValid) {
|
||||
throw new Error("Secure value hash validation failed");
|
||||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
};
|
||||
@@ -10,7 +10,10 @@ export default class Password {
|
||||
|
||||
public static hash(password: string, options?: HashPasswordOptions): string {
|
||||
const salt =
|
||||
options?.overrideSalt ?? Password.generateSalt(options?.saltOpts);
|
||||
options?.overrideSalt ??
|
||||
(options?.saltOpts?.length
|
||||
? Password.generateSalt(options?.saltOpts)
|
||||
: "");
|
||||
const hash = blake2sHex(`$BLAKE2s$${password}$${salt}`);
|
||||
return `BLAKE2s$${hash}$${salt}`;
|
||||
}
|
||||
@@ -19,7 +22,7 @@ export default class Password {
|
||||
const [algo, oldHash, salt] = hashed.split(/\$/g);
|
||||
return crypto.timingSafeEqual(
|
||||
Buffer.from(hashed),
|
||||
Buffer.from(Password.hash(newPass, { overrideSalt: salt }))
|
||||
Buffer.from(Password.hash(newPass, { overrideSalt: salt })),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user