fix: remove nested .git folders, re-add as normal directories
This commit is contained in:
Vendored
BIN
Binary file not shown.
@@ -0,0 +1,8 @@
|
||||
export default class AuthenticationError extends Error {
|
||||
constructor(message: string, cause?: string) {
|
||||
super();
|
||||
this.name = "AuthenticationError";
|
||||
this.message = message;
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export default class AuthorizationError extends Error {
|
||||
public status: number;
|
||||
|
||||
constructor(message: string, cause?: string, status?: number) {
|
||||
super();
|
||||
this.name = "AuthorizationError";
|
||||
this.status = status ?? 401;
|
||||
this.message = message;
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export default class BodyError extends Error {
|
||||
constructor(message: string, cause?: string) {
|
||||
super();
|
||||
this.name = "BodyError";
|
||||
this.message = message;
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export default class ExpiredAccessTokenError extends Error {
|
||||
constructor(cause?: string) {
|
||||
super();
|
||||
this.name = "ExpiredAccessTokenError";
|
||||
this.message = "The provided access token has expired.";
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export default class ExpiredRefreshTokenError extends Error {
|
||||
constructor(cause?: string) {
|
||||
super();
|
||||
this.name = "ExpiredRefreshTokenError";
|
||||
this.message = "The provided refresh token has expired.";
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export default class GenericError extends Error {
|
||||
public status: number;
|
||||
|
||||
constructor(info: {
|
||||
name: string;
|
||||
message: string;
|
||||
cause?: string;
|
||||
status?: number;
|
||||
}) {
|
||||
super();
|
||||
this.name = info.name;
|
||||
this.status = info.status ?? 400;
|
||||
this.message = info.message;
|
||||
this.cause = info.cause;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
export default class InsufficientPermission extends Error {
|
||||
public status: number;
|
||||
|
||||
constructor(message: string, cause?: string) {
|
||||
super();
|
||||
this.name = "InsufficientPermission";
|
||||
this.status = 403;
|
||||
this.message = message;
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export default class MissingBodyValue extends Error {
|
||||
constructor(valueName: string) {
|
||||
super();
|
||||
this.name = "MissingBodyValue";
|
||||
this.message = `Value '${valueName}' is missing from the body.`;
|
||||
this.cause =
|
||||
"A value that was required by the body of this request is missing.";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export default class PermissionsVerificationError extends Error {
|
||||
constructor(message: string, cause?: string) {
|
||||
super();
|
||||
this.name = "PermissionsVerificationError";
|
||||
this.message = message;
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export default class RoleError extends Error {
|
||||
constructor(message: string, cause?: string) {
|
||||
super();
|
||||
this.name = "RoleError";
|
||||
this.message = message;
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export default class SessionError extends Error {
|
||||
constructor(message: string, cause?: string) {
|
||||
super();
|
||||
this.name = "SessionError";
|
||||
this.message = message;
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export default class SessionTokenError extends Error {
|
||||
constructor(message: string, cause?: string) {
|
||||
super();
|
||||
this.name = "SessionTokenError";
|
||||
this.message = message;
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export default class UserError extends Error {
|
||||
constructor(message: string, cause?: string) {
|
||||
super();
|
||||
this.name = "UserError";
|
||||
this.message = message;
|
||||
this.cause = cause;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as redirect } from "./redirect";
|
||||
export { default as refresh } from "./refresh";
|
||||
export { default as uri } from "./uri";
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import * as msal from "@azure/msal-node";
|
||||
import { API_BASE_URL, io, msalClient } from "../../constants";
|
||||
import { users } from "../../managers/users";
|
||||
|
||||
/* /v1/auth/redirect */
|
||||
export default createRoute("get", ["/redirect"], async (c) => {
|
||||
c.status(200);
|
||||
|
||||
const tokenRequest: msal.AuthorizationCodeRequest = {
|
||||
code: c.req.query().code as string,
|
||||
scopes: ["user.read"],
|
||||
redirectUri: `${API_BASE_URL}/v1/auth/redirect`,
|
||||
};
|
||||
|
||||
const authResult = await msalClient.acquireTokenByCode(tokenRequest);
|
||||
const callbackKey = c.req.query().state as string;
|
||||
const tokens = await users.authenticate(authResult);
|
||||
|
||||
io.of(`/auth_callback`).emit(`auth:login:callback:${callbackKey}`, {
|
||||
accessToken: tokens.accessToken,
|
||||
refreshToken: tokens.refreshToken,
|
||||
});
|
||||
|
||||
// Close the window because duh
|
||||
return c.html(
|
||||
`<script>
|
||||
window.close();
|
||||
</script>`,
|
||||
);
|
||||
|
||||
return c.json({
|
||||
status: 200,
|
||||
message: "Auth Redirect Endpoint",
|
||||
data: authResult,
|
||||
successful: true,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { sessions } from "../../managers/sessions";
|
||||
|
||||
/* /v1/auth/refresh */
|
||||
export default createRoute("post", ["/refresh"], async (c) => {
|
||||
c.status(201);
|
||||
|
||||
const refreshToken = c.req.header("x-refresh-token") || "";
|
||||
|
||||
const session = await sessions.fetch({
|
||||
refreshToken: refreshToken,
|
||||
});
|
||||
|
||||
const newAccessToken = await session.refresh(refreshToken);
|
||||
|
||||
return c.json({
|
||||
status: 201,
|
||||
message: "Token refreshed successfully!",
|
||||
data: {
|
||||
accessToken: newAccessToken,
|
||||
refreshToken,
|
||||
},
|
||||
successful: true,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { API_BASE_URL } from "../../constants";
|
||||
import cuid from "cuid";
|
||||
|
||||
/* /v1/auth/uri */
|
||||
export default createRoute("get", ["/uri"], (c) => {
|
||||
c.status(200);
|
||||
|
||||
const callbackKey = cuid();
|
||||
const redirectUri = encodeURIComponent(`${API_BASE_URL}/v1/auth/redirect`);
|
||||
const msUri = `https://login.microsoftonline.com/${process.env.MICROSOFT_TENANT_ID}/oauth2/v2.0/authorize?client_id=${process.env.MICROSOFT_CLIENT_ID}&response_type=code&redirect_uri=${redirectUri}&scope=openid+User.Read&state=${callbackKey}&prompt=login`;
|
||||
|
||||
return c.json({
|
||||
status: 200,
|
||||
message: "Successfully fetch Auth URI",
|
||||
data: {
|
||||
uri: msUri,
|
||||
callbackKey: callbackKey,
|
||||
},
|
||||
successful: true,
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { companies } from "../../../managers/companies";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
|
||||
/* /v1/company/companies/[id]/configurations */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/companies/:identifier/configurations"],
|
||||
|
||||
async (c) => {
|
||||
const company = await companies.fetch(c.req.param("identifier"));
|
||||
const configurations = await company.fetchConfigurations();
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Company Configurations Fetched Successfully!",
|
||||
configurations,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({
|
||||
permissions: ["company.fetch", "company.fetch.configurations"],
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,64 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { companies } from "../../../managers/companies";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
import GenericError from "../../../Errors/GenericError";
|
||||
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
|
||||
|
||||
/* /v1/company/companies/[id] */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/companies/:identifier"],
|
||||
|
||||
async (c) => {
|
||||
const company = await companies.fetch(c.req.param("identifier"));
|
||||
const includeAddress = c.req.query("includeAddress") === "true";
|
||||
const includePrimaryContact =
|
||||
c.req.query("includePrimaryContact") === "true";
|
||||
const includeAllContacts = c.req.query("includeAllContacts") === "true";
|
||||
|
||||
// Check for address-specific permission if includeAddress is requested
|
||||
if (includeAddress) {
|
||||
const user = c.get("user");
|
||||
if (!user || !(await user.hasPermission("company.fetch.address"))) {
|
||||
throw new GenericError({
|
||||
name: "InsufficientPermission",
|
||||
message: "You do not have permission to view company addresses.",
|
||||
status: 403,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check for contacts permission if includeAllContacts is requested
|
||||
if (includeAllContacts) {
|
||||
const user = c.get("user");
|
||||
if (!user || !(await user.hasPermission("company.fetch.contacts"))) {
|
||||
throw new GenericError({
|
||||
name: "InsufficientPermission",
|
||||
message: "You do not have permission to view company contacts.",
|
||||
status: 403,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const companyData = company.toJson({
|
||||
includeAddress,
|
||||
includePrimaryContact,
|
||||
includeAllContacts,
|
||||
});
|
||||
const gatedData = await processObjectValuePerms(
|
||||
companyData,
|
||||
"obj.company",
|
||||
c.get("user"),
|
||||
);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Company Fetched Successfully!",
|
||||
gatedData,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["company.fetch"] }),
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { companies } from "../../../managers/companies";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
|
||||
/* /v1/company/companies/[id]/sites */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/companies/:identifier/sites"],
|
||||
|
||||
async (c) => {
|
||||
const company = await companies.fetch(c.req.param("identifier"));
|
||||
const sites = await company.fetchSites();
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Company Sites Fetched Successfully!",
|
||||
sites,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({
|
||||
permissions: ["company.fetch", "company.fetch.sites"],
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,32 @@
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { unifiSites } from "../../../managers/unifiSites";
|
||||
import { companies } from "../../../managers/companies";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
|
||||
|
||||
/* GET /v1/company/companies/:identifier/unifi/sites */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/companies/:identifier/unifi/sites"],
|
||||
async (c) => {
|
||||
const company = await companies.fetch(c.req.param("identifier"));
|
||||
const sites = await unifiSites.fetchByCompany(company.id);
|
||||
|
||||
const gatedData = await Promise.all(
|
||||
sites.map((site) =>
|
||||
processObjectValuePerms(site, "obj.unifiSite", c.get("user")),
|
||||
),
|
||||
);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Company UniFi Sites Fetched Successfully!",
|
||||
gatedData,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({
|
||||
permissions: ["unifi.access", "company.fetch"],
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { companies } from "../../managers/companies";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
|
||||
/* /v1/company/count */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/count"],
|
||||
async (c) => {
|
||||
const count = await companies.count();
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Company count fetched successfully!",
|
||||
{
|
||||
count,
|
||||
},
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["company.fetch.many"] }),
|
||||
);
|
||||
@@ -0,0 +1,50 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { companies } from "../../managers/companies";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||
|
||||
/* /v1/company/companies */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/companies"],
|
||||
async (c) => {
|
||||
const page = new Number(c.req.query("page") ?? 1) as number;
|
||||
const rpp = new Number(c.req.query("rpp") ?? 30) as number; // Records Per Page
|
||||
const search = c.req.query("search") as string;
|
||||
|
||||
const data = search
|
||||
? await companies.search(search, page, rpp)
|
||||
: await companies.fetchPages(page, rpp);
|
||||
|
||||
const companyQty = search
|
||||
? (await companies.search(search, 1, 999999)).length
|
||||
: await companies.count();
|
||||
|
||||
const gatedData = await Promise.all(
|
||||
data.map((item) =>
|
||||
processObjectValuePerms(item, "obj.company", c.get("user")),
|
||||
),
|
||||
);
|
||||
|
||||
let response = apiResponse.successful(
|
||||
"Companies Fetched Successfully!",
|
||||
gatedData,
|
||||
{
|
||||
pagination: {
|
||||
previousPage: page == 1 ? null : page - 1, // Previous Page
|
||||
currentPage: page, // Current Page
|
||||
nextPage: page >= companyQty / rpp ? null : page + 1, // Next Page
|
||||
totalPages: Math.ceil(companyQty / rpp), // Total Number of Pages
|
||||
totalRecords: companyQty, // Total Number of Records
|
||||
listedRecords: rpp, // Total Number of Records being recieved at time of request.
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["company.fetch.many"] }),
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
import { default as fetchAll } from "./fetchAll";
|
||||
import { default as fetch } from "./[id]/fetch";
|
||||
import { default as configurations } from "./[id]/configurations";
|
||||
import { default as sites } from "./[id]/sites";
|
||||
import { default as unifiSites } from "./[id]/unifiSites";
|
||||
import { default as count } from "./count";
|
||||
|
||||
export { configurations, count, fetch, fetchAll, sites, unifiSites };
|
||||
@@ -0,0 +1,49 @@
|
||||
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";
|
||||
import { ValueType } from "../../modules/credentials/credentialTypeDefs";
|
||||
|
||||
/* /v1/credential-type */
|
||||
export default createRoute(
|
||||
"post",
|
||||
["/"],
|
||||
|
||||
async (c) => {
|
||||
const body = await c.req.json();
|
||||
|
||||
const fieldSchema: z.ZodType<any> = z.lazy(() =>
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
required: z.boolean(),
|
||||
secure: z.boolean(),
|
||||
valueType: z.enum(Object.values(ValueType)),
|
||||
subFields: z.array(fieldSchema).optional(),
|
||||
}),
|
||||
);
|
||||
|
||||
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(fieldSchema),
|
||||
});
|
||||
|
||||
const data = schema.parse(body);
|
||||
|
||||
console.log("Creating Credential Type with data:", data);
|
||||
|
||||
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,32 @@
|
||||
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 { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||
|
||||
/* /v1/credential-type/:identifier */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/:identifier"],
|
||||
|
||||
async (c) => {
|
||||
const credentialType = await credentialTypes.fetch(
|
||||
c.req.param("identifier"),
|
||||
);
|
||||
|
||||
const gatedData = await processObjectValuePerms(
|
||||
credentialType.toJson({ includeCredentialCount: true }),
|
||||
"obj.credentialType",
|
||||
c.get("user"),
|
||||
);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Credential Type Fetched Successfully!",
|
||||
gatedData,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["credential_type.fetch"] }),
|
||||
);
|
||||
@@ -0,0 +1,34 @@
|
||||
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 { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||
|
||||
/* /v1/credential-type */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/"],
|
||||
|
||||
async (c) => {
|
||||
const allCredentialTypes = await credentialTypes.fetchAll();
|
||||
|
||||
const gatedData = await Promise.all(
|
||||
allCredentialTypes.map((ct) =>
|
||||
processObjectValuePerms(
|
||||
ct.toJson({ includeCredentialCount: true }),
|
||||
"obj.credentialType",
|
||||
c.get("user"),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Credential Types Fetched Successfully!",
|
||||
gatedData,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["credential_type.fetch.many"] }),
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
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 { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||
|
||||
/* /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 gatedData = await Promise.all(
|
||||
credentials.map((cred) =>
|
||||
processObjectValuePerms(cred.toJson(), "obj.credential", c.get("user")),
|
||||
),
|
||||
);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Credentials Fetched Successfully!",
|
||||
gatedData,
|
||||
);
|
||||
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,48 @@
|
||||
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";
|
||||
import { ValueType } from "../../modules/credentials/credentialTypeDefs";
|
||||
|
||||
/* /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 fieldSchema: z.ZodType<any> = z.lazy(() =>
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
required: z.boolean(),
|
||||
secure: z.boolean(),
|
||||
valueType: z.enum(Object.values(ValueType)),
|
||||
subFields: z.array(fieldSchema).optional(),
|
||||
}),
|
||||
);
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().optional(),
|
||||
permissionScope: z.string().optional(),
|
||||
icon: z.string().optional(),
|
||||
fields: z.array(fieldSchema).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,48 @@
|
||||
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";
|
||||
|
||||
/* POST /v1/credential/credentials/:id/sub-credentials */
|
||||
export default createRoute(
|
||||
"post",
|
||||
["/credentials/:id/sub-credentials"],
|
||||
|
||||
async (c) => {
|
||||
const parentId = c.req.param("id");
|
||||
const body = await c.req.json();
|
||||
|
||||
const schema = z.object({
|
||||
fieldId: z.string().min(1, "Field ID is required"),
|
||||
name: z.string().min(1, "Name is required"),
|
||||
fields: z.array(
|
||||
z.object({
|
||||
fieldId: z.string(),
|
||||
value: z.string(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const data = schema.parse(body);
|
||||
|
||||
const subCredential = await credentials.addSubCredential(
|
||||
parentId,
|
||||
data.fieldId,
|
||||
{
|
||||
name: data.name,
|
||||
fields: data.fields,
|
||||
},
|
||||
);
|
||||
|
||||
const response = apiResponse.created(
|
||||
"Sub-Credential Created Successfully!",
|
||||
subCredential.toJson(),
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({
|
||||
permissions: ["credential.fetch", "credential.sub_credentials.create"],
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,57 @@
|
||||
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 */
|
||||
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"),
|
||||
notes: z.string().optional(),
|
||||
typeId: z.string().min(1, "Type ID is required"),
|
||||
companyId: z.string().min(1, "Company ID is required"),
|
||||
fields: z.array(
|
||||
z.object({
|
||||
fieldId: z.string(),
|
||||
value: z.string(),
|
||||
}),
|
||||
),
|
||||
subCredentials: z
|
||||
.record(
|
||||
z.string(),
|
||||
z.array(
|
||||
z.object({
|
||||
name: z.string().min(1, "Sub-credential name is required"),
|
||||
fields: z.array(
|
||||
z.object({
|
||||
fieldId: z.string(),
|
||||
value: z.string(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
),
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
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,29 @@
|
||||
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 { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||
|
||||
/* /v1/credential/:id */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/credentials/:id"],
|
||||
|
||||
async (c) => {
|
||||
const credential = await credentials.fetch(c.req.param("id"));
|
||||
const gatedData = await processObjectValuePerms(
|
||||
credential.toJson(),
|
||||
"obj.credential",
|
||||
c.get("user"),
|
||||
);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Credential Fetched Successfully!",
|
||||
gatedData,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["credential.fetch"] }),
|
||||
);
|
||||
@@ -0,0 +1,32 @@
|
||||
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 { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||
|
||||
/* /v1/credential/company/:companyId */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/credentials/company/:companyId"],
|
||||
|
||||
async (c) => {
|
||||
const companyCredentials = await credentials.fetchByCompany(
|
||||
c.req.param("companyId"),
|
||||
);
|
||||
|
||||
const gatedData = await Promise.all(
|
||||
companyCredentials.map((cred) =>
|
||||
processObjectValuePerms(cred.toJson(), "obj.credential", c.get("user")),
|
||||
),
|
||||
);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Company Credentials Fetched Successfully!",
|
||||
gatedData,
|
||||
);
|
||||
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,36 @@
|
||||
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 { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||
|
||||
/* GET /v1/credential/credentials/:id/sub-credentials */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/credentials/:id/sub-credentials"],
|
||||
|
||||
async (c) => {
|
||||
const parentId = c.req.param("id");
|
||||
|
||||
// Verify the parent credential exists
|
||||
await credentials.fetch(parentId);
|
||||
|
||||
const subCredentials = await credentials.fetchSubCredentials(parentId);
|
||||
|
||||
const gatedData = await Promise.all(
|
||||
subCredentials.map((sc) =>
|
||||
processObjectValuePerms(sc.toJson(), "obj.credential", c.get("user")),
|
||||
),
|
||||
);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Sub-Credentials Fetched Successfully!",
|
||||
gatedData,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({
|
||||
permissions: ["credential.fetch", "credential.sub_credentials.fetch"],
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,29 @@
|
||||
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 readSecureValue } from "./readSecureValue";
|
||||
import { default as deleteCredential } from "./delete";
|
||||
import { default as valueTypes } from "./valueTypes";
|
||||
import { default as fetchSubCredentials } from "./fetchSubCredentials";
|
||||
import { default as addSubCredential } from "./addSubCredential";
|
||||
import { default as removeSubCredential } from "./removeSubCredential";
|
||||
|
||||
export {
|
||||
valueTypes,
|
||||
fetch,
|
||||
fetchByCompany,
|
||||
create,
|
||||
update,
|
||||
updateFields,
|
||||
fetchFields,
|
||||
readSecureValues,
|
||||
readSecureValue,
|
||||
deleteCredential as delete,
|
||||
fetchSubCredentials,
|
||||
addSubCredential,
|
||||
removeSubCredential,
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
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";
|
||||
|
||||
/* GET /v1/credential/credentials/:id/secure-values/:fieldId */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/credentials/:id/secure-values/:fieldId"],
|
||||
|
||||
async (c) => {
|
||||
const credential = await credentials.fetch(c.req.param("id"));
|
||||
const fieldId = c.req.param("fieldId");
|
||||
const value = await credential.readSecureFieldValue(fieldId);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Secure Value Fetched Successfully!",
|
||||
{ fieldId, value },
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({
|
||||
permissions: ["credential.fetch", "credential.secure_values.read"],
|
||||
}),
|
||||
);
|
||||
@@ -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,27 @@
|
||||
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";
|
||||
|
||||
/* DELETE /v1/credential/credentials/:id/sub-credentials/:subId */
|
||||
export default createRoute(
|
||||
"delete",
|
||||
["/credentials/:id/sub-credentials/:subId"],
|
||||
|
||||
async (c) => {
|
||||
const parentId = c.req.param("id");
|
||||
const subId = c.req.param("subId");
|
||||
|
||||
await credentials.removeSubCredential(parentId, subId);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Sub-Credential Removed Successfully!",
|
||||
null,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({
|
||||
permissions: ["credential.fetch", "credential.sub_credentials.delete"],
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,51 @@
|
||||
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(),
|
||||
notes: z.string().nullable().optional(),
|
||||
fields: z
|
||||
.array(
|
||||
z.object({
|
||||
fieldId: z.string(),
|
||||
value: z.string(),
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const data = schema.parse(body);
|
||||
|
||||
if (data.fields) {
|
||||
await credential.validateAndUpdateFields(data.fields);
|
||||
}
|
||||
|
||||
if (data.name !== undefined || data.notes !== undefined) {
|
||||
await credential.update({
|
||||
...(data.name !== undefined && { name: data.name }),
|
||||
...(data.notes !== undefined && { notes: data.notes }),
|
||||
});
|
||||
}
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Credential Updated Successfully!",
|
||||
credential.toJson(),
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["credential.update"] }),
|
||||
);
|
||||
@@ -0,0 +1,40 @@
|
||||
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({
|
||||
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,22 @@
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { ValueType } from "../../modules/credentials/credentialTypeDefs";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
|
||||
/* GET /v1/credential/valuetypes */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/valuetypes"],
|
||||
|
||||
async (c) => {
|
||||
const valueTypes = Object.values(ValueType);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Value Types Fetched Successfully!",
|
||||
valueTypes,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware(),
|
||||
);
|
||||
@@ -0,0 +1,164 @@
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { z } from "zod";
|
||||
import GenericError from "../../Errors/GenericError";
|
||||
|
||||
type ParsedJson = Record<string, unknown> | unknown[];
|
||||
|
||||
const callbackResource = z.enum([
|
||||
"opportunity",
|
||||
"ticket",
|
||||
"company",
|
||||
"activity",
|
||||
]);
|
||||
|
||||
const safeParseJson = (value: string): ParsedJson | null => {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
const isObject = typeof parsed === "object" && parsed !== null;
|
||||
|
||||
return isObject ? (parsed as ParsedJson) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const asObject = (value: ParsedJson | null): Record<string, unknown> | null => {
|
||||
if (!value) return null;
|
||||
if (Array.isArray(value)) return null;
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
const parseJsonStringFields = (
|
||||
value: Record<string, unknown> | null,
|
||||
): Record<string, unknown> | null => {
|
||||
if (!value) return null;
|
||||
|
||||
return Object.entries(value).reduce<Record<string, unknown>>(
|
||||
(acc, [key, current]) => {
|
||||
if (typeof current !== "string") {
|
||||
acc[key] = current;
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
const looksLikeJson = current.startsWith("{") || current.startsWith("[");
|
||||
if (!looksLikeJson) {
|
||||
acc[key] = current;
|
||||
|
||||
return acc;
|
||||
}
|
||||
|
||||
const parsed = safeParseJson(current);
|
||||
acc[key] = parsed ?? current;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
};
|
||||
|
||||
const parseEntity = (value: unknown): ParsedJson | null => {
|
||||
if (typeof value === "string") return safeParseJson(value);
|
||||
if (typeof value !== "object" || value === null) return null;
|
||||
|
||||
return value as ParsedJson;
|
||||
};
|
||||
|
||||
const buildSummary = (
|
||||
resource: z.infer<typeof callbackResource>,
|
||||
parsedBody: Record<string, unknown> | null,
|
||||
parsedEntity: Record<string, unknown> | null,
|
||||
) => {
|
||||
if (!parsedBody) return null;
|
||||
|
||||
return {
|
||||
resource,
|
||||
messageId: parsedBody.MessageId ?? null,
|
||||
action: parsedBody.Action ?? null,
|
||||
type: parsedBody.Type ?? null,
|
||||
id: parsedBody.ID ?? null,
|
||||
memberId: parsedBody.MemberId ?? null,
|
||||
entityStatus:
|
||||
parsedEntity?.StatusName ??
|
||||
parsedEntity?.TicketStatus ??
|
||||
parsedEntity?.Status ??
|
||||
null,
|
||||
entitySummary: parsedEntity?.Summary ?? parsedEntity?.CompanyName ?? null,
|
||||
entityUpdatedBy: parsedEntity?.UpdatedBy ?? null,
|
||||
entityLastUpdated:
|
||||
parsedEntity?.LastUpdatedUTC ?? parsedEntity?.LastUpdated ?? null,
|
||||
};
|
||||
};
|
||||
|
||||
const parseHeaders = (headers: Headers): Record<string, string> =>
|
||||
Object.fromEntries(headers.entries());
|
||||
|
||||
const callbackHeaderSummary = (headers: Record<string, string>) => ({
|
||||
contentType: headers["content-type"] ?? null,
|
||||
userAgent: headers["user-agent"] ?? null,
|
||||
host: headers.host ?? null,
|
||||
forwardedFor: headers["x-forwarded-for"] ?? null,
|
||||
callbackId:
|
||||
headers["x-cw-request-id"] ??
|
||||
headers["x-request-id"] ??
|
||||
headers["x-correlation-id"] ??
|
||||
null,
|
||||
});
|
||||
|
||||
/* /v1/cw/callback/:resource */
|
||||
export default createRoute("post", ["/callback/:secret/:resource"], async (c) => {
|
||||
const suppliedSecret = c.req.param("secret");
|
||||
const expectedSecret = process.env.CW_CALLBACK_SECRET;
|
||||
|
||||
if (expectedSecret && suppliedSecret !== expectedSecret) {
|
||||
throw new GenericError({
|
||||
name: "Unauthorized",
|
||||
message: "Invalid callback secret.",
|
||||
cause: "Path secret mismatch",
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
if (!expectedSecret) {
|
||||
console.warn(
|
||||
"[cw-callback] CW_CALLBACK_SECRET is not configured; accepting path secret without verification",
|
||||
);
|
||||
}
|
||||
|
||||
const resource = callbackResource.parse(c.req.param("resource"));
|
||||
const headers = parseHeaders(c.req.raw.headers);
|
||||
const headerSummary = callbackHeaderSummary(headers);
|
||||
const rawBody = await c.req.text();
|
||||
const parsedJson = safeParseJson(rawBody);
|
||||
const parsedBody = asObject(parsedJson);
|
||||
const parsedBodyExpanded = parseJsonStringFields(parsedBody);
|
||||
const parsedEntity = asObject(parseEntity(parsedBodyExpanded?.Entity));
|
||||
const summary = buildSummary(resource, parsedBodyExpanded, parsedEntity);
|
||||
|
||||
const line = [
|
||||
`[cw-callback] resource=${resource}`,
|
||||
`action=${String(summary?.action ?? "-")}`,
|
||||
`type=${String(summary?.type ?? "-")}`,
|
||||
`id=${String(summary?.id ?? "-")}`,
|
||||
`by=${String(summary?.entityUpdatedBy ?? summary?.memberId ?? "-")}`,
|
||||
`requestId=${String(headerSummary.callbackId ?? "-")}`,
|
||||
`status=${String(summary?.entityStatus ?? "-")}`,
|
||||
`summary=${String(summary?.entitySummary ?? "-")}`,
|
||||
].join(" ");
|
||||
console.log(line);
|
||||
|
||||
const response = apiResponse.successful("CW callback received.", {
|
||||
resource,
|
||||
secretValidated: Boolean(expectedSecret),
|
||||
summary,
|
||||
headers,
|
||||
headerSummary,
|
||||
bodyParsed: parsedBodyExpanded,
|
||||
receivedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
import { getMemberCache } from "../../modules/cw-utils/members/memberCache";
|
||||
|
||||
/* GET /v1/cw/members */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/members"],
|
||||
async (c) => {
|
||||
const cache = await getMemberCache();
|
||||
|
||||
const activeOnly = c.req.query("active") !== "false";
|
||||
|
||||
const members = cache
|
||||
.filter((m) => (activeOnly ? !m.inactiveFlag : true))
|
||||
.map((m) => ({
|
||||
id: m.id,
|
||||
identifier: m.identifier,
|
||||
firstName: m.firstName,
|
||||
lastName: m.lastName,
|
||||
name: `${m.firstName} ${m.lastName}`.trim(),
|
||||
officeEmail: m.officeEmail,
|
||||
inactive: m.inactiveFlag,
|
||||
}));
|
||||
|
||||
const sorted = members.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"CW members fetched successfully!",
|
||||
sorted,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware(),
|
||||
);
|
||||
@@ -0,0 +1,4 @@
|
||||
import { default as callback } from "./callback";
|
||||
import { default as fetchMembers } from "./fetchMembers";
|
||||
|
||||
export { callback, fetchMembers };
|
||||
@@ -0,0 +1,80 @@
|
||||
import { Context, Env, MiddlewareHandler } from "hono";
|
||||
import AuthorizationError from "../../Errors/AuthorizationError";
|
||||
import { sessions } from "../../managers/sessions";
|
||||
import { Variables } from "../../types/HonoTypes";
|
||||
import GenericError from "../../Errors/GenericError";
|
||||
import { events } from "../../modules/globalEvents";
|
||||
|
||||
/**
|
||||
* Authorization Middleware
|
||||
*
|
||||
* This middleware will do all of the authorization for all the routes that may need authorization.
|
||||
* It will check which auth type is being used and pull the correct credentials from said auth type and
|
||||
* supply them as a variable to the route. If there is an error thrown at any point in this middleware, it
|
||||
* will hault and will not proceed to the route handler.
|
||||
*
|
||||
* Eventually this method will analyze roles and permissions and supply those as objects to the route handler.
|
||||
*
|
||||
* ## Auth Types
|
||||
* - Bearer: Access Token for user authentication
|
||||
* - Key: API Key
|
||||
*
|
||||
* @param ctx - Hono Context Object
|
||||
* @param next - Move onto the handler
|
||||
*/
|
||||
export const authMiddleware = (permParams?: {
|
||||
permissions?: string[];
|
||||
scopes?: string[];
|
||||
forbiddenAuthTypes?: string[];
|
||||
}): MiddlewareHandler<{
|
||||
Variables: Variables;
|
||||
}> => {
|
||||
return async (ctx, next) => {
|
||||
const authorization = ctx.req.header()["authorization"];
|
||||
if (!authorization)
|
||||
throw new AuthorizationError("Missing 'authorization' header.");
|
||||
|
||||
const components = authorization.match(
|
||||
/^(Bearer|Key)\s([a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+)$/,
|
||||
);
|
||||
if (!components)
|
||||
throw new AuthorizationError(
|
||||
"Invalid or malformed authorization header...",
|
||||
);
|
||||
|
||||
const authType: string = components[1] ?? "";
|
||||
const authValue: string = components[2] ?? "";
|
||||
|
||||
if (permParams?.forbiddenAuthTypes?.includes(authType))
|
||||
throw new GenericError({
|
||||
name: "NonpermittedAuthType",
|
||||
message:
|
||||
"The authorization method you are using is not permitted for this API request.",
|
||||
cause: `Type '${authType}' is not permitted.`,
|
||||
status: 403,
|
||||
});
|
||||
|
||||
if (authType) {
|
||||
const session = await sessions.fetch({ accessToken: authValue });
|
||||
const user = await session.fetchUser();
|
||||
ctx.set("user", user);
|
||||
|
||||
if (
|
||||
permParams?.permissions &&
|
||||
permParams?.permissions.length > 0 &&
|
||||
(
|
||||
await Promise.all(
|
||||
permParams?.permissions.map((p) => user.hasPermission(p)),
|
||||
)
|
||||
).includes(false)
|
||||
)
|
||||
throw new GenericError({
|
||||
name: "InsufficentPermission",
|
||||
message: "You do not have sufficent permissions to do this.",
|
||||
status: 403,
|
||||
});
|
||||
}
|
||||
|
||||
await next();
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
import { PERMISSION_NODES } from "../../types/PermissionNodes";
|
||||
|
||||
/* /v1/permissions */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/"],
|
||||
|
||||
async (c) => {
|
||||
const response = apiResponse.successful(
|
||||
"Permission Nodes Fetched Successfully!",
|
||||
PERMISSION_NODES,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["role.read"] }),
|
||||
);
|
||||
@@ -0,0 +1,34 @@
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
import { PERMISSION_NODES } from "../../types/PermissionNodes";
|
||||
import GenericError from "../../Errors/GenericError";
|
||||
|
||||
/* /v1/permissions/:category */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/:category"],
|
||||
|
||||
async (c) => {
|
||||
const categoryKey = c.req.param(
|
||||
"category",
|
||||
) as keyof typeof PERMISSION_NODES;
|
||||
|
||||
if (!(categoryKey in PERMISSION_NODES)) {
|
||||
throw new GenericError({
|
||||
name: "NotFound",
|
||||
message: `Permission category "${categoryKey}" not found`,
|
||||
status: 404,
|
||||
cause: `Valid categories: ${Object.keys(PERMISSION_NODES).join(", ")}`,
|
||||
});
|
||||
}
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Permission Category Fetched Successfully!",
|
||||
PERMISSION_NODES[categoryKey],
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["role.read"] }),
|
||||
);
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
import { getAllPermissionNodes } from "../../types/PermissionNodes";
|
||||
|
||||
/* /v1/permissions/nodes */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/nodes"],
|
||||
|
||||
async (c) => {
|
||||
const allNodes = getAllPermissionNodes();
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"All Permission Nodes Fetched Successfully!",
|
||||
allNodes,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["role.read"] }),
|
||||
);
|
||||
@@ -0,0 +1,5 @@
|
||||
import { default as fetchAll } from "./fetchAll";
|
||||
import { default as fetchByCategory } from "./fetchByCategory";
|
||||
import { default as fetchNodes } from "./fetchNodes";
|
||||
|
||||
export { fetchAll, fetchByCategory, fetchNodes };
|
||||
@@ -0,0 +1,32 @@
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { procurement } from "../../../managers/procurement";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
|
||||
|
||||
/* /v1/procurement/items/:identifier */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/items/:identifier"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const includeLinkedItems = c.req.query("includeLinkedItems") === "true";
|
||||
|
||||
const item = await procurement.fetchItem(identifier);
|
||||
|
||||
const gatedData = await processObjectValuePerms(
|
||||
item.toJson({ includeLinkedItems }),
|
||||
"obj.catalogItem",
|
||||
c.get("user"),
|
||||
);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Catalog item fetched successfully!",
|
||||
gatedData,
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["procurement.catalog.fetch"] }),
|
||||
);
|
||||
@@ -0,0 +1,32 @@
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { procurement } from "../../../managers/procurement";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
|
||||
|
||||
/* GET /v1/procurement/items/:identifier/linked */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/items/:identifier/linked"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const item = await procurement.fetchItem(identifier);
|
||||
|
||||
const linkedItems = item.getLinkedItems().map((linked) => linked.toJson());
|
||||
|
||||
const gatedData = await Promise.all(
|
||||
linkedItems.map((linked) =>
|
||||
processObjectValuePerms(linked, "obj.catalogItem", c.get("user")),
|
||||
),
|
||||
);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Linked catalog items fetched successfully!",
|
||||
gatedData,
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["procurement.catalog.fetch"] }),
|
||||
);
|
||||
@@ -0,0 +1,28 @@
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { procurement } from "../../../managers/procurement";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
import { z } from "zod";
|
||||
|
||||
/* POST /v1/procurement/items/:identifier/link */
|
||||
export default createRoute(
|
||||
"post",
|
||||
["/items/:identifier/link"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const body = await c.req.json();
|
||||
const schema = z.object({ targetId: z.string() }).strict();
|
||||
const { targetId } = schema.parse(body);
|
||||
|
||||
const item = await procurement.linkItems(identifier, targetId);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Catalog item linked successfully!",
|
||||
item.toJson({ includeLinkedItems: true }),
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["procurement.catalog.link"] }),
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { procurement } from "../../../managers/procurement";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
|
||||
/* /v1/procurement/items/:identifier/refresh-inventory */
|
||||
export default createRoute(
|
||||
"post",
|
||||
["/items/:identifier/refresh-inventory"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const item = await procurement.fetchItem(identifier);
|
||||
|
||||
await item.refreshInventory();
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Inventory refreshed successfully!",
|
||||
item.toJson(),
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["procurement.catalog.inventory.refresh"] }),
|
||||
);
|
||||
@@ -0,0 +1,28 @@
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { procurement } from "../../../managers/procurement";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
import { z } from "zod";
|
||||
|
||||
/* POST /v1/procurement/items/:identifier/unlink */
|
||||
export default createRoute(
|
||||
"post",
|
||||
["/items/:identifier/unlink"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const body = await c.req.json();
|
||||
const schema = z.object({ targetId: z.string() }).strict();
|
||||
const { targetId } = schema.parse(body);
|
||||
|
||||
const item = await procurement.unlinkItems(identifier, targetId);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Catalog item unlinked successfully!",
|
||||
item.toJson({ includeLinkedItems: true }),
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["procurement.catalog.link"] }),
|
||||
);
|
||||
@@ -0,0 +1,26 @@
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
import {
|
||||
serializeCategoryTree,
|
||||
serializeEcosystemTree,
|
||||
} from "../../modules/catalog-categories/catalogCategories";
|
||||
|
||||
/* /v1/procurement/categories */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/categories"],
|
||||
async (c) => {
|
||||
const categories = serializeCategoryTree();
|
||||
const ecosystems = serializeEcosystemTree();
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Category and ecosystem data fetched successfully!",
|
||||
{ categories, ecosystems },
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["procurement.catalog.fetch.many"] }),
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { procurement } from "../../managers/procurement";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
|
||||
/* /v1/procurement/count */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/count"],
|
||||
async (c) => {
|
||||
const activeOnly = c.req.query("activeOnly") === "true";
|
||||
|
||||
const count = await procurement.count({ activeOnly });
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Catalog item count fetched successfully!",
|
||||
{ count },
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["procurement.catalog.fetch.many"] }),
|
||||
);
|
||||
@@ -0,0 +1,80 @@
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { procurement, CatalogFilterOpts } from "../../managers/procurement";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||
|
||||
/* /v1/procurement/items */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/items"],
|
||||
async (c) => {
|
||||
const page = Number(c.req.query("page") ?? 1);
|
||||
const rpp = Number(c.req.query("rpp") ?? 30);
|
||||
const search = c.req.query("search") as string;
|
||||
const includeInactive = c.req.query("includeInactive") === "true";
|
||||
|
||||
// Category / filter params
|
||||
const category = c.req.query("category") as string | undefined;
|
||||
const subcategory = c.req.query("subcategory") as string | undefined;
|
||||
const group = c.req.query("group") as string | undefined;
|
||||
const manufacturer = c.req.query("manufacturer") as string | undefined;
|
||||
const ecosystem = c.req.query("ecosystem") as string | undefined;
|
||||
const inStock = c.req.query("inStock") === "true" ? true : undefined;
|
||||
const minPrice = c.req.query("minPrice")
|
||||
? Number(c.req.query("minPrice"))
|
||||
: undefined;
|
||||
const maxPrice = c.req.query("maxPrice")
|
||||
? Number(c.req.query("maxPrice"))
|
||||
: undefined;
|
||||
|
||||
const filterOpts: CatalogFilterOpts = {
|
||||
includeInactive,
|
||||
category,
|
||||
subcategory,
|
||||
group,
|
||||
manufacturer,
|
||||
ecosystem,
|
||||
inStock,
|
||||
minPrice,
|
||||
maxPrice,
|
||||
};
|
||||
|
||||
const data = search
|
||||
? await procurement.search(search, page, rpp, filterOpts)
|
||||
: await procurement.fetchPages(page, rpp, filterOpts);
|
||||
|
||||
const totalRecords = search
|
||||
? await procurement.countSearch(search, filterOpts)
|
||||
: await procurement.count(filterOpts);
|
||||
|
||||
const gatedData = await Promise.all(
|
||||
data.map((item) =>
|
||||
processObjectValuePerms(
|
||||
item.toJson(),
|
||||
"obj.catalogItem",
|
||||
c.get("user"),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Catalog items fetched successfully!",
|
||||
gatedData,
|
||||
{
|
||||
pagination: {
|
||||
previousPage: page <= 1 ? null : page - 1,
|
||||
currentPage: page,
|
||||
nextPage: page >= totalRecords / rpp ? null : page + 1,
|
||||
totalPages: Math.ceil(totalRecords / rpp),
|
||||
totalRecords,
|
||||
listedRecords: rpp,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["procurement.catalog.fetch.many"] }),
|
||||
);
|
||||
@@ -0,0 +1,32 @@
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { procurement } from "../../managers/procurement";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
|
||||
/* /v1/procurement/filters */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/filters"],
|
||||
async (c) => {
|
||||
const category = c.req.query("category") as string | undefined;
|
||||
const subcategory = c.req.query("subcategory") as string | undefined;
|
||||
const includeInactive = c.req.query("includeInactive") === "true";
|
||||
|
||||
const filterOpts = { category, subcategory, includeInactive };
|
||||
|
||||
const [categories, subcategories, manufacturers] = await Promise.all([
|
||||
procurement.fetchDistinctValues("category", filterOpts),
|
||||
procurement.fetchDistinctValues("subcategory", filterOpts),
|
||||
procurement.fetchDistinctValues("manufacturer", filterOpts),
|
||||
]);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Available filter values fetched successfully!",
|
||||
{ categories, subcategories, manufacturers },
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["procurement.catalog.fetch.many"] }),
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
import { default as fetchAll } from "./fetchAll";
|
||||
import { default as fetch } from "./[id]/fetch";
|
||||
import { default as refreshInventory } from "./[id]/refreshInventory";
|
||||
import { default as link } from "./[id]/link";
|
||||
import { default as unlink } from "./[id]/unlink";
|
||||
import { default as fetchLinked } from "./[id]/fetchLinked";
|
||||
import { default as count } from "./count";
|
||||
import { default as categories } from "./categories";
|
||||
import { default as filters } from "./filters";
|
||||
|
||||
export {
|
||||
categories,
|
||||
count,
|
||||
fetch,
|
||||
fetchAll,
|
||||
fetchLinked,
|
||||
filters,
|
||||
link,
|
||||
refreshInventory,
|
||||
unlink,
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { roles } from "../../managers/roles";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
import { z } from "zod";
|
||||
|
||||
/* POST /v1/role/:identifier/permissions */
|
||||
export default createRoute(
|
||||
"post",
|
||||
["/:identifier/permissions"],
|
||||
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const body = await c.req.json();
|
||||
|
||||
const schema = z.object({
|
||||
permissions: z
|
||||
.array(z.string().min(1, "Permission node cannot be empty"))
|
||||
.min(1, "At least one permission is required"),
|
||||
});
|
||||
|
||||
const data = schema.parse(body);
|
||||
|
||||
const role = await roles.fetch(identifier);
|
||||
await role.addPermissions(...data.permissions);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Permissions Added Successfully!",
|
||||
role.toJson({ viewPermissions: true }),
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["role.update"] }),
|
||||
);
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { roles } from "../../managers/roles";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
import { z } from "zod";
|
||||
|
||||
/* POST /v1/role */
|
||||
export default createRoute(
|
||||
"post",
|
||||
["/"],
|
||||
|
||||
async (c) => {
|
||||
const body = await c.req.json();
|
||||
|
||||
const schema = z.object({
|
||||
title: z.string().min(1, "Title is required"),
|
||||
moniker: z.string().min(1, "Moniker is required"),
|
||||
permissions: z
|
||||
.array(z.string().min(1, "Permission node cannot be empty"))
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const data = schema.parse(body);
|
||||
|
||||
const role = await roles.create(data);
|
||||
|
||||
const response = apiResponse.created(
|
||||
"Role Created Successfully!",
|
||||
role.toJson({ viewPermissions: true }),
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["role.create"] }),
|
||||
);
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { roles } from "../../managers/roles";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
|
||||
/* DELETE /v1/role/:identifier */
|
||||
export default createRoute(
|
||||
"delete",
|
||||
["/:identifier"],
|
||||
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
|
||||
const role = await roles.fetch(identifier);
|
||||
await role.delete();
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Role Deleted Successfully!",
|
||||
role.toJson(),
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["role.delete"] }),
|
||||
);
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { roles } from "../../managers/roles";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||
|
||||
/* GET /v1/role/:identifier */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/:identifier"],
|
||||
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
|
||||
const role = await roles.fetch(identifier);
|
||||
|
||||
const gatedData = await processObjectValuePerms(
|
||||
role.toJson({ viewPermissions: true }),
|
||||
"obj.role",
|
||||
c.get("user"),
|
||||
);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Role Fetched Successfully!",
|
||||
gatedData,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["role.read"] }),
|
||||
);
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { roles } from "../../managers/roles";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||
|
||||
/* GET /v1/role */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/"],
|
||||
|
||||
async (c) => {
|
||||
const allRoles = await roles.fetchAllRoles();
|
||||
|
||||
const gatedData = await Promise.all(
|
||||
allRoles.map((role) =>
|
||||
processObjectValuePerms(
|
||||
role.toJson({ viewPermissions: true }),
|
||||
"obj.role",
|
||||
c.get("user"),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Roles Fetched Successfully!",
|
||||
gatedData,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["role.read", "role.list"] }),
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { roles } from "../../managers/roles";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
import { processObjectValuePerms } from "../../modules/permission-utils/processObjectPermissions";
|
||||
|
||||
/* GET /v1/role/:identifier/users */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/:identifier/users"],
|
||||
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
|
||||
const role = await roles.fetch(identifier);
|
||||
const users = role.getUsers();
|
||||
|
||||
const gatedData = await Promise.all(
|
||||
users.map((user) =>
|
||||
processObjectValuePerms(user.toJson(), "obj.user", c.get("user")),
|
||||
),
|
||||
);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Users Fetched Successfully!",
|
||||
gatedData,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["role.read", "user.read"] }),
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
export { default as create } from "./create";
|
||||
export { default as fetch } from "./fetch";
|
||||
export { default as fetchAll } from "./fetchAll";
|
||||
export { default as update } from "./update";
|
||||
export { default as deleteRole } from "./delete";
|
||||
export { default as addPermissions } from "./addPermissions";
|
||||
export { default as removePermissions } from "./removePermissions";
|
||||
export { default as getUsers } from "./getUsers";
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { roles } from "../../managers/roles";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
import { z } from "zod";
|
||||
|
||||
/* DELETE /v1/role/:identifier/permissions */
|
||||
export default createRoute(
|
||||
"delete",
|
||||
["/:identifier/permissions"],
|
||||
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const body = await c.req.json();
|
||||
|
||||
const schema = z.object({
|
||||
permissions: z
|
||||
.array(z.string().min(1, "Permission node cannot be empty"))
|
||||
.min(1, "At least one permission is required"),
|
||||
});
|
||||
|
||||
const data = schema.parse(body);
|
||||
|
||||
const role = await roles.fetch(identifier);
|
||||
await role.removePermissions(...data.permissions);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Permissions Removed Successfully!",
|
||||
role.toJson({ viewPermissions: true }),
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["role.update"] }),
|
||||
);
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Hono } from "hono/tiny";
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { roles } from "../../managers/roles";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
import { z } from "zod";
|
||||
|
||||
/* PATCH /v1/role/:identifier */
|
||||
export default createRoute(
|
||||
"patch",
|
||||
["/:identifier"],
|
||||
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const body = await c.req.json();
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
title: z.string().min(1, "Title cannot be empty"),
|
||||
moniker: z.string().min(1, "Moniker cannot be empty"),
|
||||
permissions: z.array(
|
||||
z.string().min(1, "Permission node cannot be empty"),
|
||||
),
|
||||
})
|
||||
.partial()
|
||||
.strict();
|
||||
|
||||
const data = schema.parse(body);
|
||||
|
||||
const role = await roles.fetch(identifier);
|
||||
await role.update(data);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Role Updated Successfully!",
|
||||
role.toJson({ viewPermissions: true }),
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["role.update"] }),
|
||||
);
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Hono } from "hono";
|
||||
import * as authRoles from "../auth";
|
||||
|
||||
const authRouter = new Hono();
|
||||
Object.values(authRoles).map((r) => authRouter.route("/", r));
|
||||
|
||||
export default authRouter;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Hono } from "hono";
|
||||
import * as companyRoutes from "../companies";
|
||||
|
||||
const companyRouter = new Hono();
|
||||
Object.values(companyRoutes).map((r) => companyRouter.route("/", r));
|
||||
|
||||
export default companyRouter;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Hono } from "hono";
|
||||
import * as cwRoutes from "../cw";
|
||||
|
||||
const cwRouter = new Hono();
|
||||
Object.values(cwRoutes).map((r) => cwRouter.route("/", r));
|
||||
|
||||
export default cwRouter;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Hono } from "hono";
|
||||
import * as permissionRoutes from "../permissions";
|
||||
|
||||
const permissionRouter = new Hono();
|
||||
Object.values(permissionRoutes).map((r) => permissionRouter.route("/", r));
|
||||
|
||||
export default permissionRouter;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Hono } from "hono";
|
||||
import * as procurementRoutes from "../procurement";
|
||||
|
||||
const procurementRouter = new Hono();
|
||||
Object.values(procurementRoutes).map((r) => procurementRouter.route("/", r));
|
||||
|
||||
export default procurementRouter;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Hono } from "hono";
|
||||
import * as roleRoutes from "../roles";
|
||||
|
||||
const roleRouter = new Hono();
|
||||
Object.values(roleRoutes).map((r) => roleRouter.route("/", r));
|
||||
|
||||
export default roleRouter;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Hono } from "hono";
|
||||
import * as salesRoutes from "../sales";
|
||||
|
||||
const salesRouter = new Hono();
|
||||
Object.values(salesRoutes).map((r) => salesRouter.route("/", r));
|
||||
|
||||
export default salesRouter;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Hono } from "hono";
|
||||
import * as unifiRoutes from "../unifi";
|
||||
|
||||
const unifiRouter = new Hono();
|
||||
Object.values(unifiRoutes).map((r) => unifiRouter.route("/", r));
|
||||
|
||||
export default unifiRouter;
|
||||
@@ -0,0 +1,9 @@
|
||||
import { Hono } from "hono";
|
||||
import * as meRoutes from "../user/@me";
|
||||
import * as userRoutes from "../user";
|
||||
|
||||
const authRouter = new Hono();
|
||||
Object.values(meRoutes).map((r) => authRouter.route("/", r));
|
||||
Object.values(userRoutes).map((r) => authRouter.route("/", r));
|
||||
|
||||
export default authRouter;
|
||||
@@ -0,0 +1,183 @@
|
||||
import { createRoute } from "../../../modules/api-utils/createRoute";
|
||||
import { opportunities } from "../../../managers/opportunities";
|
||||
import { apiResponse } from "../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../middleware/authorization";
|
||||
import { processObjectValuePerms } from "../../../modules/permission-utils/processObjectPermissions";
|
||||
import GenericError from "../../../Errors/GenericError";
|
||||
import { prisma } from "../../../constants";
|
||||
import { computeSubResourceCacheTTL } from "../../../modules/algorithms/computeSubResourceCacheTTL";
|
||||
import { computeProductsCacheTTL } from "../../../modules/algorithms/computeProductsCacheTTL";
|
||||
import {
|
||||
getCachedSite,
|
||||
getCachedNotes,
|
||||
getCachedContacts,
|
||||
getCachedProducts,
|
||||
fetchAndCacheNotes,
|
||||
fetchAndCacheContacts,
|
||||
fetchAndCacheProducts,
|
||||
fetchAndCacheSite,
|
||||
} from "../../../modules/cache/opportunityCache";
|
||||
import { generatedQuotes } from "../../../managers/generatedQuotes";
|
||||
|
||||
/* GET /v1/sales/opportunities/opportunity/:identifier?include=notes,contacts,products,quotes */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/opportunities/opportunity/:identifier"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const includeParam = c.req.query("include") ?? "";
|
||||
const includes = new Set(
|
||||
includeParam
|
||||
.split(",")
|
||||
.map((s) => s.trim().toLowerCase())
|
||||
.filter(Boolean),
|
||||
);
|
||||
|
||||
// ── Quick DB lookup (≈3ms) to get cwOpportunityId for pre-warming ──
|
||||
const isNumeric = /^\d+$/.test(identifier);
|
||||
const dbRecord = await prisma.opportunity.findFirst({
|
||||
where: isNumeric
|
||||
? { cwOpportunityId: Number(identifier) }
|
||||
: { id: identifier },
|
||||
select: {
|
||||
cwOpportunityId: true,
|
||||
companyCwId: true,
|
||||
siteCwId: true,
|
||||
closedFlag: true,
|
||||
closedDate: true,
|
||||
expectedCloseDate: true,
|
||||
cwLastUpdated: true,
|
||||
statusCwId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!dbRecord) {
|
||||
throw new GenericError({
|
||||
message: "Opportunity not found",
|
||||
name: "OpportunityNotFound",
|
||||
cause: `No opportunity exists with identifier '${identifier}'`,
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
// Compute TTLs from DB state
|
||||
const subTtl = computeSubResourceCacheTTL({
|
||||
closedFlag: dbRecord.closedFlag,
|
||||
closedDate: dbRecord.closedDate,
|
||||
expectedCloseDate: dbRecord.expectedCloseDate,
|
||||
lastUpdated: dbRecord.cwLastUpdated,
|
||||
});
|
||||
const prodTtl = computeProductsCacheTTL({
|
||||
closedFlag: dbRecord.closedFlag,
|
||||
closedDate: dbRecord.closedDate,
|
||||
expectedCloseDate: dbRecord.expectedCloseDate,
|
||||
lastUpdated: dbRecord.cwLastUpdated,
|
||||
statusCwId: dbRecord.statusCwId,
|
||||
});
|
||||
|
||||
// ── Pre-warm sub-resources only on cache miss ───────────────────────
|
||||
// Check Redis first — if the background refresh has kept the keys warm,
|
||||
// skip the CW calls entirely. Only fetch-and-cache on a miss.
|
||||
const cwOppId = dbRecord.cwOpportunityId;
|
||||
const _ignoreErrors = (p: Promise<any>) => p.catch(() => {});
|
||||
|
||||
const prewarmPromises: Promise<any>[] = [];
|
||||
if (dbRecord.companyCwId && dbRecord.siteCwId) {
|
||||
const compId = dbRecord.companyCwId,
|
||||
siteId = dbRecord.siteCwId;
|
||||
prewarmPromises.push(
|
||||
_ignoreErrors(
|
||||
getCachedSite(compId, siteId).then(
|
||||
(c) => c ?? fetchAndCacheSite(compId, siteId),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (includes.has("notes") && subTtl)
|
||||
prewarmPromises.push(
|
||||
_ignoreErrors(
|
||||
getCachedNotes(cwOppId).then(
|
||||
(c) => c ?? fetchAndCacheNotes(cwOppId, subTtl),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (includes.has("contacts") && subTtl)
|
||||
prewarmPromises.push(
|
||||
_ignoreErrors(
|
||||
getCachedContacts(cwOppId).then(
|
||||
(c) => c ?? fetchAndCacheContacts(cwOppId, subTtl),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (includes.has("products") && prodTtl)
|
||||
prewarmPromises.push(
|
||||
_ignoreErrors(
|
||||
getCachedProducts(cwOppId).then(
|
||||
(c) => c ?? fetchAndCacheProducts(cwOppId, prodTtl),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// fetchItem runs its own CW calls (opp, activities, company) —
|
||||
// these execute concurrently with the sub-resource pre-warming above.
|
||||
const [item] = await Promise.all([
|
||||
opportunities.fetchItem(identifier),
|
||||
...prewarmPromises,
|
||||
]);
|
||||
|
||||
// Sub-resources now hit warm Redis cache (near-instant)
|
||||
const subResourcePromises: Record<string, Promise<any>> = {
|
||||
_site: item.fetchSite(),
|
||||
};
|
||||
if (includes.has("notes")) {
|
||||
subResourcePromises.notes = item.fetchNotes();
|
||||
}
|
||||
if (includes.has("contacts")) {
|
||||
subResourcePromises.contacts = item.fetchContacts();
|
||||
}
|
||||
if (includes.has("products")) {
|
||||
subResourcePromises.products = item
|
||||
.fetchProducts()
|
||||
.then((products) => products.map((p) => p.toJson()));
|
||||
}
|
||||
if (includes.has("quotes")) {
|
||||
subResourcePromises.quotes = generatedQuotes
|
||||
.fetchByOpportunity(item.id)
|
||||
.then((quotes) => quotes.map((q) => q.toJson()));
|
||||
}
|
||||
|
||||
const keys = Object.keys(subResourcePromises);
|
||||
const results = await Promise.all(keys.map((k) => subResourcePromises[k]));
|
||||
|
||||
// Apply toJson after site is hydrated (side-effect from fetchSite)
|
||||
const gatedData = await processObjectValuePerms(
|
||||
item.toJson(),
|
||||
"obj.opportunity",
|
||||
c.get("user"),
|
||||
);
|
||||
|
||||
const originalOpportunityNoteText = (gatedData as any).notes;
|
||||
|
||||
// Attach sub-resources (skip the internal _site key)
|
||||
keys.forEach((k, i) => {
|
||||
if (k !== "_site") {
|
||||
(gatedData as any)[k] = results[i];
|
||||
}
|
||||
});
|
||||
|
||||
if (includes.has("notes")) {
|
||||
(gatedData as any).opportunityNoteText =
|
||||
typeof originalOpportunityNoteText === "string"
|
||||
? originalOpportunityNoteText
|
||||
: null;
|
||||
}
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Opportunity fetched successfully!",
|
||||
gatedData,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
import { createRoute } from "../../modules/api-utils/createRoute";
|
||||
import { apiResponse } from "../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../middleware/authorization";
|
||||
import { QUOTE_STATUSES } from "../../types/QuoteStatuses";
|
||||
|
||||
/* GET /v1/sales/opportunity-types */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/opportunity-types"],
|
||||
|
||||
async (c) => {
|
||||
const response = apiResponse.successful(
|
||||
"Opportunity Types Fetched Successfully!",
|
||||
QUOTE_STATUSES,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["sales.opportunity.fetch.many"] }),
|
||||
);
|
||||
@@ -0,0 +1,71 @@
|
||||
import { default as fetchAll } from "./opportunities/fetchAll";
|
||||
import { default as metrics } from "./opportunities/metrics";
|
||||
import { default as createOpportunity } from "./opportunities/create";
|
||||
import { default as fetchOpportunityTypes } from "./fetchOpportunityTypes";
|
||||
import { default as count } from "./opportunities/count";
|
||||
import { default as fetch } from "./opportunities/[id]/fetch";
|
||||
import { default as refresh } from "./opportunities/[id]/refresh";
|
||||
import { default as updateOpportunity } from "./opportunities/[id]/update";
|
||||
import { default as deleteOpportunity } from "./opportunities/[id]/delete";
|
||||
import { default as products } from "./opportunities/[id]/products/fetchAll";
|
||||
import { default as addProduct } from "./opportunities/[id]/products/add";
|
||||
import { default as addSpecialOrderProduct } from "./opportunities/[id]/products/addSpecialOrder";
|
||||
import { default as addLabor } from "./opportunities/[id]/products/addLabor";
|
||||
import { default as laborOptions } from "./opportunities/[id]/products/laborOptions";
|
||||
import { default as resequenceProducts } from "./opportunities/[id]/products/resequence";
|
||||
import { default as updateProduct } from "./opportunities/[id]/products/update";
|
||||
import { default as cancelProduct } from "./opportunities/[id]/products/cancel";
|
||||
import { default as deleteProduct } from "./opportunities/[id]/products/delete";
|
||||
import { default as notes } from "./opportunities/[id]/notes/fetchAll";
|
||||
import { default as fetchNote } from "./opportunities/[id]/notes/fetch";
|
||||
import { default as createNote } from "./opportunities/[id]/notes/create";
|
||||
import { default as updateNote } from "./opportunities/[id]/notes/update";
|
||||
import { default as deleteNote } from "./opportunities/[id]/notes/delete";
|
||||
import { default as contacts } from "./opportunities/[id]/contacts";
|
||||
import { default as commitQuote } from "./opportunities/[id]/quotes/commit";
|
||||
import { default as fetchQuotes } from "./opportunities/[id]/quotes/fetchAll";
|
||||
import { default as previewQuote } from "./opportunities/[id]/quotes/preview";
|
||||
import { default as downloadQuote } from "./opportunities/[id]/quotes/download";
|
||||
import { default as fetchDownloads } from "./opportunities/[id]/quotes/fetchDownloads";
|
||||
import { default as fetchByUser } from "./opportunities/fetchByUser";
|
||||
import { default as fetchByUserId } from "./opportunities/fetchByUserId";
|
||||
import { default as workflowDispatch } from "./opportunities/[id]/workflow/dispatch";
|
||||
import { default as workflowStatus } from "./opportunities/[id]/workflow/status";
|
||||
import { default as workflowHistory } from "./opportunities/[id]/workflow/history";
|
||||
|
||||
export {
|
||||
addProduct,
|
||||
fetchByUser,
|
||||
fetchByUserId,
|
||||
addLabor,
|
||||
laborOptions,
|
||||
addSpecialOrderProduct,
|
||||
count,
|
||||
createOpportunity,
|
||||
deleteOpportunity,
|
||||
metrics,
|
||||
fetch,
|
||||
fetchAll,
|
||||
fetchOpportunityTypes,
|
||||
products,
|
||||
resequenceProducts,
|
||||
updateProduct,
|
||||
cancelProduct,
|
||||
deleteProduct,
|
||||
notes,
|
||||
fetchNote,
|
||||
createNote,
|
||||
updateNote,
|
||||
deleteNote,
|
||||
contacts,
|
||||
commitQuote,
|
||||
fetchQuotes,
|
||||
previewQuote,
|
||||
downloadQuote,
|
||||
fetchDownloads,
|
||||
refresh,
|
||||
updateOpportunity,
|
||||
workflowDispatch,
|
||||
workflowStatus,
|
||||
workflowHistory,
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createRoute } from "../../../../modules/api-utils/createRoute";
|
||||
import { opportunities } from "../../../../managers/opportunities";
|
||||
import { apiResponse } from "../../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../../middleware/authorization";
|
||||
|
||||
/* GET /v1/sales/opportunities/opportunity/:identifier/contacts */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/opportunities/opportunity/:identifier/contacts"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const item = await opportunities.fetchRecord(identifier);
|
||||
|
||||
const data = await item.fetchContacts();
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Opportunity contacts fetched successfully!",
|
||||
data,
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
|
||||
);
|
||||
@@ -0,0 +1,50 @@
|
||||
import { createRoute } from "../../../../modules/api-utils/createRoute";
|
||||
import { opportunities } from "../../../../managers/opportunities";
|
||||
import { apiResponse } from "../../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../../middleware/authorization";
|
||||
import GenericError from "../../../../Errors/GenericError";
|
||||
|
||||
/* DELETE /v1/sales/opportunities/opportunity/:identifier */
|
||||
export default createRoute(
|
||||
"delete",
|
||||
["/opportunities/opportunity/:identifier"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
|
||||
try {
|
||||
await opportunities.deleteItem(identifier);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Opportunity deleted successfully!",
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
} catch (err) {
|
||||
const isAxios =
|
||||
err != null && typeof err === "object" && "isAxiosError" in err;
|
||||
|
||||
if (isAxios) {
|
||||
const axiosErr = err as any;
|
||||
const cwStatus: number = axiosErr.response?.status ?? 502;
|
||||
const cwMessage: string =
|
||||
axiosErr.response?.data?.message ??
|
||||
"Failed to delete the opportunity in ConnectWise";
|
||||
|
||||
return c.json(
|
||||
{
|
||||
status: cwStatus,
|
||||
message: cwMessage,
|
||||
error: "ConnectWiseDeleteError",
|
||||
successful: false,
|
||||
meta: { timestamp: Date.now() },
|
||||
},
|
||||
cwStatus as ContentfulStatusCode,
|
||||
);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
authMiddleware({ permissions: ["sales.opportunity.delete"] }),
|
||||
);
|
||||
@@ -0,0 +1,187 @@
|
||||
import { createRoute } from "../../../../modules/api-utils/createRoute";
|
||||
import { opportunities } from "../../../../managers/opportunities";
|
||||
import { apiResponse } from "../../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../../middleware/authorization";
|
||||
import { processObjectValuePerms } from "../../../../modules/permission-utils/processObjectPermissions";
|
||||
import GenericError from "../../../../Errors/GenericError";
|
||||
import { prisma } from "../../../../constants";
|
||||
import { computeSubResourceCacheTTL } from "../../../../modules/algorithms/computeSubResourceCacheTTL";
|
||||
import { computeProductsCacheTTL } from "../../../../modules/algorithms/computeProductsCacheTTL";
|
||||
import {
|
||||
getCachedSite,
|
||||
getCachedNotes,
|
||||
getCachedContacts,
|
||||
getCachedProducts,
|
||||
fetchAndCacheNotes,
|
||||
fetchAndCacheContacts,
|
||||
fetchAndCacheProducts,
|
||||
fetchAndCacheSite,
|
||||
} from "../../../../modules/cache/opportunityCache";
|
||||
import { generatedQuotes } from "../../../../managers/generatedQuotes";
|
||||
|
||||
/* GET /v1/sales/opportunities/opportunity/:identifier?include=notes,contacts,products,quotes */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/opportunities/opportunity/:identifier"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const includeParam = c.req.query("include") ?? "";
|
||||
const includes = new Set(
|
||||
includeParam
|
||||
.split(",")
|
||||
.map((s) => s.trim().toLowerCase())
|
||||
.filter(Boolean),
|
||||
);
|
||||
|
||||
// ── Quick DB lookup (≈3ms) to get cwOpportunityId for pre-warming ──
|
||||
const isNumeric = /^\d+$/.test(identifier);
|
||||
const dbRecord = await prisma.opportunity.findFirst({
|
||||
where: isNumeric
|
||||
? { cwOpportunityId: Number(identifier) }
|
||||
: { id: identifier },
|
||||
select: {
|
||||
cwOpportunityId: true,
|
||||
companyCwId: true,
|
||||
siteCwId: true,
|
||||
closedFlag: true,
|
||||
closedDate: true,
|
||||
expectedCloseDate: true,
|
||||
cwLastUpdated: true,
|
||||
statusCwId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!dbRecord) {
|
||||
throw new GenericError({
|
||||
message: "Opportunity not found",
|
||||
name: "OpportunityNotFound",
|
||||
cause: `No opportunity exists with identifier '${identifier}'`,
|
||||
status: 404,
|
||||
});
|
||||
}
|
||||
|
||||
// Compute TTLs from DB state
|
||||
const subTtl = computeSubResourceCacheTTL({
|
||||
closedFlag: dbRecord.closedFlag,
|
||||
closedDate: dbRecord.closedDate,
|
||||
expectedCloseDate: dbRecord.expectedCloseDate,
|
||||
lastUpdated: dbRecord.cwLastUpdated,
|
||||
});
|
||||
const prodTtl = computeProductsCacheTTL({
|
||||
closedFlag: dbRecord.closedFlag,
|
||||
closedDate: dbRecord.closedDate,
|
||||
expectedCloseDate: dbRecord.expectedCloseDate,
|
||||
lastUpdated: dbRecord.cwLastUpdated,
|
||||
statusCwId: dbRecord.statusCwId,
|
||||
});
|
||||
|
||||
// ── Pre-warm sub-resources only on cache miss ───────────────────────
|
||||
// Check Redis first — if the background refresh has kept the keys warm,
|
||||
// skip the CW calls entirely. Only fetch-and-cache on a miss.
|
||||
const cwOppId = dbRecord.cwOpportunityId;
|
||||
const _ignoreErrors = (p: Promise<any>) => p.catch(() => {});
|
||||
|
||||
const prewarmPromises: Promise<any>[] = [];
|
||||
if (dbRecord.companyCwId && dbRecord.siteCwId) {
|
||||
const compId = dbRecord.companyCwId,
|
||||
siteId = dbRecord.siteCwId;
|
||||
prewarmPromises.push(
|
||||
_ignoreErrors(
|
||||
getCachedSite(compId, siteId).then(
|
||||
(c) => c ?? fetchAndCacheSite(compId, siteId),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (includes.has("notes") && subTtl)
|
||||
prewarmPromises.push(
|
||||
_ignoreErrors(
|
||||
getCachedNotes(cwOppId).then(
|
||||
(c) => c ?? fetchAndCacheNotes(cwOppId, subTtl),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (includes.has("contacts") && subTtl)
|
||||
prewarmPromises.push(
|
||||
_ignoreErrors(
|
||||
getCachedContacts(cwOppId).then(
|
||||
(c) => c ?? fetchAndCacheContacts(cwOppId, subTtl),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (includes.has("products") && prodTtl)
|
||||
prewarmPromises.push(
|
||||
_ignoreErrors(
|
||||
getCachedProducts(cwOppId).then(
|
||||
(c) => c ?? fetchAndCacheProducts(cwOppId, prodTtl),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// fetchItem runs its own CW calls (opp, activities, company) —
|
||||
// these execute concurrently with the sub-resource pre-warming above.
|
||||
const [item] = await Promise.all([
|
||||
opportunities.fetchItem(identifier),
|
||||
...prewarmPromises,
|
||||
]);
|
||||
|
||||
// Sub-resources now hit warm Redis cache (near-instant)
|
||||
const subResourcePromises: Record<string, Promise<any>> = {
|
||||
_site: item.fetchSite(),
|
||||
};
|
||||
if (includes.has("notes")) {
|
||||
subResourcePromises.notes = item.fetchNotes();
|
||||
}
|
||||
if (includes.has("contacts")) {
|
||||
subResourcePromises.contacts = item.fetchContacts();
|
||||
}
|
||||
if (includes.has("products")) {
|
||||
subResourcePromises.products = item
|
||||
.fetchProducts()
|
||||
.then((products) => products.map((p) => p.toJson()));
|
||||
}
|
||||
if (includes.has("quotes")) {
|
||||
const includeRegenData = c.req.query("includeRegenData") === "true";
|
||||
const includeRegenParams = c.req.query("includeRegenParams") === "true";
|
||||
subResourcePromises.quotes = generatedQuotes
|
||||
.fetchByOpportunity(item.id)
|
||||
.then((quotes) =>
|
||||
quotes.map((q) => q.toJson({ includeRegenData, includeRegenParams })),
|
||||
);
|
||||
}
|
||||
|
||||
const keys = Object.keys(subResourcePromises);
|
||||
const results = await Promise.all(keys.map((k) => subResourcePromises[k]));
|
||||
|
||||
// Apply toJson after site is hydrated (side-effect from fetchSite)
|
||||
const gatedData = await processObjectValuePerms(
|
||||
item.toJson(),
|
||||
"obj.opportunity",
|
||||
c.get("user"),
|
||||
);
|
||||
|
||||
const originalOpportunityNoteText = (gatedData as any).notes;
|
||||
|
||||
// Attach sub-resources (skip the internal _site key)
|
||||
keys.forEach((k, i) => {
|
||||
if (k !== "_site") {
|
||||
(gatedData as any)[k] = results[i];
|
||||
}
|
||||
});
|
||||
|
||||
if (includes.has("notes")) {
|
||||
(gatedData as any).opportunityNoteText =
|
||||
typeof originalOpportunityNoteText === "string"
|
||||
? originalOpportunityNoteText
|
||||
: null;
|
||||
}
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Opportunity fetched successfully!",
|
||||
gatedData,
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
|
||||
);
|
||||
@@ -0,0 +1,47 @@
|
||||
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||
import { opportunities } from "../../../../../managers/opportunities";
|
||||
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../../../middleware/authorization";
|
||||
import { resolveMember } from "../../../../../modules/cw-utils/members/memberCache";
|
||||
import { z } from "zod";
|
||||
|
||||
/* POST /v1/sales/opportunities/opportunity/:identifier/notes */
|
||||
export default createRoute(
|
||||
"post",
|
||||
["/opportunities/opportunity/:identifier/notes"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const body = await c.req.json();
|
||||
|
||||
const schema = z.object({
|
||||
text: z.string().min(1, "Note text is required"),
|
||||
flagged: z.boolean().optional(),
|
||||
});
|
||||
|
||||
const data = schema.parse(body);
|
||||
|
||||
const item = await opportunities.fetchRecord(identifier);
|
||||
const user = c.get("user");
|
||||
|
||||
const created = await item.addNote(data.text, user.login, {
|
||||
flagged: data.flagged,
|
||||
});
|
||||
|
||||
const response = apiResponse.created(
|
||||
"Opportunity note created successfully!",
|
||||
{
|
||||
id: created.id,
|
||||
text: created.text,
|
||||
type: created.type
|
||||
? { id: created.type.id, name: created.type.name }
|
||||
: null,
|
||||
flagged: created.flagged,
|
||||
enteredBy: await resolveMember(created.enteredBy),
|
||||
},
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["sales.opportunity.note.create"] }),
|
||||
);
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||
import { opportunities } from "../../../../../managers/opportunities";
|
||||
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../../../middleware/authorization";
|
||||
import GenericError from "../../../../../Errors/GenericError";
|
||||
|
||||
/* DELETE /v1/sales/opportunities/opportunity/:identifier/notes/:noteId */
|
||||
export default createRoute(
|
||||
"delete",
|
||||
["/opportunities/opportunity/:identifier/notes/:noteId"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const noteId = Number(c.req.param("noteId"));
|
||||
|
||||
if (isNaN(noteId))
|
||||
throw new GenericError({
|
||||
status: 400,
|
||||
name: "InvalidNoteId",
|
||||
message: "Note ID must be a number",
|
||||
});
|
||||
|
||||
const item = await opportunities.fetchRecord(identifier);
|
||||
await item.deleteNote(noteId);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Opportunity note deleted successfully!",
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["sales.opportunity.note.delete"] }),
|
||||
);
|
||||
@@ -0,0 +1,34 @@
|
||||
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||
import { opportunities } from "../../../../../managers/opportunities";
|
||||
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../../../middleware/authorization";
|
||||
import GenericError from "../../../../../Errors/GenericError";
|
||||
|
||||
/* GET /v1/sales/opportunities/opportunity/:identifier/notes/:noteId */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/opportunities/opportunity/:identifier/notes/:noteId"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const noteId = Number(c.req.param("noteId"));
|
||||
|
||||
if (isNaN(noteId))
|
||||
throw new GenericError({
|
||||
status: 400,
|
||||
name: "InvalidNoteId",
|
||||
message: "Note ID must be a number",
|
||||
});
|
||||
|
||||
const item = await opportunities.fetchRecord(identifier);
|
||||
const data = await item.fetchNote(noteId);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Opportunity note fetched successfully!",
|
||||
data,
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||
import { opportunities } from "../../../../../managers/opportunities";
|
||||
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../../../middleware/authorization";
|
||||
|
||||
/* GET /v1/sales/opportunities/opportunity/:identifier/notes */
|
||||
export default createRoute(
|
||||
"get",
|
||||
["/opportunities/opportunity/:identifier/notes"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const item = await opportunities.fetchRecord(identifier);
|
||||
|
||||
const data = await item.fetchNotes();
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Opportunity notes fetched successfully!",
|
||||
data,
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["sales.opportunity.fetch"] }),
|
||||
);
|
||||
@@ -0,0 +1,57 @@
|
||||
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||
import { opportunities } from "../../../../../managers/opportunities";
|
||||
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../../../middleware/authorization";
|
||||
import GenericError from "../../../../../Errors/GenericError";
|
||||
import { resolveMember } from "../../../../../modules/cw-utils/members/memberCache";
|
||||
import { z } from "zod";
|
||||
|
||||
/* PATCH /v1/sales/opportunities/opportunity/:identifier/notes/:noteId */
|
||||
export default createRoute(
|
||||
"patch",
|
||||
["/opportunities/opportunity/:identifier/notes/:noteId"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const noteId = Number(c.req.param("noteId"));
|
||||
|
||||
if (isNaN(noteId))
|
||||
throw new GenericError({
|
||||
status: 400,
|
||||
name: "InvalidNoteId",
|
||||
message: "Note ID must be a number",
|
||||
});
|
||||
|
||||
const body = await c.req.json();
|
||||
|
||||
const schema = z
|
||||
.object({
|
||||
text: z.string().min(1).optional(),
|
||||
flagged: z.boolean().optional(),
|
||||
})
|
||||
.refine((d) => d.text !== undefined || d.flagged !== undefined, {
|
||||
message: "At least one of 'text' or 'flagged' must be provided",
|
||||
});
|
||||
|
||||
const data = schema.parse(body);
|
||||
|
||||
const item = await opportunities.fetchRecord(identifier);
|
||||
const updated = await item.updateNote(noteId, data);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Opportunity note updated successfully!",
|
||||
{
|
||||
id: updated.id,
|
||||
text: updated.text,
|
||||
type: updated.type
|
||||
? { id: updated.type.id, name: updated.type.name }
|
||||
: null,
|
||||
flagged: updated.flagged,
|
||||
enteredBy: await resolveMember(updated.enteredBy),
|
||||
},
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["sales.opportunity.note.update"] }),
|
||||
);
|
||||
@@ -0,0 +1,103 @@
|
||||
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||
import { opportunities } from "../../../../../managers/opportunities";
|
||||
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../../../middleware/authorization";
|
||||
import { processObjectValuePerms } from "../../../../../modules/permission-utils/processObjectPermissions";
|
||||
import { z } from "zod";
|
||||
|
||||
const productItemSchema = z
|
||||
.object({
|
||||
catalogItem: z.object({ id: z.number().int().positive() }).optional(),
|
||||
forecastDescription: z.string().optional(),
|
||||
productDescription: z.string().optional(),
|
||||
customerDescription: z.string().nullable().optional(),
|
||||
quantity: z.number().positive().optional(),
|
||||
status: z.object({ id: z.number().int().positive() }).optional(),
|
||||
productClass: z.string().optional(),
|
||||
forecastType: z.string().optional(),
|
||||
revenue: z.number().optional(),
|
||||
cost: z.number().optional(),
|
||||
includeFlag: z.boolean().optional(),
|
||||
linkFlag: z.boolean().optional(),
|
||||
recurringFlag: z.boolean().optional(),
|
||||
taxableFlag: z.boolean().optional(),
|
||||
recurringRevenue: z.number().optional(),
|
||||
recurringCost: z.number().optional(),
|
||||
cycles: z.number().int().min(0).optional(),
|
||||
sequenceNumber: z.number().int().min(0).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const addProductSchema = z.union([
|
||||
productItemSchema,
|
||||
z.array(productItemSchema).min(1, "At least one product is required"),
|
||||
]);
|
||||
|
||||
/* POST /v1/sales/opportunities/opportunity/:identifier/products */
|
||||
export default createRoute(
|
||||
"post",
|
||||
["/opportunities/opportunity/:identifier/products"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const body = await c.req.json();
|
||||
|
||||
const validated = addProductSchema.parse(body);
|
||||
const inputItems = Array.isArray(validated) ? validated : [validated];
|
||||
|
||||
// Gate each submitted field against user permissions.
|
||||
// Only fields the user has permission for are forwarded to ConnectWise.
|
||||
const user = c.get("user");
|
||||
const gatedItems = await Promise.all(
|
||||
inputItems.map((item) =>
|
||||
processObjectValuePerms(item, "sales.opportunity.product.field", user),
|
||||
),
|
||||
);
|
||||
|
||||
const item = await opportunities.fetchRecord(identifier);
|
||||
|
||||
// Strip customerDescription from forecast payloads — CW only accepts
|
||||
// it on procurement products, not forecast items.
|
||||
const customerDescriptions = gatedItems.map(
|
||||
(g: any) => g.customerDescription,
|
||||
);
|
||||
const forecastPayloads = gatedItems.map(
|
||||
({ customerDescription, ...rest }: any) => rest,
|
||||
);
|
||||
|
||||
const created = await item.addProducts(forecastPayloads);
|
||||
|
||||
// If any items included customerDescription, patch the linked
|
||||
// procurement products after creation. This is best-effort since
|
||||
// newly created forecast items may not have a linked procurement
|
||||
// product yet.
|
||||
const procurementUpdates = created
|
||||
.map((product, idx) => ({
|
||||
product,
|
||||
customerDescription: customerDescriptions[idx],
|
||||
}))
|
||||
.filter((entry) => entry.customerDescription != null);
|
||||
|
||||
if (procurementUpdates.length > 0) {
|
||||
await Promise.all(
|
||||
procurementUpdates.map(({ product, customerDescription }) =>
|
||||
item
|
||||
.updateProcurementProductByForecastItem(product.cwForecastId, {
|
||||
customerDescription,
|
||||
})
|
||||
.catch(() => null),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const isBatch = Array.isArray(body);
|
||||
const response = apiResponse.created(
|
||||
isBatch
|
||||
? `${created.length} product(s) added to opportunity successfully!`
|
||||
: "Product added to opportunity successfully!",
|
||||
isBatch ? created.map((p) => p.toJson()) : created[0]!.toJson(),
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["sales.opportunity.product.add"] }),
|
||||
);
|
||||
@@ -0,0 +1,147 @@
|
||||
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||
import { opportunities } from "../../../../../managers/opportunities";
|
||||
import { procurement } from "../../../../../managers/procurement";
|
||||
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../../../middleware/authorization";
|
||||
import { z } from "zod";
|
||||
|
||||
const LABOR_DEFAULT_RATE = {
|
||||
corporate: 100,
|
||||
residential: 85,
|
||||
} as const;
|
||||
|
||||
const roundMoney = (value: number) => Math.round(value * 100) / 100;
|
||||
|
||||
const addLaborSchema = z
|
||||
.object({
|
||||
laborStyle: z.enum(["field", "tech"]),
|
||||
customerType: z.enum(["corporate", "residential"]).optional(),
|
||||
hours: z.number().positive().optional(),
|
||||
rate: z.number().min(0).optional(),
|
||||
ppu: z.number().min(0).optional(),
|
||||
cpu: z.number().min(0).optional(),
|
||||
taxable: z.boolean().optional(),
|
||||
taxableFlag: z.boolean().optional(),
|
||||
description: z.string().min(1).optional(),
|
||||
customerDescription: z.string().min(1).optional(),
|
||||
procurementNotes: z.string().optional(),
|
||||
productNarrative: z.string().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
/* POST /v1/sales/opportunities/opportunity/:identifier/products/labor */
|
||||
export default createRoute(
|
||||
"post",
|
||||
["/opportunities/opportunity/:identifier/products/labor"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const body = await c.req.json();
|
||||
const input = addLaborSchema.parse(body);
|
||||
|
||||
const laborCatalog = await procurement.fetchLaborCatalogItems();
|
||||
const selectedCatalog =
|
||||
input.laborStyle === "tech" ? laborCatalog.tech : laborCatalog.field;
|
||||
|
||||
const customerType = input.customerType ?? "corporate";
|
||||
const defaultRate = LABOR_DEFAULT_RATE[customerType];
|
||||
const quantity = input.hours ?? 1;
|
||||
const ppu = input.ppu ?? input.rate ?? defaultRate;
|
||||
const cpu = input.cpu ?? roundMoney(ppu * 0.5);
|
||||
const taxableFlag =
|
||||
input.taxable ?? input.taxableFlag ?? selectedCatalog.salesTaxable;
|
||||
|
||||
const makeCustomField = (
|
||||
caption: string,
|
||||
value: string,
|
||||
fieldId: number,
|
||||
) => ({
|
||||
id: fieldId,
|
||||
caption,
|
||||
type: "Text",
|
||||
entryMethod: "EntryField",
|
||||
value,
|
||||
});
|
||||
|
||||
const payload = {
|
||||
...(input.procurementNotes || input.productNarrative
|
||||
? {
|
||||
customFields: [
|
||||
...(input.procurementNotes
|
||||
? [
|
||||
makeCustomField(
|
||||
"Procurement Notes",
|
||||
input.procurementNotes,
|
||||
29,
|
||||
),
|
||||
]
|
||||
: []),
|
||||
...(input.productNarrative
|
||||
? [
|
||||
makeCustomField(
|
||||
"Product Narrative",
|
||||
input.productNarrative,
|
||||
46,
|
||||
),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
catalogItem: { id: selectedCatalog.cwCatalogId },
|
||||
description:
|
||||
input.description ??
|
||||
selectedCatalog.name ??
|
||||
selectedCatalog.identifier ??
|
||||
`${input.laborStyle.toUpperCase()} Labor`,
|
||||
customerDescription: input.customerDescription,
|
||||
quantity,
|
||||
price: ppu,
|
||||
cost: cpu,
|
||||
taxableFlag,
|
||||
dropshipFlag: false,
|
||||
billableOption: "Billable",
|
||||
};
|
||||
|
||||
const opportunity = await opportunities.fetchRecord(identifier);
|
||||
const [created] = await opportunity.addProcurementProducts(payload);
|
||||
|
||||
const fields = Array.isArray(created?.customFields)
|
||||
? created.customFields
|
||||
: [];
|
||||
const procurementNotes =
|
||||
fields.find((f: any) => f?.id === 29)?.value ?? null;
|
||||
const productNarrative =
|
||||
fields.find((f: any) => f?.id === 46)?.value ?? null;
|
||||
|
||||
const response = apiResponse.created(
|
||||
"Labor added to opportunity successfully!",
|
||||
{
|
||||
id: created?.id ?? null,
|
||||
forecastDetailId: created?.forecastDetailId ?? null,
|
||||
laborStyle: input.laborStyle,
|
||||
customerType,
|
||||
catalogItem: {
|
||||
id: selectedCatalog.cwCatalogId,
|
||||
identifier: selectedCatalog.identifier,
|
||||
name: selectedCatalog.name,
|
||||
},
|
||||
description: created?.description ?? payload.description,
|
||||
customerDescription:
|
||||
created?.customerDescription ?? input.customerDescription ?? null,
|
||||
quantity: created?.quantity ?? quantity,
|
||||
rate: ppu,
|
||||
ppu,
|
||||
cpu,
|
||||
revenue: roundMoney((created?.quantity ?? quantity) * ppu),
|
||||
cost: roundMoney((created?.quantity ?? quantity) * cpu),
|
||||
taxableFlag: created?.taxableFlag ?? taxableFlag,
|
||||
procurementNotes,
|
||||
productNarrative,
|
||||
},
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["sales.opportunity.product.add.labor"] }),
|
||||
);
|
||||
@@ -0,0 +1,134 @@
|
||||
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||
import { opportunities } from "../../../../../managers/opportunities";
|
||||
import { procurement } from "../../../../../managers/procurement";
|
||||
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../../../middleware/authorization";
|
||||
import { z } from "zod";
|
||||
|
||||
const specialOrderItemSchema = z
|
||||
.object({
|
||||
desc: z.string().min(1),
|
||||
customerDesc: z.string().min(1).optional(),
|
||||
qty: z.number().positive().optional(),
|
||||
price: z.number(),
|
||||
cost: z.number().optional(),
|
||||
taxable: z.boolean().optional(),
|
||||
taxableFlag: z.boolean().optional(),
|
||||
procurementNotes: z.string().optional(),
|
||||
productNarrative: z.string().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const addSpecialOrderSchema = z.union([
|
||||
specialOrderItemSchema,
|
||||
z
|
||||
.array(specialOrderItemSchema)
|
||||
.min(1, "At least one special-order product is required"),
|
||||
]);
|
||||
|
||||
/* POST /v1/sales/opportunities/opportunity/:identifier/products/special-order */
|
||||
export default createRoute(
|
||||
"post",
|
||||
["/opportunities/opportunity/:identifier/products/special-order"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const body = await c.req.json();
|
||||
|
||||
const validated = addSpecialOrderSchema.parse(body);
|
||||
const inputItems = Array.isArray(validated) ? validated : [validated];
|
||||
const specialOrderCatalogItem =
|
||||
await procurement.fetchItem("SPECIAL ORDER");
|
||||
|
||||
const makeCustomField = (
|
||||
caption: string,
|
||||
value: string,
|
||||
fieldId: number,
|
||||
) => ({
|
||||
id: fieldId,
|
||||
caption,
|
||||
type: "Text",
|
||||
entryMethod: "EntryField",
|
||||
value,
|
||||
});
|
||||
|
||||
const normalizedItems = inputItems.map((item) => ({
|
||||
...(item.procurementNotes || item.productNarrative
|
||||
? {
|
||||
customFields: [
|
||||
...(item.procurementNotes
|
||||
? [
|
||||
makeCustomField(
|
||||
"Procurement Notes",
|
||||
item.procurementNotes,
|
||||
29,
|
||||
),
|
||||
]
|
||||
: []),
|
||||
...(item.productNarrative
|
||||
? [
|
||||
makeCustomField(
|
||||
"Product Narrative",
|
||||
item.productNarrative,
|
||||
46,
|
||||
),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
catalogItem: { id: specialOrderCatalogItem.cwCatalogId },
|
||||
description: item.desc,
|
||||
customerDescription: item.customerDesc,
|
||||
quantity: item.qty ?? 1,
|
||||
price: item.price,
|
||||
cost: item.cost,
|
||||
taxableFlag:
|
||||
item.taxable ??
|
||||
item.taxableFlag ??
|
||||
specialOrderCatalogItem.salesTaxable,
|
||||
dropshipFlag: false,
|
||||
billableOption: "Billable",
|
||||
}));
|
||||
|
||||
const opportunity = await opportunities.fetchRecord(identifier);
|
||||
const created = await opportunity.addProcurementProducts(normalizedItems);
|
||||
|
||||
const serialized = created.map((item: any) => {
|
||||
const fields = Array.isArray(item?.customFields) ? item.customFields : [];
|
||||
const procurementNotes =
|
||||
fields.find((f: any) => f?.id === 29)?.value ?? null;
|
||||
const productNarrative =
|
||||
fields.find((f: any) => f?.id === 46)?.value ?? null;
|
||||
|
||||
return {
|
||||
id: item?.id ?? null,
|
||||
forecastDetailId: item?.forecastDetailId ?? null,
|
||||
description: item?.description ?? null,
|
||||
productDescription: item?.description ?? null,
|
||||
customerDescription: item?.customerDescription ?? null,
|
||||
quantity: item?.quantity ?? null,
|
||||
price: item?.price ?? null,
|
||||
revenue: item?.price ?? null,
|
||||
cost: item?.cost ?? null,
|
||||
taxableFlag: item?.taxableFlag ?? null,
|
||||
specialOrderFlag: item?.specialOrderFlag ?? null,
|
||||
procurementNotes,
|
||||
productNarrative,
|
||||
};
|
||||
});
|
||||
|
||||
const isBatch = Array.isArray(body);
|
||||
const response = apiResponse.created(
|
||||
isBatch
|
||||
? `${created.length} special-order product(s) added successfully!`
|
||||
: "Special-order product added successfully!",
|
||||
isBatch ? serialized : serialized[0]!,
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({
|
||||
permissions: ["sales.opportunity.product.add.specialOrder"],
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,84 @@
|
||||
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||
import { opportunities } from "../../../../../managers/opportunities";
|
||||
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../../../middleware/authorization";
|
||||
import GenericError from "../../../../../Errors/GenericError";
|
||||
import { z } from "zod";
|
||||
|
||||
const cancelProductSchema = z
|
||||
.object({
|
||||
quantityCancelled: z.number().int().min(0),
|
||||
cancellationReason: z.string().nullable().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
/* PATCH /v1/sales/opportunities/opportunity/:identifier/products/:productId/cancel */
|
||||
export default createRoute(
|
||||
"patch",
|
||||
["/opportunities/opportunity/:identifier/products/:productId/cancel"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const productId = Number(c.req.param("productId"));
|
||||
const body = await c.req.json();
|
||||
|
||||
if (!Number.isInteger(productId) || productId <= 0) {
|
||||
throw new GenericError({
|
||||
status: 400,
|
||||
name: "InvalidProductId",
|
||||
message: "productId must be a positive integer",
|
||||
});
|
||||
}
|
||||
|
||||
const input = cancelProductSchema.parse(body);
|
||||
const opportunity = await opportunities.fetchRecord(identifier);
|
||||
|
||||
const products = await opportunity.fetchProducts();
|
||||
const product = products.find((item) => item.cwForecastId === productId);
|
||||
|
||||
if (!product) {
|
||||
throw new GenericError({
|
||||
status: 404,
|
||||
name: "ForecastItemNotFound",
|
||||
message: `Forecast item ${productId} not found on opportunity`,
|
||||
});
|
||||
}
|
||||
|
||||
const quantity = product.quantity ?? 0;
|
||||
if (input.quantityCancelled > quantity) {
|
||||
throw new GenericError({
|
||||
status: 400,
|
||||
name: "InvalidCancelledQuantity",
|
||||
message: `quantityCancelled cannot exceed product quantity (${quantity})`,
|
||||
});
|
||||
}
|
||||
|
||||
await opportunity.setProductCancellation(productId, {
|
||||
quantityCancelled: input.quantityCancelled,
|
||||
cancellationReason: input.cancellationReason,
|
||||
});
|
||||
|
||||
const refreshedProducts = await opportunity.fetchProducts({ fresh: true });
|
||||
const updated = refreshedProducts.find(
|
||||
(item) => item.cwForecastId === productId,
|
||||
);
|
||||
|
||||
if (!updated) {
|
||||
throw new GenericError({
|
||||
status: 404,
|
||||
name: "ForecastItemNotFound",
|
||||
message: `Forecast item ${productId} not found on opportunity`,
|
||||
});
|
||||
}
|
||||
|
||||
const response = apiResponse.successful(
|
||||
input.quantityCancelled === 0
|
||||
? "Product uncancelled successfully!"
|
||||
: "Product cancellation updated successfully!",
|
||||
updated.toJson(),
|
||||
);
|
||||
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
},
|
||||
authMiddleware({ permissions: ["sales.opportunity.product.update"] }),
|
||||
);
|
||||
@@ -0,0 +1,72 @@
|
||||
import { createRoute } from "../../../../../modules/api-utils/createRoute";
|
||||
import { opportunities } from "../../../../../managers/opportunities";
|
||||
import { apiResponse } from "../../../../../modules/api-utils/apiResponse";
|
||||
import { ContentfulStatusCode } from "hono/utils/http-status";
|
||||
import { authMiddleware } from "../../../../middleware/authorization";
|
||||
import GenericError from "../../../../../Errors/GenericError";
|
||||
|
||||
/* DELETE /v1/sales/opportunities/opportunity/:identifier/products/:productId */
|
||||
export default createRoute(
|
||||
"delete",
|
||||
["/opportunities/opportunity/:identifier/products/:productId"],
|
||||
async (c) => {
|
||||
const identifier = c.req.param("identifier");
|
||||
const productId = Number(c.req.param("productId"));
|
||||
|
||||
if (!Number.isInteger(productId) || productId <= 0) {
|
||||
throw new GenericError({
|
||||
status: 400,
|
||||
name: "InvalidProductId",
|
||||
message: "productId must be a positive integer",
|
||||
});
|
||||
}
|
||||
|
||||
const opportunity = await opportunities.fetchRecord(identifier);
|
||||
|
||||
// Verify the forecast item exists before attempting deletion
|
||||
const products = await opportunity.fetchProducts();
|
||||
const product = products.find((item) => item.cwForecastId === productId);
|
||||
|
||||
if (!product) {
|
||||
throw new GenericError({
|
||||
status: 404,
|
||||
name: "ForecastItemNotFound",
|
||||
message: `Forecast item ${productId} not found on opportunity`,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await opportunity.deleteProduct(productId);
|
||||
|
||||
const response = apiResponse.successful(
|
||||
"Product deleted from opportunity successfully!",
|
||||
);
|
||||
return c.json(response, response.status as ContentfulStatusCode);
|
||||
} catch (err) {
|
||||
const isAxios =
|
||||
err != null && typeof err === "object" && "isAxiosError" in err;
|
||||
|
||||
if (isAxios) {
|
||||
const axiosErr = err as any;
|
||||
const cwStatus: number = axiosErr.response?.status ?? 502;
|
||||
const cwMessage: string =
|
||||
axiosErr.response?.data?.message ??
|
||||
"Failed to delete the product in ConnectWise";
|
||||
|
||||
return c.json(
|
||||
{
|
||||
status: cwStatus,
|
||||
message: cwMessage,
|
||||
error: "ConnectWiseDeleteError",
|
||||
successful: false,
|
||||
meta: { timestamp: Date.now() },
|
||||
},
|
||||
cwStatus as ContentfulStatusCode,
|
||||
);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
authMiddleware({ permissions: ["sales.opportunity.product.delete"] }),
|
||||
);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user