Compare commits
No commits in common. "oss" and "v1.3.1" have entirely different histories.
73 changed files with 668 additions and 5593 deletions
2
LICENSE
2
LICENSE
|
@ -1,3 +1,5 @@
|
||||||
|
Copyright (c) 2025 Fossorial, LLC.
|
||||||
|
|
||||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
Version 3, 19 November 2007
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
|
|
@ -64,14 +64,14 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
var config Config
|
var config Config
|
||||||
|
config.DoCrowdsecInstall = false
|
||||||
|
config.Secret = generateRandomSecretKey()
|
||||||
|
|
||||||
// check if there is already a config file
|
// check if there is already a config file
|
||||||
if _, err := os.Stat("config/config.yml"); err != nil {
|
if _, err := os.Stat("config/config.yml"); err != nil {
|
||||||
config = collectUserInput(reader)
|
config = collectUserInput(reader)
|
||||||
|
|
||||||
loadVersions(&config)
|
loadVersions(&config)
|
||||||
config.DoCrowdsecInstall = false
|
|
||||||
config.Secret = generateRandomSecretKey()
|
|
||||||
|
|
||||||
if err := createConfigFiles(config); err != nil {
|
if err := createConfigFiles(config); err != nil {
|
||||||
fmt.Printf("Error creating config files: %v\n", err)
|
fmt.Printf("Error creating config files: %v\n", err)
|
||||||
|
|
|
@ -109,7 +109,7 @@ export async function checkUserActionPermission(
|
||||||
try {
|
try {
|
||||||
let userRoleIds = req.userRoleIds;
|
let userRoleIds = req.userRoleIds;
|
||||||
|
|
||||||
// If userRoleIds is not available on the request, fetch it
|
// If userOrgRoleId is not available on the request, fetch it
|
||||||
if (userRoleIds === undefined) {
|
if (userRoleIds === undefined) {
|
||||||
const userOrgRoles = await db
|
const userOrgRoles = await db
|
||||||
.select({ roleId: userOrgs.roleId })
|
.select({ roleId: userOrgs.roleId })
|
||||||
|
|
|
@ -5,8 +5,7 @@ import { createApiServer } from "./apiServer";
|
||||||
import { createNextServer } from "./nextServer";
|
import { createNextServer } from "./nextServer";
|
||||||
import { createInternalServer } from "./internalServer";
|
import { createInternalServer } from "./internalServer";
|
||||||
import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "./db/schemas";
|
import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "./db/schemas";
|
||||||
import { createIntegrationApiServer } from "./integrationApiServer";
|
// import { createIntegrationApiServer } from "./integrationApiServer";
|
||||||
import config from "@server/lib/config";
|
|
||||||
|
|
||||||
async function startServers() {
|
async function startServers() {
|
||||||
await runSetupFunctions();
|
await runSetupFunctions();
|
||||||
|
@ -17,9 +16,7 @@ async function startServers() {
|
||||||
const nextServer = await createNextServer();
|
const nextServer = await createNextServer();
|
||||||
|
|
||||||
let integrationServer;
|
let integrationServer;
|
||||||
if (config.getRawConfig().flags?.enable_integration_api) {
|
// integrationServer = createIntegrationApiServer();
|
||||||
integrationServer = createIntegrationApiServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
apiServer,
|
apiServer,
|
||||||
|
|
|
@ -1,102 +0,0 @@
|
||||||
import express from "express";
|
|
||||||
import cors from "cors";
|
|
||||||
import cookieParser from "cookie-parser";
|
|
||||||
import config from "@server/lib/config";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import {
|
|
||||||
errorHandlerMiddleware,
|
|
||||||
notFoundMiddleware,
|
|
||||||
} from "@server/middlewares";
|
|
||||||
import { authenticated, unauthenticated } from "@server/routers/integration";
|
|
||||||
import { logIncomingMiddleware } from "./middlewares/logIncoming";
|
|
||||||
import helmet from "helmet";
|
|
||||||
import swaggerUi from "swagger-ui-express";
|
|
||||||
import { OpenApiGeneratorV3 } from "@asteasolutions/zod-to-openapi";
|
|
||||||
import { registry } from "./openApi";
|
|
||||||
|
|
||||||
const dev = process.env.ENVIRONMENT !== "prod";
|
|
||||||
const externalPort = config.getRawConfig().server.integration_port;
|
|
||||||
|
|
||||||
export function createIntegrationApiServer() {
|
|
||||||
const apiServer = express();
|
|
||||||
|
|
||||||
if (config.getRawConfig().server.trust_proxy) {
|
|
||||||
apiServer.set("trust proxy", 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
apiServer.use(cors());
|
|
||||||
|
|
||||||
if (!dev) {
|
|
||||||
apiServer.use(helmet());
|
|
||||||
}
|
|
||||||
|
|
||||||
apiServer.use(cookieParser());
|
|
||||||
apiServer.use(express.json());
|
|
||||||
|
|
||||||
apiServer.use(
|
|
||||||
"/v1/docs",
|
|
||||||
swaggerUi.serve,
|
|
||||||
swaggerUi.setup(getOpenApiDocumentation())
|
|
||||||
);
|
|
||||||
|
|
||||||
// API routes
|
|
||||||
const prefix = `/v1`;
|
|
||||||
apiServer.use(logIncomingMiddleware);
|
|
||||||
apiServer.use(prefix, unauthenticated);
|
|
||||||
apiServer.use(prefix, authenticated);
|
|
||||||
|
|
||||||
// Error handling
|
|
||||||
apiServer.use(notFoundMiddleware);
|
|
||||||
apiServer.use(errorHandlerMiddleware);
|
|
||||||
|
|
||||||
// Create HTTP server
|
|
||||||
const httpServer = apiServer.listen(externalPort, (err?: any) => {
|
|
||||||
if (err) throw err;
|
|
||||||
logger.info(
|
|
||||||
`Integration API server is running on http://localhost:${externalPort}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
return httpServer;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getOpenApiDocumentation() {
|
|
||||||
const bearerAuth = registry.registerComponent(
|
|
||||||
"securitySchemes",
|
|
||||||
"Bearer Auth",
|
|
||||||
{
|
|
||||||
type: "http",
|
|
||||||
scheme: "bearer"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const def of registry.definitions) {
|
|
||||||
if (def.type === "route") {
|
|
||||||
def.route.security = [
|
|
||||||
{
|
|
||||||
[bearerAuth.name]: []
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
registry.registerPath({
|
|
||||||
method: "get",
|
|
||||||
path: "/",
|
|
||||||
description: "Health check",
|
|
||||||
tags: [],
|
|
||||||
request: {},
|
|
||||||
responses: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
const generator = new OpenApiGeneratorV3(registry.definitions);
|
|
||||||
|
|
||||||
return generator.generateDocument({
|
|
||||||
openapi: "3.0.0",
|
|
||||||
info: {
|
|
||||||
version: "v1",
|
|
||||||
title: "Pangolin Integration API"
|
|
||||||
},
|
|
||||||
servers: [{ url: "/v1" }]
|
|
||||||
});
|
|
||||||
}
|
|
|
@ -25,12 +25,9 @@ const configSchema = z.object({
|
||||||
.optional()
|
.optional()
|
||||||
.pipe(z.string().url())
|
.pipe(z.string().url())
|
||||||
.transform((url) => url.toLowerCase()),
|
.transform((url) => url.toLowerCase()),
|
||||||
log_level: z
|
log_level: z.enum(["debug", "info", "warn", "error"]),
|
||||||
.enum(["debug", "info", "warn", "error"])
|
save_logs: z.boolean(),
|
||||||
.optional()
|
log_failed_attempts: z.boolean().optional()
|
||||||
.default("info"),
|
|
||||||
save_logs: z.boolean().optional().default(false),
|
|
||||||
log_failed_attempts: z.boolean().optional().default(false)
|
|
||||||
}),
|
}),
|
||||||
domains: z
|
domains: z
|
||||||
.record(
|
.record(
|
||||||
|
@ -40,8 +37,8 @@ const configSchema = z.object({
|
||||||
.string()
|
.string()
|
||||||
.nonempty("base_domain must not be empty")
|
.nonempty("base_domain must not be empty")
|
||||||
.transform((url) => url.toLowerCase()),
|
.transform((url) => url.toLowerCase()),
|
||||||
cert_resolver: z.string().optional().default("letsencrypt"),
|
cert_resolver: z.string().optional(),
|
||||||
prefer_wildcard_cert: z.boolean().optional().default(false)
|
prefer_wildcard_cert: z.boolean().optional()
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
|
@ -61,42 +58,19 @@ const configSchema = z.object({
|
||||||
server: z.object({
|
server: z.object({
|
||||||
integration_port: portSchema
|
integration_port: portSchema
|
||||||
.optional()
|
.optional()
|
||||||
.default(3003)
|
|
||||||
.transform(stoi)
|
.transform(stoi)
|
||||||
.pipe(portSchema.optional()),
|
.pipe(portSchema.optional()),
|
||||||
external_port: portSchema
|
external_port: portSchema.optional().transform(stoi).pipe(portSchema),
|
||||||
.optional()
|
internal_port: portSchema.optional().transform(stoi).pipe(portSchema),
|
||||||
.default(3000)
|
next_port: portSchema.optional().transform(stoi).pipe(portSchema),
|
||||||
.transform(stoi)
|
internal_hostname: z.string().transform((url) => url.toLowerCase()),
|
||||||
.pipe(portSchema),
|
session_cookie_name: z.string(),
|
||||||
internal_port: portSchema
|
resource_access_token_param: z.string(),
|
||||||
.optional()
|
resource_access_token_headers: z.object({
|
||||||
.default(3001)
|
id: z.string(),
|
||||||
.transform(stoi)
|
token: z.string()
|
||||||
.pipe(portSchema),
|
}),
|
||||||
next_port: portSchema
|
resource_session_request_param: z.string(),
|
||||||
.optional()
|
|
||||||
.default(3002)
|
|
||||||
.transform(stoi)
|
|
||||||
.pipe(portSchema),
|
|
||||||
internal_hostname: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.default("pangolin")
|
|
||||||
.transform((url) => url.toLowerCase()),
|
|
||||||
session_cookie_name: z.string().optional().default("p_session_token"),
|
|
||||||
resource_access_token_param: z.string().optional().default("p_token"),
|
|
||||||
resource_access_token_headers: z
|
|
||||||
.object({
|
|
||||||
id: z.string().optional().default("P-Access-Token-Id"),
|
|
||||||
token: z.string().optional().default("P-Access-Token")
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.default({}),
|
|
||||||
resource_session_request_param: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.default("resource_session_request_param"),
|
|
||||||
dashboard_session_length_hours: z
|
dashboard_session_length_hours: z
|
||||||
.number()
|
.number()
|
||||||
.positive()
|
.positive()
|
||||||
|
@ -124,61 +98,35 @@ const configSchema = z.object({
|
||||||
.transform(getEnvOrYaml("SERVER_SECRET"))
|
.transform(getEnvOrYaml("SERVER_SECRET"))
|
||||||
.pipe(z.string().min(8))
|
.pipe(z.string().min(8))
|
||||||
}),
|
}),
|
||||||
traefik: z
|
traefik: z.object({
|
||||||
.object({
|
http_entrypoint: z.string(),
|
||||||
http_entrypoint: z.string().optional().default("web"),
|
https_entrypoint: z.string().optional(),
|
||||||
https_entrypoint: z.string().optional().default("websecure"),
|
|
||||||
additional_middlewares: z.array(z.string()).optional()
|
additional_middlewares: z.array(z.string()).optional()
|
||||||
})
|
}),
|
||||||
.optional()
|
gerbil: z.object({
|
||||||
.default({}),
|
start_port: portSchema.optional().transform(stoi).pipe(portSchema),
|
||||||
gerbil: z
|
|
||||||
.object({
|
|
||||||
start_port: portSchema
|
|
||||||
.optional()
|
|
||||||
.default(51820)
|
|
||||||
.transform(stoi)
|
|
||||||
.pipe(portSchema),
|
|
||||||
base_endpoint: z
|
base_endpoint: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
.pipe(z.string())
|
.pipe(z.string())
|
||||||
.transform((url) => url.toLowerCase()),
|
.transform((url) => url.toLowerCase()),
|
||||||
use_subdomain: z.boolean().optional().default(false),
|
use_subdomain: z.boolean(),
|
||||||
subnet_group: z.string().optional().default("100.89.137.0/20"),
|
subnet_group: z.string(),
|
||||||
block_size: z.number().positive().gt(0).optional().default(24),
|
block_size: z.number().positive().gt(0),
|
||||||
site_block_size: z.number().positive().gt(0).optional().default(30)
|
site_block_size: z.number().positive().gt(0)
|
||||||
})
|
}),
|
||||||
.optional()
|
rate_limits: z.object({
|
||||||
.default({}),
|
global: z.object({
|
||||||
rate_limits: z
|
window_minutes: z.number().positive().gt(0),
|
||||||
.object({
|
max_requests: z.number().positive().gt(0)
|
||||||
global: z
|
}),
|
||||||
.object({
|
|
||||||
window_minutes: z
|
|
||||||
.number()
|
|
||||||
.positive()
|
|
||||||
.gt(0)
|
|
||||||
.optional()
|
|
||||||
.default(1),
|
|
||||||
max_requests: z
|
|
||||||
.number()
|
|
||||||
.positive()
|
|
||||||
.gt(0)
|
|
||||||
.optional()
|
|
||||||
.default(500)
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.default({}),
|
|
||||||
auth: z
|
auth: z
|
||||||
.object({
|
.object({
|
||||||
window_minutes: z.number().positive().gt(0),
|
window_minutes: z.number().positive().gt(0),
|
||||||
max_requests: z.number().positive().gt(0)
|
max_requests: z.number().positive().gt(0)
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
})
|
}),
|
||||||
.optional()
|
|
||||||
.default({}),
|
|
||||||
email: z
|
email: z
|
||||||
.object({
|
.object({
|
||||||
smtp_host: z.string().optional(),
|
smtp_host: z.string().optional(),
|
||||||
|
@ -212,8 +160,7 @@ const configSchema = z.object({
|
||||||
disable_user_create_org: z.boolean().optional(),
|
disable_user_create_org: z.boolean().optional(),
|
||||||
allow_raw_resources: z.boolean().optional(),
|
allow_raw_resources: z.boolean().optional(),
|
||||||
allow_base_domain_resources: z.boolean().optional(),
|
allow_base_domain_resources: z.boolean().optional(),
|
||||||
allow_local_sites: z.boolean().optional(),
|
allow_local_sites: z.boolean().optional()
|
||||||
enable_integration_api: z.boolean().optional()
|
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,7 +2,7 @@ import path from "path";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
|
|
||||||
// This is a placeholder value replaced by the build process
|
// This is a placeholder value replaced by the build process
|
||||||
export const APP_VERSION = "1.4.0";
|
export const APP_VERSION = "1.3.0";
|
||||||
|
|
||||||
export const __FILENAME = fileURLToPath(import.meta.url);
|
export const __FILENAME = fileURLToPath(import.meta.url);
|
||||||
export const __DIRNAME = path.dirname(__FILENAME);
|
export const __DIRNAME = path.dirname(__FILENAME);
|
||||||
|
|
|
@ -9,10 +9,6 @@ export function isValidIP(ip: string): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValidUrlGlobPattern(pattern: string): boolean {
|
export function isValidUrlGlobPattern(pattern: string): boolean {
|
||||||
if (pattern === "/") {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove leading slash if present
|
// Remove leading slash if present
|
||||||
pattern = pattern.startsWith("/") ? pattern.slice(1) : pattern;
|
pattern = pattern.startsWith("/") ? pattern.slice(1) : pattern;
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,6 @@ export * from "./verifyUserInRole";
|
||||||
export * from "./verifyAccessTokenAccess";
|
export * from "./verifyAccessTokenAccess";
|
||||||
export * from "./verifyUserIsServerAdmin";
|
export * from "./verifyUserIsServerAdmin";
|
||||||
export * from "./verifyIsLoggedInUser";
|
export * from "./verifyIsLoggedInUser";
|
||||||
export * from "./integration";
|
// export * from "./integration";
|
||||||
export * from "./verifyUserHasAction";
|
export * from "./verifyUserHasAction";
|
||||||
export * from "./verifyApiKeyAccess";
|
// export * from "./verifyApiKeyAccess";
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
export * from "./verifyApiKey";
|
|
||||||
export * from "./verifyApiKeyOrgAccess";
|
|
||||||
export * from "./verifyApiKeyHasAction";
|
|
||||||
export * from "./verifyApiKeySiteAccess";
|
|
||||||
export * from "./verifyApiKeyResourceAccess";
|
|
||||||
export * from "./verifyApiKeyTargetAccess";
|
|
||||||
export * from "./verifyApiKeyRoleAccess";
|
|
||||||
export * from "./verifyApiKeyUserAccess";
|
|
||||||
export * from "./verifyApiKeySetResourceUsers";
|
|
||||||
export * from "./verifyAccessTokenAccess";
|
|
||||||
export * from "./verifyApiKeyIsRoot";
|
|
||||||
export * from "./verifyApiKeyApiKeyAccess";
|
|
|
@ -1,110 +0,0 @@
|
||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { db } from "@server/db";
|
|
||||||
import { resourceAccessToken, resources, apiKeyOrg } from "@server/db/schemas";
|
|
||||||
import { and, eq } from "drizzle-orm";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
|
|
||||||
export async function verifyApiKeyAccessTokenAccess(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const apiKey = req.apiKey;
|
|
||||||
const accessTokenId = req.params.accessTokenId;
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [accessToken] = await db
|
|
||||||
.select()
|
|
||||||
.from(resourceAccessToken)
|
|
||||||
.where(eq(resourceAccessToken.accessTokenId, accessTokenId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!accessToken) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`Access token with ID ${accessTokenId} not found`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const resourceId = accessToken.resourceId;
|
|
||||||
|
|
||||||
if (!resourceId) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
`Access token with ID ${accessTokenId} does not have a resource ID`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [resource] = await db
|
|
||||||
.select()
|
|
||||||
.from(resources)
|
|
||||||
.where(eq(resources.resourceId, resourceId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!resource) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`Resource with ID ${resourceId} not found`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!resource.orgId) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
`Resource with ID ${resourceId} does not have an organization ID`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that the API key is linked to the resource's organization
|
|
||||||
if (!req.apiKeyOrg) {
|
|
||||||
const apiKeyOrgResult = await db
|
|
||||||
.select()
|
|
||||||
.from(apiKeyOrg)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
|
||||||
eq(apiKeyOrg.orgId, resource.orgId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (apiKeyOrgResult.length > 0) {
|
|
||||||
req.apiKeyOrg = apiKeyOrgResult[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!req.apiKeyOrg) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
"Key does not have access to this organization"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return next();
|
|
||||||
} catch (e) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Error verifying access token access"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,60 +0,0 @@
|
||||||
import { verifyPassword } from "@server/auth/password";
|
|
||||||
import db from "@server/db";
|
|
||||||
import { apiKeys } from "@server/db/schemas";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
|
|
||||||
export async function verifyApiKey(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
const authHeader = req.headers["authorization"];
|
|
||||||
|
|
||||||
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.UNAUTHORIZED, "API key required")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = authHeader.split(" ")[1]; // Get the token part after "Bearer"
|
|
||||||
const [apiKeyId, apiKeySecret] = key.split(".");
|
|
||||||
|
|
||||||
const [apiKey] = await db
|
|
||||||
.select()
|
|
||||||
.from(apiKeys)
|
|
||||||
.where(eq(apiKeys.apiKeyId, apiKeyId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.UNAUTHORIZED, "Invalid API key")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const secretHash = apiKey.apiKeyHash;
|
|
||||||
const valid = await verifyPassword(apiKeySecret, secretHash);
|
|
||||||
|
|
||||||
if (!valid) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.UNAUTHORIZED, "Invalid API key")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
req.apiKey = apiKey;
|
|
||||||
|
|
||||||
return next();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error);
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"An error occurred checking API key"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,81 +0,0 @@
|
||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { db } from "@server/db";
|
|
||||||
import { apiKeys, apiKeyOrg } from "@server/db/schemas";
|
|
||||||
import { and, eq, or } from "drizzle-orm";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
|
|
||||||
export async function verifyApiKeyApiKeyAccess(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const {apiKey: callerApiKey } = req;
|
|
||||||
|
|
||||||
const apiKeyId =
|
|
||||||
req.params.apiKeyId || req.body.apiKeyId || req.query.apiKeyId;
|
|
||||||
const orgId = req.params.orgId;
|
|
||||||
|
|
||||||
if (!callerApiKey) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!orgId) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!apiKeyId) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid key ID")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [callerApiKeyOrg] = await db
|
|
||||||
.select()
|
|
||||||
.from(apiKeyOrg)
|
|
||||||
.where(
|
|
||||||
and(eq(apiKeys.apiKeyId, callerApiKey.apiKeyId), eq(apiKeyOrg.orgId, orgId))
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!callerApiKeyOrg) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
`API key with ID ${apiKeyId} does not have an organization ID`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [otherApiKeyOrg] = await db
|
|
||||||
.select()
|
|
||||||
.from(apiKeyOrg)
|
|
||||||
.where(
|
|
||||||
and(eq(apiKeys.apiKeyId, apiKeyId), eq(apiKeyOrg.orgId, orgId))
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!otherApiKeyOrg) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
`API key with ID ${apiKeyId} does not have access to organization with ID ${orgId}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return next();
|
|
||||||
} catch (error) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Error verifying key access"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import { ActionsEnum } from "@server/auth/actions";
|
|
||||||
import db from "@server/db";
|
|
||||||
import { apiKeyActions } from "@server/db/schemas";
|
|
||||||
import { and, eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
export function verifyApiKeyHasAction(action: ActionsEnum) {
|
|
||||||
return async function (
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
): Promise<any> {
|
|
||||||
try {
|
|
||||||
if (!req.apiKey) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.UNAUTHORIZED,
|
|
||||||
"API Key not authenticated"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [actionRes] = await db
|
|
||||||
.select()
|
|
||||||
.from(apiKeyActions)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(apiKeyActions.apiKeyId, req.apiKey.apiKeyId),
|
|
||||||
eq(apiKeyActions.actionId, action)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!actionRes) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
"Key does not have permission perform this action"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return next();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error verifying key action access:", error);
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Error verifying key action access"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
import logger from "@server/logger";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
|
|
||||||
export async function verifyApiKeyIsRoot(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
|
||||||
const { apiKey } = req;
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!apiKey.isRoot) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
"Key does not have root access"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return next();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error);
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"An error occurred checking API key"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { db } from "@server/db";
|
|
||||||
import { apiKeyOrg } from "@server/db/schemas";
|
|
||||||
import { and, eq } from "drizzle-orm";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
|
|
||||||
export async function verifyApiKeyOrgAccess(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const apiKeyId = req.apiKey?.apiKeyId;
|
|
||||||
const orgId = req.params.orgId;
|
|
||||||
|
|
||||||
if (!apiKeyId) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!orgId) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!req.apiKeyOrg) {
|
|
||||||
const apiKeyOrgRes = await db
|
|
||||||
.select()
|
|
||||||
.from(apiKeyOrg)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(apiKeyOrg.apiKeyId, apiKeyId),
|
|
||||||
eq(apiKeyOrg.orgId, orgId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
req.apiKeyOrg = apiKeyOrgRes[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!req.apiKeyOrg) {
|
|
||||||
next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
"Key does not have access to this organization"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return next();
|
|
||||||
} catch (e) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Error verifying organization access"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,85 +0,0 @@
|
||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { db } from "@server/db";
|
|
||||||
import { resources, apiKeyOrg } from "@server/db/schemas";
|
|
||||||
import { eq, and } from "drizzle-orm";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
|
|
||||||
export async function verifyApiKeyResourceAccess(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) {
|
|
||||||
const apiKey = req.apiKey;
|
|
||||||
const resourceId =
|
|
||||||
req.params.resourceId || req.body.resourceId || req.query.resourceId;
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Retrieve the resource
|
|
||||||
const [resource] = await db
|
|
||||||
.select()
|
|
||||||
.from(resources)
|
|
||||||
.where(eq(resources.resourceId, resourceId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!resource) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`Resource with ID ${resourceId} not found`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!resource.orgId) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
`Resource with ID ${resourceId} does not have an organization ID`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that the API key is linked to the resource's organization
|
|
||||||
if (!req.apiKeyOrg) {
|
|
||||||
const apiKeyOrgResult = await db
|
|
||||||
.select()
|
|
||||||
.from(apiKeyOrg)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
|
||||||
eq(apiKeyOrg.orgId, resource.orgId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (apiKeyOrgResult.length > 0) {
|
|
||||||
req.apiKeyOrg = apiKeyOrgResult[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!req.apiKeyOrg) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
"Key does not have access to this organization"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return next();
|
|
||||||
} catch (error) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Error verifying resource access"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,127 +0,0 @@
|
||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { db } from "@server/db";
|
|
||||||
import { roles, apiKeyOrg } from "@server/db/schemas";
|
|
||||||
import { and, eq, inArray } from "drizzle-orm";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
|
|
||||||
export async function verifyApiKeyRoleAccess(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const apiKey = req.apiKey;
|
|
||||||
const singleRoleId = parseInt(
|
|
||||||
req.params.roleId || req.body.roleId || req.query.roleId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { roleIds } = req.body;
|
|
||||||
const allRoleIds =
|
|
||||||
roleIds || (isNaN(singleRoleId) ? [] : [singleRoleId]);
|
|
||||||
|
|
||||||
if (allRoleIds.length === 0) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
const rolesData = await db
|
|
||||||
.select()
|
|
||||||
.from(roles)
|
|
||||||
.where(inArray(roles.roleId, allRoleIds));
|
|
||||||
|
|
||||||
if (rolesData.length !== allRoleIds.length) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
"One or more roles not found"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const orgIds = new Set(rolesData.map((role) => role.orgId));
|
|
||||||
|
|
||||||
for (const role of rolesData) {
|
|
||||||
const apiKeyOrgAccess = await db
|
|
||||||
.select()
|
|
||||||
.from(apiKeyOrg)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
|
||||||
eq(apiKeyOrg.orgId, role.orgId!)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (apiKeyOrgAccess.length === 0) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
`Key does not have access to organization for role ID ${role.roleId}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (orgIds.size > 1) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
"Roles must belong to the same organization"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const orgId = orgIds.values().next().value;
|
|
||||||
|
|
||||||
if (!orgId) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Roles do not have an organization ID"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!req.apiKeyOrg) {
|
|
||||||
// Retrieve the API key's organization link if not already set
|
|
||||||
const apiKeyOrgRes = await db
|
|
||||||
.select()
|
|
||||||
.from(apiKeyOrg)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
|
||||||
eq(apiKeyOrg.orgId, orgId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (apiKeyOrgRes.length === 0) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
"Key does not have access to this organization"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
req.apiKeyOrg = apiKeyOrgRes[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
return next();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error verifying role access:", error);
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Error verifying role access"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,69 +0,0 @@
|
||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { db } from "@server/db";
|
|
||||||
import { userOrgs } from "@server/db/schemas";
|
|
||||||
import { and, eq, inArray } from "drizzle-orm";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
|
|
||||||
export async function verifyApiKeySetResourceUsers(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) {
|
|
||||||
const apiKey = req.apiKey;
|
|
||||||
const userIds = req.body.userIds;
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!req.apiKeyOrg) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
"Key does not have access to this organization"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!userIds) {
|
|
||||||
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid user IDs"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userIds.length === 0) {
|
|
||||||
return next();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const orgId = req.apiKeyOrg.orgId;
|
|
||||||
const userOrgsData = await db
|
|
||||||
.select()
|
|
||||||
.from(userOrgs)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
inArray(userOrgs.userId, userIds),
|
|
||||||
eq(userOrgs.orgId, orgId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (userOrgsData.length !== userIds.length) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
"Key does not have access to one or more specified users"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return next();
|
|
||||||
} catch (error) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Error checking if key has access to the specified users"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,89 +0,0 @@
|
||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { db } from "@server/db";
|
|
||||||
import {
|
|
||||||
sites,
|
|
||||||
apiKeyOrg
|
|
||||||
} from "@server/db/schemas";
|
|
||||||
import { and, eq, or } from "drizzle-orm";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
|
|
||||||
export async function verifyApiKeySiteAccess(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const apiKey = req.apiKey;
|
|
||||||
const siteId = parseInt(
|
|
||||||
req.params.siteId || req.body.siteId || req.query.siteId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNaN(siteId)) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid site ID")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const site = await db
|
|
||||||
.select()
|
|
||||||
.from(sites)
|
|
||||||
.where(eq(sites.siteId, siteId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (site.length === 0) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`Site with ID ${siteId} not found`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!site[0].orgId) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
`Site with ID ${siteId} does not have an organization ID`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!req.apiKeyOrg) {
|
|
||||||
const apiKeyOrgRes = await db
|
|
||||||
.select()
|
|
||||||
.from(apiKeyOrg)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
|
||||||
eq(apiKeyOrg.orgId, site[0].orgId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
req.apiKeyOrg = apiKeyOrgRes[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!req.apiKeyOrg) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
"Key does not have access to this organization"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return next();
|
|
||||||
} catch (error) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Error verifying site access"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,112 +0,0 @@
|
||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { db } from "@server/db";
|
|
||||||
import { resources, targets, apiKeyOrg } from "@server/db/schemas";
|
|
||||||
import { and, eq } from "drizzle-orm";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
|
|
||||||
export async function verifyApiKeyTargetAccess(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const apiKey = req.apiKey;
|
|
||||||
const targetId = parseInt(req.params.targetId);
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isNaN(targetId)) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid target ID")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [target] = await db
|
|
||||||
.select()
|
|
||||||
.from(targets)
|
|
||||||
.where(eq(targets.targetId, targetId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!target) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`Target with ID ${targetId} not found`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const resourceId = target.resourceId;
|
|
||||||
if (!resourceId) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
`Target with ID ${targetId} does not have a resource ID`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [resource] = await db
|
|
||||||
.select()
|
|
||||||
.from(resources)
|
|
||||||
.where(eq(resources.resourceId, resourceId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!resource) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`Resource with ID ${resourceId} not found`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!resource.orgId) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
`Resource with ID ${resourceId} does not have an organization ID`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!req.apiKeyOrg) {
|
|
||||||
const apiKeyOrgResult = await db
|
|
||||||
.select()
|
|
||||||
.from(apiKeyOrg)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId),
|
|
||||||
eq(apiKeyOrg.orgId, resource.orgId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
if (apiKeyOrgResult.length > 0) {
|
|
||||||
req.apiKeyOrg = apiKeyOrgResult[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!req.apiKeyOrg) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
"Key does not have access to this organization"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return next();
|
|
||||||
} catch (error) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Error verifying target access"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,67 +0,0 @@
|
||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { db } from "@server/db";
|
|
||||||
import { userOrgs } from "@server/db/schemas";
|
|
||||||
import { and, eq } from "drizzle-orm";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
|
|
||||||
export async function verifyApiKeyUserAccess(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const apiKey = req.apiKey;
|
|
||||||
const reqUserId =
|
|
||||||
req.params.userId || req.body.userId || req.query.userId;
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!reqUserId) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid user ID")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!req.apiKeyOrg || !req.apiKeyOrg.orgId) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
"Key does not have organization access"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const orgId = req.apiKeyOrg.orgId;
|
|
||||||
|
|
||||||
const [userOrgRecord] = await db
|
|
||||||
.select()
|
|
||||||
.from(userOrgs)
|
|
||||||
.where(
|
|
||||||
and(eq(userOrgs.userId, reqUserId), eq(userOrgs.orgId, orgId))
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!userOrgRecord) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
"Key does not have access to this user"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return next();
|
|
||||||
} catch (error) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Error checking if key has access to this user"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,96 +0,0 @@
|
||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { db } from "@server/db";
|
|
||||||
import { userOrgs, apiKeys, apiKeyOrg } from "@server/db/schemas";
|
|
||||||
import { and, eq, or } from "drizzle-orm";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
|
|
||||||
export async function verifyApiKeyAccess(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const userId = req.user!.userId;
|
|
||||||
const apiKeyId =
|
|
||||||
req.params.apiKeyId || req.body.apiKeyId || req.query.apiKeyId;
|
|
||||||
const orgId = req.params.orgId;
|
|
||||||
|
|
||||||
if (!userId) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.UNAUTHORIZED, "User not authenticated")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!orgId) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid organization ID")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!apiKeyId) {
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.BAD_REQUEST, "Invalid key ID")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [apiKey] = await db
|
|
||||||
.select()
|
|
||||||
.from(apiKeys)
|
|
||||||
.innerJoin(apiKeyOrg, eq(apiKeys.apiKeyId, apiKeyOrg.apiKeyId))
|
|
||||||
.where(
|
|
||||||
and(eq(apiKeys.apiKeyId, apiKeyId), eq(apiKeyOrg.orgId, orgId))
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!apiKey.apiKeys) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`API key with ID ${apiKeyId} not found`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!apiKeyOrg.orgId) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
`API key with ID ${apiKeyId} does not have an organization ID`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!req.userOrg) {
|
|
||||||
const userOrgRole = await db
|
|
||||||
.select()
|
|
||||||
.from(userOrgs)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(userOrgs.userId, userId),
|
|
||||||
eq(userOrgs.orgId, apiKeyOrg.orgId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
req.userOrg = userOrgRole[0];
|
|
||||||
req.userRoleIds = userOrgRole.map((r) => r.roleId);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!req.userOrg) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
"User does not have access to this organization"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return next();
|
|
||||||
} catch (error) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Error verifying key access"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,128 +0,0 @@
|
||||||
import { NextFunction, Request, Response } from "express";
|
|
||||||
import db from "@server/db";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { apiKeyOrg, apiKeys } from "@server/db/schemas";
|
|
||||||
import { fromError } from "zod-validation-error";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import response from "@server/lib/response";
|
|
||||||
import moment from "moment";
|
|
||||||
import {
|
|
||||||
generateId,
|
|
||||||
generateIdFromEntropySize
|
|
||||||
} from "@server/auth/sessions/app";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import { hashPassword } from "@server/auth/password";
|
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
|
||||||
|
|
||||||
const paramsSchema = z.object({
|
|
||||||
orgId: z.string().nonempty()
|
|
||||||
});
|
|
||||||
|
|
||||||
const bodySchema = z.object({
|
|
||||||
name: z.string().min(1).max(255)
|
|
||||||
});
|
|
||||||
|
|
||||||
export type CreateOrgApiKeyBody = z.infer<typeof bodySchema>;
|
|
||||||
|
|
||||||
export type CreateOrgApiKeyResponse = {
|
|
||||||
apiKeyId: string;
|
|
||||||
name: string;
|
|
||||||
apiKey: string;
|
|
||||||
lastChars: string;
|
|
||||||
createdAt: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.registerPath({
|
|
||||||
method: "put",
|
|
||||||
path: "/org/{orgId}/api-key",
|
|
||||||
description: "Create a new API key scoped to the organization.",
|
|
||||||
tags: [OpenAPITags.Org, OpenAPITags.ApiKey],
|
|
||||||
request: {
|
|
||||||
params: paramsSchema,
|
|
||||||
body: {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: bodySchema
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
responses: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function createOrgApiKey(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
): Promise<any> {
|
|
||||||
const parsedParams = paramsSchema.safeParse(req.params);
|
|
||||||
|
|
||||||
if (!parsedParams.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedParams.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedBody = bodySchema.safeParse(req.body);
|
|
||||||
|
|
||||||
if (!parsedBody.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedBody.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { orgId } = parsedParams.data;
|
|
||||||
const { name } = parsedBody.data;
|
|
||||||
|
|
||||||
const apiKeyId = generateId(15);
|
|
||||||
const apiKey = generateIdFromEntropySize(25);
|
|
||||||
const apiKeyHash = await hashPassword(apiKey);
|
|
||||||
const lastChars = apiKey.slice(-4);
|
|
||||||
const createdAt = moment().toISOString();
|
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
|
||||||
await trx.insert(apiKeys).values({
|
|
||||||
name,
|
|
||||||
apiKeyId,
|
|
||||||
apiKeyHash,
|
|
||||||
createdAt,
|
|
||||||
lastChars
|
|
||||||
});
|
|
||||||
|
|
||||||
await trx.insert(apiKeyOrg).values({
|
|
||||||
apiKeyId,
|
|
||||||
orgId
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
return response<CreateOrgApiKeyResponse>(res, {
|
|
||||||
data: {
|
|
||||||
apiKeyId,
|
|
||||||
apiKey,
|
|
||||||
name,
|
|
||||||
lastChars,
|
|
||||||
createdAt
|
|
||||||
},
|
|
||||||
success: true,
|
|
||||||
error: false,
|
|
||||||
message: "API key created",
|
|
||||||
status: HttpCode.CREATED
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(e);
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Failed to create API key"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,100 +0,0 @@
|
||||||
import { NextFunction, Request, Response } from "express";
|
|
||||||
import db from "@server/db";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { apiKeyOrg, apiKeys, orgs } from "@server/db/schemas";
|
|
||||||
import { fromError } from "zod-validation-error";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import response from "@server/lib/response";
|
|
||||||
import moment from "moment";
|
|
||||||
import {
|
|
||||||
generateId,
|
|
||||||
generateIdFromEntropySize
|
|
||||||
} from "@server/auth/sessions/app";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import { hashPassword } from "@server/auth/password";
|
|
||||||
|
|
||||||
const bodySchema = z
|
|
||||||
.object({
|
|
||||||
name: z.string().min(1).max(255)
|
|
||||||
})
|
|
||||||
.strict();
|
|
||||||
|
|
||||||
export type CreateRootApiKeyBody = z.infer<typeof bodySchema>;
|
|
||||||
|
|
||||||
export type CreateRootApiKeyResponse = {
|
|
||||||
apiKeyId: string;
|
|
||||||
name: string;
|
|
||||||
apiKey: string;
|
|
||||||
lastChars: string;
|
|
||||||
createdAt: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function createRootApiKey(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
): Promise<any> {
|
|
||||||
const parsedBody = bodySchema.safeParse(req.body);
|
|
||||||
|
|
||||||
if (!parsedBody.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedBody.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { name } = parsedBody.data;
|
|
||||||
|
|
||||||
const apiKeyId = generateId(15);
|
|
||||||
const apiKey = generateIdFromEntropySize(25);
|
|
||||||
const apiKeyHash = await hashPassword(apiKey);
|
|
||||||
const lastChars = apiKey.slice(-4);
|
|
||||||
const createdAt = moment().toISOString();
|
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
|
||||||
await trx.insert(apiKeys).values({
|
|
||||||
apiKeyId,
|
|
||||||
name,
|
|
||||||
apiKeyHash,
|
|
||||||
createdAt,
|
|
||||||
lastChars,
|
|
||||||
isRoot: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const allOrgs = await trx.select().from(orgs);
|
|
||||||
|
|
||||||
for (const org of allOrgs) {
|
|
||||||
await trx.insert(apiKeyOrg).values({
|
|
||||||
apiKeyId,
|
|
||||||
orgId: org.orgId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
return response<CreateRootApiKeyResponse>(res, {
|
|
||||||
data: {
|
|
||||||
apiKeyId,
|
|
||||||
name,
|
|
||||||
apiKey,
|
|
||||||
lastChars,
|
|
||||||
createdAt
|
|
||||||
},
|
|
||||||
success: true,
|
|
||||||
error: false,
|
|
||||||
message: "API key created",
|
|
||||||
status: HttpCode.CREATED
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(e);
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Failed to create API key"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { db } from "@server/db";
|
|
||||||
import { apiKeys } from "@server/db/schemas";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import response from "@server/lib/response";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import { fromError } from "zod-validation-error";
|
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
|
||||||
|
|
||||||
const paramsSchema = z.object({
|
|
||||||
apiKeyId: z.string().nonempty()
|
|
||||||
});
|
|
||||||
|
|
||||||
registry.registerPath({
|
|
||||||
method: "delete",
|
|
||||||
path: "/org/{orgId}/api-key/{apiKeyId}",
|
|
||||||
description: "Delete an API key.",
|
|
||||||
tags: [OpenAPITags.Org, OpenAPITags.ApiKey],
|
|
||||||
request: {
|
|
||||||
params: paramsSchema
|
|
||||||
},
|
|
||||||
responses: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function deleteApiKey(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
): Promise<any> {
|
|
||||||
try {
|
|
||||||
const parsedParams = paramsSchema.safeParse(req.params);
|
|
||||||
if (!parsedParams.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedParams.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { apiKeyId } = parsedParams.data;
|
|
||||||
|
|
||||||
const [apiKey] = await db
|
|
||||||
.select()
|
|
||||||
.from(apiKeys)
|
|
||||||
.where(eq(apiKeys.apiKeyId, apiKeyId))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`API Key with ID ${apiKeyId} not found`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.delete(apiKeys).where(eq(apiKeys.apiKeyId, apiKeyId));
|
|
||||||
|
|
||||||
return response(res, {
|
|
||||||
data: null,
|
|
||||||
success: true,
|
|
||||||
error: false,
|
|
||||||
message: "API key deleted successfully",
|
|
||||||
status: HttpCode.OK
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error);
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,99 +0,0 @@
|
||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { db } from "@server/db";
|
|
||||||
import { apiKeyOrg, apiKeys } from "@server/db/schemas";
|
|
||||||
import { and, eq } from "drizzle-orm";
|
|
||||||
import response from "@server/lib/response";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import { fromError } from "zod-validation-error";
|
|
||||||
|
|
||||||
const paramsSchema = z.object({
|
|
||||||
apiKeyId: z.string().nonempty(),
|
|
||||||
orgId: z.string().nonempty()
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function deleteOrgApiKey(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
): Promise<any> {
|
|
||||||
try {
|
|
||||||
const parsedParams = paramsSchema.safeParse(req.params);
|
|
||||||
if (!parsedParams.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedParams.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { apiKeyId, orgId } = parsedParams.data;
|
|
||||||
|
|
||||||
const [apiKey] = await db
|
|
||||||
.select()
|
|
||||||
.from(apiKeys)
|
|
||||||
.where(eq(apiKeys.apiKeyId, apiKeyId))
|
|
||||||
.innerJoin(
|
|
||||||
apiKeyOrg,
|
|
||||||
and(
|
|
||||||
eq(apiKeys.apiKeyId, apiKeyOrg.apiKeyId),
|
|
||||||
eq(apiKeyOrg.orgId, orgId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`API Key with ID ${apiKeyId} not found`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (apiKey.apiKeys.isRoot) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
"Cannot delete root API key"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
|
||||||
await trx
|
|
||||||
.delete(apiKeyOrg)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(apiKeyOrg.apiKeyId, apiKeyId),
|
|
||||||
eq(apiKeyOrg.orgId, orgId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const apiKeyOrgs = await db
|
|
||||||
.select()
|
|
||||||
.from(apiKeyOrg)
|
|
||||||
.where(eq(apiKeyOrg.apiKeyId, apiKeyId));
|
|
||||||
|
|
||||||
if (apiKeyOrgs.length === 0) {
|
|
||||||
await trx.delete(apiKeys).where(eq(apiKeys.apiKeyId, apiKeyId));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return response(res, {
|
|
||||||
data: null,
|
|
||||||
success: true,
|
|
||||||
error: false,
|
|
||||||
message: "API removed from organization",
|
|
||||||
status: HttpCode.OK
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error);
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { db } from "@server/db";
|
|
||||||
import { apiKeys } from "@server/db/schemas";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import response from "@server/lib/response";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import { fromError } from "zod-validation-error";
|
|
||||||
|
|
||||||
const paramsSchema = z.object({
|
|
||||||
apiKeyId: z.string().nonempty()
|
|
||||||
});
|
|
||||||
|
|
||||||
async function query(apiKeyId: string) {
|
|
||||||
return await db
|
|
||||||
.select({
|
|
||||||
apiKeyId: apiKeys.apiKeyId,
|
|
||||||
lastChars: apiKeys.lastChars,
|
|
||||||
createdAt: apiKeys.createdAt,
|
|
||||||
isRoot: apiKeys.isRoot,
|
|
||||||
name: apiKeys.name
|
|
||||||
})
|
|
||||||
.from(apiKeys)
|
|
||||||
.where(eq(apiKeys.apiKeyId, apiKeyId))
|
|
||||||
.limit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type GetApiKeyResponse = NonNullable<
|
|
||||||
Awaited<ReturnType<typeof query>>[0]
|
|
||||||
>;
|
|
||||||
|
|
||||||
export async function getApiKey(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
): Promise<any> {
|
|
||||||
try {
|
|
||||||
const parsedParams = paramsSchema.safeParse(req.params);
|
|
||||||
if (!parsedParams.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedParams.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { apiKeyId } = parsedParams.data;
|
|
||||||
|
|
||||||
const [apiKey] = await query(apiKeyId);
|
|
||||||
|
|
||||||
if (!apiKey) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`API Key with ID ${apiKeyId} not found`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response<GetApiKeyResponse>(res, {
|
|
||||||
data: apiKey,
|
|
||||||
success: true,
|
|
||||||
error: false,
|
|
||||||
message: "API key deleted successfully",
|
|
||||||
status: HttpCode.OK
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error);
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
export * from "./createRootApiKey";
|
|
||||||
export * from "./deleteApiKey";
|
|
||||||
export * from "./getApiKey";
|
|
||||||
export * from "./listApiKeyActions";
|
|
||||||
export * from "./listOrgApiKeys";
|
|
||||||
export * from "./listApiKeyActions";
|
|
||||||
export * from "./listRootApiKeys";
|
|
||||||
export * from "./setApiKeyActions";
|
|
||||||
export * from "./setApiKeyOrgs";
|
|
||||||
export * from "./createOrgApiKey";
|
|
||||||
export * from "./deleteOrgApiKey";
|
|
|
@ -1,113 +0,0 @@
|
||||||
import { db } from "@server/db";
|
|
||||||
import { actions, apiKeyActions, apiKeyOrg, apiKeys } from "@server/db/schemas";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import response from "@server/lib/response";
|
|
||||||
import { NextFunction, Request, Response } from "express";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { fromError } from "zod-validation-error";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
|
||||||
|
|
||||||
const paramsSchema = z.object({
|
|
||||||
apiKeyId: z.string().nonempty()
|
|
||||||
});
|
|
||||||
|
|
||||||
const querySchema = z.object({
|
|
||||||
limit: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.default("1000")
|
|
||||||
.transform(Number)
|
|
||||||
.pipe(z.number().int().positive()),
|
|
||||||
offset: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.default("0")
|
|
||||||
.transform(Number)
|
|
||||||
.pipe(z.number().int().nonnegative())
|
|
||||||
});
|
|
||||||
|
|
||||||
function queryActions(apiKeyId: string) {
|
|
||||||
return db
|
|
||||||
.select({
|
|
||||||
actionId: actions.actionId
|
|
||||||
})
|
|
||||||
.from(apiKeyActions)
|
|
||||||
.where(eq(apiKeyActions.apiKeyId, apiKeyId))
|
|
||||||
.innerJoin(actions, eq(actions.actionId, apiKeyActions.actionId));
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ListApiKeyActionsResponse = {
|
|
||||||
actions: Awaited<ReturnType<typeof queryActions>>;
|
|
||||||
pagination: { total: number; limit: number; offset: number };
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.registerPath({
|
|
||||||
method: "get",
|
|
||||||
path: "/org/{orgId}/api-key/{apiKeyId}/actions",
|
|
||||||
description:
|
|
||||||
"List all actions set for an API key.",
|
|
||||||
tags: [OpenAPITags.Org, OpenAPITags.ApiKey],
|
|
||||||
request: {
|
|
||||||
params: paramsSchema,
|
|
||||||
query: querySchema
|
|
||||||
},
|
|
||||||
responses: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function listApiKeyActions(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
): Promise<any> {
|
|
||||||
try {
|
|
||||||
const parsedQuery = querySchema.safeParse(req.query);
|
|
||||||
if (!parsedQuery.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedQuery.error)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedParams = paramsSchema.safeParse(req.params);
|
|
||||||
if (!parsedParams.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedParams.error)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { limit, offset } = parsedQuery.data;
|
|
||||||
const { apiKeyId } = parsedParams.data;
|
|
||||||
|
|
||||||
const baseQuery = queryActions(apiKeyId);
|
|
||||||
|
|
||||||
const actionsList = await baseQuery.limit(limit).offset(offset);
|
|
||||||
|
|
||||||
return response<ListApiKeyActionsResponse>(res, {
|
|
||||||
data: {
|
|
||||||
actions: actionsList,
|
|
||||||
pagination: {
|
|
||||||
total: actionsList.length,
|
|
||||||
limit,
|
|
||||||
offset
|
|
||||||
}
|
|
||||||
},
|
|
||||||
success: true,
|
|
||||||
error: false,
|
|
||||||
message: "API keys retrieved successfully",
|
|
||||||
status: HttpCode.OK
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error);
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,116 +0,0 @@
|
||||||
import { db } from "@server/db";
|
|
||||||
import { apiKeyOrg, apiKeys } from "@server/db/schemas";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import response from "@server/lib/response";
|
|
||||||
import { NextFunction, Request, Response } from "express";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { fromError } from "zod-validation-error";
|
|
||||||
import { eq, and } from "drizzle-orm";
|
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
|
||||||
|
|
||||||
const querySchema = z.object({
|
|
||||||
limit: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.default("1000")
|
|
||||||
.transform(Number)
|
|
||||||
.pipe(z.number().int().positive()),
|
|
||||||
offset: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.default("0")
|
|
||||||
.transform(Number)
|
|
||||||
.pipe(z.number().int().nonnegative())
|
|
||||||
});
|
|
||||||
|
|
||||||
const paramsSchema = z.object({
|
|
||||||
orgId: z.string()
|
|
||||||
});
|
|
||||||
|
|
||||||
function queryApiKeys(orgId: string) {
|
|
||||||
return db
|
|
||||||
.select({
|
|
||||||
apiKeyId: apiKeys.apiKeyId,
|
|
||||||
orgId: apiKeyOrg.orgId,
|
|
||||||
lastChars: apiKeys.lastChars,
|
|
||||||
createdAt: apiKeys.createdAt,
|
|
||||||
name: apiKeys.name
|
|
||||||
})
|
|
||||||
.from(apiKeyOrg)
|
|
||||||
.where(and(eq(apiKeyOrg.orgId, orgId), eq(apiKeys.isRoot, false)))
|
|
||||||
.innerJoin(apiKeys, eq(apiKeys.apiKeyId, apiKeyOrg.apiKeyId));
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ListOrgApiKeysResponse = {
|
|
||||||
apiKeys: Awaited<ReturnType<typeof queryApiKeys>>;
|
|
||||||
pagination: { total: number; limit: number; offset: number };
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.registerPath({
|
|
||||||
method: "get",
|
|
||||||
path: "/org/{orgId}/api-keys",
|
|
||||||
description: "List all API keys for an organization",
|
|
||||||
tags: [OpenAPITags.Org, OpenAPITags.ApiKey],
|
|
||||||
request: {
|
|
||||||
params: paramsSchema,
|
|
||||||
query: querySchema
|
|
||||||
},
|
|
||||||
responses: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function listOrgApiKeys(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
): Promise<any> {
|
|
||||||
try {
|
|
||||||
const parsedQuery = querySchema.safeParse(req.query);
|
|
||||||
if (!parsedQuery.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedQuery.error)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedParams = paramsSchema.safeParse(req.params);
|
|
||||||
if (!parsedParams.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedParams.error)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { limit, offset } = parsedQuery.data;
|
|
||||||
const { orgId } = parsedParams.data;
|
|
||||||
|
|
||||||
const baseQuery = queryApiKeys(orgId);
|
|
||||||
|
|
||||||
const apiKeysList = await baseQuery.limit(limit).offset(offset);
|
|
||||||
|
|
||||||
return response<ListOrgApiKeysResponse>(res, {
|
|
||||||
data: {
|
|
||||||
apiKeys: apiKeysList,
|
|
||||||
pagination: {
|
|
||||||
total: apiKeysList.length,
|
|
||||||
limit,
|
|
||||||
offset
|
|
||||||
}
|
|
||||||
},
|
|
||||||
success: true,
|
|
||||||
error: false,
|
|
||||||
message: "API keys retrieved successfully",
|
|
||||||
status: HttpCode.OK
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error);
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,85 +0,0 @@
|
||||||
import { db } from "@server/db";
|
|
||||||
import { apiKeys } from "@server/db/schemas";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import response from "@server/lib/response";
|
|
||||||
import { NextFunction, Request, Response } from "express";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { fromError } from "zod-validation-error";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
const querySchema = z.object({
|
|
||||||
limit: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.default("1000")
|
|
||||||
.transform(Number)
|
|
||||||
.pipe(z.number().int().positive()),
|
|
||||||
offset: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.default("0")
|
|
||||||
.transform(Number)
|
|
||||||
.pipe(z.number().int().nonnegative())
|
|
||||||
});
|
|
||||||
|
|
||||||
function queryApiKeys() {
|
|
||||||
return db
|
|
||||||
.select({
|
|
||||||
apiKeyId: apiKeys.apiKeyId,
|
|
||||||
lastChars: apiKeys.lastChars,
|
|
||||||
createdAt: apiKeys.createdAt,
|
|
||||||
name: apiKeys.name
|
|
||||||
})
|
|
||||||
.from(apiKeys)
|
|
||||||
.where(eq(apiKeys.isRoot, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ListRootApiKeysResponse = {
|
|
||||||
apiKeys: Awaited<ReturnType<typeof queryApiKeys>>;
|
|
||||||
pagination: { total: number; limit: number; offset: number };
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function listRootApiKeys(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
): Promise<any> {
|
|
||||||
try {
|
|
||||||
const parsedQuery = querySchema.safeParse(req.query);
|
|
||||||
if (!parsedQuery.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedQuery.error)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const { limit, offset } = parsedQuery.data;
|
|
||||||
|
|
||||||
const baseQuery = queryApiKeys();
|
|
||||||
|
|
||||||
const apiKeysList = await baseQuery.limit(limit).offset(offset);
|
|
||||||
|
|
||||||
return response<ListRootApiKeysResponse>(res, {
|
|
||||||
data: {
|
|
||||||
apiKeys: apiKeysList,
|
|
||||||
pagination: {
|
|
||||||
total: apiKeysList.length,
|
|
||||||
limit,
|
|
||||||
offset
|
|
||||||
}
|
|
||||||
},
|
|
||||||
success: true,
|
|
||||||
error: false,
|
|
||||||
message: "API keys retrieved successfully",
|
|
||||||
status: HttpCode.OK
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error);
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,136 +0,0 @@
|
||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { db } from "@server/db";
|
|
||||||
import { actions, apiKeyActions } from "@server/db/schemas";
|
|
||||||
import response from "@server/lib/response";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import { fromError } from "zod-validation-error";
|
|
||||||
import { eq, and, inArray } from "drizzle-orm";
|
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
|
||||||
|
|
||||||
const bodySchema = z
|
|
||||||
.object({
|
|
||||||
actionIds: z
|
|
||||||
.array(z.string().nonempty())
|
|
||||||
.transform((v) => Array.from(new Set(v)))
|
|
||||||
})
|
|
||||||
.strict();
|
|
||||||
|
|
||||||
const paramsSchema = z.object({
|
|
||||||
apiKeyId: z.string().nonempty()
|
|
||||||
});
|
|
||||||
|
|
||||||
registry.registerPath({
|
|
||||||
method: "post",
|
|
||||||
path: "/org/{orgId}/api-key/{apiKeyId}/actions",
|
|
||||||
description:
|
|
||||||
"Set actions for an API key. This will replace any existing actions.",
|
|
||||||
tags: [OpenAPITags.Org, OpenAPITags.ApiKey],
|
|
||||||
request: {
|
|
||||||
params: paramsSchema,
|
|
||||||
body: {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: bodySchema
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
responses: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function setApiKeyActions(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
): Promise<any> {
|
|
||||||
try {
|
|
||||||
const parsedBody = bodySchema.safeParse(req.body);
|
|
||||||
if (!parsedBody.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedBody.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { actionIds: newActionIds } = parsedBody.data;
|
|
||||||
|
|
||||||
const parsedParams = paramsSchema.safeParse(req.params);
|
|
||||||
if (!parsedParams.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedParams.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { apiKeyId } = parsedParams.data;
|
|
||||||
|
|
||||||
const actionsExist = await db
|
|
||||||
.select()
|
|
||||||
.from(actions)
|
|
||||||
.where(inArray(actions.actionId, newActionIds));
|
|
||||||
|
|
||||||
if (actionsExist.length !== newActionIds.length) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
"One or more actions do not exist"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
|
||||||
const existingActions = await trx
|
|
||||||
.select()
|
|
||||||
.from(apiKeyActions)
|
|
||||||
.where(eq(apiKeyActions.apiKeyId, apiKeyId));
|
|
||||||
|
|
||||||
const existingActionIds = existingActions.map((a) => a.actionId);
|
|
||||||
|
|
||||||
const actionIdsToAdd = newActionIds.filter(
|
|
||||||
(id) => !existingActionIds.includes(id)
|
|
||||||
);
|
|
||||||
const actionIdsToRemove = existingActionIds.filter(
|
|
||||||
(id) => !newActionIds.includes(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (actionIdsToRemove.length > 0) {
|
|
||||||
await trx
|
|
||||||
.delete(apiKeyActions)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(apiKeyActions.apiKeyId, apiKeyId),
|
|
||||||
inArray(apiKeyActions.actionId, actionIdsToRemove)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (actionIdsToAdd.length > 0) {
|
|
||||||
const insertValues = actionIdsToAdd.map((actionId) => ({
|
|
||||||
apiKeyId,
|
|
||||||
actionId
|
|
||||||
}));
|
|
||||||
await trx.insert(apiKeyActions).values(insertValues);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return response(res, {
|
|
||||||
data: {},
|
|
||||||
success: true,
|
|
||||||
error: false,
|
|
||||||
message: "API key actions updated successfully",
|
|
||||||
status: HttpCode.OK
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error);
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,117 +0,0 @@
|
||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { db } from "@server/db";
|
|
||||||
import { apiKeyOrg, orgs } from "@server/db/schemas";
|
|
||||||
import response from "@server/lib/response";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import { fromError } from "zod-validation-error";
|
|
||||||
import { eq, and, inArray } from "drizzle-orm";
|
|
||||||
|
|
||||||
const bodySchema = z
|
|
||||||
.object({
|
|
||||||
orgIds: z
|
|
||||||
.array(z.string().nonempty())
|
|
||||||
.transform((v) => Array.from(new Set(v)))
|
|
||||||
})
|
|
||||||
.strict();
|
|
||||||
|
|
||||||
const paramsSchema = z.object({
|
|
||||||
apiKeyId: z.string().nonempty()
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function setApiKeyOrgs(
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction
|
|
||||||
): Promise<any> {
|
|
||||||
try {
|
|
||||||
const parsedBody = bodySchema.safeParse(req.body);
|
|
||||||
if (!parsedBody.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedBody.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { orgIds: newOrgIds } = parsedBody.data;
|
|
||||||
|
|
||||||
const parsedParams = paramsSchema.safeParse(req.params);
|
|
||||||
if (!parsedParams.success) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
fromError(parsedParams.error).toString()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { apiKeyId } = parsedParams.data;
|
|
||||||
|
|
||||||
// make sure all orgs exist
|
|
||||||
const allOrgs = await db
|
|
||||||
.select()
|
|
||||||
.from(orgs)
|
|
||||||
.where(inArray(orgs.orgId, newOrgIds));
|
|
||||||
|
|
||||||
if (allOrgs.length !== newOrgIds.length) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.BAD_REQUEST,
|
|
||||||
"One or more orgs do not exist"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
|
||||||
const existingOrgs = await trx
|
|
||||||
.select({ orgId: apiKeyOrg.orgId })
|
|
||||||
.from(apiKeyOrg)
|
|
||||||
.where(eq(apiKeyOrg.apiKeyId, apiKeyId));
|
|
||||||
|
|
||||||
const existingOrgIds = existingOrgs.map((a) => a.orgId);
|
|
||||||
|
|
||||||
const orgIdsToAdd = newOrgIds.filter(
|
|
||||||
(id) => !existingOrgIds.includes(id)
|
|
||||||
);
|
|
||||||
const orgIdsToRemove = existingOrgIds.filter(
|
|
||||||
(id) => !newOrgIds.includes(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (orgIdsToRemove.length > 0) {
|
|
||||||
await trx
|
|
||||||
.delete(apiKeyOrg)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(apiKeyOrg.apiKeyId, apiKeyId),
|
|
||||||
inArray(apiKeyOrg.orgId, orgIdsToRemove)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (orgIdsToAdd.length > 0) {
|
|
||||||
const insertValues = orgIdsToAdd.map((orgId) => ({
|
|
||||||
apiKeyId,
|
|
||||||
orgId
|
|
||||||
}));
|
|
||||||
await trx.insert(apiKeyOrg).values(insertValues);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response(res, {
|
|
||||||
data: {},
|
|
||||||
success: true,
|
|
||||||
error: false,
|
|
||||||
message: "API key orgs updated successfully",
|
|
||||||
status: HttpCode.OK
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error);
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,78 +1,6 @@
|
||||||
|
import { isPathAllowed } from './verifySession';
|
||||||
import { assertEquals } from '@test/assert';
|
import { assertEquals } from '@test/assert';
|
||||||
|
|
||||||
function isPathAllowed(pattern: string, path: string): boolean {
|
|
||||||
|
|
||||||
// Normalize and split paths into segments
|
|
||||||
const normalize = (p: string) => p.split("/").filter(Boolean);
|
|
||||||
const patternParts = normalize(pattern);
|
|
||||||
const pathParts = normalize(path);
|
|
||||||
|
|
||||||
|
|
||||||
// Recursive function to try different wildcard matches
|
|
||||||
function matchSegments(patternIndex: number, pathIndex: number): boolean {
|
|
||||||
const indent = " ".repeat(pathIndex); // Indent based on recursion depth
|
|
||||||
const currentPatternPart = patternParts[patternIndex];
|
|
||||||
const currentPathPart = pathParts[pathIndex];
|
|
||||||
|
|
||||||
// If we've consumed all pattern parts, we should have consumed all path parts
|
|
||||||
if (patternIndex >= patternParts.length) {
|
|
||||||
const result = pathIndex >= pathParts.length;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we've consumed all path parts but still have pattern parts
|
|
||||||
if (pathIndex >= pathParts.length) {
|
|
||||||
// The only way this can match is if all remaining pattern parts are wildcards
|
|
||||||
const remainingPattern = patternParts.slice(patternIndex);
|
|
||||||
const result = remainingPattern.every((p) => p === "*");
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For full segment wildcards, try consuming different numbers of path segments
|
|
||||||
if (currentPatternPart === "*") {
|
|
||||||
|
|
||||||
// Try consuming 0 segments (skip the wildcard)
|
|
||||||
if (matchSegments(patternIndex + 1, pathIndex)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try consuming current segment and recursively try rest
|
|
||||||
if (matchSegments(patternIndex, pathIndex + 1)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for in-segment wildcard (e.g., "prefix*" or "prefix*suffix")
|
|
||||||
if (currentPatternPart.includes("*")) {
|
|
||||||
// Convert the pattern segment to a regex pattern
|
|
||||||
const regexPattern = currentPatternPart
|
|
||||||
.replace(/\*/g, ".*") // Replace * with .* for regex wildcard
|
|
||||||
.replace(/\?/g, "."); // Replace ? with . for single character wildcard if needed
|
|
||||||
|
|
||||||
const regex = new RegExp(`^${regexPattern}$`);
|
|
||||||
|
|
||||||
if (regex.test(currentPathPart)) {
|
|
||||||
return matchSegments(patternIndex + 1, pathIndex + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For regular segments, they must match exactly
|
|
||||||
if (currentPatternPart !== currentPathPart) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Move to next segments in both pattern and path
|
|
||||||
return matchSegments(patternIndex + 1, pathIndex + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = matchSegments(0, 0);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function runTests() {
|
function runTests() {
|
||||||
console.log('Running path matching tests...');
|
console.log('Running path matching tests...');
|
||||||
|
|
||||||
|
@ -128,9 +56,6 @@ function runTests() {
|
||||||
assertEquals(isPathAllowed('test*', 'testuser'), true, 'Asterisk as part of segment name is treated as a literal, not a wildcard');
|
assertEquals(isPathAllowed('test*', 'testuser'), true, 'Asterisk as part of segment name is treated as a literal, not a wildcard');
|
||||||
assertEquals(isPathAllowed('my*app', 'myapp'), true, 'Asterisk in middle of segment name is treated as a literal, not a wildcard');
|
assertEquals(isPathAllowed('my*app', 'myapp'), true, 'Asterisk in middle of segment name is treated as a literal, not a wildcard');
|
||||||
|
|
||||||
assertEquals(isPathAllowed('/', '/'), true, 'Root path should match root path');
|
|
||||||
assertEquals(isPathAllowed('/', '/test'), false, 'Root path should not match non-root path');
|
|
||||||
|
|
||||||
console.log('All tests passed!');
|
console.log('All tests passed!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ import * as auth from "./auth";
|
||||||
import * as role from "./role";
|
import * as role from "./role";
|
||||||
import * as accessToken from "./accessToken";
|
import * as accessToken from "./accessToken";
|
||||||
import * as idp from "./idp";
|
import * as idp from "./idp";
|
||||||
import * as apiKeys from "./apiKeys";
|
// import * as apiKeys from "./apiKeys";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import {
|
import {
|
||||||
verifyAccessTokenAccess,
|
verifyAccessTokenAccess,
|
||||||
|
@ -26,8 +26,8 @@ import {
|
||||||
verifyUserAccess,
|
verifyUserAccess,
|
||||||
getUserOrgs,
|
getUserOrgs,
|
||||||
verifyUserIsServerAdmin,
|
verifyUserIsServerAdmin,
|
||||||
verifyIsLoggedInUser,
|
verifyIsLoggedInUser
|
||||||
verifyApiKeyAccess,
|
// verifyApiKeyAccess
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import { verifyUserHasAction } from "../middlewares/verifyUserHasAction";
|
import { verifyUserHasAction } from "../middlewares/verifyUserHasAction";
|
||||||
import { ActionsEnum } from "@server/auth/actions";
|
import { ActionsEnum } from "@server/auth/actions";
|
||||||
|
@ -555,6 +555,7 @@ authenticated.get(
|
||||||
authenticated.get("/idp", idp.listIdps); // anyone can see this; it's just a list of idp names and ids
|
authenticated.get("/idp", idp.listIdps); // anyone can see this; it's just a list of idp names and ids
|
||||||
authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp);
|
authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp);
|
||||||
|
|
||||||
|
/*
|
||||||
authenticated.get(
|
authenticated.get(
|
||||||
`/api-key/:apiKeyId`,
|
`/api-key/:apiKeyId`,
|
||||||
verifyUserIsServerAdmin,
|
verifyUserIsServerAdmin,
|
||||||
|
@ -636,6 +637,7 @@ authenticated.get(
|
||||||
verifyUserHasAction(ActionsEnum.getApiKey),
|
verifyUserHasAction(ActionsEnum.getApiKey),
|
||||||
apiKeys.getApiKey
|
apiKeys.getApiKey
|
||||||
);
|
);
|
||||||
|
*/
|
||||||
|
|
||||||
// Auth routes
|
// Auth routes
|
||||||
export const authRouter = Router();
|
export const authRouter = Router();
|
||||||
|
|
|
@ -6,12 +6,8 @@ import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import {
|
import { idp, idpOidcConfig, users } from "@server/db/schemas";
|
||||||
idp,
|
import { and, eq, inArray } from "drizzle-orm";
|
||||||
idpOidcConfig,
|
|
||||||
users
|
|
||||||
} from "@server/db/schemas";
|
|
||||||
import { and, eq } from "drizzle-orm";
|
|
||||||
import * as arctic from "arctic";
|
import * as arctic from "arctic";
|
||||||
import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl";
|
import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl";
|
||||||
import jmespath from "jmespath";
|
import jmespath from "jmespath";
|
||||||
|
@ -163,9 +159,7 @@ export async function validateOidcCallback(
|
||||||
);
|
);
|
||||||
|
|
||||||
const idToken = tokens.idToken();
|
const idToken = tokens.idToken();
|
||||||
logger.debug("ID token", { idToken });
|
|
||||||
const claims = arctic.decodeIdToken(idToken);
|
const claims = arctic.decodeIdToken(idToken);
|
||||||
logger.debug("ID token claims", { claims });
|
|
||||||
|
|
||||||
const userIdentifier = jmespath.search(
|
const userIdentifier = jmespath.search(
|
||||||
claims,
|
claims,
|
||||||
|
|
|
@ -1,494 +0,0 @@
|
||||||
import * as site from "./site";
|
|
||||||
import * as org from "./org";
|
|
||||||
import * as resource from "./resource";
|
|
||||||
import * as domain from "./domain";
|
|
||||||
import * as target from "./target";
|
|
||||||
import * as user from "./user";
|
|
||||||
import * as role from "./role";
|
|
||||||
// import * as client from "./client";
|
|
||||||
import * as accessToken from "./accessToken";
|
|
||||||
import * as apiKeys from "./apiKeys";
|
|
||||||
import * as idp from "./idp";
|
|
||||||
import {
|
|
||||||
verifyApiKey,
|
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyApiKeyHasAction,
|
|
||||||
verifyApiKeySiteAccess,
|
|
||||||
verifyApiKeyResourceAccess,
|
|
||||||
verifyApiKeyTargetAccess,
|
|
||||||
verifyApiKeyRoleAccess,
|
|
||||||
verifyApiKeyUserAccess,
|
|
||||||
verifyApiKeySetResourceUsers,
|
|
||||||
verifyApiKeyAccessTokenAccess,
|
|
||||||
verifyApiKeyIsRoot
|
|
||||||
} from "@server/middlewares";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import { Router } from "express";
|
|
||||||
import { ActionsEnum } from "@server/auth/actions";
|
|
||||||
|
|
||||||
export const unauthenticated = Router();
|
|
||||||
|
|
||||||
unauthenticated.get("/", (_, res) => {
|
|
||||||
res.status(HttpCode.OK).json({ message: "Healthy" });
|
|
||||||
});
|
|
||||||
|
|
||||||
export const authenticated = Router();
|
|
||||||
authenticated.use(verifyApiKey);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/org/checkId",
|
|
||||||
verifyApiKeyIsRoot,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.checkOrgId),
|
|
||||||
org.checkId
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.put(
|
|
||||||
"/org",
|
|
||||||
verifyApiKeyIsRoot,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.createOrg),
|
|
||||||
org.createOrg
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/orgs",
|
|
||||||
verifyApiKeyIsRoot,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.listOrgs),
|
|
||||||
org.listOrgs
|
|
||||||
); // TODO we need to check the orgs here
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/org/:orgId",
|
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.getOrg),
|
|
||||||
org.getOrg
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
"/org/:orgId",
|
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.updateOrg),
|
|
||||||
org.updateOrg
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.delete(
|
|
||||||
"/org/:orgId",
|
|
||||||
verifyApiKeyIsRoot,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.deleteOrg),
|
|
||||||
org.deleteOrg
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.put(
|
|
||||||
"/org/:orgId/site",
|
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.createSite),
|
|
||||||
site.createSite
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/org/:orgId/sites",
|
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.listSites),
|
|
||||||
site.listSites
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/org/:orgId/site/:niceId",
|
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.getSite),
|
|
||||||
site.getSite
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/org/:orgId/pick-site-defaults",
|
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.createSite),
|
|
||||||
site.pickSiteDefaults
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/site/:siteId",
|
|
||||||
verifyApiKeySiteAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.getSite),
|
|
||||||
site.getSite
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
"/site/:siteId",
|
|
||||||
verifyApiKeySiteAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.updateSite),
|
|
||||||
site.updateSite
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.delete(
|
|
||||||
"/site/:siteId",
|
|
||||||
verifyApiKeySiteAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.deleteSite),
|
|
||||||
site.deleteSite
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.put(
|
|
||||||
"/org/:orgId/site/:siteId/resource",
|
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.createResource),
|
|
||||||
resource.createResource
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/site/:siteId/resources",
|
|
||||||
verifyApiKeySiteAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.listResources),
|
|
||||||
resource.listResources
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/org/:orgId/resources",
|
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.listResources),
|
|
||||||
resource.listResources
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/org/:orgId/domains",
|
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.listOrgDomains),
|
|
||||||
domain.listDomains
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
"/org/:orgId/create-invite",
|
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.inviteUser),
|
|
||||||
user.inviteUser
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/resource/:resourceId/roles",
|
|
||||||
verifyApiKeyResourceAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.listResourceRoles),
|
|
||||||
resource.listResourceRoles
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/resource/:resourceId/users",
|
|
||||||
verifyApiKeyResourceAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.listResourceUsers),
|
|
||||||
resource.listResourceUsers
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/resource/:resourceId",
|
|
||||||
verifyApiKeyResourceAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.getResource),
|
|
||||||
resource.getResource
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
"/resource/:resourceId",
|
|
||||||
verifyApiKeyResourceAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.updateResource),
|
|
||||||
resource.updateResource
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.delete(
|
|
||||||
"/resource/:resourceId",
|
|
||||||
verifyApiKeyResourceAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.deleteResource),
|
|
||||||
resource.deleteResource
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.put(
|
|
||||||
"/resource/:resourceId/target",
|
|
||||||
verifyApiKeyResourceAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.createTarget),
|
|
||||||
target.createTarget
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/resource/:resourceId/targets",
|
|
||||||
verifyApiKeyResourceAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.listTargets),
|
|
||||||
target.listTargets
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.put(
|
|
||||||
"/resource/:resourceId/rule",
|
|
||||||
verifyApiKeyResourceAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.createResourceRule),
|
|
||||||
resource.createResourceRule
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/resource/:resourceId/rules",
|
|
||||||
verifyApiKeyResourceAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.listResourceRules),
|
|
||||||
resource.listResourceRules
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
"/resource/:resourceId/rule/:ruleId",
|
|
||||||
verifyApiKeyResourceAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.updateResourceRule),
|
|
||||||
resource.updateResourceRule
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.delete(
|
|
||||||
"/resource/:resourceId/rule/:ruleId",
|
|
||||||
verifyApiKeyResourceAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.deleteResourceRule),
|
|
||||||
resource.deleteResourceRule
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/target/:targetId",
|
|
||||||
verifyApiKeyTargetAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.getTarget),
|
|
||||||
target.getTarget
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
"/target/:targetId",
|
|
||||||
verifyApiKeyTargetAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.updateTarget),
|
|
||||||
target.updateTarget
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.delete(
|
|
||||||
"/target/:targetId",
|
|
||||||
verifyApiKeyTargetAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.deleteTarget),
|
|
||||||
target.deleteTarget
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.put(
|
|
||||||
"/org/:orgId/role",
|
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.createRole),
|
|
||||||
role.createRole
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/org/:orgId/roles",
|
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.listRoles),
|
|
||||||
role.listRoles
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.delete(
|
|
||||||
"/role/:roleId",
|
|
||||||
verifyApiKeyRoleAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.deleteRole),
|
|
||||||
role.deleteRole
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
"/role/:roleId/add/:userId",
|
|
||||||
verifyApiKeyRoleAccess,
|
|
||||||
verifyApiKeyUserAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.addUserRole),
|
|
||||||
user.addUserRole
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
"/resource/:resourceId/roles",
|
|
||||||
verifyApiKeyResourceAccess,
|
|
||||||
verifyApiKeyRoleAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.setResourceRoles),
|
|
||||||
resource.setResourceRoles
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
"/resource/:resourceId/users",
|
|
||||||
verifyApiKeyResourceAccess,
|
|
||||||
verifyApiKeySetResourceUsers,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
|
|
||||||
resource.setResourceUsers
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
`/resource/:resourceId/password`,
|
|
||||||
verifyApiKeyResourceAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.setResourcePassword),
|
|
||||||
resource.setResourcePassword
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
`/resource/:resourceId/pincode`,
|
|
||||||
verifyApiKeyResourceAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.setResourcePincode),
|
|
||||||
resource.setResourcePincode
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
`/resource/:resourceId/whitelist`,
|
|
||||||
verifyApiKeyResourceAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist),
|
|
||||||
resource.setResourceWhitelist
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
`/resource/:resourceId/whitelist`,
|
|
||||||
verifyApiKeyResourceAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.getResourceWhitelist),
|
|
||||||
resource.getResourceWhitelist
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
`/resource/:resourceId/transfer`,
|
|
||||||
verifyApiKeyResourceAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.updateResource),
|
|
||||||
resource.transferResource
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
`/resource/:resourceId/access-token`,
|
|
||||||
verifyApiKeyResourceAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.generateAccessToken),
|
|
||||||
accessToken.generateAccessToken
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.delete(
|
|
||||||
`/access-token/:accessTokenId`,
|
|
||||||
verifyApiKeyAccessTokenAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.deleteAcessToken),
|
|
||||||
accessToken.deleteAccessToken
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
`/org/:orgId/access-tokens`,
|
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.listAccessTokens),
|
|
||||||
accessToken.listAccessTokens
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
`/resource/:resourceId/access-tokens`,
|
|
||||||
verifyApiKeyResourceAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.listAccessTokens),
|
|
||||||
accessToken.listAccessTokens
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/org/:orgId/user/:userId",
|
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.getOrgUser),
|
|
||||||
user.getOrgUser
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/org/:orgId/users",
|
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.listUsers),
|
|
||||||
user.listUsers
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.delete(
|
|
||||||
"/org/:orgId/user/:userId",
|
|
||||||
verifyApiKeyOrgAccess,
|
|
||||||
verifyApiKeyUserAccess,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.removeUser),
|
|
||||||
user.removeUserOrg
|
|
||||||
);
|
|
||||||
|
|
||||||
// authenticated.put(
|
|
||||||
// "/newt",
|
|
||||||
// verifyApiKeyHasAction(ActionsEnum.createNewt),
|
|
||||||
// newt.createNewt
|
|
||||||
// );
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
`/org/:orgId/api-keys`,
|
|
||||||
verifyApiKeyIsRoot,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.listApiKeys),
|
|
||||||
apiKeys.listOrgApiKeys
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
`/org/:orgId/api-key/:apiKeyId/actions`,
|
|
||||||
verifyApiKeyIsRoot,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.setApiKeyActions),
|
|
||||||
apiKeys.setApiKeyActions
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
`/org/:orgId/api-key/:apiKeyId/actions`,
|
|
||||||
verifyApiKeyIsRoot,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.listApiKeyActions),
|
|
||||||
apiKeys.listApiKeyActions
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.put(
|
|
||||||
`/org/:orgId/api-key`,
|
|
||||||
verifyApiKeyIsRoot,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.createApiKey),
|
|
||||||
apiKeys.createOrgApiKey
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.delete(
|
|
||||||
`/org/:orgId/api-key/:apiKeyId`,
|
|
||||||
verifyApiKeyIsRoot,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.deleteApiKey),
|
|
||||||
apiKeys.deleteApiKey
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.put(
|
|
||||||
"/idp/oidc",
|
|
||||||
verifyApiKeyIsRoot,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.createIdp),
|
|
||||||
idp.createOidcIdp
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
"/idp/:idpId/oidc",
|
|
||||||
verifyApiKeyIsRoot,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.updateIdp),
|
|
||||||
idp.updateOidcIdp
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.delete(
|
|
||||||
"/idp/:idpId",
|
|
||||||
verifyApiKeyIsRoot,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.deleteIdp),
|
|
||||||
idp.deleteIdp
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/idp",
|
|
||||||
verifyApiKeyIsRoot,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.listIdps),
|
|
||||||
idp.listIdps
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/idp/:idpId",
|
|
||||||
verifyApiKeyIsRoot,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.getIdp),
|
|
||||||
idp.getIdp
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.put(
|
|
||||||
"/idp/:idpId/org/:orgId",
|
|
||||||
verifyApiKeyIsRoot,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.createIdpOrg),
|
|
||||||
idp.createIdpOrgPolicy
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.post(
|
|
||||||
"/idp/:idpId/org/:orgId",
|
|
||||||
verifyApiKeyIsRoot,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.updateIdpOrg),
|
|
||||||
idp.updateIdpOrgPolicy
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.delete(
|
|
||||||
"/idp/:idpId/org/:orgId",
|
|
||||||
verifyApiKeyIsRoot,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.deleteIdpOrg),
|
|
||||||
idp.deleteIdpOrgPolicy
|
|
||||||
);
|
|
||||||
|
|
||||||
authenticated.get(
|
|
||||||
"/idp/:idpId/org",
|
|
||||||
verifyApiKeyIsRoot,
|
|
||||||
verifyApiKeyHasAction(ActionsEnum.listIdpOrgs),
|
|
||||||
idp.listIdpOrgPolicies
|
|
||||||
);
|
|
|
@ -318,8 +318,8 @@ async function updateHttpResource(
|
||||||
domainId: updatePayload.domainId,
|
domainId: updatePayload.domainId,
|
||||||
enabled: updatePayload.enabled,
|
enabled: updatePayload.enabled,
|
||||||
stickySession: updatePayload.stickySession,
|
stickySession: updatePayload.stickySession,
|
||||||
tlsServerName: updatePayload.tlsServerName,
|
tlsServerName: updatePayload.tlsServerName || null,
|
||||||
setHostHeader: updatePayload.setHostHeader,
|
setHostHeader: updatePayload.setHostHeader || null,
|
||||||
fullDomain: updatePayload.fullDomain
|
fullDomain: updatePayload.fullDomain
|
||||||
})
|
})
|
||||||
.where(eq(resources.resourceId, resource.resourceId))
|
.where(eq(resources.resourceId, resource.resourceId))
|
||||||
|
|
|
@ -35,7 +35,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
import { Tag, TagInput } from "@app/components/tags/tag-input";
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
username: z.string(),
|
email: z.string().email({ message: "Please enter a valid email" }),
|
||||||
roles: z
|
roles: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
|
@ -64,7 +64,7 @@ export default function AccessControlsPage() {
|
||||||
const form = useForm<z.infer<typeof formSchema>>({
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
username: user.username!,
|
email: user.email!,
|
||||||
roles: []
|
roles: []
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { DataTable } from "@app/components/ui/data-table";
|
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
|
||||||
|
|
||||||
interface DataTableProps<TData, TValue> {
|
|
||||||
columns: ColumnDef<TData, TValue>[];
|
|
||||||
data: TData[];
|
|
||||||
addApiKey?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function OrgApiKeysDataTable<TData, TValue>({
|
|
||||||
addApiKey,
|
|
||||||
columns,
|
|
||||||
data
|
|
||||||
}: DataTableProps<TData, TValue>) {
|
|
||||||
return (
|
|
||||||
<DataTable
|
|
||||||
columns={columns}
|
|
||||||
data={data}
|
|
||||||
title="API Keys"
|
|
||||||
searchPlaceholder="Search API keys..."
|
|
||||||
searchColumn="name"
|
|
||||||
onAdd={addApiKey}
|
|
||||||
addButtonText="Generate API Key"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,199 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
|
||||||
import { OrgApiKeysDataTable } from "./OrgApiKeysDataTable";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger
|
|
||||||
} from "@app/components/ui/dropdown-menu";
|
|
||||||
import { Button } from "@app/components/ui/button";
|
|
||||||
import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useState } from "react";
|
|
||||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
|
||||||
import { toast } from "@app/hooks/useToast";
|
|
||||||
import { formatAxiosError } from "@app/lib/api";
|
|
||||||
import { createApiClient } from "@app/lib/api";
|
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
|
||||||
import moment from "moment";
|
|
||||||
|
|
||||||
export type OrgApiKeyRow = {
|
|
||||||
id: string;
|
|
||||||
key: string;
|
|
||||||
name: string;
|
|
||||||
createdAt: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type OrgApiKeyTableProps = {
|
|
||||||
apiKeys: OrgApiKeyRow[];
|
|
||||||
orgId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function OrgApiKeysTable({
|
|
||||||
apiKeys,
|
|
||||||
orgId
|
|
||||||
}: OrgApiKeyTableProps) {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
|
||||||
const [selected, setSelected] = useState<OrgApiKeyRow | null>(null);
|
|
||||||
const [rows, setRows] = useState<OrgApiKeyRow[]>(apiKeys);
|
|
||||||
|
|
||||||
const api = createApiClient(useEnvContext());
|
|
||||||
|
|
||||||
const deleteSite = (apiKeyId: string) => {
|
|
||||||
api.delete(`/org/${orgId}/api-key/${apiKeyId}`)
|
|
||||||
.catch((e) => {
|
|
||||||
console.error("Error deleting API key", e);
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: "Error deleting API key",
|
|
||||||
description: formatAxiosError(e, "Error deleting API key")
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
router.refresh();
|
|
||||||
setIsDeleteModalOpen(false);
|
|
||||||
|
|
||||||
const newRows = rows.filter((row) => row.id !== apiKeyId);
|
|
||||||
|
|
||||||
setRows(newRows);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns: ColumnDef<OrgApiKeyRow>[] = [
|
|
||||||
{
|
|
||||||
id: "dots",
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const apiKeyROw = row.original;
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
|
||||||
<span className="sr-only">Open menu</span>
|
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => {
|
|
||||||
setSelected(apiKeyROw);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>View settings</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => {
|
|
||||||
setSelected(apiKeyROw);
|
|
||||||
setIsDeleteModalOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="text-red-500">Delete</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "name",
|
|
||||||
header: ({ column }) => {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() =>
|
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Name
|
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "key",
|
|
||||||
header: "Key",
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const r = row.original;
|
|
||||||
return <span className="font-mono">{r.key}</span>;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "createdAt",
|
|
||||||
header: "Created At",
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const r = row.original;
|
|
||||||
return <span>{moment(r.createdAt).format("lll")} </span>;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "actions",
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const r = row.original;
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-end">
|
|
||||||
<Link href={`/${orgId}/settings/api-keys/${r.id}`}>
|
|
||||||
<Button variant={"outlinePrimary"} className="ml-2">
|
|
||||||
Edit
|
|
||||||
<ArrowRight className="ml-2 w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{selected && (
|
|
||||||
<ConfirmDeleteDialog
|
|
||||||
open={isDeleteModalOpen}
|
|
||||||
setOpen={(val) => {
|
|
||||||
setIsDeleteModalOpen(val);
|
|
||||||
setSelected(null);
|
|
||||||
}}
|
|
||||||
dialog={
|
|
||||||
<div className="space-y-4">
|
|
||||||
<p>
|
|
||||||
Are you sure you want to remove the API key{" "}
|
|
||||||
<b>{selected?.name || selected?.id}</b> from the
|
|
||||||
organization?
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<b>
|
|
||||||
Once removed, the API key will no longer be
|
|
||||||
able to be used.
|
|
||||||
</b>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
To confirm, please type the name of the API key
|
|
||||||
below.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
buttonText="Confirm Delete API Key"
|
|
||||||
onConfirm={async () => deleteSite(selected!.id)}
|
|
||||||
string={selected.name}
|
|
||||||
title="Delete API Key"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<OrgApiKeysDataTable
|
|
||||||
columns={columns}
|
|
||||||
data={rows}
|
|
||||||
addApiKey={() => {
|
|
||||||
router.push(`/${orgId}/settings/api-keys/create`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
import { internal } from "@app/lib/api";
|
|
||||||
import { AxiosResponse } from "axios";
|
|
||||||
import { redirect } from "next/navigation";
|
|
||||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
|
||||||
import { SidebarSettings } from "@app/components/SidebarSettings";
|
|
||||||
import Link from "next/link";
|
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
|
||||||
import {
|
|
||||||
Breadcrumb,
|
|
||||||
BreadcrumbItem,
|
|
||||||
BreadcrumbList,
|
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator
|
|
||||||
} from "@app/components/ui/breadcrumb";
|
|
||||||
import { GetApiKeyResponse } from "@server/routers/apiKeys";
|
|
||||||
import ApiKeyProvider from "@app/providers/ApiKeyProvider";
|
|
||||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
|
||||||
|
|
||||||
interface SettingsLayoutProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
params: Promise<{ apiKeyId: string; orgId: string }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function SettingsLayout(props: SettingsLayoutProps) {
|
|
||||||
const params = await props.params;
|
|
||||||
|
|
||||||
const { children } = props;
|
|
||||||
|
|
||||||
let apiKey = null;
|
|
||||||
try {
|
|
||||||
const res = await internal.get<AxiosResponse<GetApiKeyResponse>>(
|
|
||||||
`/org/${params.orgId}/api-key/${params.apiKeyId}`,
|
|
||||||
await authCookieHeader()
|
|
||||||
);
|
|
||||||
apiKey = res.data.data;
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
redirect(`/${params.orgId}/settings/api-keys`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const navItems = [
|
|
||||||
{
|
|
||||||
title: "Permissions",
|
|
||||||
href: "/{orgId}/settings/api-keys/{apiKeyId}/permissions"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SettingsSectionTitle title={`${apiKey?.name} Settings`} />
|
|
||||||
|
|
||||||
<ApiKeyProvider apiKey={apiKey}>
|
|
||||||
<HorizontalTabs items={navItems}>{children}</HorizontalTabs>
|
|
||||||
</ApiKeyProvider>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { redirect } from "next/navigation";
|
|
||||||
|
|
||||||
export default async function ApiKeysPage(props: {
|
|
||||||
params: Promise<{ orgId: string; apiKeyId: string }>;
|
|
||||||
}) {
|
|
||||||
const params = await props.params;
|
|
||||||
redirect(`/${params.orgId}/settings/api-keys/${params.apiKeyId}/permissions`);
|
|
||||||
}
|
|
|
@ -1,133 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import PermissionsSelectBox from "@app/components/PermissionsSelectBox";
|
|
||||||
import {
|
|
||||||
SettingsContainer,
|
|
||||||
SettingsSection,
|
|
||||||
SettingsSectionBody,
|
|
||||||
SettingsSectionDescription,
|
|
||||||
SettingsSectionFooter,
|
|
||||||
SettingsSectionHeader,
|
|
||||||
SettingsSectionTitle
|
|
||||||
} from "@app/components/Settings";
|
|
||||||
import { Button } from "@app/components/ui/button";
|
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
|
||||||
import { toast } from "@app/hooks/useToast";
|
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
|
||||||
import { ListApiKeyActionsResponse } from "@server/routers/apiKeys";
|
|
||||||
import { AxiosResponse } from "axios";
|
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
const { env } = useEnvContext();
|
|
||||||
const api = createApiClient({ env });
|
|
||||||
const { orgId, apiKeyId } = useParams();
|
|
||||||
|
|
||||||
const [loadingPage, setLoadingPage] = useState<boolean>(true);
|
|
||||||
const [selectedPermissions, setSelectedPermissions] = useState<
|
|
||||||
Record<string, boolean>
|
|
||||||
>({});
|
|
||||||
const [loadingSavePermissions, setLoadingSavePermissions] =
|
|
||||||
useState<boolean>(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function load() {
|
|
||||||
setLoadingPage(true);
|
|
||||||
|
|
||||||
const res = await api
|
|
||||||
.get<
|
|
||||||
AxiosResponse<ListApiKeyActionsResponse>
|
|
||||||
>(`/org/${orgId}/api-key/${apiKeyId}/actions`)
|
|
||||||
.catch((e) => {
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: "Error loading API key actions",
|
|
||||||
description: formatAxiosError(
|
|
||||||
e,
|
|
||||||
"Error loading API key actions"
|
|
||||||
)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res && res.status === 200) {
|
|
||||||
const data = res.data.data;
|
|
||||||
for (const action of data.actions) {
|
|
||||||
setSelectedPermissions((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[action.actionId]: true
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoadingPage(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
load();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function savePermissions() {
|
|
||||||
setLoadingSavePermissions(true);
|
|
||||||
|
|
||||||
const actionsRes = await api
|
|
||||||
.post(`/org/${orgId}/api-key/${apiKeyId}/actions`, {
|
|
||||||
actionIds: Object.keys(selectedPermissions).filter(
|
|
||||||
(key) => selectedPermissions[key]
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error("Error setting permissions", e);
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: "Error setting permissions",
|
|
||||||
description: formatAxiosError(e)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (actionsRes && actionsRes.status === 200) {
|
|
||||||
toast({
|
|
||||||
title: "Permissions updated",
|
|
||||||
description: "The permissions have been updated."
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoadingSavePermissions(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!loadingPage && (
|
|
||||||
<SettingsContainer>
|
|
||||||
<SettingsSection>
|
|
||||||
<SettingsSectionHeader>
|
|
||||||
<SettingsSectionTitle>
|
|
||||||
Permissions
|
|
||||||
</SettingsSectionTitle>
|
|
||||||
<SettingsSectionDescription>
|
|
||||||
Determine what this API key can do
|
|
||||||
</SettingsSectionDescription>
|
|
||||||
</SettingsSectionHeader>
|
|
||||||
<SettingsSectionBody>
|
|
||||||
<PermissionsSelectBox
|
|
||||||
selectedPermissions={selectedPermissions}
|
|
||||||
onChange={setSelectedPermissions}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SettingsSectionFooter>
|
|
||||||
<Button
|
|
||||||
onClick={async () => {
|
|
||||||
await savePermissions();
|
|
||||||
}}
|
|
||||||
loading={loadingSavePermissions}
|
|
||||||
disabled={loadingSavePermissions}
|
|
||||||
>
|
|
||||||
Save Permissions
|
|
||||||
</Button>
|
|
||||||
</SettingsSectionFooter>
|
|
||||||
</SettingsSectionBody>
|
|
||||||
</SettingsSection>
|
|
||||||
</SettingsContainer>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,407 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
|
||||||
SettingsContainer,
|
|
||||||
SettingsSection,
|
|
||||||
SettingsSectionBody,
|
|
||||||
SettingsSectionDescription,
|
|
||||||
SettingsSectionForm,
|
|
||||||
SettingsSectionHeader,
|
|
||||||
SettingsSectionTitle
|
|
||||||
} from "@app/components/Settings";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage
|
|
||||||
} from "@app/components/ui/form";
|
|
||||||
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { Input } from "@app/components/ui/input";
|
|
||||||
import { InfoIcon } from "lucide-react";
|
|
||||||
import { Button } from "@app/components/ui/button";
|
|
||||||
import { Checkbox, CheckboxWithLabel } from "@app/components/ui/checkbox";
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
|
||||||
import { toast } from "@app/hooks/useToast";
|
|
||||||
import { AxiosResponse } from "axios";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import {
|
|
||||||
Breadcrumb,
|
|
||||||
BreadcrumbItem,
|
|
||||||
BreadcrumbList,
|
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator
|
|
||||||
} from "@app/components/ui/breadcrumb";
|
|
||||||
import Link from "next/link";
|
|
||||||
import {
|
|
||||||
CreateOrgApiKeyBody,
|
|
||||||
CreateOrgApiKeyResponse
|
|
||||||
} from "@server/routers/apiKeys";
|
|
||||||
import { ApiKey } from "@server/db/schemas";
|
|
||||||
import {
|
|
||||||
InfoSection,
|
|
||||||
InfoSectionContent,
|
|
||||||
InfoSections,
|
|
||||||
InfoSectionTitle
|
|
||||||
} from "@app/components/InfoSection";
|
|
||||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
|
||||||
import moment from "moment";
|
|
||||||
import CopyCodeBox from "@server/emails/templates/components/CopyCodeBox";
|
|
||||||
import CopyTextBox from "@app/components/CopyTextBox";
|
|
||||||
import PermissionsSelectBox from "@app/components/PermissionsSelectBox";
|
|
||||||
|
|
||||||
const createFormSchema = z.object({
|
|
||||||
name: z
|
|
||||||
.string()
|
|
||||||
.min(2, {
|
|
||||||
message: "Name must be at least 2 characters."
|
|
||||||
})
|
|
||||||
.max(255, {
|
|
||||||
message: "Name must not be longer than 255 characters."
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
type CreateFormValues = z.infer<typeof createFormSchema>;
|
|
||||||
|
|
||||||
const copiedFormSchema = z
|
|
||||||
.object({
|
|
||||||
copied: z.boolean()
|
|
||||||
})
|
|
||||||
.refine(
|
|
||||||
(data) => {
|
|
||||||
return data.copied;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "You must confirm that you have copied the API key.",
|
|
||||||
path: ["copied"]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
type CopiedFormValues = z.infer<typeof copiedFormSchema>;
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
const { env } = useEnvContext();
|
|
||||||
const api = createApiClient({ env });
|
|
||||||
const { orgId } = useParams();
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [loadingPage, setLoadingPage] = useState(true);
|
|
||||||
const [createLoading, setCreateLoading] = useState(false);
|
|
||||||
const [apiKey, setApiKey] = useState<CreateOrgApiKeyResponse | null>(null);
|
|
||||||
const [selectedPermissions, setSelectedPermissions] = useState<
|
|
||||||
Record<string, boolean>
|
|
||||||
>({});
|
|
||||||
|
|
||||||
const form = useForm<CreateFormValues>({
|
|
||||||
resolver: zodResolver(createFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: ""
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const copiedForm = useForm<CopiedFormValues>({
|
|
||||||
resolver: zodResolver(copiedFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
copied: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function onSubmit(data: CreateFormValues) {
|
|
||||||
setCreateLoading(true);
|
|
||||||
|
|
||||||
let payload: CreateOrgApiKeyBody = {
|
|
||||||
name: data.name
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = await api
|
|
||||||
.put<
|
|
||||||
AxiosResponse<CreateOrgApiKeyResponse>
|
|
||||||
>(`/org/${orgId}/api-key/`, payload)
|
|
||||||
.catch((e) => {
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: "Error creating API key",
|
|
||||||
description: formatAxiosError(e)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res && res.status === 201) {
|
|
||||||
const data = res.data.data;
|
|
||||||
|
|
||||||
console.log({
|
|
||||||
actionIds: Object.keys(selectedPermissions).filter(
|
|
||||||
(key) => selectedPermissions[key]
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
const actionsRes = await api
|
|
||||||
.post(`/org/${orgId}/api-key/${data.apiKeyId}/actions`, {
|
|
||||||
actionIds: Object.keys(selectedPermissions).filter(
|
|
||||||
(key) => selectedPermissions[key]
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error("Error setting permissions", e);
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: "Error setting permissions",
|
|
||||||
description: formatAxiosError(e)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (actionsRes) {
|
|
||||||
setApiKey(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setCreateLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onCopiedSubmit(data: CopiedFormValues) {
|
|
||||||
if (!data.copied) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
router.push(`/${orgId}/settings/api-keys`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatLabel = (str: string) => {
|
|
||||||
return str
|
|
||||||
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
||||||
.replace(/^./, (char) => char.toUpperCase());
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const load = async () => {
|
|
||||||
setLoadingPage(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
load();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<HeaderTitle
|
|
||||||
title="Generate API Key"
|
|
||||||
description="Generate a new API key for your organization"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
router.push(`/${orgId}/settings/api-keys`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
See All API Keys
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!loadingPage && (
|
|
||||||
<div>
|
|
||||||
<SettingsContainer>
|
|
||||||
{!apiKey && (
|
|
||||||
<>
|
|
||||||
<SettingsSection>
|
|
||||||
<SettingsSectionHeader>
|
|
||||||
<SettingsSectionTitle>
|
|
||||||
API Key Information
|
|
||||||
</SettingsSectionTitle>
|
|
||||||
</SettingsSectionHeader>
|
|
||||||
<SettingsSectionBody>
|
|
||||||
<SettingsSectionForm>
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
className="space-y-4"
|
|
||||||
id="create-site-form"
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
Name
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
autoComplete="off"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</SettingsSectionForm>
|
|
||||||
</SettingsSectionBody>
|
|
||||||
</SettingsSection>
|
|
||||||
|
|
||||||
<SettingsSection>
|
|
||||||
<SettingsSectionHeader>
|
|
||||||
<SettingsSectionTitle>
|
|
||||||
Permissions
|
|
||||||
</SettingsSectionTitle>
|
|
||||||
<SettingsSectionDescription>
|
|
||||||
Determine what this API key can do
|
|
||||||
</SettingsSectionDescription>
|
|
||||||
</SettingsSectionHeader>
|
|
||||||
<SettingsSectionBody>
|
|
||||||
<PermissionsSelectBox
|
|
||||||
selectedPermissions={
|
|
||||||
selectedPermissions
|
|
||||||
}
|
|
||||||
onChange={setSelectedPermissions}
|
|
||||||
/>
|
|
||||||
</SettingsSectionBody>
|
|
||||||
</SettingsSection>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{apiKey && (
|
|
||||||
<SettingsSection>
|
|
||||||
<SettingsSectionHeader>
|
|
||||||
<SettingsSectionTitle>
|
|
||||||
Your API Key
|
|
||||||
</SettingsSectionTitle>
|
|
||||||
</SettingsSectionHeader>
|
|
||||||
<SettingsSectionBody>
|
|
||||||
<InfoSections cols={2}>
|
|
||||||
<InfoSection>
|
|
||||||
<InfoSectionTitle>
|
|
||||||
Name
|
|
||||||
</InfoSectionTitle>
|
|
||||||
<InfoSectionContent>
|
|
||||||
<CopyToClipboard
|
|
||||||
text={apiKey.name}
|
|
||||||
/>
|
|
||||||
</InfoSectionContent>
|
|
||||||
</InfoSection>
|
|
||||||
<InfoSection>
|
|
||||||
<InfoSectionTitle>
|
|
||||||
Created
|
|
||||||
</InfoSectionTitle>
|
|
||||||
<InfoSectionContent>
|
|
||||||
{moment(
|
|
||||||
apiKey.createdAt
|
|
||||||
).format("lll")}
|
|
||||||
</InfoSectionContent>
|
|
||||||
</InfoSection>
|
|
||||||
</InfoSections>
|
|
||||||
|
|
||||||
<Alert variant="neutral">
|
|
||||||
<InfoIcon className="h-4 w-4" />
|
|
||||||
<AlertTitle className="font-semibold">
|
|
||||||
Save Your API Key
|
|
||||||
</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
You will only be able to see this
|
|
||||||
once. Make sure to copy it to a
|
|
||||||
secure place.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<h4 className="font-semibold">
|
|
||||||
Your API key is:
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
<CopyTextBox
|
|
||||||
text={`${apiKey.apiKeyId}.${apiKey.apiKey}`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Form {...copiedForm}>
|
|
||||||
<form
|
|
||||||
className="space-y-4"
|
|
||||||
id="copied-form"
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={copiedForm.control}
|
|
||||||
name="copied"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
id="terms"
|
|
||||||
defaultChecked={
|
|
||||||
copiedForm.getValues(
|
|
||||||
"copied"
|
|
||||||
) as boolean
|
|
||||||
}
|
|
||||||
onCheckedChange={(
|
|
||||||
e
|
|
||||||
) => {
|
|
||||||
copiedForm.setValue(
|
|
||||||
"copied",
|
|
||||||
e as boolean
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="terms"
|
|
||||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
||||||
>
|
|
||||||
I have copied
|
|
||||||
the API key
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</SettingsSectionBody>
|
|
||||||
</SettingsSection>
|
|
||||||
)}
|
|
||||||
</SettingsContainer>
|
|
||||||
|
|
||||||
<div className="flex justify-end space-x-2 mt-8">
|
|
||||||
{!apiKey && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
disabled={createLoading || apiKey !== null}
|
|
||||||
onClick={() => {
|
|
||||||
router.push(`/${orgId}/settings/api-keys`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{!apiKey && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
loading={createLoading}
|
|
||||||
disabled={createLoading || apiKey !== null}
|
|
||||||
onClick={() => {
|
|
||||||
form.handleSubmit(onSubmit)();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Generate
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{apiKey && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
copiedForm.handleSubmit(onCopiedSubmit)();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Done
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,44 +0,0 @@
|
||||||
import { internal } from "@app/lib/api";
|
|
||||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
|
||||||
import { AxiosResponse } from "axios";
|
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
|
||||||
import OrgApiKeysTable, { OrgApiKeyRow } from "./OrgApiKeysTable";
|
|
||||||
import { ListOrgApiKeysResponse } from "@server/routers/apiKeys";
|
|
||||||
|
|
||||||
type ApiKeyPageProps = {
|
|
||||||
params: Promise<{ orgId: string }>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
|
||||||
|
|
||||||
export default async function ApiKeysPage(props: ApiKeyPageProps) {
|
|
||||||
const params = await props.params;
|
|
||||||
let apiKeys: ListOrgApiKeysResponse["apiKeys"] = [];
|
|
||||||
try {
|
|
||||||
const res = await internal.get<AxiosResponse<ListOrgApiKeysResponse>>(
|
|
||||||
`/org/${params.orgId}/api-keys`,
|
|
||||||
await authCookieHeader()
|
|
||||||
);
|
|
||||||
apiKeys = res.data.data.apiKeys;
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
const rows: OrgApiKeyRow[] = apiKeys.map((key) => {
|
|
||||||
return {
|
|
||||||
name: key.name,
|
|
||||||
id: key.apiKeyId,
|
|
||||||
key: `${key.apiKeyId}••••••••••••••••••••${key.lastChars}`,
|
|
||||||
createdAt: key.createdAt
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SettingsSectionTitle
|
|
||||||
title="Manage API Keys"
|
|
||||||
description="API keys are used to authenticate with the integration API"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<OrgApiKeysTable apiKeys={rows} orgId={params.orgId} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,6 +1,8 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
import {
|
||||||
|
ColumnDef,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
import { DataTable } from "@app/components/ui/data-table";
|
import { DataTable } from "@app/components/ui/data-table";
|
||||||
|
|
||||||
interface DataTableProps<TData, TValue> {
|
interface DataTableProps<TData, TValue> {
|
||||||
|
@ -23,10 +25,6 @@ export function ResourcesDataTable<TData, TValue>({
|
||||||
searchColumn="name"
|
searchColumn="name"
|
||||||
onAdd={createResource}
|
onAdd={createResource}
|
||||||
addButtonText="Add Resource"
|
addButtonText="Add Resource"
|
||||||
defaultSort={{
|
|
||||||
id: "name",
|
|
||||||
desc: false
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -320,10 +320,8 @@ export default function ReverseProxyTargets(props: {
|
||||||
AxiosResponse<CreateTargetResponse>
|
AxiosResponse<CreateTargetResponse>
|
||||||
>(`/resource/${params.resourceId}/target`, data);
|
>(`/resource/${params.resourceId}/target`, data);
|
||||||
target.targetId = res.data.data.targetId;
|
target.targetId = res.data.data.targetId;
|
||||||
target.new = false;
|
|
||||||
} else if (target.updated) {
|
} else if (target.updated) {
|
||||||
await api.post(`/target/${target.targetId}`, data);
|
await api.post(`/target/${target.targetId}`, data);
|
||||||
target.updated = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -798,12 +796,6 @@ export default function ReverseProxyTargets(props: {
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="outlinePrimary"
|
variant="outlinePrimary"
|
||||||
className="mt-6"
|
className="mt-6"
|
||||||
disabled={
|
|
||||||
!(
|
|
||||||
addTargetForm.getValues("ip") &&
|
|
||||||
addTargetForm.getValues("port")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
Add Target
|
Add Target
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -64,6 +64,7 @@ import {
|
||||||
InfoSections,
|
InfoSections,
|
||||||
InfoSectionTitle
|
InfoSectionTitle
|
||||||
} from "@app/components/InfoSection";
|
} from "@app/components/InfoSection";
|
||||||
|
import { Separator } from "@app/components/ui/separator";
|
||||||
import { InfoPopup } from "@app/components/ui/info-popup";
|
import { InfoPopup } from "@app/components/ui/info-popup";
|
||||||
import {
|
import {
|
||||||
isValidCIDR,
|
isValidCIDR,
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
import {
|
||||||
|
ColumnDef,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
import { DataTable } from "@app/components/ui/data-table";
|
import { DataTable } from "@app/components/ui/data-table";
|
||||||
|
|
||||||
interface DataTableProps<TData, TValue> {
|
interface DataTableProps<TData, TValue> {
|
||||||
|
@ -23,10 +25,6 @@ export function SitesDataTable<TData, TValue>({
|
||||||
searchColumn="name"
|
searchColumn="name"
|
||||||
onAdd={createSite}
|
onAdd={createSite}
|
||||||
addButtonText="Add Site"
|
addButtonText="Add Site"
|
||||||
defaultSort={{
|
|
||||||
id: "name",
|
|
||||||
desc: false
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
|
||||||
ColumnDef,
|
|
||||||
flexRender,
|
|
||||||
getCoreRowModel,
|
|
||||||
useReactTable,
|
|
||||||
getPaginationRowModel,
|
|
||||||
SortingState,
|
|
||||||
getSortedRowModel,
|
|
||||||
ColumnFiltersState,
|
|
||||||
getFilteredRowModel
|
|
||||||
} from "@tanstack/react-table";
|
|
||||||
|
|
||||||
import {
|
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableContainer,
|
|
||||||
TableHead,
|
|
||||||
TableHeader,
|
|
||||||
TableRow
|
|
||||||
} from "@/components/ui/table";
|
|
||||||
import { Button } from "@app/components/ui/button";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { Input } from "@app/components/ui/input";
|
|
||||||
import { DataTablePagination } from "@app/components/DataTablePagination";
|
|
||||||
import { Plus, Search } from "lucide-react";
|
|
||||||
import { DataTable } from "@app/components/ui/data-table";
|
|
||||||
|
|
||||||
interface DataTableProps<TData, TValue> {
|
|
||||||
columns: ColumnDef<TData, TValue>[];
|
|
||||||
data: TData[];
|
|
||||||
addApiKey?: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ApiKeysDataTable<TData, TValue>({
|
|
||||||
addApiKey,
|
|
||||||
columns,
|
|
||||||
data
|
|
||||||
}: DataTableProps<TData, TValue>) {
|
|
||||||
return (
|
|
||||||
<DataTable
|
|
||||||
columns={columns}
|
|
||||||
data={data}
|
|
||||||
title="API Keys"
|
|
||||||
searchPlaceholder="Search API keys..."
|
|
||||||
searchColumn="name"
|
|
||||||
onAdd={addApiKey}
|
|
||||||
addButtonText="Generate API Key"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,194 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
DropdownMenuTrigger
|
|
||||||
} from "@app/components/ui/dropdown-menu";
|
|
||||||
import { Button } from "@app/components/ui/button";
|
|
||||||
import { ArrowRight, ArrowUpDown, MoreHorizontal } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import { useState } from "react";
|
|
||||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
|
||||||
import { toast } from "@app/hooks/useToast";
|
|
||||||
import { formatAxiosError } from "@app/lib/api";
|
|
||||||
import { createApiClient } from "@app/lib/api";
|
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
|
||||||
import moment from "moment";
|
|
||||||
import { ApiKeysDataTable } from "./ApiKeysDataTable";
|
|
||||||
|
|
||||||
export type ApiKeyRow = {
|
|
||||||
id: string;
|
|
||||||
key: string;
|
|
||||||
name: string;
|
|
||||||
createdAt: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type ApiKeyTableProps = {
|
|
||||||
apiKeys: ApiKeyRow[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
|
||||||
const [selected, setSelected] = useState<ApiKeyRow | null>(null);
|
|
||||||
const [rows, setRows] = useState<ApiKeyRow[]>(apiKeys);
|
|
||||||
|
|
||||||
const api = createApiClient(useEnvContext());
|
|
||||||
|
|
||||||
const deleteSite = (apiKeyId: string) => {
|
|
||||||
api.delete(`/api-key/${apiKeyId}`)
|
|
||||||
.catch((e) => {
|
|
||||||
console.error("Error deleting API key", e);
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: "Error deleting API key",
|
|
||||||
description: formatAxiosError(e, "Error deleting API key")
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
router.refresh();
|
|
||||||
setIsDeleteModalOpen(false);
|
|
||||||
|
|
||||||
const newRows = rows.filter((row) => row.id !== apiKeyId);
|
|
||||||
|
|
||||||
setRows(newRows);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const columns: ColumnDef<ApiKeyRow>[] = [
|
|
||||||
{
|
|
||||||
id: "dots",
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const apiKeyROw = row.original;
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
|
||||||
<span className="sr-only">Open menu</span>
|
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end">
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => {
|
|
||||||
setSelected(apiKeyROw);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>View settings</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => {
|
|
||||||
setSelected(apiKeyROw);
|
|
||||||
setIsDeleteModalOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span className="text-red-500">Delete</span>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "name",
|
|
||||||
header: ({ column }) => {
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() =>
|
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Name
|
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "key",
|
|
||||||
header: "Key",
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const r = row.original;
|
|
||||||
return <span className="font-mono">{r.key}</span>;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
accessorKey: "createdAt",
|
|
||||||
header: "Created At",
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const r = row.original;
|
|
||||||
return <span>{moment(r.createdAt).format("lll")} </span>;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "actions",
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const r = row.original;
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-end">
|
|
||||||
<Link href={`/admin/api-keys/${r.id}`}>
|
|
||||||
<Button variant={"outlinePrimary"} className="ml-2">
|
|
||||||
Edit
|
|
||||||
<ArrowRight className="ml-2 w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{selected && (
|
|
||||||
<ConfirmDeleteDialog
|
|
||||||
open={isDeleteModalOpen}
|
|
||||||
setOpen={(val) => {
|
|
||||||
setIsDeleteModalOpen(val);
|
|
||||||
setSelected(null);
|
|
||||||
}}
|
|
||||||
dialog={
|
|
||||||
<div className="space-y-4">
|
|
||||||
<p>
|
|
||||||
Are you sure you want to remove the API key{" "}
|
|
||||||
<b>{selected?.name || selected?.id}</b>?
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
<b>
|
|
||||||
Once removed, the API key will no longer be
|
|
||||||
able to be used.
|
|
||||||
</b>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
To confirm, please type the name of the API key
|
|
||||||
below.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
buttonText="Confirm Delete API Key"
|
|
||||||
onConfirm={async () => deleteSite(selected!.id)}
|
|
||||||
string={selected.name}
|
|
||||||
title="Delete API Key"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ApiKeysDataTable
|
|
||||||
columns={columns}
|
|
||||||
data={rows}
|
|
||||||
addApiKey={() => {
|
|
||||||
router.push(`/admin/api-keys/create`);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,57 +0,0 @@
|
||||||
import { internal } from "@app/lib/api";
|
|
||||||
import { AxiosResponse } from "axios";
|
|
||||||
import { redirect } from "next/navigation";
|
|
||||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
|
||||||
import { SidebarSettings } from "@app/components/SidebarSettings";
|
|
||||||
import Link from "next/link";
|
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
|
||||||
import {
|
|
||||||
Breadcrumb,
|
|
||||||
BreadcrumbItem,
|
|
||||||
BreadcrumbList,
|
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator
|
|
||||||
} from "@app/components/ui/breadcrumb";
|
|
||||||
import { GetApiKeyResponse } from "@server/routers/apiKeys";
|
|
||||||
import ApiKeyProvider from "@app/providers/ApiKeyProvider";
|
|
||||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
|
||||||
|
|
||||||
interface SettingsLayoutProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
params: Promise<{ apiKeyId: string }>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function SettingsLayout(props: SettingsLayoutProps) {
|
|
||||||
const params = await props.params;
|
|
||||||
|
|
||||||
const { children } = props;
|
|
||||||
|
|
||||||
let apiKey = null;
|
|
||||||
try {
|
|
||||||
const res = await internal.get<AxiosResponse<GetApiKeyResponse>>(
|
|
||||||
`/api-key/${params.apiKeyId}`,
|
|
||||||
await authCookieHeader()
|
|
||||||
);
|
|
||||||
apiKey = res.data.data;
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
redirect(`/admin/api-keys`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const navItems = [
|
|
||||||
{
|
|
||||||
title: "Permissions",
|
|
||||||
href: "/admin/api-keys/{apiKeyId}/permissions"
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SettingsSectionTitle title={`${apiKey?.name} Settings`} />
|
|
||||||
|
|
||||||
<ApiKeyProvider apiKey={apiKey}>
|
|
||||||
<HorizontalTabs items={navItems}>{children}</HorizontalTabs>
|
|
||||||
</ApiKeyProvider>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { redirect } from "next/navigation";
|
|
||||||
|
|
||||||
export default async function ApiKeysPage(props: {
|
|
||||||
params: Promise<{ apiKeyId: string }>;
|
|
||||||
}) {
|
|
||||||
const params = await props.params;
|
|
||||||
redirect(`/admin/api-keys/${params.apiKeyId}/permissions`);
|
|
||||||
}
|
|
|
@ -1,134 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import PermissionsSelectBox from "@app/components/PermissionsSelectBox";
|
|
||||||
import {
|
|
||||||
SettingsContainer,
|
|
||||||
SettingsSection,
|
|
||||||
SettingsSectionBody,
|
|
||||||
SettingsSectionDescription,
|
|
||||||
SettingsSectionFooter,
|
|
||||||
SettingsSectionHeader,
|
|
||||||
SettingsSectionTitle
|
|
||||||
} from "@app/components/Settings";
|
|
||||||
import { Button } from "@app/components/ui/button";
|
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
|
||||||
import { toast } from "@app/hooks/useToast";
|
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
|
||||||
import { ListApiKeyActionsResponse } from "@server/routers/apiKeys";
|
|
||||||
import { AxiosResponse } from "axios";
|
|
||||||
import { useParams } from "next/navigation";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
const { env } = useEnvContext();
|
|
||||||
const api = createApiClient({ env });
|
|
||||||
const { apiKeyId } = useParams();
|
|
||||||
|
|
||||||
const [loadingPage, setLoadingPage] = useState<boolean>(true);
|
|
||||||
const [selectedPermissions, setSelectedPermissions] = useState<
|
|
||||||
Record<string, boolean>
|
|
||||||
>({});
|
|
||||||
const [loadingSavePermissions, setLoadingSavePermissions] =
|
|
||||||
useState<boolean>(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function load() {
|
|
||||||
setLoadingPage(true);
|
|
||||||
|
|
||||||
const res = await api
|
|
||||||
.get<
|
|
||||||
AxiosResponse<ListApiKeyActionsResponse>
|
|
||||||
>(`/api-key/${apiKeyId}/actions`)
|
|
||||||
.catch((e) => {
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: "Error loading API key actions",
|
|
||||||
description: formatAxiosError(
|
|
||||||
e,
|
|
||||||
"Error loading API key actions"
|
|
||||||
)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res && res.status === 200) {
|
|
||||||
const data = res.data.data;
|
|
||||||
for (const action of data.actions) {
|
|
||||||
setSelectedPermissions((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[action.actionId]: true
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoadingPage(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
load();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function savePermissions() {
|
|
||||||
setLoadingSavePermissions(true);
|
|
||||||
|
|
||||||
const actionsRes = await api
|
|
||||||
.post(`/api-key/${apiKeyId}/actions`, {
|
|
||||||
actionIds: Object.keys(selectedPermissions).filter(
|
|
||||||
(key) => selectedPermissions[key]
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error("Error setting permissions", e);
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: "Error setting permissions",
|
|
||||||
description: formatAxiosError(e)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (actionsRes && actionsRes.status === 200) {
|
|
||||||
toast({
|
|
||||||
title: "Permissions updated",
|
|
||||||
description: "The permissions have been updated."
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoadingSavePermissions(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{!loadingPage && (
|
|
||||||
<SettingsContainer>
|
|
||||||
<SettingsSection>
|
|
||||||
<SettingsSectionHeader>
|
|
||||||
<SettingsSectionTitle>
|
|
||||||
Permissions
|
|
||||||
</SettingsSectionTitle>
|
|
||||||
<SettingsSectionDescription>
|
|
||||||
Determine what this API key can do
|
|
||||||
</SettingsSectionDescription>
|
|
||||||
</SettingsSectionHeader>
|
|
||||||
<SettingsSectionBody>
|
|
||||||
<PermissionsSelectBox
|
|
||||||
selectedPermissions={selectedPermissions}
|
|
||||||
onChange={setSelectedPermissions}
|
|
||||||
root={true}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SettingsSectionFooter>
|
|
||||||
<Button
|
|
||||||
onClick={async () => {
|
|
||||||
await savePermissions();
|
|
||||||
}}
|
|
||||||
loading={loadingSavePermissions}
|
|
||||||
disabled={loadingSavePermissions}
|
|
||||||
>
|
|
||||||
Save Permissions
|
|
||||||
</Button>
|
|
||||||
</SettingsSectionFooter>
|
|
||||||
</SettingsSectionBody>
|
|
||||||
</SettingsSection>
|
|
||||||
</SettingsContainer>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,397 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import {
|
|
||||||
SettingsContainer,
|
|
||||||
SettingsSection,
|
|
||||||
SettingsSectionBody,
|
|
||||||
SettingsSectionDescription,
|
|
||||||
SettingsSectionForm,
|
|
||||||
SettingsSectionHeader,
|
|
||||||
SettingsSectionTitle
|
|
||||||
} from "@app/components/Settings";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage
|
|
||||||
} from "@app/components/ui/form";
|
|
||||||
import HeaderTitle from "@app/components/SettingsSectionTitle";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { Input } from "@app/components/ui/input";
|
|
||||||
import { InfoIcon } from "lucide-react";
|
|
||||||
import { Button } from "@app/components/ui/button";
|
|
||||||
import { Checkbox, CheckboxWithLabel } from "@app/components/ui/checkbox";
|
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
|
||||||
import { toast } from "@app/hooks/useToast";
|
|
||||||
import { AxiosResponse } from "axios";
|
|
||||||
import { useParams, useRouter } from "next/navigation";
|
|
||||||
import {
|
|
||||||
Breadcrumb,
|
|
||||||
BreadcrumbItem,
|
|
||||||
BreadcrumbList,
|
|
||||||
BreadcrumbPage,
|
|
||||||
BreadcrumbSeparator
|
|
||||||
} from "@app/components/ui/breadcrumb";
|
|
||||||
import Link from "next/link";
|
|
||||||
import {
|
|
||||||
CreateOrgApiKeyBody,
|
|
||||||
CreateOrgApiKeyResponse
|
|
||||||
} from "@server/routers/apiKeys";
|
|
||||||
import {
|
|
||||||
InfoSection,
|
|
||||||
InfoSectionContent,
|
|
||||||
InfoSections,
|
|
||||||
InfoSectionTitle
|
|
||||||
} from "@app/components/InfoSection";
|
|
||||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
|
||||||
import moment from "moment";
|
|
||||||
import CopyTextBox from "@app/components/CopyTextBox";
|
|
||||||
import PermissionsSelectBox from "@app/components/PermissionsSelectBox";
|
|
||||||
|
|
||||||
const createFormSchema = z.object({
|
|
||||||
name: z
|
|
||||||
.string()
|
|
||||||
.min(2, {
|
|
||||||
message: "Name must be at least 2 characters."
|
|
||||||
})
|
|
||||||
.max(255, {
|
|
||||||
message: "Name must not be longer than 255 characters."
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
type CreateFormValues = z.infer<typeof createFormSchema>;
|
|
||||||
|
|
||||||
const copiedFormSchema = z
|
|
||||||
.object({
|
|
||||||
copied: z.boolean()
|
|
||||||
})
|
|
||||||
.refine(
|
|
||||||
(data) => {
|
|
||||||
return data.copied;
|
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "You must confirm that you have copied the API key.",
|
|
||||||
path: ["copied"]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
type CopiedFormValues = z.infer<typeof copiedFormSchema>;
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
const { env } = useEnvContext();
|
|
||||||
const api = createApiClient({ env });
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const [loadingPage, setLoadingPage] = useState(true);
|
|
||||||
const [createLoading, setCreateLoading] = useState(false);
|
|
||||||
const [apiKey, setApiKey] = useState<CreateOrgApiKeyResponse | null>(null);
|
|
||||||
const [selectedPermissions, setSelectedPermissions] = useState<
|
|
||||||
Record<string, boolean>
|
|
||||||
>({});
|
|
||||||
|
|
||||||
const form = useForm<CreateFormValues>({
|
|
||||||
resolver: zodResolver(createFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
name: ""
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const copiedForm = useForm<CopiedFormValues>({
|
|
||||||
resolver: zodResolver(copiedFormSchema),
|
|
||||||
defaultValues: {
|
|
||||||
copied: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function onSubmit(data: CreateFormValues) {
|
|
||||||
setCreateLoading(true);
|
|
||||||
|
|
||||||
let payload: CreateOrgApiKeyBody = {
|
|
||||||
name: data.name
|
|
||||||
};
|
|
||||||
|
|
||||||
const res = await api
|
|
||||||
.put<AxiosResponse<CreateOrgApiKeyResponse>>(`/api-key`, payload)
|
|
||||||
.catch((e) => {
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: "Error creating API key",
|
|
||||||
description: formatAxiosError(e)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res && res.status === 201) {
|
|
||||||
const data = res.data.data;
|
|
||||||
|
|
||||||
console.log({
|
|
||||||
actionIds: Object.keys(selectedPermissions).filter(
|
|
||||||
(key) => selectedPermissions[key]
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
const actionsRes = await api
|
|
||||||
.post(`/api-key/${data.apiKeyId}/actions`, {
|
|
||||||
actionIds: Object.keys(selectedPermissions).filter(
|
|
||||||
(key) => selectedPermissions[key]
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
console.error("Error setting permissions", e);
|
|
||||||
toast({
|
|
||||||
variant: "destructive",
|
|
||||||
title: "Error setting permissions",
|
|
||||||
description: formatAxiosError(e)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (actionsRes) {
|
|
||||||
setApiKey(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setCreateLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onCopiedSubmit(data: CopiedFormValues) {
|
|
||||||
if (!data.copied) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
router.push(`/admin/api-keys`);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const load = async () => {
|
|
||||||
setLoadingPage(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
load();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<HeaderTitle
|
|
||||||
title="Generate API Key"
|
|
||||||
description="Generate a new root access API key"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
onClick={() => {
|
|
||||||
router.push(`/admin/api-keys`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
See All API Keys
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!loadingPage && (
|
|
||||||
<div>
|
|
||||||
<SettingsContainer>
|
|
||||||
{!apiKey && (
|
|
||||||
<>
|
|
||||||
<SettingsSection>
|
|
||||||
<SettingsSectionHeader>
|
|
||||||
<SettingsSectionTitle>
|
|
||||||
API Key Information
|
|
||||||
</SettingsSectionTitle>
|
|
||||||
</SettingsSectionHeader>
|
|
||||||
<SettingsSectionBody>
|
|
||||||
<SettingsSectionForm>
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
className="space-y-4"
|
|
||||||
id="create-site-form"
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
Name
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
autoComplete="off"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</SettingsSectionForm>
|
|
||||||
</SettingsSectionBody>
|
|
||||||
</SettingsSection>
|
|
||||||
|
|
||||||
<SettingsSection>
|
|
||||||
<SettingsSectionHeader>
|
|
||||||
<SettingsSectionTitle>
|
|
||||||
Permissions
|
|
||||||
</SettingsSectionTitle>
|
|
||||||
<SettingsSectionDescription>
|
|
||||||
Determine what this API key can do
|
|
||||||
</SettingsSectionDescription>
|
|
||||||
</SettingsSectionHeader>
|
|
||||||
<SettingsSectionBody>
|
|
||||||
<PermissionsSelectBox
|
|
||||||
root={true}
|
|
||||||
selectedPermissions={
|
|
||||||
selectedPermissions
|
|
||||||
}
|
|
||||||
onChange={setSelectedPermissions}
|
|
||||||
/>
|
|
||||||
</SettingsSectionBody>
|
|
||||||
</SettingsSection>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{apiKey && (
|
|
||||||
<SettingsSection>
|
|
||||||
<SettingsSectionHeader>
|
|
||||||
<SettingsSectionTitle>
|
|
||||||
Your API Key
|
|
||||||
</SettingsSectionTitle>
|
|
||||||
</SettingsSectionHeader>
|
|
||||||
<SettingsSectionBody>
|
|
||||||
<InfoSections cols={2}>
|
|
||||||
<InfoSection>
|
|
||||||
<InfoSectionTitle>
|
|
||||||
Name
|
|
||||||
</InfoSectionTitle>
|
|
||||||
<InfoSectionContent>
|
|
||||||
<CopyToClipboard
|
|
||||||
text={apiKey.name}
|
|
||||||
/>
|
|
||||||
</InfoSectionContent>
|
|
||||||
</InfoSection>
|
|
||||||
<InfoSection>
|
|
||||||
<InfoSectionTitle>
|
|
||||||
Created
|
|
||||||
</InfoSectionTitle>
|
|
||||||
<InfoSectionContent>
|
|
||||||
{moment(
|
|
||||||
apiKey.createdAt
|
|
||||||
).format("lll")}
|
|
||||||
</InfoSectionContent>
|
|
||||||
</InfoSection>
|
|
||||||
</InfoSections>
|
|
||||||
|
|
||||||
<Alert variant="neutral">
|
|
||||||
<InfoIcon className="h-4 w-4" />
|
|
||||||
<AlertTitle className="font-semibold">
|
|
||||||
Save Your API Key
|
|
||||||
</AlertTitle>
|
|
||||||
<AlertDescription>
|
|
||||||
You will only be able to see this
|
|
||||||
once. Make sure to copy it to a
|
|
||||||
secure place.
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
|
|
||||||
<h4 className="font-semibold">
|
|
||||||
Your API key is:
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
<CopyTextBox
|
|
||||||
text={`${apiKey.apiKeyId}.${apiKey.apiKey}`}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Form {...copiedForm}>
|
|
||||||
<form
|
|
||||||
className="space-y-4"
|
|
||||||
id="copied-form"
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={copiedForm.control}
|
|
||||||
name="copied"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
id="terms"
|
|
||||||
defaultChecked={
|
|
||||||
copiedForm.getValues(
|
|
||||||
"copied"
|
|
||||||
) as boolean
|
|
||||||
}
|
|
||||||
onCheckedChange={(
|
|
||||||
e
|
|
||||||
) => {
|
|
||||||
copiedForm.setValue(
|
|
||||||
"copied",
|
|
||||||
e as boolean
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="terms"
|
|
||||||
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
|
||||||
>
|
|
||||||
I have copied
|
|
||||||
the API key
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</SettingsSectionBody>
|
|
||||||
</SettingsSection>
|
|
||||||
)}
|
|
||||||
</SettingsContainer>
|
|
||||||
|
|
||||||
<div className="flex justify-end space-x-2 mt-8">
|
|
||||||
{!apiKey && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="outline"
|
|
||||||
disabled={createLoading || apiKey !== null}
|
|
||||||
onClick={() => {
|
|
||||||
router.push(`/admin/api-keys`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{!apiKey && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
loading={createLoading}
|
|
||||||
disabled={createLoading || apiKey !== null}
|
|
||||||
onClick={() => {
|
|
||||||
form.handleSubmit(onSubmit)();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Generate
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{apiKey && (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
copiedForm.handleSubmit(onCopiedSubmit)();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Done
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,41 +0,0 @@
|
||||||
import { internal } from "@app/lib/api";
|
|
||||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
|
||||||
import { AxiosResponse } from "axios";
|
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
|
||||||
import { ListRootApiKeysResponse } from "@server/routers/apiKeys";
|
|
||||||
import ApiKeysTable, { ApiKeyRow } from "./ApiKeysTable";
|
|
||||||
|
|
||||||
type ApiKeyPageProps = {};
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
|
||||||
|
|
||||||
export default async function ApiKeysPage(props: ApiKeyPageProps) {
|
|
||||||
let apiKeys: ListRootApiKeysResponse["apiKeys"] = [];
|
|
||||||
try {
|
|
||||||
const res = await internal.get<AxiosResponse<ListRootApiKeysResponse>>(
|
|
||||||
`/api-keys`,
|
|
||||||
await authCookieHeader()
|
|
||||||
);
|
|
||||||
apiKeys = res.data.data.apiKeys;
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
const rows: ApiKeyRow[] = apiKeys.map((key) => {
|
|
||||||
return {
|
|
||||||
name: key.name,
|
|
||||||
id: key.apiKeyId,
|
|
||||||
key: `${key.apiKeyId}••••••••••••••••••••${key.lastChars}`,
|
|
||||||
createdAt: key.createdAt
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SettingsSectionTitle
|
|
||||||
title="Manage API Keys"
|
|
||||||
description="API keys are used to authenticate with the integration API"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ApiKeysTable apiKeys={rows} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -33,7 +33,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Organization Policies",
|
title: "Organization Policies",
|
||||||
href: `/admin/idp/${params.idpId}/policies`
|
href: `/admin/idp/${params.idpId}/policies`,
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
368
src/app/admin/idp/[idpId]/policies/EditPolicyForm.tsx
Normal file
368
src/app/admin/idp/[idpId]/policies/EditPolicyForm.tsx
Normal file
|
@ -0,0 +1,368 @@
|
||||||
|
import {
|
||||||
|
Credenza,
|
||||||
|
CredenzaBody,
|
||||||
|
CredenzaClose,
|
||||||
|
CredenzaContent,
|
||||||
|
CredenzaDescription,
|
||||||
|
CredenzaFooter,
|
||||||
|
CredenzaHeader,
|
||||||
|
CredenzaTitle
|
||||||
|
} from "@app/components/Credenza";
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { PolicyRow } from "./PolicyTable";
|
||||||
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage
|
||||||
|
} from "@app/components/ui/form";
|
||||||
|
import { Input } from "@app/components/ui/input";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger
|
||||||
|
} from "@app/components/ui/popover";
|
||||||
|
import { cn } from "@app/lib/cn";
|
||||||
|
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList
|
||||||
|
} from "@app/components/ui/command";
|
||||||
|
import type { Org } from "@server/db/schemas";
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import { CreateIdpOrgPolicyResponse } from "@server/routers/idp";
|
||||||
|
import { toast } from "@app/hooks/useToast";
|
||||||
|
|
||||||
|
type EditPolicyFormProps = {
|
||||||
|
idpId: string;
|
||||||
|
orgs: Org[];
|
||||||
|
policies: PolicyRow[];
|
||||||
|
policyToEdit: PolicyRow | null;
|
||||||
|
open: boolean;
|
||||||
|
setOpen: (open: boolean) => void;
|
||||||
|
afterCreate?: (policy: PolicyRow) => void;
|
||||||
|
afterEdit?: (policy: PolicyRow) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formSchema = z.object({
|
||||||
|
orgId: z.string(),
|
||||||
|
roleMapping: z.string().optional(),
|
||||||
|
orgMapping: z.string().optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function EditPolicyForm({
|
||||||
|
idpId,
|
||||||
|
orgs,
|
||||||
|
policies,
|
||||||
|
policyToEdit,
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
afterCreate,
|
||||||
|
afterEdit
|
||||||
|
}: EditPolicyFormProps) {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [orgsPopoverOpen, setOrgsPopoverOpen] = useState(false);
|
||||||
|
|
||||||
|
const api = createApiClient(useEnvContext());
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
|
roleMapping: "",
|
||||||
|
orgMapping: ""
|
||||||
|
};
|
||||||
|
|
||||||
|
const form = useForm<z.infer<typeof formSchema>>({
|
||||||
|
resolver: zodResolver(formSchema),
|
||||||
|
defaultValues,
|
||||||
|
// @ts-ignore
|
||||||
|
values: policyToEdit
|
||||||
|
? {
|
||||||
|
orgId: policyToEdit.orgId,
|
||||||
|
roleMapping: policyToEdit.roleMapping || "",
|
||||||
|
orgMapping: policyToEdit.orgMapping || ""
|
||||||
|
}
|
||||||
|
: defaultValues
|
||||||
|
});
|
||||||
|
|
||||||
|
async function onSubmit(values: z.infer<typeof formSchema>) {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
if (policyToEdit) {
|
||||||
|
const res = await api
|
||||||
|
.post<AxiosResponse<CreateIdpOrgPolicyResponse>>(
|
||||||
|
`/idp/${idpId}/org/${values.orgId}`,
|
||||||
|
{
|
||||||
|
roleMapping: values.roleMapping,
|
||||||
|
orgMapping: values.orgMapping
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch((e) => {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Failed to create org policy",
|
||||||
|
description: formatAxiosError(
|
||||||
|
e,
|
||||||
|
"An error occurred while updating the org policy."
|
||||||
|
)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res && res.status === 200) {
|
||||||
|
toast({
|
||||||
|
variant: "default",
|
||||||
|
title: "Org policy created",
|
||||||
|
description: "The org policy has been successfully updated."
|
||||||
|
});
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
|
||||||
|
if (afterEdit) {
|
||||||
|
afterEdit({
|
||||||
|
orgId: values.orgId,
|
||||||
|
roleMapping: values.roleMapping ?? null,
|
||||||
|
orgMapping: values.orgMapping ?? null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const res = await api
|
||||||
|
.put<AxiosResponse<CreateIdpOrgPolicyResponse>>(
|
||||||
|
`/idp/${idpId}/org/${values.orgId}`,
|
||||||
|
{
|
||||||
|
roleMapping: values.roleMapping,
|
||||||
|
orgMapping: values.orgMapping
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch((e) => {
|
||||||
|
toast({
|
||||||
|
variant: "destructive",
|
||||||
|
title: "Failed to create role",
|
||||||
|
description: formatAxiosError(
|
||||||
|
e,
|
||||||
|
"An error occurred while creating the role."
|
||||||
|
)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res && res.status === 201) {
|
||||||
|
toast({
|
||||||
|
variant: "default",
|
||||||
|
title: "Org policy created",
|
||||||
|
description: "The org policy has been successfully created."
|
||||||
|
});
|
||||||
|
|
||||||
|
setOpen(false);
|
||||||
|
|
||||||
|
if (afterCreate) {
|
||||||
|
afterCreate({
|
||||||
|
orgId: values.orgId,
|
||||||
|
roleMapping: values.roleMapping ?? null,
|
||||||
|
orgMapping: values.orgMapping ?? null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Credenza
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(val) => {
|
||||||
|
setOpen(val);
|
||||||
|
setLoading(false);
|
||||||
|
setOrgsPopoverOpen(false);
|
||||||
|
form.reset();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CredenzaContent>
|
||||||
|
<CredenzaHeader>
|
||||||
|
<CredenzaTitle>
|
||||||
|
{policyToEdit ? "Edit" : "Create"} Organization Policy
|
||||||
|
</CredenzaTitle>
|
||||||
|
<CredenzaDescription>
|
||||||
|
Configure access for an organization
|
||||||
|
</CredenzaDescription>
|
||||||
|
</CredenzaHeader>
|
||||||
|
<CredenzaBody>
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="space-y-4"
|
||||||
|
id="edit-policy-form"
|
||||||
|
>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="orgId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-col">
|
||||||
|
<FormLabel>Organization</FormLabel>
|
||||||
|
{policyToEdit ? (
|
||||||
|
<Input {...field} disabled />
|
||||||
|
) : (
|
||||||
|
<Popover
|
||||||
|
open={orgsPopoverOpen}
|
||||||
|
onOpenChange={
|
||||||
|
setOrgsPopoverOpen
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
className={cn(
|
||||||
|
"justify-between",
|
||||||
|
!field.value &&
|
||||||
|
"text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{field.value ??
|
||||||
|
"Select organization"}
|
||||||
|
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="Search site" />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>
|
||||||
|
No site found.
|
||||||
|
</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{orgs.map(
|
||||||
|
(org) => {
|
||||||
|
if (
|
||||||
|
policies.find(
|
||||||
|
(
|
||||||
|
p
|
||||||
|
) =>
|
||||||
|
p.orgId ===
|
||||||
|
org.orgId
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<CommandItem
|
||||||
|
value={
|
||||||
|
org.orgId
|
||||||
|
}
|
||||||
|
key={
|
||||||
|
org.orgId
|
||||||
|
}
|
||||||
|
onSelect={() => {
|
||||||
|
form.setValue(
|
||||||
|
"orgId",
|
||||||
|
org.orgId
|
||||||
|
);
|
||||||
|
setOrgsPopoverOpen(
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
org.orgId ===
|
||||||
|
field.value
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
org.name
|
||||||
|
}
|
||||||
|
</CommandItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="roleMapping"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
Role Mapping Path (Optional)
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
JMESPath to extract role information
|
||||||
|
from the ID token. The result of
|
||||||
|
this expression must return the role
|
||||||
|
name(s) as defined in the
|
||||||
|
organization as a string/list of
|
||||||
|
strings.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="orgMapping"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
Organization Mapping Path (Optional)
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
JMESPath to extract organization
|
||||||
|
information from the ID token. This
|
||||||
|
expression must return thr org ID or
|
||||||
|
true for the user to be allowed to
|
||||||
|
access the organization.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</CredenzaBody>
|
||||||
|
<CredenzaFooter>
|
||||||
|
<CredenzaClose asChild>
|
||||||
|
<Button variant="outline">Close</Button>
|
||||||
|
</CredenzaClose>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
form="edit-policy-form"
|
||||||
|
loading={loading}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{policyToEdit ? "Edit" : "Create"} Policy
|
||||||
|
</Button>
|
||||||
|
</CredenzaFooter>
|
||||||
|
</CredenzaContent>
|
||||||
|
</Credenza>
|
||||||
|
);
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ import { DataTable } from "@app/components/ui/data-table";
|
||||||
interface DataTableProps<TData, TValue> {
|
interface DataTableProps<TData, TValue> {
|
||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
data: TData[];
|
data: TData[];
|
||||||
onAdd: () => void;
|
onAdd?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PolicyDataTable<TData, TValue>({
|
export function PolicyDataTable<TData, TValue>({
|
||||||
|
@ -18,11 +18,11 @@ export function PolicyDataTable<TData, TValue>({
|
||||||
<DataTable
|
<DataTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={data}
|
data={data}
|
||||||
title="Organization Policies"
|
title="Idp organization policies"
|
||||||
searchPlaceholder="Search organization policies..."
|
searchPlaceholder="Search organization policies..."
|
||||||
searchColumn="orgId"
|
searchColumn="orgId"
|
||||||
addButtonText="Add Organization Policy"
|
|
||||||
onAdd={onAdd}
|
onAdd={onAdd}
|
||||||
|
addButtonText="Add Organization Policy"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,62 +1,78 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
import { Button } from "@app/components/ui/button";
|
|
||||||
import {
|
|
||||||
ArrowUpDown,
|
|
||||||
MoreHorizontal,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { PolicyDataTable } from "./PolicyDataTable";
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from "@app/components/ui/dropdown-menu";
|
} from "@app/components/ui/dropdown-menu";
|
||||||
import { InfoPopup } from "@app/components/ui/info-popup";
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import { ArrowUpDown, MoreHorizontal } from "lucide-react";
|
||||||
|
import { PolicyDataTable } from "./PolicyDataTable";
|
||||||
|
|
||||||
export interface PolicyRow {
|
export interface PolicyRow {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
roleMapping?: string;
|
roleMapping: string | null;
|
||||||
orgMapping?: string;
|
orgMapping: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
type PolicyTableProps = {
|
||||||
policies: PolicyRow[];
|
policies: PolicyRow[];
|
||||||
onDelete: (orgId: string) => void;
|
|
||||||
onAdd: () => void;
|
onAdd: () => void;
|
||||||
onEdit: (policy: PolicyRow) => void;
|
onEdit: (row: PolicyRow) => void;
|
||||||
}
|
onDelete: (row: PolicyRow) => void;
|
||||||
|
};
|
||||||
|
|
||||||
export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props) {
|
export default function PolicyTable({
|
||||||
|
policies,
|
||||||
|
onAdd,
|
||||||
|
onEdit,
|
||||||
|
onDelete
|
||||||
|
}: PolicyTableProps) {
|
||||||
const columns: ColumnDef<PolicyRow>[] = [
|
const columns: ColumnDef<PolicyRow>[] = [
|
||||||
{
|
{
|
||||||
id: "dots",
|
id: "actions",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const r = row.original;
|
const policyRow = row.original;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
<Button
|
||||||
<span className="sr-only">Open menu</span>
|
variant="ghost"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<span className="sr-only">
|
||||||
|
Open menu
|
||||||
|
</span>
|
||||||
<MoreHorizontal className="h-4 w-4" />
|
<MoreHorizontal className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => {
|
onClick={() => onEdit(policyRow)}
|
||||||
onDelete(r.orgId);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<span className="text-red-500">Delete</span>
|
Edit Policy
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => onDelete(policyRow)}
|
||||||
|
>
|
||||||
|
<span className="text-red-500">
|
||||||
|
Delete Policy
|
||||||
|
</span>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "orgId",
|
||||||
accessorKey: "orgId",
|
accessorKey: "orgId",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
|
@ -74,74 +90,24 @@ export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "roleMapping",
|
accessorKey: "roleMapping",
|
||||||
header: ({ column }) => {
|
header: "Role Mapping"
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() =>
|
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Role Mapping
|
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const mapping = row.original.roleMapping;
|
|
||||||
return mapping ? (
|
|
||||||
<InfoPopup
|
|
||||||
text={mapping.length > 50 ? `${mapping.substring(0, 50)}...` : mapping}
|
|
||||||
info={mapping}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
"--"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "orgMapping",
|
accessorKey: "orgMapping",
|
||||||
header: ({ column }) => {
|
header: "Organization Mapping"
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={() =>
|
|
||||||
column.toggleSorting(column.getIsSorted() === "asc")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Organization Mapping
|
|
||||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const mapping = row.original.orgMapping;
|
|
||||||
return mapping ? (
|
|
||||||
<InfoPopup
|
|
||||||
text={mapping.length > 50 ? `${mapping.substring(0, 50)}...` : mapping}
|
|
||||||
info={mapping}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
"--"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "actions",
|
id: "edit",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => (
|
||||||
const policy = row.original;
|
<div className="flex items-center justify-end space-x-2">
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-end">
|
|
||||||
<Button
|
<Button
|
||||||
variant={"outlinePrimary"}
|
variant="outlinePrimary"
|
||||||
className="ml-2"
|
onClick={() => onEdit(row.original)}
|
||||||
onClick={() => onEdit(policy)}
|
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -1,22 +1,8 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { z } from "zod";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
|
||||||
import { toast } from "@app/hooks/useToast";
|
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { Input } from "@app/components/ui/input";
|
|
||||||
import {
|
|
||||||
Credenza,
|
|
||||||
CredenzaBody,
|
|
||||||
CredenzaClose,
|
|
||||||
CredenzaContent,
|
|
||||||
CredenzaDescription,
|
|
||||||
CredenzaFooter,
|
|
||||||
CredenzaHeader,
|
|
||||||
CredenzaTitle
|
|
||||||
} from "@app/components/Credenza";
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
@ -26,33 +12,10 @@ import {
|
||||||
FormLabel,
|
FormLabel,
|
||||||
FormMessage
|
FormMessage
|
||||||
} from "@app/components/ui/form";
|
} from "@app/components/ui/form";
|
||||||
|
import { Input } from "@app/components/ui/input";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { z } from "zod";
|
import { useRouter, useParams } from "next/navigation";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
|
||||||
import { InfoIcon, ExternalLink, CheckIcon } from "lucide-react";
|
|
||||||
import PolicyTable, { PolicyRow } from "./PolicyTable";
|
|
||||||
import { AxiosResponse } from "axios";
|
|
||||||
import { ListOrgsResponse } from "@server/routers/org";
|
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger
|
|
||||||
} from "@app/components/ui/popover";
|
|
||||||
import { cn } from "@app/lib/cn";
|
|
||||||
import {
|
|
||||||
Command,
|
|
||||||
CommandEmpty,
|
|
||||||
CommandGroup,
|
|
||||||
CommandInput,
|
|
||||||
CommandItem,
|
|
||||||
CommandList
|
|
||||||
} from "@app/components/ui/command";
|
|
||||||
import { CaretSortIcon } from "@radix-ui/react-icons";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { Textarea } from "@app/components/ui/textarea";
|
|
||||||
import { InfoPopup } from "@app/components/ui/info-popup";
|
|
||||||
import { GetIdpResponse } from "@server/routers/idp";
|
|
||||||
import {
|
import {
|
||||||
SettingsContainer,
|
SettingsContainer,
|
||||||
SettingsSection,
|
SettingsSection,
|
||||||
|
@ -60,64 +23,51 @@ import {
|
||||||
SettingsSectionTitle,
|
SettingsSectionTitle,
|
||||||
SettingsSectionDescription,
|
SettingsSectionDescription,
|
||||||
SettingsSectionBody,
|
SettingsSectionBody,
|
||||||
SettingsSectionFooter,
|
SettingsSectionFooter
|
||||||
SettingsSectionForm
|
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
|
import { formatAxiosError } from "@app/lib/api";
|
||||||
|
import { createApiClient } from "@app/lib/api";
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert";
|
||||||
|
import { InfoIcon, ExternalLink } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import {
|
||||||
|
GetIdpResponse,
|
||||||
|
ListIdpOrgPoliciesResponse
|
||||||
|
} from "@server/routers/idp";
|
||||||
|
import PolicyTable, { PolicyRow } from "./PolicyTable";
|
||||||
|
import EditPolicyForm from "./EditPolicyForm";
|
||||||
|
import { ListOrgsResponse } from "@server/routers/org";
|
||||||
|
import type { Org } from "@server/db/schemas";
|
||||||
|
|
||||||
type Organization = {
|
const DefaultMappingsFormSchema = z.object({
|
||||||
orgId: string;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const policyFormSchema = z.object({
|
|
||||||
orgId: z.string().min(1, { message: "Organization is required" }),
|
|
||||||
roleMapping: z.string().optional(),
|
|
||||||
orgMapping: z.string().optional()
|
|
||||||
});
|
|
||||||
|
|
||||||
const defaultMappingsSchema = z.object({
|
|
||||||
defaultRoleMapping: z.string().optional(),
|
defaultRoleMapping: z.string().optional(),
|
||||||
defaultOrgMapping: z.string().optional()
|
defaultOrgMapping: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
type PolicyFormValues = z.infer<typeof policyFormSchema>;
|
type DefaultMappingsFormValues = z.infer<typeof DefaultMappingsFormSchema>;
|
||||||
type DefaultMappingsValues = z.infer<typeof defaultMappingsSchema>;
|
|
||||||
|
|
||||||
export default function PoliciesPage() {
|
export default function PoliciesPage() {
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
const api = createApiClient({ env });
|
const api = createApiClient({ env });
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { idpId } = useParams();
|
const { idpId } = useParams();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const [pageLoading, setPageLoading] = useState(true);
|
|
||||||
const [addPolicyLoading, setAddPolicyLoading] = useState(false);
|
|
||||||
const [editPolicyLoading, setEditPolicyLoading] = useState(false);
|
|
||||||
const [deletePolicyLoading, setDeletePolicyLoading] = useState(false);
|
|
||||||
const [updateDefaultMappingsLoading, setUpdateDefaultMappingsLoading] =
|
const [updateDefaultMappingsLoading, setUpdateDefaultMappingsLoading] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [policies, setPolicies] = useState<PolicyRow[]>([]);
|
const [policies, setPolicies] = useState<PolicyRow[]>([]);
|
||||||
const [organizations, setOrganizations] = useState<Organization[]>([]);
|
const [editPolicyFormOpen, setEditPolicyFormOpen] = useState(false);
|
||||||
const [showAddDialog, setShowAddDialog] = useState(false);
|
const [policyToEdit, setPolicyToEdit] = useState<PolicyRow | null>(null);
|
||||||
const [editingPolicy, setEditingPolicy] = useState<PolicyRow | null>(null);
|
const [orgs, setOrgs] = useState<Org[]>([]);
|
||||||
|
|
||||||
const form = useForm<PolicyFormValues>({
|
const defaultMappingsForm = useForm<DefaultMappingsFormValues>({
|
||||||
resolver: zodResolver(policyFormSchema),
|
resolver: zodResolver(DefaultMappingsFormSchema),
|
||||||
defaultValues: {
|
defaultValues: { defaultRoleMapping: "", defaultOrgMapping: "" }
|
||||||
orgId: "",
|
|
||||||
roleMapping: "",
|
|
||||||
orgMapping: ""
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const defaultMappingsForm = useForm<DefaultMappingsValues>({
|
async function loadIdp() {
|
||||||
resolver: zodResolver(defaultMappingsSchema),
|
|
||||||
defaultValues: {
|
|
||||||
defaultRoleMapping: "",
|
|
||||||
defaultOrgMapping: ""
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const loadIdp = async () => {
|
|
||||||
try {
|
try {
|
||||||
const res = await api.get<AxiosResponse<GetIdpResponse>>(
|
const res = await api.get<AxiosResponse<GetIdpResponse>>(
|
||||||
`/idp/${idpId}`
|
`/idp/${idpId}`
|
||||||
|
@ -136,11 +86,13 @@ export default function PoliciesPage() {
|
||||||
variant: "destructive"
|
variant: "destructive"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const loadPolicies = async () => {
|
async function loadIdpOrgPolicies() {
|
||||||
try {
|
try {
|
||||||
const res = await api.get(`/idp/${idpId}/org`);
|
const res = await api.get<
|
||||||
|
AxiosResponse<ListIdpOrgPoliciesResponse>
|
||||||
|
>(`/idp/${idpId}/org`);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
setPolicies(res.data.data.policies);
|
setPolicies(res.data.data.policies);
|
||||||
}
|
}
|
||||||
|
@ -151,17 +103,13 @@ export default function PoliciesPage() {
|
||||||
variant: "destructive"
|
variant: "destructive"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const loadOrganizations = async () => {
|
async function loadOrgs() {
|
||||||
try {
|
try {
|
||||||
const res = await api.get<AxiosResponse<ListOrgsResponse>>("/orgs");
|
const res = await api.get<AxiosResponse<ListOrgsResponse>>(`/orgs`);
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
const existingOrgIds = policies.map((p) => p.orgId);
|
setOrgs(res.data.data.orgs);
|
||||||
const availableOrgs = res.data.data.orgs.filter(
|
|
||||||
(org) => !existingOrgIds.includes(org.orgId)
|
|
||||||
);
|
|
||||||
setOrganizations(availableOrgs);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({
|
toast({
|
||||||
|
@ -170,121 +118,19 @@ export default function PoliciesPage() {
|
||||||
variant: "destructive"
|
variant: "destructive"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function load() {
|
const load = async () => {
|
||||||
setPageLoading(true);
|
setLoading(true);
|
||||||
await loadPolicies();
|
await Promise.all([loadIdp(), loadIdpOrgPolicies(), loadOrgs()]);
|
||||||
await loadIdp();
|
setLoading(false);
|
||||||
setPageLoading(false);
|
};
|
||||||
}
|
|
||||||
load();
|
load();
|
||||||
}, [idpId]);
|
}, [idpId, api, router]);
|
||||||
|
|
||||||
const onAddPolicy = async (data: PolicyFormValues) => {
|
async function onDefaultMappingsSubmit(data: DefaultMappingsFormValues) {
|
||||||
setAddPolicyLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await api.put(`/idp/${idpId}/org/${data.orgId}`, {
|
|
||||||
roleMapping: data.roleMapping,
|
|
||||||
orgMapping: data.orgMapping
|
|
||||||
});
|
|
||||||
if (res.status === 201) {
|
|
||||||
const newPolicy = {
|
|
||||||
orgId: data.orgId,
|
|
||||||
name:
|
|
||||||
organizations.find((org) => org.orgId === data.orgId)
|
|
||||||
?.name || "",
|
|
||||||
roleMapping: data.roleMapping,
|
|
||||||
orgMapping: data.orgMapping
|
|
||||||
};
|
|
||||||
setPolicies([...policies, newPolicy]);
|
|
||||||
toast({
|
|
||||||
title: "Success",
|
|
||||||
description: "Policy added successfully"
|
|
||||||
});
|
|
||||||
setShowAddDialog(false);
|
|
||||||
form.reset();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: formatAxiosError(e),
|
|
||||||
variant: "destructive"
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setAddPolicyLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onEditPolicy = async (data: PolicyFormValues) => {
|
|
||||||
if (!editingPolicy) return;
|
|
||||||
|
|
||||||
setEditPolicyLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await api.post(
|
|
||||||
`/idp/${idpId}/org/${editingPolicy.orgId}`,
|
|
||||||
{
|
|
||||||
roleMapping: data.roleMapping,
|
|
||||||
orgMapping: data.orgMapping
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (res.status === 200) {
|
|
||||||
setPolicies(
|
|
||||||
policies.map((policy) =>
|
|
||||||
policy.orgId === editingPolicy.orgId
|
|
||||||
? {
|
|
||||||
...policy,
|
|
||||||
roleMapping: data.roleMapping,
|
|
||||||
orgMapping: data.orgMapping
|
|
||||||
}
|
|
||||||
: policy
|
|
||||||
)
|
|
||||||
);
|
|
||||||
toast({
|
|
||||||
title: "Success",
|
|
||||||
description: "Policy updated successfully"
|
|
||||||
});
|
|
||||||
setShowAddDialog(false);
|
|
||||||
setEditingPolicy(null);
|
|
||||||
form.reset();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: formatAxiosError(e),
|
|
||||||
variant: "destructive"
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setEditPolicyLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDeletePolicy = async (orgId: string) => {
|
|
||||||
setDeletePolicyLoading(true);
|
|
||||||
try {
|
|
||||||
const res = await api.delete(`/idp/${idpId}/org/${orgId}`);
|
|
||||||
if (res.status === 200) {
|
|
||||||
setPolicies(
|
|
||||||
policies.filter((policy) => policy.orgId !== orgId)
|
|
||||||
);
|
|
||||||
toast({
|
|
||||||
title: "Success",
|
|
||||||
description: "Policy deleted successfully"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
toast({
|
|
||||||
title: "Error",
|
|
||||||
description: formatAxiosError(e),
|
|
||||||
variant: "destructive"
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setDeletePolicyLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const onUpdateDefaultMappings = async (data: DefaultMappingsValues) => {
|
|
||||||
setUpdateDefaultMappingsLoading(true);
|
setUpdateDefaultMappingsLoading(true);
|
||||||
try {
|
try {
|
||||||
const res = await api.post(`/idp/${idpId}/oidc`, {
|
const res = await api.post(`/idp/${idpId}/oidc`, {
|
||||||
|
@ -306,26 +152,60 @@ export default function PoliciesPage() {
|
||||||
} finally {
|
} finally {
|
||||||
setUpdateDefaultMappingsLoading(false);
|
setUpdateDefaultMappingsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
if (pageLoading) {
|
// Button clicks
|
||||||
return null;
|
|
||||||
|
function onAdd() {
|
||||||
|
setPolicyToEdit(null);
|
||||||
|
setEditPolicyFormOpen(true);
|
||||||
|
}
|
||||||
|
function onEdit(row: PolicyRow) {
|
||||||
|
setPolicyToEdit(row);
|
||||||
|
setEditPolicyFormOpen(true);
|
||||||
|
}
|
||||||
|
function onDelete(row: PolicyRow) {
|
||||||
|
api.delete(`/idp/${idpId}/org/${row.orgId}`)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.status === 200) {
|
||||||
|
toast({
|
||||||
|
title: "Success",
|
||||||
|
description: "Org policy deleted successfully"
|
||||||
|
});
|
||||||
|
const p2 = policies.filter((p) => p.orgId !== row.orgId);
|
||||||
|
setPolicies(p2);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
toast({
|
||||||
|
title: "Error",
|
||||||
|
description: formatAxiosError(e),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function afterCreate(row: PolicyRow) {
|
||||||
|
setPolicies([...policies, row]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function afterEdit(row: PolicyRow) {
|
||||||
|
const p2 = policies.map((p) => (p.orgId === row.orgId ? row : p));
|
||||||
|
setPolicies(p2);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
<Alert variant="neutral" className="mb-6">
|
<Alert variant="neutral" className="">
|
||||||
<InfoIcon className="h-4 w-4" />
|
<InfoIcon className="h-4 w-4" />
|
||||||
<AlertTitle className="font-semibold">
|
<AlertTitle className="font-semibold">
|
||||||
About Organization Policies
|
About Organization Policies
|
||||||
</AlertTitle>
|
</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
Organization policies are used to control access to
|
Organization policies are used to configure access
|
||||||
organizations based on the user's ID token. You can
|
control for a specific organization based on the user's
|
||||||
specify JMESPath expressions to extract role and
|
ID token. For more information, see{" "}
|
||||||
organization information from the ID token. For more
|
|
||||||
information, see{" "}
|
|
||||||
<Link
|
<Link
|
||||||
href="https://docs.fossorial.io/Pangolin/Identity%20Providers/auto-provision"
|
href="https://docs.fossorial.io/Pangolin/Identity%20Providers/auto-provision"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@ -344,9 +224,9 @@ export default function PoliciesPage() {
|
||||||
Default Mappings (Optional)
|
Default Mappings (Optional)
|
||||||
</SettingsSectionTitle>
|
</SettingsSectionTitle>
|
||||||
<SettingsSectionDescription>
|
<SettingsSectionDescription>
|
||||||
The default mappings are used when when there is not
|
The default mappings are used when there is no
|
||||||
an organization policy defined for an organization.
|
organization policy defined for an organization. You
|
||||||
You can specify the default role and organization
|
can specify the default role and organization
|
||||||
mappings to fall back to here.
|
mappings to fall back to here.
|
||||||
</SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
</SettingsSectionHeader>
|
</SettingsSectionHeader>
|
||||||
|
@ -354,10 +234,10 @@ export default function PoliciesPage() {
|
||||||
<Form {...defaultMappingsForm}>
|
<Form {...defaultMappingsForm}>
|
||||||
<form
|
<form
|
||||||
onSubmit={defaultMappingsForm.handleSubmit(
|
onSubmit={defaultMappingsForm.handleSubmit(
|
||||||
onUpdateDefaultMappings
|
onDefaultMappingsSubmit
|
||||||
)}
|
)}
|
||||||
id="policy-default-mappings-form"
|
|
||||||
className="space-y-4"
|
className="space-y-4"
|
||||||
|
id="policy-default-mappings-form"
|
||||||
>
|
>
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
<FormField
|
<FormField
|
||||||
|
@ -372,16 +252,18 @@ export default function PoliciesPage() {
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
The result of this
|
JMESPath to extract role
|
||||||
|
information from the ID
|
||||||
|
token. The result of this
|
||||||
expression must return the
|
expression must return the
|
||||||
role name as defined in the
|
role name(s) as defined in
|
||||||
organization as a string.
|
the organization as a
|
||||||
|
string/list of strings.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={defaultMappingsForm.control}
|
control={defaultMappingsForm.control}
|
||||||
name="defaultOrgMapping"
|
name="defaultOrgMapping"
|
||||||
|
@ -394,10 +276,13 @@ export default function PoliciesPage() {
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
This expression must return
|
JMESPath to extract
|
||||||
thr org ID or true for the
|
organization information
|
||||||
user to be allowed to access
|
from the ID token. This
|
||||||
the organization.
|
expression must return thr
|
||||||
|
org ID or true for the user
|
||||||
|
to be allowed to access the
|
||||||
|
organization.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
@ -420,212 +305,22 @@ export default function PoliciesPage() {
|
||||||
|
|
||||||
<PolicyTable
|
<PolicyTable
|
||||||
policies={policies}
|
policies={policies}
|
||||||
onDelete={onDeletePolicy}
|
onAdd={onAdd}
|
||||||
onAdd={() => {
|
onEdit={onEdit}
|
||||||
loadOrganizations();
|
onDelete={onDelete}
|
||||||
form.reset({
|
/>
|
||||||
orgId: "",
|
|
||||||
roleMapping: "",
|
<EditPolicyForm
|
||||||
orgMapping: ""
|
open={editPolicyFormOpen}
|
||||||
});
|
setOpen={setEditPolicyFormOpen}
|
||||||
setEditingPolicy(null);
|
policyToEdit={policyToEdit}
|
||||||
setShowAddDialog(true);
|
idpId={idpId!.toString()}
|
||||||
}}
|
orgs={orgs}
|
||||||
onEdit={(policy) => {
|
policies={policies}
|
||||||
setEditingPolicy(policy);
|
afterCreate={afterCreate}
|
||||||
form.reset({
|
afterEdit={afterEdit}
|
||||||
orgId: policy.orgId,
|
|
||||||
roleMapping: policy.roleMapping || "",
|
|
||||||
orgMapping: policy.orgMapping || ""
|
|
||||||
});
|
|
||||||
setShowAddDialog(true);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</SettingsContainer>
|
</SettingsContainer>
|
||||||
|
|
||||||
<Credenza
|
|
||||||
open={showAddDialog}
|
|
||||||
onOpenChange={(val) => {
|
|
||||||
setShowAddDialog(val);
|
|
||||||
setEditingPolicy(null);
|
|
||||||
form.reset();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CredenzaContent>
|
|
||||||
<CredenzaHeader>
|
|
||||||
<CredenzaTitle>
|
|
||||||
{editingPolicy
|
|
||||||
? "Edit Organization Policy"
|
|
||||||
: "Add Organization Policy"}
|
|
||||||
</CredenzaTitle>
|
|
||||||
<CredenzaDescription>
|
|
||||||
Configure access for an organization
|
|
||||||
</CredenzaDescription>
|
|
||||||
</CredenzaHeader>
|
|
||||||
<CredenzaBody>
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={form.handleSubmit(
|
|
||||||
editingPolicy ? onEditPolicy : onAddPolicy
|
|
||||||
)}
|
|
||||||
className="space-y-4"
|
|
||||||
id="policy-form"
|
|
||||||
>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="orgId"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-col">
|
|
||||||
<FormLabel>Organization</FormLabel>
|
|
||||||
{editingPolicy ? (
|
|
||||||
<Input {...field} disabled />
|
|
||||||
) : (
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<FormControl>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
|
||||||
"justify-between",
|
|
||||||
!field.value &&
|
|
||||||
"text-muted-foreground"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{field.value
|
|
||||||
? organizations.find(
|
|
||||||
(
|
|
||||||
org
|
|
||||||
) =>
|
|
||||||
org.orgId ===
|
|
||||||
field.value
|
|
||||||
)?.name
|
|
||||||
: "Select organization"}
|
|
||||||
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</FormControl>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="p-0">
|
|
||||||
<Command>
|
|
||||||
<CommandInput placeholder="Search org" />
|
|
||||||
<CommandList>
|
|
||||||
<CommandEmpty>
|
|
||||||
No org
|
|
||||||
found.
|
|
||||||
</CommandEmpty>
|
|
||||||
<CommandGroup>
|
|
||||||
{organizations.map(
|
|
||||||
(
|
|
||||||
org
|
|
||||||
) => (
|
|
||||||
<CommandItem
|
|
||||||
value={`${org.orgId}`}
|
|
||||||
key={
|
|
||||||
org.orgId
|
|
||||||
}
|
|
||||||
onSelect={() => {
|
|
||||||
form.setValue(
|
|
||||||
"orgId",
|
|
||||||
org.orgId
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CheckIcon
|
|
||||||
className={cn(
|
|
||||||
"mr-2 h-4 w-4",
|
|
||||||
org.orgId ===
|
|
||||||
field.value
|
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{
|
|
||||||
org.name
|
|
||||||
}
|
|
||||||
</CommandItem>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
)}
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="roleMapping"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
Role Mapping Path (Optional)
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
The result of this expression
|
|
||||||
must return the role name as
|
|
||||||
defined in the organization as a
|
|
||||||
string.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="orgMapping"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
Organization Mapping Path
|
|
||||||
(Optional)
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
This expression must return the
|
|
||||||
org ID or true for the user to
|
|
||||||
be allowed to access the
|
|
||||||
organization.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</CredenzaBody>
|
|
||||||
<CredenzaFooter>
|
|
||||||
<CredenzaClose asChild>
|
|
||||||
<Button variant="outline">Cancel</Button>
|
|
||||||
</CredenzaClose>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
form="policy-form"
|
|
||||||
loading={
|
|
||||||
editingPolicy
|
|
||||||
? editPolicyLoading
|
|
||||||
: addPolicyLoading
|
|
||||||
}
|
|
||||||
disabled={
|
|
||||||
editingPolicy
|
|
||||||
? editPolicyLoading
|
|
||||||
: addPolicyLoading
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{editingPolicy ? "Update Policy" : "Add Policy"}
|
|
||||||
</Button>
|
|
||||||
</CredenzaFooter>
|
|
||||||
</CredenzaContent>
|
|
||||||
</Credenza>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -410,7 +410,7 @@ export default function Page() {
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
The path to the user
|
The JMESPath to the user
|
||||||
identifier in the ID
|
identifier in the ID
|
||||||
token
|
token
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
|
@ -431,7 +431,7 @@ export default function Page() {
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
The path to the
|
The JMESPath to the
|
||||||
user's email in the ID
|
user's email in the ID
|
||||||
token
|
token
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
|
@ -452,7 +452,7 @@ export default function Page() {
|
||||||
<Input {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
The path to the
|
The JMESPath to the
|
||||||
user's name in the ID
|
user's name in the ID
|
||||||
token
|
token
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import ProfileIcon from "@app/components/ProfileIcon";
|
import ProfileIcon from "@app/components/ProfileIcon";
|
||||||
import { Separator } from "@app/components/ui/separator";
|
|
||||||
import { verifySession } from "@app/lib/auth/verifySession";
|
import { verifySession } from "@app/lib/auth/verifySession";
|
||||||
import UserProvider from "@app/providers/UserProvider";
|
import UserProvider from "@app/providers/UserProvider";
|
||||||
import { ExternalLink } from "lucide-react";
|
|
||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
import { cache } from "react";
|
import { cache } from "react";
|
||||||
|
|
||||||
|
@ -23,52 +21,17 @@ export default async function AuthLayout({ children }: AuthLayoutProps) {
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
{user && (
|
{user && (
|
||||||
<UserProvider user={user}>
|
<UserProvider user={user}>
|
||||||
<div className="p-3 ml-auto">
|
<div className="p-3">
|
||||||
<ProfileIcon />
|
<ProfileIcon />
|
||||||
</div>
|
</div>
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex-1 flex items-center justify-center">
|
<div className="flex-1 flex items-center justify-center">
|
||||||
<div className="w-full max-w-md p-3">{children}</div>
|
<div className="w-full max-w-md p-3">
|
||||||
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer className="hidden md:block w-full mt-12 py-3 mb-6 px-4">
|
|
||||||
<div className="container mx-auto flex flex-wrap justify-center items-center h-3 space-x-4 text-sm text-neutral-400 dark:text-neutral-600">
|
|
||||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
|
||||||
<span>Pangolin</span>
|
|
||||||
</div>
|
</div>
|
||||||
<Separator orientation="vertical" />
|
|
||||||
<a
|
|
||||||
href="https://fossorial.io/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
aria-label="Built by Fossorial"
|
|
||||||
className="flex items-center space-x-2 whitespace-nowrap"
|
|
||||||
>
|
|
||||||
<span>Fossorial</span>
|
|
||||||
<ExternalLink className="w-3 h-3" />
|
|
||||||
</a>
|
|
||||||
<Separator orientation="vertical" />
|
|
||||||
<a
|
|
||||||
href="https://code.thetadev.de/ThetaDev/pangolin"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
aria-label="Repository"
|
|
||||||
className="flex items-center space-x-2 whitespace-nowrap"
|
|
||||||
>
|
|
||||||
<span>Open Source</span>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="currentColor"
|
|
||||||
className="w-3 h-3"
|
|
||||||
>
|
|
||||||
<path d="M12 0C5.37 0 0 5.373 0 12c0 5.303 3.438 9.8 8.207 11.385.6.11.82-.26.82-.577v-2.17c-3.338.726-4.042-1.61-4.042-1.61-.546-1.385-1.333-1.755-1.333-1.755-1.09-.744.082-.73.082-.73 1.205.085 1.84 1.24 1.84 1.24 1.07 1.835 2.807 1.305 3.492.997.107-.775.42-1.305.763-1.605-2.665-.305-5.467-1.335-5.467-5.93 0-1.31.468-2.382 1.236-3.22-.123-.303-.535-1.523.117-3.176 0 0 1.008-.322 3.3 1.23a11.52 11.52 0 013.006-.403c1.02.005 2.045.137 3.006.403 2.29-1.552 3.295-1.23 3.295-1.23.654 1.653.242 2.873.12 3.176.77.838 1.235 1.91 1.235 3.22 0 4.605-2.805 5.623-5.475 5.92.43.37.814 1.1.814 2.22v3.293c0 .32.217.693.825.576C20.565 21.795 24 17.298 24 12 24 5.373 18.627 0 12 0z" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
Combine,
|
Combine,
|
||||||
Fingerprint,
|
Fingerprint,
|
||||||
KeyRound,
|
KeyRound,
|
||||||
|
TicketCheck
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
export const orgLangingNavItems: SidebarNavItem[] = [
|
export const orgLangingNavItems: SidebarNavItem[] = [
|
||||||
|
@ -64,11 +65,14 @@ export const orgNavItems: SidebarNavItem[] = [
|
||||||
href: "/{orgId}/settings/share-links",
|
href: "/{orgId}/settings/share-links",
|
||||||
icon: <LinkIcon className="h-4 w-4" />
|
icon: <LinkIcon className="h-4 w-4" />
|
||||||
},
|
},
|
||||||
|
/*
|
||||||
|
TODO:
|
||||||
{
|
{
|
||||||
title: "API Keys",
|
title: "API Keys",
|
||||||
href: "/{orgId}/settings/api-keys",
|
href: "/{orgId}/settings/api-keys",
|
||||||
icon: <KeyRound className="h-4 w-4" />
|
icon: <KeyRound className="h-4 w-4" />,
|
||||||
},
|
},
|
||||||
|
*/
|
||||||
{
|
{
|
||||||
title: "Settings",
|
title: "Settings",
|
||||||
href: "/{orgId}/settings/general",
|
href: "/{orgId}/settings/general",
|
||||||
|
@ -82,11 +86,14 @@ export const adminNavItems: SidebarNavItem[] = [
|
||||||
href: "/admin/users",
|
href: "/admin/users",
|
||||||
icon: <Users className="h-4 w-4" />
|
icon: <Users className="h-4 w-4" />
|
||||||
},
|
},
|
||||||
|
/*
|
||||||
|
TODO:
|
||||||
{
|
{
|
||||||
title: "API Keys",
|
title: "API Keys",
|
||||||
href: "/admin/api-keys",
|
href: "/admin/api-keys",
|
||||||
icon: <KeyRound className="h-4 w-4" />
|
icon: <KeyRound className="h-4 w-4" />,
|
||||||
},
|
},
|
||||||
|
*/
|
||||||
{
|
{
|
||||||
title: "Identity Providers",
|
title: "Identity Providers",
|
||||||
href: "/admin/idp",
|
href: "/admin/idp",
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
export function AuthFooter() {}
|
|
|
@ -1,229 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { CheckboxWithLabel } from "@app/components/ui/checkbox";
|
|
||||||
import {
|
|
||||||
InfoSection,
|
|
||||||
InfoSectionContent,
|
|
||||||
InfoSections,
|
|
||||||
InfoSectionTitle
|
|
||||||
} from "@app/components/InfoSection";
|
|
||||||
|
|
||||||
type PermissionsSelectBoxProps = {
|
|
||||||
root?: boolean;
|
|
||||||
selectedPermissions: Record<string, boolean>;
|
|
||||||
onChange: (updated: Record<string, boolean>) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
function getActionsCategories(root: boolean) {
|
|
||||||
const actionsByCategory: Record<string, Record<string, string>> = {
|
|
||||||
Organization: {
|
|
||||||
"Get Organization": "getOrg",
|
|
||||||
"Update Organization": "updateOrg",
|
|
||||||
"Get Organization User": "getOrgUser",
|
|
||||||
"List Organization Domains": "listOrgDomains",
|
|
||||||
},
|
|
||||||
|
|
||||||
Site: {
|
|
||||||
"Create Site": "createSite",
|
|
||||||
"Delete Site": "deleteSite",
|
|
||||||
"Get Site": "getSite",
|
|
||||||
"List Sites": "listSites",
|
|
||||||
"Update Site": "updateSite",
|
|
||||||
"List Allowed Site Roles": "listSiteRoles"
|
|
||||||
},
|
|
||||||
|
|
||||||
Resource: {
|
|
||||||
"Create Resource": "createResource",
|
|
||||||
"Delete Resource": "deleteResource",
|
|
||||||
"Get Resource": "getResource",
|
|
||||||
"List Resources": "listResources",
|
|
||||||
"Update Resource": "updateResource",
|
|
||||||
"List Resource Users": "listResourceUsers",
|
|
||||||
"Set Resource Users": "setResourceUsers",
|
|
||||||
"Set Allowed Resource Roles": "setResourceRoles",
|
|
||||||
"List Allowed Resource Roles": "listResourceRoles",
|
|
||||||
"Set Resource Password": "setResourcePassword",
|
|
||||||
"Set Resource Pincode": "setResourcePincode",
|
|
||||||
"Set Resource Email Whitelist": "setResourceWhitelist",
|
|
||||||
"Get Resource Email Whitelist": "getResourceWhitelist"
|
|
||||||
},
|
|
||||||
|
|
||||||
Target: {
|
|
||||||
"Create Target": "createTarget",
|
|
||||||
"Delete Target": "deleteTarget",
|
|
||||||
"Get Target": "getTarget",
|
|
||||||
"List Targets": "listTargets",
|
|
||||||
"Update Target": "updateTarget"
|
|
||||||
},
|
|
||||||
|
|
||||||
Role: {
|
|
||||||
"Create Role": "createRole",
|
|
||||||
"Delete Role": "deleteRole",
|
|
||||||
"Get Role": "getRole",
|
|
||||||
"List Roles": "listRoles",
|
|
||||||
"Update Role": "updateRole",
|
|
||||||
"List Allowed Role Resources": "listRoleResources"
|
|
||||||
},
|
|
||||||
|
|
||||||
User: {
|
|
||||||
"Invite User": "inviteUser",
|
|
||||||
"Remove User": "removeUser",
|
|
||||||
"List Users": "listUsers",
|
|
||||||
"Add User Role": "addUserRole"
|
|
||||||
},
|
|
||||||
|
|
||||||
"Access Token": {
|
|
||||||
"Generate Access Token": "generateAccessToken",
|
|
||||||
"Delete Access Token": "deleteAcessToken",
|
|
||||||
"List Access Tokens": "listAccessTokens"
|
|
||||||
},
|
|
||||||
|
|
||||||
"Resource Rule": {
|
|
||||||
"Create Resource Rule": "createResourceRule",
|
|
||||||
"Delete Resource Rule": "deleteResourceRule",
|
|
||||||
"List Resource Rules": "listResourceRules",
|
|
||||||
"Update Resource Rule": "updateResourceRule"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (root) {
|
|
||||||
actionsByCategory["Organization"] = {
|
|
||||||
"List Organizations": "listOrgs",
|
|
||||||
"Check ID": "checkOrgId",
|
|
||||||
"Create Organization": "createOrg",
|
|
||||||
"Delete Organization": "deleteOrg",
|
|
||||||
"List API Keys": "listApiKeys",
|
|
||||||
"List API Key Actions": "listApiKeyActions",
|
|
||||||
"Set API Key Allowed Actions": "setApiKeyActions",
|
|
||||||
"Create API Key": "createApiKey",
|
|
||||||
"Delete API Key": "deleteApiKey",
|
|
||||||
...actionsByCategory["Organization"]
|
|
||||||
};
|
|
||||||
|
|
||||||
actionsByCategory["Identity Provider (IDP)"] = {
|
|
||||||
"Create IDP": "createIdp",
|
|
||||||
"Update IDP": "updateIdp",
|
|
||||||
"Delete IDP": "deleteIdp",
|
|
||||||
"List IDP": "listIdps",
|
|
||||||
"Get IDP": "getIdp",
|
|
||||||
"Create IDP Org Policy": "createIdpOrg",
|
|
||||||
"Delete IDP Org Policy": "deleteIdpOrg",
|
|
||||||
"List IDP Orgs": "listIdpOrgs",
|
|
||||||
"Update IDP Org": "updateIdpOrg"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return actionsByCategory;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function PermissionsSelectBox({
|
|
||||||
root,
|
|
||||||
selectedPermissions,
|
|
||||||
onChange
|
|
||||||
}: PermissionsSelectBoxProps) {
|
|
||||||
const actionsByCategory = getActionsCategories(root ?? false);
|
|
||||||
|
|
||||||
const togglePermission = (key: string, checked: boolean) => {
|
|
||||||
onChange({
|
|
||||||
...selectedPermissions,
|
|
||||||
[key]: checked
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const areAllCheckedInCategory = (actions: Record<string, string>) => {
|
|
||||||
return Object.values(actions).every(
|
|
||||||
(action) => selectedPermissions[action]
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleAllInCategory = (
|
|
||||||
actions: Record<string, string>,
|
|
||||||
value: boolean
|
|
||||||
) => {
|
|
||||||
const updated = { ...selectedPermissions };
|
|
||||||
Object.values(actions).forEach((action) => {
|
|
||||||
updated[action] = value;
|
|
||||||
});
|
|
||||||
onChange(updated);
|
|
||||||
};
|
|
||||||
|
|
||||||
const allActions = Object.values(actionsByCategory).flatMap(Object.values);
|
|
||||||
const allPermissionsChecked = allActions.every(
|
|
||||||
(action) => selectedPermissions[action]
|
|
||||||
);
|
|
||||||
|
|
||||||
const toggleAllPermissions = (checked: boolean) => {
|
|
||||||
const updated: Record<string, boolean> = {};
|
|
||||||
allActions.forEach((action) => {
|
|
||||||
updated[action] = checked;
|
|
||||||
});
|
|
||||||
onChange(updated);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="mb-4">
|
|
||||||
<CheckboxWithLabel
|
|
||||||
variant="outlinePrimarySquare"
|
|
||||||
id="toggle-all-permissions"
|
|
||||||
label="Allow All Permissions"
|
|
||||||
checked={allPermissionsChecked}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
toggleAllPermissions(checked as boolean)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<InfoSections cols={5}>
|
|
||||||
{Object.entries(actionsByCategory).map(
|
|
||||||
([category, actions]) => {
|
|
||||||
const allChecked = areAllCheckedInCategory(actions);
|
|
||||||
return (
|
|
||||||
<InfoSection key={category}>
|
|
||||||
<InfoSectionTitle>{category}</InfoSectionTitle>
|
|
||||||
<InfoSectionContent>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<CheckboxWithLabel
|
|
||||||
variant="outlinePrimarySquare"
|
|
||||||
id={`toggle-all-${category}`}
|
|
||||||
label="Allow All"
|
|
||||||
checked={allChecked}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
toggleAllInCategory(
|
|
||||||
actions,
|
|
||||||
checked as boolean
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{Object.entries(actions).map(
|
|
||||||
([label, value]) => (
|
|
||||||
<CheckboxWithLabel
|
|
||||||
variant="outlineSquare"
|
|
||||||
key={value}
|
|
||||||
id={value}
|
|
||||||
label={label}
|
|
||||||
checked={
|
|
||||||
!!selectedPermissions[
|
|
||||||
value
|
|
||||||
]
|
|
||||||
}
|
|
||||||
onCheckedChange={(
|
|
||||||
checked
|
|
||||||
) =>
|
|
||||||
togglePermission(
|
|
||||||
value,
|
|
||||||
checked as boolean
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</InfoSectionContent>
|
|
||||||
</InfoSection>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</InfoSections>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -55,7 +55,7 @@ export function SettingsSectionFooter({
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return <div className="flex justify-end space-x-2 mt-auto pt-6">{children}</div>;
|
return <div className="flex justify-end space-x-2 mt-auto pt-8">{children}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SettingsSectionGrid({
|
export function SettingsSectionGrid({
|
||||||
|
|
|
@ -31,7 +31,7 @@ import {
|
||||||
CardTitle
|
CardTitle
|
||||||
} from "@app/components/ui/card";
|
} from "@app/components/ui/card";
|
||||||
|
|
||||||
type DataTableProps<TData, TValue> = {
|
interface DataTableProps<TData, TValue> {
|
||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
data: TData[];
|
data: TData[];
|
||||||
title?: string;
|
title?: string;
|
||||||
|
@ -39,11 +39,7 @@ type DataTableProps<TData, TValue> = {
|
||||||
onAdd?: () => void;
|
onAdd?: () => void;
|
||||||
searchPlaceholder?: string;
|
searchPlaceholder?: string;
|
||||||
searchColumn?: string;
|
searchColumn?: string;
|
||||||
defaultSort?: {
|
}
|
||||||
id: string;
|
|
||||||
desc: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function DataTable<TData, TValue>({
|
export function DataTable<TData, TValue>({
|
||||||
columns,
|
columns,
|
||||||
|
@ -52,12 +48,9 @@ export function DataTable<TData, TValue>({
|
||||||
addButtonText,
|
addButtonText,
|
||||||
onAdd,
|
onAdd,
|
||||||
searchPlaceholder = "Search...",
|
searchPlaceholder = "Search...",
|
||||||
searchColumn = "name",
|
searchColumn = "name"
|
||||||
defaultSort
|
|
||||||
}: DataTableProps<TData, TValue>) {
|
}: DataTableProps<TData, TValue>) {
|
||||||
const [sorting, setSorting] = useState<SortingState>(
|
const [sorting, setSorting] = useState<SortingState>([]);
|
||||||
defaultSort ? [defaultSort] : []
|
|
||||||
);
|
|
||||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||||
const [globalFilter, setGlobalFilter] = useState<any>([]);
|
const [globalFilter, setGlobalFilter] = useState<any>([]);
|
||||||
|
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
import { GetApiKeyResponse } from "@server/routers/apiKeys";
|
|
||||||
import { createContext } from "react";
|
|
||||||
|
|
||||||
interface ApiKeyContextType {
|
|
||||||
apiKey: GetApiKeyResponse;
|
|
||||||
updateApiKey: (updatedApiKey: Partial<GetApiKeyResponse>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ApiKeyContext = createContext<ApiKeyContextType | undefined>(undefined);
|
|
||||||
|
|
||||||
export default ApiKeyContext;
|
|
|
@ -1,12 +0,0 @@
|
||||||
import ApiKeyContext from "@app/contexts/apiKeyContext";
|
|
||||||
import { useContext } from "react";
|
|
||||||
|
|
||||||
export function useApiKeyContext() {
|
|
||||||
const context = useContext(ApiKeyContext);
|
|
||||||
if (context === undefined) {
|
|
||||||
throw new Error(
|
|
||||||
"useApiKeyContext must be used within a ApiKeyProvider"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
|
@ -1,37 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import ApiKeyContext from "@app/contexts/apiKeyContext";
|
|
||||||
import { GetApiKeyResponse } from "@server/routers/apiKeys";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
interface ApiKeyProviderProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
apiKey: GetApiKeyResponse;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ApiKeyProvider({ children, apiKey: ak }: ApiKeyProviderProps) {
|
|
||||||
const [apiKey, setApiKey] = useState<GetApiKeyResponse>(ak);
|
|
||||||
|
|
||||||
const updateApiKey = (updatedApiKey: Partial<GetApiKeyResponse>) => {
|
|
||||||
if (!apiKey) {
|
|
||||||
throw new Error("No API key to update");
|
|
||||||
}
|
|
||||||
setApiKey((prev) => {
|
|
||||||
if (!prev) {
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
...updatedApiKey
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ApiKeyContext.Provider value={{ apiKey, updateApiKey }}>
|
|
||||||
{children}
|
|
||||||
</ApiKeyContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ApiKeyProvider;
|
|
Loading…
Add table
Reference in a new issue