mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-21 11:28:43 +00:00
feat: completed license server integration
This commit is contained in:
@@ -1,18 +1,24 @@
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { TProjectDalFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { TAuditLogDalFactory } from "./audit-log-dal";
|
||||
import { TCreateAuditLogDTO } from "./audit-log-types";
|
||||
|
||||
type TAuditLogQueueServiceFactoryDep = {
|
||||
auditLogDal: TAuditLogDalFactory;
|
||||
queueService: TQueueServiceFactory;
|
||||
projectDal: Pick<TProjectDalFactory, "findById">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
export type TAuditLogQueueServiceFactory = ReturnType<typeof auditLogQueueServiceFactory>;
|
||||
|
||||
export const auditLogQueueServiceFactory = ({
|
||||
auditLogDal,
|
||||
queueService
|
||||
queueService,
|
||||
projectDal,
|
||||
licenseService
|
||||
}: TAuditLogQueueServiceFactoryDep) => {
|
||||
const pushToLog = async (data: TCreateAuditLogDTO) => {
|
||||
await queueService.queue(QueueName.AuditLog, QueueJobs.AuditLog, data, {
|
||||
@@ -24,8 +30,19 @@ export const auditLogQueueServiceFactory = ({
|
||||
};
|
||||
|
||||
queueService.start(QueueName.AuditLog, async (job) => {
|
||||
const { actor, orgId, event, ipAddress, projectId, userAgent, userAgentType } = job.data;
|
||||
const { actor, event, ipAddress, projectId, userAgent, userAgentType } = job.data;
|
||||
let { orgId } = job.data;
|
||||
const MS_IN_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
if (!orgId) {
|
||||
// it will never be undefined for both org and project id
|
||||
// TODO(akhilmhdh): use caching here in dal to avoid db calls
|
||||
const project = await projectDal.findById(projectId as string);
|
||||
orgId = project.orgId;
|
||||
}
|
||||
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
const ttl = plan.auditLogsRetentionDays * MS_IN_DAY;
|
||||
await auditLogDal.create({
|
||||
actor: actor.type,
|
||||
actorMetadata: actor.metadata,
|
||||
@@ -34,7 +51,7 @@ export const auditLogQueueServiceFactory = ({
|
||||
ipAddress,
|
||||
orgId,
|
||||
eventType: event.type,
|
||||
expiresAt: new Date(Date.now() + 30 * MS_IN_DAY),
|
||||
expiresAt: new Date(Date.now() + ttl),
|
||||
eventMetadata: event.metadata,
|
||||
userAgentType
|
||||
});
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import axios, { AxiosError } from "axios";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { request } from "@app/lib/config/request";
|
||||
|
||||
import { TFeatureSet } from "./license-types";
|
||||
|
||||
export const getDefaultOnPremFeatures = (): TFeatureSet => ({
|
||||
_id: null,
|
||||
slug: null,
|
||||
tier: -1,
|
||||
workspaceLimit: null,
|
||||
workspacesUsed: 0,
|
||||
memberLimit: null,
|
||||
membersUsed: 0,
|
||||
environmentLimit: null,
|
||||
environmentsUsed: 0,
|
||||
secretVersioning: true,
|
||||
pitRecovery: false,
|
||||
ipAllowlisting: false,
|
||||
rbac: false,
|
||||
customRateLimits: false,
|
||||
customAlerts: false,
|
||||
auditLogs: false,
|
||||
auditLogsRetentionDays: 0,
|
||||
samlSSO: false,
|
||||
status: null,
|
||||
trial_end: null,
|
||||
has_used_trial: true,
|
||||
secretApproval: false,
|
||||
secretRotation: true
|
||||
});
|
||||
|
||||
export const setupLicenceRequestWithStore = (
|
||||
baseURL: string,
|
||||
refreshUrl: string,
|
||||
licenseKey: string
|
||||
) => {
|
||||
let token: string;
|
||||
const licenceReq = axios.create({
|
||||
baseURL,
|
||||
timeout: 15 * 1000,
|
||||
signal: AbortSignal.timeout(15 * 1000)
|
||||
});
|
||||
|
||||
const refreshLicence = async () => {
|
||||
const appCfg = getConfig();
|
||||
const {
|
||||
data: { token: authToken }
|
||||
} = await request.post(
|
||||
refreshUrl,
|
||||
{},
|
||||
{
|
||||
baseURL: appCfg.LICENSE_SERVER_URL,
|
||||
headers: {
|
||||
"X-API-KEY": licenseKey
|
||||
}
|
||||
}
|
||||
);
|
||||
token = authToken;
|
||||
return token;
|
||||
};
|
||||
|
||||
licenceReq.interceptors.request.use(
|
||||
(config) => {
|
||||
if (token && config.headers) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(err) => Promise.reject(err)
|
||||
);
|
||||
|
||||
licenceReq.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (err) => {
|
||||
const originalRequest = err.config;
|
||||
|
||||
// eslint-disable-next-line
|
||||
if ((err as AxiosError)?.response?.status === 401 && !originalRequest._retry) {
|
||||
// eslint-disable-next-line
|
||||
originalRequest._retry = true;
|
||||
|
||||
// refresh
|
||||
await refreshLicence();
|
||||
|
||||
licenceReq.defaults.headers.common.Authorization = `Bearer ${token}`;
|
||||
return licenceReq(originalRequest);
|
||||
}
|
||||
|
||||
return Promise.reject(err);
|
||||
}
|
||||
);
|
||||
|
||||
return { request: licenceReq, refreshLicence };
|
||||
};
|
||||
@@ -1,6 +1,27 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { OrgMembershipStatus, TableName } from "@app/db/schemas";
|
||||
import { DatabaseError } from "@app/lib/errors";
|
||||
|
||||
export type TLicenseDalFactory = ReturnType<typeof licenseDalFactory>;
|
||||
|
||||
export const licenseDalFactory = (db: TDbClient) => ({ });
|
||||
export const licenseDalFactory = (db: TDbClient) => {
|
||||
const countOfOrgMembers = async (orgId: string | null, tx?: Knex) => {
|
||||
try {
|
||||
const doc = await (tx || db)(TableName.OrgMembership)
|
||||
.where({ status: OrgMembershipStatus.Accepted })
|
||||
.andWhere((bd) => {
|
||||
if (orgId) {
|
||||
bd.where({ orgId });
|
||||
}
|
||||
})
|
||||
.count();
|
||||
return doc?.[0].count;
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Count of Org Members" });
|
||||
}
|
||||
};
|
||||
|
||||
return { countOfOrgMembers };
|
||||
};
|
||||
|
||||
@@ -1,34 +1,523 @@
|
||||
import axios from "axios";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import NodeCache from "node-cache";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { TOrgDalFactory } from "@app/services/org/org-dal";
|
||||
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { getDefaultOnPremFeatures, setupLicenceRequestWithStore } from "./licence-fns";
|
||||
import { TLicenseDalFactory } from "./license-dal";
|
||||
import {
|
||||
InstanceType,
|
||||
TAddOrgPmtMethodDTO,
|
||||
TAddOrgTaxIdDTO,
|
||||
TDelOrgPmtMethodDTO,
|
||||
TDelOrgTaxIdDTO,
|
||||
TFeatureSet,
|
||||
TGetOrgBillInfoDTO,
|
||||
TGetOrgTaxIdDTO,
|
||||
TOrgInvoiceDTO,
|
||||
TOrgLicensesDTO,
|
||||
TOrgPlanDTO,
|
||||
TOrgPlansTableDTO,
|
||||
TOrgPmtMethodsDTO,
|
||||
TStartOrgTrailDTO,
|
||||
TUpdateOrgBillingDetailsDTO
|
||||
} from "./license-types";
|
||||
|
||||
type TLicenseServiceFactoryDep = {
|
||||
orgDal: Pick<TOrgDalFactory, "findOrgById">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseDal: TLicenseDalFactory;
|
||||
};
|
||||
|
||||
export type TLicenseServiceFactory = ReturnType<typeof licenseServiceFactory>;
|
||||
|
||||
export const licenseServiceFactory = ({ licenseDal }: TLicenseServiceFactoryDep) => {
|
||||
const LICENSE_SERVER_CLOUD_LOGIN = "/api/auth/v1/license-server-login";
|
||||
const LICENSE_SERVER_ON_PREM_LOGIN = "/api/auth/v1/licence-login";
|
||||
|
||||
const FEATURE_CACHE_KEY = (orgId: string, projectId?: string) => `${orgId}-${projectId || ""}`;
|
||||
export const licenseServiceFactory = ({
|
||||
orgDal,
|
||||
permissionService,
|
||||
licenseDal
|
||||
}: TLicenseServiceFactoryDep) => {
|
||||
let isValidLicense = false;
|
||||
let instanceType = InstanceType.OnPrem;
|
||||
let onPremFeatures: TFeatureSet = getDefaultOnPremFeatures();
|
||||
const featureStore = new NodeCache({ stdTTL: 60 });
|
||||
|
||||
const appCfg = getConfig();
|
||||
const licenceApi = axios.create({
|
||||
baseURL: appCfg.LICENCE_SERVER_URL
|
||||
});
|
||||
const licenseServerCloudApi = setupLicenceRequestWithStore(
|
||||
appCfg.LICENSE_SERVER_URL || "",
|
||||
LICENSE_SERVER_CLOUD_LOGIN,
|
||||
appCfg.LICENSE_SERVER_KEY || ""
|
||||
);
|
||||
|
||||
const licenseServerOnPremApi = setupLicenceRequestWithStore(
|
||||
appCfg.LICENSE_SERVER_URL || "",
|
||||
LICENSE_SERVER_ON_PREM_LOGIN,
|
||||
appCfg.LICENSE_KEY || ""
|
||||
);
|
||||
|
||||
const init = async () => {
|
||||
try {
|
||||
if (appCfg.LICENSE_SERVER_KEY) {
|
||||
const token = await licenseServerCloudApi.refreshLicence();
|
||||
if (token) instanceType = InstanceType.Cloud;
|
||||
logger.info(`Instance type: ${InstanceType.Cloud}`);
|
||||
isValidLicense = true;
|
||||
return;
|
||||
}
|
||||
if (appCfg.LICENSE_KEY) {
|
||||
const token = await licenseServerOnPremApi.refreshLicence();
|
||||
if (token) {
|
||||
const {
|
||||
data: { currentPlan }
|
||||
} = await licenseServerOnPremApi.request.get<{ currentPlan: TFeatureSet }>(
|
||||
"/api/license/v1/plan"
|
||||
);
|
||||
onPremFeatures = currentPlan;
|
||||
instanceType = InstanceType.EnterpriseOnPrem;
|
||||
logger.info(`Instance type: ${InstanceType.EnterpriseOnPrem}`);
|
||||
isValidLicense = true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const getPlan = async (orgId: string, projectId?: string) => {
|
||||
try {
|
||||
if (instanceType === InstanceType.Cloud) {
|
||||
const cachedPlan = featureStore.get<TFeatureSet>(FEATURE_CACHE_KEY(orgId, projectId));
|
||||
if (cachedPlan) return cachedPlan;
|
||||
|
||||
const org = await orgDal.findOrgById(orgId);
|
||||
if (!org) throw new BadRequestError({ message: "Org not found" });
|
||||
const {
|
||||
data: { currentPlan }
|
||||
} = await licenseServerCloudApi.request.get<{ currentPlan: TFeatureSet }>(
|
||||
`/api/license-server/v1/customers/${org.customerId}/cloud-plan`,
|
||||
{
|
||||
params: {
|
||||
workspaceId: projectId
|
||||
}
|
||||
}
|
||||
);
|
||||
featureStore.set(FEATURE_CACHE_KEY(org.id, projectId), currentPlan);
|
||||
return currentPlan;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error);
|
||||
return onPremFeatures;
|
||||
}
|
||||
return onPremFeatures;
|
||||
};
|
||||
|
||||
const refreshPlan = async (orgId: string, projectId?: string) => {
|
||||
if (instanceType === InstanceType.Cloud) {
|
||||
featureStore.del(FEATURE_CACHE_KEY(orgId, projectId));
|
||||
await getPlan(orgId, projectId);
|
||||
}
|
||||
};
|
||||
|
||||
const generateOrgCustomerId = async (orgName: string, email: string) => {
|
||||
const {
|
||||
data: { customerId }
|
||||
} = await licenceApi.post("/api/license-server/v1/customers", { email, name: orgName });
|
||||
return customerId;
|
||||
if (instanceType === InstanceType.Cloud) {
|
||||
const {
|
||||
data: { customerId }
|
||||
} = await licenseServerCloudApi.request.post(
|
||||
"/api/license-server/v1/customers",
|
||||
{
|
||||
email,
|
||||
name: orgName
|
||||
},
|
||||
{ timeout: 5000, signal: AbortSignal.timeout(5000) }
|
||||
);
|
||||
return customerId;
|
||||
}
|
||||
};
|
||||
|
||||
const removeOrgCustomer = async (customerId: string) => {
|
||||
await licenceApi.delete(`/api/license-server/v1/customers/${customerId}`);
|
||||
await licenseServerCloudApi.request.delete(`/api/license-server/v1/customers/${customerId}`);
|
||||
};
|
||||
|
||||
const updateSubscriptionOrgMemberCount = async (orgId: string) => {
|
||||
if (instanceType === InstanceType.Cloud) {
|
||||
const org = await orgDal.findOrgById(orgId);
|
||||
if (!org) throw new BadRequestError({ message: "Org not found" });
|
||||
|
||||
const count = await licenseDal.countOfOrgMembers(orgId);
|
||||
if (org?.customerId) {
|
||||
await licenseServerCloudApi.request.patch(
|
||||
`/api/license-server/v1/customers/${org.customerId}/cloud-plan`,
|
||||
{
|
||||
quantity: count
|
||||
}
|
||||
);
|
||||
}
|
||||
featureStore.del(orgId);
|
||||
} else if (instanceType === InstanceType.EnterpriseOnPrem) {
|
||||
const usedSeats = await licenseDal.countOfOrgMembers(null);
|
||||
await licenseServerOnPremApi.request.patch(`/api/license/v1/license`, { usedSeats });
|
||||
}
|
||||
await refreshPlan(orgId);
|
||||
};
|
||||
|
||||
// below all are api calls
|
||||
const getOrgPlansTableByBillCycle = async ({
|
||||
orgId,
|
||||
actor,
|
||||
actorId,
|
||||
billingCycle
|
||||
}: TOrgPlansTableDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
const { data } = await licenseServerCloudApi.request.get(
|
||||
`/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
const getOrgPlan = async ({ orgId, actor, actorId, projectId }: TOrgPlanDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
const plan = await getPlan(orgId, projectId);
|
||||
return plan;
|
||||
};
|
||||
|
||||
const startOrgTrail = async ({ orgId, actorId, actor, success_url }: TStartOrgTrailDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Create,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Edit,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await orgDal.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
data: { url }
|
||||
} = await licenseServerCloudApi.request.post(
|
||||
`/api/license-server/v1/customers/${organization.customerId}/session/trail`,
|
||||
{ success_url }
|
||||
);
|
||||
featureStore.del(FEATURE_CACHE_KEY(orgId));
|
||||
return { url };
|
||||
};
|
||||
|
||||
const getOrgBillingInfo = async ({ orgId, actor, actorId }: TGetOrgBillInfoDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await orgDal.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
const { data } = await licenseServerCloudApi.request.get(
|
||||
`/api/license-server/v1/customers/${organization.customerId}/cloud-plan/billing`
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
// returns org current plan feature table
|
||||
const getOrgPlanTable = async ({ orgId, actor, actorId }: TGetOrgBillInfoDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await orgDal.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
const { data } = await licenseServerCloudApi.request.get(
|
||||
`/api/license-server/v1/customers/${organization.customerId}/cloud-plan/table`
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
const getOrgBillingDetails = async ({ orgId, actor, actorId }: TGetOrgBillInfoDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await orgDal.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const { data } = await licenseServerCloudApi.request.get(
|
||||
`/api/license-server/v1/customers/${organization.customerId}/billing-details`
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
const updateOrgBillingDetails = async ({
|
||||
actorId,
|
||||
actor,
|
||||
orgId,
|
||||
name,
|
||||
email
|
||||
}: TUpdateOrgBillingDetailsDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await orgDal.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
const { data } = await licenseServerCloudApi.request.patch(
|
||||
`/api/license-server/v1/customers/${organization.customerId}/billing-details`,
|
||||
{
|
||||
name,
|
||||
email
|
||||
}
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
const getOrgPmtMethods = async ({ orgId, actor, actorId }: TOrgPmtMethodsDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await orgDal.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
data: { pmtMethods }
|
||||
} = await licenseServerCloudApi.request.get(
|
||||
`/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods`
|
||||
);
|
||||
return pmtMethods;
|
||||
};
|
||||
|
||||
const addOrgPmtMethods = async ({
|
||||
orgId,
|
||||
actor,
|
||||
actorId,
|
||||
success_url,
|
||||
cancel_url
|
||||
}: TAddOrgPmtMethodDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await orgDal.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
const {
|
||||
data: { url }
|
||||
} = await licenseServerCloudApi.request.post(
|
||||
`/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods`,
|
||||
{
|
||||
success_url,
|
||||
cancel_url
|
||||
}
|
||||
);
|
||||
return { url };
|
||||
};
|
||||
|
||||
const delOrgPmtMethods = async ({ actorId, actor, orgId, pmtMethodId }: TDelOrgPmtMethodDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await orgDal.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const { data } = await licenseServerCloudApi.request.delete(
|
||||
`/api/license-server/v1/customers/${organization.customerId}/billing-details/payment-methods/${pmtMethodId}`
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
const getOrgTaxIds = async ({ orgId, actor, actorId }: TGetOrgTaxIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await orgDal.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
const {
|
||||
data: { tax_ids: taxIds }
|
||||
} = await licenseServerCloudApi.request.get(
|
||||
`/api/license-server/v1/customers/${organization.customerId}/billing-details/tax-ids`
|
||||
);
|
||||
return taxIds;
|
||||
};
|
||||
|
||||
const addOrgTaxId = async ({ actorId, actor, orgId, type, value }: TAddOrgTaxIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await orgDal.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const { data } = await licenseServerCloudApi.request.post(
|
||||
`/api/license-server/v1/customers/${organization.customerId}/billing-details/tax-ids`,
|
||||
{
|
||||
type,
|
||||
value
|
||||
}
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
const delOrgTaxId = async ({ orgId, actor, actorId, taxId }: TDelOrgTaxIdDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await orgDal.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const { data } = await licenseServerCloudApi.request.delete(
|
||||
`/api/license-server/v1/customers/${organization.customerId}/billing-details/tax-ids/${taxId}`
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
const getOrgTaxInvoices = async ({ actorId, actor, orgId }: TOrgInvoiceDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await orgDal.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
data: { invoices }
|
||||
} = await licenseServerCloudApi.request.get(
|
||||
`/api/license-server/v1/customers/${organization.customerId}/invoices`
|
||||
);
|
||||
return invoices;
|
||||
};
|
||||
|
||||
const getOrgLicenses = async ({ orgId, actor, actorId }: TOrgLicensesDTO) => {
|
||||
const { permission } = await permissionService.getOrgPermission(actor, actorId, orgId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionActions.Read,
|
||||
OrgPermissionSubjects.Billing
|
||||
);
|
||||
|
||||
const organization = await orgDal.findOrgById(orgId);
|
||||
if (!organization) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to find organization"
|
||||
});
|
||||
}
|
||||
|
||||
const {
|
||||
data: { licenses }
|
||||
} = await licenseServerCloudApi.request.get(
|
||||
`/api/license-server/v1/customers/${organization.customerId}/licenses`
|
||||
);
|
||||
return licenses;
|
||||
};
|
||||
|
||||
return {
|
||||
generateOrgCustomerId,
|
||||
removeOrgCustomer
|
||||
removeOrgCustomer,
|
||||
init,
|
||||
get isValidLicense() {
|
||||
return isValidLicense;
|
||||
},
|
||||
getPlan,
|
||||
updateSubscriptionOrgMemberCount,
|
||||
refreshPlan,
|
||||
getOrgPlan,
|
||||
getOrgPlansTableByBillCycle,
|
||||
startOrgTrail,
|
||||
getOrgBillingInfo,
|
||||
getOrgPlanTable,
|
||||
getOrgBillingDetails,
|
||||
updateOrgBillingDetails,
|
||||
addOrgPmtMethods,
|
||||
delOrgPmtMethods,
|
||||
getOrgPmtMethods,
|
||||
getOrgLicenses,
|
||||
getOrgTaxInvoices,
|
||||
getOrgTaxIds,
|
||||
addOrgTaxId,
|
||||
delOrgTaxId
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { TOrgPermission } from "@app/lib/types";
|
||||
|
||||
export enum InstanceType {
|
||||
OnPrem = "self-hosted",
|
||||
EnterpriseOnPrem = "enterprise-self-hosted",
|
||||
Cloud = "cloud"
|
||||
}
|
||||
|
||||
export type TFeatureSet = {
|
||||
_id: null;
|
||||
slug: null;
|
||||
tier: -1;
|
||||
workspaceLimit: null;
|
||||
workspacesUsed: 0;
|
||||
memberLimit: null;
|
||||
membersUsed: 0;
|
||||
environmentLimit: null;
|
||||
environmentsUsed: 0;
|
||||
secretVersioning: true;
|
||||
pitRecovery: false;
|
||||
ipAllowlisting: false;
|
||||
rbac: false;
|
||||
customRateLimits: false;
|
||||
customAlerts: false;
|
||||
auditLogs: false;
|
||||
auditLogsRetentionDays: 0;
|
||||
samlSSO: false;
|
||||
status: null;
|
||||
trial_end: null;
|
||||
has_used_trial: true;
|
||||
secretApproval: false;
|
||||
secretRotation: true;
|
||||
};
|
||||
|
||||
export type TOrgPlansTableDTO = {
|
||||
billingCycle: string;
|
||||
} & TOrgPermission;
|
||||
|
||||
export type TOrgPlanDTO = {
|
||||
projectId?: string;
|
||||
} & TOrgPermission;
|
||||
|
||||
export type TStartOrgTrailDTO = {
|
||||
success_url: string;
|
||||
} & TOrgPermission;
|
||||
|
||||
export type TGetOrgBillInfoDTO = TOrgPermission;
|
||||
|
||||
export type TOrgPlanTableDTO = TOrgPermission;
|
||||
|
||||
export type TOrgBillingDetailsDTO = TOrgPermission;
|
||||
|
||||
export type TUpdateOrgBillingDetailsDTO = TOrgPermission & {
|
||||
name?: string;
|
||||
email?: string;
|
||||
};
|
||||
|
||||
export type TOrgPmtMethodsDTO = TOrgPermission;
|
||||
|
||||
export type TAddOrgPmtMethodDTO = TOrgPermission & { success_url: string; cancel_url: string };
|
||||
|
||||
export type TDelOrgPmtMethodDTO = TOrgPermission & { pmtMethodId: string };
|
||||
|
||||
export type TGetOrgTaxIdDTO = TOrgPermission;
|
||||
|
||||
export type TAddOrgTaxIdDTO = TOrgPermission & { type: string; value: string };
|
||||
|
||||
export type TDelOrgTaxIdDTO = TOrgPermission & { taxId: string };
|
||||
|
||||
export type TOrgInvoiceDTO = TOrgPermission;
|
||||
|
||||
export type TOrgLicensesDTO = TOrgPermission;
|
||||
|
||||
@@ -20,6 +20,7 @@ import { TOrgBotDalFactory } from "@app/services/org/org-bot-dal";
|
||||
import { TOrgDalFactory } from "@app/services/org/org-dal";
|
||||
import { TUserDalFactory } from "@app/services/user/user-dal";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { TSamlConfigDalFactory } from "./saml-config-dal";
|
||||
@@ -40,6 +41,7 @@ type TSamlConfigServiceFactoryDep = {
|
||||
>;
|
||||
orgBotDal: Pick<TOrgBotDalFactory, "findOne">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
};
|
||||
|
||||
export type TSamlConfigServiceFactory = ReturnType<typeof samlConfigServiceFactory>;
|
||||
@@ -49,7 +51,8 @@ export const samlConfigServiceFactory = ({
|
||||
orgBotDal,
|
||||
orgDal,
|
||||
userDal,
|
||||
permissionService
|
||||
permissionService,
|
||||
licenseService
|
||||
}: TSamlConfigServiceFactoryDep) => {
|
||||
const createSamlCfg = async ({
|
||||
cert,
|
||||
@@ -67,7 +70,12 @@ export const samlConfigServiceFactory = ({
|
||||
OrgPermissionSubjects.Sso
|
||||
);
|
||||
|
||||
// TODO(akhilmhdh-pg): licence check
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.samlSSO)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
|
||||
});
|
||||
|
||||
const orgBot = await orgBotDal.findOne({ orgId });
|
||||
if (!orgBot)
|
||||
@@ -123,6 +131,13 @@ export const samlConfigServiceFactory = ({
|
||||
OrgPermissionActions.Edit,
|
||||
OrgPermissionSubjects.Sso
|
||||
);
|
||||
const plan = await licenseService.getPlan(orgId);
|
||||
if (!plan.samlSSO)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
|
||||
});
|
||||
|
||||
const updateQuery: TSamlConfigsUpdate = { authProvider, isActive };
|
||||
const orgBot = await orgBotDal.findOne({ orgId });
|
||||
if (!orgBot)
|
||||
|
||||
@@ -4,8 +4,10 @@ import Ajv from "ajv";
|
||||
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { TProjectDalFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectEnvDalFactory } from "@app/services/project-env/project-env-dal";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TSecretRotationDalFactory } from "./secret-rotation-dal";
|
||||
@@ -22,6 +24,8 @@ import { rotationTemplates } from "./templates";
|
||||
|
||||
type TSecretRotationServiceFactoryDep = {
|
||||
secretRotationDal: TSecretRotationDalFactory;
|
||||
projectDal: Pick<TProjectDalFactory, "findById">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
projectEnvDal: Pick<TProjectEnvDalFactory, "findOne">;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
secretRotationQueue: TSecretRotationQueueFactory;
|
||||
@@ -34,7 +38,9 @@ export const secretRotationServiceFactory = ({
|
||||
secretRotationDal,
|
||||
permissionService,
|
||||
projectEnvDal,
|
||||
secretRotationQueue
|
||||
secretRotationQueue,
|
||||
licenseService,
|
||||
projectDal
|
||||
}: TSecretRotationServiceFactoryDep) => {
|
||||
const getProviderTemplates = async ({ actor, actorId, projectId }: TProjectPermission) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
@@ -68,6 +74,14 @@ export const secretRotationServiceFactory = ({
|
||||
const env = await projectEnvDal.findOne({ slug: environment });
|
||||
if (!env) throw new BadRequestError({ message: "Environment not found" });
|
||||
|
||||
const project = await projectDal.findById(projectId);
|
||||
const plan = await licenseService.getPlan(project.orgId);
|
||||
if (!plan.secretRotation)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to add secret rotation due to plan restriction. Upgrade plan to add secret rotation."
|
||||
});
|
||||
|
||||
const selectedTemplate = rotationTemplates.find(({ name }) => name === provider);
|
||||
if (!selectedTemplate) throw new BadRequestError({ message: "Provider not found" });
|
||||
const formattedInputs: Record<string, unknown> = {};
|
||||
@@ -149,6 +163,14 @@ export const secretRotationServiceFactory = ({
|
||||
const doc = await secretRotationDal.findById(rotationId);
|
||||
if (!doc) throw new BadRequestError({ message: "Rotation not found" });
|
||||
|
||||
const project = await projectDal.findById(doc.projectId);
|
||||
const plan = await licenseService.getPlan(project.orgId);
|
||||
if (!plan.secretRotation)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to add secret rotation due to plan restriction. Upgrade plan to add secret rotation."
|
||||
});
|
||||
|
||||
const { permission } = await permissionService.getProjectPermission(
|
||||
actor,
|
||||
actorId,
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, InternalServerError } from "@app/lib/errors";
|
||||
import { groupBy } from "@app/lib/fn";
|
||||
import { TSecretDalFactory } from "@app/services/secret/secret-dal";
|
||||
import { TSecretVersionDalFactory } from "@app/services/secret/secret-version-dal";
|
||||
import { TSecretFolderDalFactory } from "@app/services/secret-folder/secret-folder-dal";
|
||||
import { TSecretFolderVersionDalFactory } from "@app/services/secret-folder/secret-folder-version-dal";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import {
|
||||
@@ -34,6 +35,7 @@ type TSecretSnapshotServiceFactoryDep = {
|
||||
"findById" | "findBySecretPath" | "delete" | "insertMany"
|
||||
>;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "isValidLicense">;
|
||||
};
|
||||
|
||||
export type TSecretSnapshotServiceFactory = ReturnType<typeof secretSnapshotServiceFactory>;
|
||||
@@ -46,7 +48,8 @@ export const secretSnapshotServiceFactory = ({
|
||||
snapshotFolderDal,
|
||||
folderDal,
|
||||
secretDal,
|
||||
permissionService
|
||||
permissionService,
|
||||
licenseService
|
||||
}: TSecretSnapshotServiceFactoryDep) => {
|
||||
const projectSecretSnapshotCount = async ({
|
||||
environment,
|
||||
@@ -109,6 +112,9 @@ export const secretSnapshotServiceFactory = ({
|
||||
};
|
||||
|
||||
const performSnapshot = async (folderId: string) => {
|
||||
if (!licenseService.isValidLicense)
|
||||
throw new InternalServerError({ message: "Invalid license" });
|
||||
|
||||
const snapshot = await snapshotDal.transaction(async (tx) => {
|
||||
const folder = await folderDal.findById(folderId, tx);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found" });
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { TDbClient } from "@app/db";
|
||||
import { TableName } from "@app/db/schemas";
|
||||
import { ormify } from "@app/lib/knex";
|
||||
|
||||
export type TTrustedIpDalFactory = ReturnType<typeof trustedIpDalFactory>;
|
||||
|
||||
export const trustedIpDalFactory = (db: TDbClient) => {
|
||||
const trustedIpOrm = ormify(db, TableName.TrustedIps);
|
||||
return trustedIpOrm;
|
||||
};
|
||||
@@ -0,0 +1,150 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
import { TProjectDalFactory } from "@app/services/project/project-dal";
|
||||
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service";
|
||||
import { ProjectPermissionActions, ProjectPermissionSub } from "../permission/project-permission";
|
||||
import { TTrustedIpDalFactory } from "./trusted-ip-dal";
|
||||
import { TCreateIpDTO, TDeleteIpDTO, TUpdateIpDTO } from "./trusted-ip-types";
|
||||
|
||||
type TTrustedIpServiceFactoryDep = {
|
||||
trustedIpDal: TTrustedIpDalFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
projectDal: Pick<TProjectDalFactory, "findById">;
|
||||
};
|
||||
|
||||
export type TTrustedIpServiceFactory = ReturnType<typeof trustedIpServiceFactory>;
|
||||
|
||||
export const trustedIpServiceFactory = ({
|
||||
trustedIpDal,
|
||||
permissionService,
|
||||
licenseService,
|
||||
projectDal
|
||||
}: TTrustedIpServiceFactoryDep) => {
|
||||
const listIpsByProjectId = async ({ projectId, actor, actorId }: TProjectPermission) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.IpAllowList
|
||||
);
|
||||
const trustedIps = await trustedIpDal.find({
|
||||
projectId
|
||||
});
|
||||
return trustedIps;
|
||||
};
|
||||
|
||||
const addProjectIp = async ({
|
||||
projectId,
|
||||
actorId,
|
||||
actor,
|
||||
ipAddress: ip,
|
||||
comment,
|
||||
isActive
|
||||
}: TCreateIpDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.IpAllowList
|
||||
);
|
||||
|
||||
const project = await projectDal.findById(projectId);
|
||||
const plan = await licenseService.getPlan(project.orgId);
|
||||
if (!plan.ipAllowlisting)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to add IP access range due to plan restriction. Upgrade plan to add IP access range."
|
||||
});
|
||||
|
||||
const isValidIp = isValidIpOrCidr(ip);
|
||||
if (!isValidIp)
|
||||
throw new BadRequestError({
|
||||
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||
});
|
||||
|
||||
const { ipAddress, type, prefix } = extractIPDetails(ip);
|
||||
const trustedIp = await trustedIpDal.create({
|
||||
projectId,
|
||||
ipAddress,
|
||||
type,
|
||||
prefix,
|
||||
isActive,
|
||||
comment
|
||||
});
|
||||
|
||||
return { trustedIp, project }; // for audit log
|
||||
};
|
||||
|
||||
const updateProjectIp = async ({
|
||||
projectId,
|
||||
actorId,
|
||||
actor,
|
||||
ipAddress: ip,
|
||||
comment,
|
||||
trustedIpId
|
||||
}: TUpdateIpDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.IpAllowList
|
||||
);
|
||||
|
||||
const project = await projectDal.findById(projectId);
|
||||
const plan = await licenseService.getPlan(project.orgId);
|
||||
if (!plan.ipAllowlisting)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to add IP access range due to plan restriction. Upgrade plan to add IP access range."
|
||||
});
|
||||
|
||||
const isValidIp = isValidIpOrCidr(ip);
|
||||
if (!isValidIp)
|
||||
throw new BadRequestError({
|
||||
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
|
||||
});
|
||||
|
||||
const { ipAddress, type, prefix } = extractIPDetails(ip);
|
||||
const [trustedIp] = await trustedIpDal.update(
|
||||
{ projectId, id: trustedIpId },
|
||||
{
|
||||
projectId,
|
||||
ipAddress,
|
||||
type,
|
||||
prefix: prefix === undefined ? null : prefix,
|
||||
comment
|
||||
}
|
||||
);
|
||||
|
||||
return { trustedIp, project }; // for audit log
|
||||
};
|
||||
|
||||
const deleteProjectIp = async ({ projectId, actorId, actor, trustedIpId }: TDeleteIpDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.IpAllowList
|
||||
);
|
||||
|
||||
const project = await projectDal.findById(projectId);
|
||||
const plan = await licenseService.getPlan(project.orgId);
|
||||
if (!plan.ipAllowlisting)
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Failed to add IP access range due to plan restriction. Upgrade plan to add IP access range."
|
||||
});
|
||||
|
||||
const [trustedIp] = await trustedIpDal.delete({ projectId, id: trustedIpId });
|
||||
|
||||
return { trustedIp, project }; // for audit log
|
||||
};
|
||||
return {
|
||||
listIpsByProjectId,
|
||||
addProjectIp,
|
||||
updateProjectIp,
|
||||
deleteProjectIp
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { TProjectPermission } from "@app/lib/types";
|
||||
|
||||
export type TCreateIpDTO = TProjectPermission & {
|
||||
comment: string;
|
||||
isActive?: boolean;
|
||||
ipAddress: string;
|
||||
};
|
||||
|
||||
export type TUpdateIpDTO = TProjectPermission & {
|
||||
trustedIpId: string;
|
||||
ipAddress: string;
|
||||
comment: string;
|
||||
};
|
||||
|
||||
export type TDeleteIpDTO = TProjectPermission & {
|
||||
trustedIpId: string;
|
||||
};
|
||||
Reference in New Issue
Block a user