mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 07:26:45 +00:00
chore: external kms api refactor
This commit is contained in:
@@ -4,15 +4,10 @@ import { ExternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
|
|||||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import {
|
import {
|
||||||
ExternalKmsAwsSchema,
|
ExternalKmsAwsSchema,
|
||||||
ExternalKmsGcpCredentialSchema,
|
|
||||||
ExternalKmsGcpSchema,
|
ExternalKmsGcpSchema,
|
||||||
ExternalKmsInputSchema,
|
ExternalKmsInputSchema,
|
||||||
ExternalKmsInputUpdateSchema,
|
ExternalKmsInputUpdateSchema
|
||||||
KmsGcpKeyFetchAuthType,
|
|
||||||
KmsProviders,
|
|
||||||
TExternalKmsGcpCredentialSchema
|
|
||||||
} from "@app/ee/services/external-kms/providers/model";
|
} from "@app/ee/services/external-kms/providers/model";
|
||||||
import { NotFoundError } from "@app/lib/errors";
|
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode } from "@app/services/auth/auth-type";
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
@@ -293,67 +288,4 @@ export const registerExternalKmsRouter = async (server: FastifyZodProvider) => {
|
|||||||
return { externalKms };
|
return { externalKms };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
server.route({
|
|
||||||
method: "POST",
|
|
||||||
url: "/gcp/keys",
|
|
||||||
config: {
|
|
||||||
rateLimit: writeLimit
|
|
||||||
},
|
|
||||||
schema: {
|
|
||||||
body: z.discriminatedUnion("authMethod", [
|
|
||||||
z.object({
|
|
||||||
authMethod: z.literal(KmsGcpKeyFetchAuthType.Credential),
|
|
||||||
region: z.string().trim().min(1),
|
|
||||||
credential: ExternalKmsGcpCredentialSchema
|
|
||||||
}),
|
|
||||||
z.object({
|
|
||||||
authMethod: z.literal(KmsGcpKeyFetchAuthType.Kms),
|
|
||||||
region: z.string().trim().min(1),
|
|
||||||
kmsId: z.string().trim().min(1)
|
|
||||||
})
|
|
||||||
]),
|
|
||||||
response: {
|
|
||||||
200: z.object({
|
|
||||||
keys: z.string().array()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
|
||||||
handler: async (req) => {
|
|
||||||
const { region, authMethod } = req.body;
|
|
||||||
let credentialJson: TExternalKmsGcpCredentialSchema | undefined;
|
|
||||||
|
|
||||||
if (authMethod === KmsGcpKeyFetchAuthType.Credential) {
|
|
||||||
credentialJson = req.body.credential;
|
|
||||||
} else if (authMethod === KmsGcpKeyFetchAuthType.Kms) {
|
|
||||||
const externalKms = await server.services.externalKms.findById({
|
|
||||||
actor: req.permission.type,
|
|
||||||
actorId: req.permission.id,
|
|
||||||
actorAuthMethod: req.permission.authMethod,
|
|
||||||
actorOrgId: req.permission.orgId,
|
|
||||||
id: req.body.kmsId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!externalKms || externalKms.external.provider !== KmsProviders.Gcp) {
|
|
||||||
throw new NotFoundError({ message: "KMS not found or not of type GCP" });
|
|
||||||
}
|
|
||||||
|
|
||||||
credentialJson = externalKms.external.providerInput.credential as TExternalKmsGcpCredentialSchema;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!credentialJson) {
|
|
||||||
throw new NotFoundError({
|
|
||||||
message: "Something went wrong while fetching the GCP credential, please check inputs and try again"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const results = await server.services.externalKms.fetchGcpKeys({
|
|
||||||
credential: credentialJson,
|
|
||||||
gcpRegion: region
|
|
||||||
});
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { ExternalKmsAwsSchema, KmsProviders } from "@app/ee/services/external-kms/providers/model";
|
||||||
|
|
||||||
|
import { registerExternalKmsEndpoints } from "./external-kms-endpoints";
|
||||||
|
|
||||||
|
export const registerAwsKmsRouter = async (server: FastifyZodProvider) => {
|
||||||
|
registerExternalKmsEndpoints({
|
||||||
|
server,
|
||||||
|
provider: KmsProviders.Aws,
|
||||||
|
createSchema: ExternalKmsAwsSchema,
|
||||||
|
updateSchema: ExternalKmsAwsSchema.partial()
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,273 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { ExternalKmsSchema, KmsKeysSchema } from "@app/db/schemas";
|
||||||
|
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||||
|
import {
|
||||||
|
ExternalKmsAwsSchema,
|
||||||
|
ExternalKmsGcpSchema,
|
||||||
|
KmsProviders,
|
||||||
|
TExternalKmsInputSchema,
|
||||||
|
TExternalKmsInputUpdateSchema
|
||||||
|
} from "@app/ee/services/external-kms/providers/model";
|
||||||
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
const sanitizedExternalSchema = KmsKeysSchema.extend({
|
||||||
|
external: ExternalKmsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
status: true,
|
||||||
|
statusDetails: true,
|
||||||
|
provider: true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const sanitizedExternalSchemaForGetById = KmsKeysSchema.extend({
|
||||||
|
external: ExternalKmsSchema.pick({
|
||||||
|
id: true,
|
||||||
|
status: true,
|
||||||
|
statusDetails: true,
|
||||||
|
provider: true
|
||||||
|
}).extend({
|
||||||
|
// for GCP, we don't return the credential object as it is sensitive data that should not be exposed
|
||||||
|
providerInput: z.union([ExternalKmsAwsSchema, ExternalKmsGcpSchema.pick({ gcpRegion: true, keyName: true })])
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const registerExternalKmsEndpoints = <
|
||||||
|
T extends { type: KmsProviders; inputs: TExternalKmsInputSchema["inputs"] }
|
||||||
|
>({
|
||||||
|
server,
|
||||||
|
provider,
|
||||||
|
createSchema,
|
||||||
|
updateSchema
|
||||||
|
}: {
|
||||||
|
server: FastifyZodProvider;
|
||||||
|
provider: T["type"];
|
||||||
|
createSchema: z.ZodType<T["inputs"]>;
|
||||||
|
updateSchema: z.ZodType<Partial<T["inputs"]>>;
|
||||||
|
}) => {
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string().trim().min(1)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
externalKms: sanitizedExternalSchemaForGetById
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const externalKms = await server.services.externalKms.findById({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.params.id
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate that the KMS is of the expected provider type
|
||||||
|
if (externalKms.external.provider !== provider) {
|
||||||
|
throw new Error(`KMS provider mismatch. Expected ${provider}, got ${externalKms.external.provider}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.GET_KMS,
|
||||||
|
metadata: {
|
||||||
|
kmsId: externalKms.id,
|
||||||
|
name: externalKms.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { externalKms };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
name: z.string().min(1).trim().toLowerCase(),
|
||||||
|
description: z.string().trim().optional(),
|
||||||
|
provider: createSchema
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
externalKms: sanitizedExternalSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
provider: providerInputs
|
||||||
|
} = req.body as {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
provider: T["inputs"];
|
||||||
|
};
|
||||||
|
|
||||||
|
const providerInput = {
|
||||||
|
type: provider,
|
||||||
|
inputs: providerInputs
|
||||||
|
} as TExternalKmsInputSchema;
|
||||||
|
|
||||||
|
const externalKms = await server.services.externalKms.create({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
name,
|
||||||
|
provider: providerInput,
|
||||||
|
description
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.CREATE_KMS,
|
||||||
|
metadata: {
|
||||||
|
kmsId: externalKms.id,
|
||||||
|
provider,
|
||||||
|
name,
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { externalKms };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "PATCH",
|
||||||
|
url: "/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string().trim().min(1)
|
||||||
|
}),
|
||||||
|
body: z.object({
|
||||||
|
name: z.string().min(1).trim().toLowerCase().optional(),
|
||||||
|
description: z.string().trim().optional(),
|
||||||
|
provider: updateSchema
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
externalKms: sanitizedExternalSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
provider: providerInputs
|
||||||
|
} = req.body as {
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
provider: Partial<T["inputs"]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const providerInput = {
|
||||||
|
type: provider,
|
||||||
|
inputs: providerInputs
|
||||||
|
} as TExternalKmsInputUpdateSchema;
|
||||||
|
|
||||||
|
const externalKms = await server.services.externalKms.updateById({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
name,
|
||||||
|
provider: providerInput,
|
||||||
|
description,
|
||||||
|
id: req.params.id
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.UPDATE_KMS,
|
||||||
|
metadata: {
|
||||||
|
kmsId: externalKms.id,
|
||||||
|
provider,
|
||||||
|
name,
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { externalKms };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "DELETE",
|
||||||
|
url: "/:id",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
params: z.object({
|
||||||
|
id: z.string().trim().min(1)
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
externalKms: sanitizedExternalSchema
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const externalKms = await server.services.externalKms.deleteById({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.params.id
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate that the KMS is of the expected provider type
|
||||||
|
if (externalKms.external.provider !== provider) {
|
||||||
|
throw new Error(`KMS provider mismatch. Expected ${provider}, got ${externalKms.external.provider}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: req.permission.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.DELETE_KMS,
|
||||||
|
metadata: {
|
||||||
|
kmsId: externalKms.id,
|
||||||
|
name: externalKms.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { externalKms };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import {
|
||||||
|
ExternalKmsGcpCredentialSchema,
|
||||||
|
ExternalKmsGcpSchema,
|
||||||
|
KmsGcpKeyFetchAuthType,
|
||||||
|
KmsProviders,
|
||||||
|
TExternalKmsGcpCredentialSchema
|
||||||
|
} from "@app/ee/services/external-kms/providers/model";
|
||||||
|
import { NotFoundError } from "@app/lib/errors";
|
||||||
|
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||||
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
|
import { AuthMode } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
|
import { registerExternalKmsEndpoints } from "./external-kms-endpoints";
|
||||||
|
|
||||||
|
export const registerGcpKmsRouter = async (server: FastifyZodProvider) => {
|
||||||
|
registerExternalKmsEndpoints({
|
||||||
|
server,
|
||||||
|
provider: KmsProviders.Gcp,
|
||||||
|
createSchema: ExternalKmsGcpSchema,
|
||||||
|
updateSchema: ExternalKmsGcpSchema.partial()
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/keys",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.discriminatedUnion("authMethod", [
|
||||||
|
z.object({
|
||||||
|
authMethod: z.literal(KmsGcpKeyFetchAuthType.Credential),
|
||||||
|
region: z.string().trim().min(1),
|
||||||
|
credential: ExternalKmsGcpCredentialSchema
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
authMethod: z.literal(KmsGcpKeyFetchAuthType.Kms),
|
||||||
|
region: z.string().trim().min(1),
|
||||||
|
kmsId: z.string().trim().min(1)
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
keys: z.string().array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const { region, authMethod } = req.body;
|
||||||
|
let credentialJson: TExternalKmsGcpCredentialSchema | undefined;
|
||||||
|
|
||||||
|
if (authMethod === KmsGcpKeyFetchAuthType.Credential && "credential" in req.body) {
|
||||||
|
credentialJson = req.body.credential;
|
||||||
|
} else if (authMethod === KmsGcpKeyFetchAuthType.Kms && "kmsId" in req.body) {
|
||||||
|
const externalKms = await server.services.externalKms.findById({
|
||||||
|
actor: req.permission.type,
|
||||||
|
actorId: req.permission.id,
|
||||||
|
actorAuthMethod: req.permission.authMethod,
|
||||||
|
actorOrgId: req.permission.orgId,
|
||||||
|
id: req.body.kmsId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!externalKms || externalKms.external.provider !== KmsProviders.Gcp) {
|
||||||
|
throw new NotFoundError({ message: "KMS not found or not of type GCP" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const providerInput = externalKms.external.providerInput as { credential: TExternalKmsGcpCredentialSchema };
|
||||||
|
credentialJson = providerInput.credential;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!credentialJson) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "Something went wrong while fetching the GCP credential, please check inputs and try again"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await server.services.externalKms.fetchGcpKeys({
|
||||||
|
credential: credentialJson,
|
||||||
|
gcpRegion: region
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { KmsProviders } from "@app/ee/services/external-kms/providers/model";
|
||||||
|
|
||||||
|
import { registerAwsKmsRouter } from "./aws-kms-router";
|
||||||
|
import { registerGcpKmsRouter } from "./gcp-kms-router";
|
||||||
|
|
||||||
|
export const EXTERNAL_KMS_REGISTER_ROUTER_MAP: Record<KmsProviders, (server: FastifyZodProvider) => Promise<void>> = {
|
||||||
|
[KmsProviders.Aws]: registerAwsKmsRouter,
|
||||||
|
[KmsProviders.Gcp]: registerGcpKmsRouter
|
||||||
|
};
|
||||||
@@ -12,6 +12,8 @@ import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router"
|
|||||||
import { registerKubernetesDynamicSecretLeaseRouter } from "./dynamic-secret-lease-routers/kubernetes-lease-router";
|
import { registerKubernetesDynamicSecretLeaseRouter } from "./dynamic-secret-lease-routers/kubernetes-lease-router";
|
||||||
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
import { registerDynamicSecretRouter } from "./dynamic-secret-router";
|
||||||
import { registerExternalKmsRouter } from "./external-kms-router";
|
import { registerExternalKmsRouter } from "./external-kms-router";
|
||||||
|
|
||||||
|
import { EXTERNAL_KMS_REGISTER_ROUTER_MAP } from "./external-kms-routers";
|
||||||
import { registerGatewayRouter } from "./gateway-router";
|
import { registerGatewayRouter } from "./gateway-router";
|
||||||
import { registerGithubOrgSyncRouter } from "./github-org-sync-router";
|
import { registerGithubOrgSyncRouter } from "./github-org-sync-router";
|
||||||
import { registerGroupRouter } from "./group-router";
|
import { registerGroupRouter } from "./group-router";
|
||||||
@@ -162,9 +164,19 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
|
|||||||
{ prefix: "/additional-privilege" }
|
{ prefix: "/additional-privilege" }
|
||||||
);
|
);
|
||||||
|
|
||||||
await server.register(registerExternalKmsRouter, {
|
await server.register(
|
||||||
prefix: "/external-kms"
|
async (externalKmsRouter) => {
|
||||||
});
|
await externalKmsRouter.register(registerExternalKmsRouter);
|
||||||
|
|
||||||
|
// Provider-specific endpoints
|
||||||
|
await Promise.all(
|
||||||
|
Object.entries(EXTERNAL_KMS_REGISTER_ROUTER_MAP).map(([provider, router]) =>
|
||||||
|
externalKmsRouter.register(router, { prefix: `/${provider}` })
|
||||||
|
)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ prefix: "/external-kms" }
|
||||||
|
);
|
||||||
await server.register(registerIdentityTemplateRouter, { prefix: "/identity-templates" });
|
await server.register(registerIdentityTemplateRouter, { prefix: "/identity-templates" });
|
||||||
|
|
||||||
await server.register(registerProjectTemplateRouter, { prefix: "/project-templates" });
|
await server.register(registerProjectTemplateRouter, { prefix: "/project-templates" });
|
||||||
|
|||||||
Reference in New Issue
Block a user