diff --git a/LICENSE b/LICENSE index 0ad25db..0e38f56 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,5 @@ +Copyright (c) 2025 Fossorial, LLC. + GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 diff --git a/install/main.go b/install/main.go index a0d74a4..abb67ac 100644 --- a/install/main.go +++ b/install/main.go @@ -64,15 +64,15 @@ func main() { } var config Config - + config.DoCrowdsecInstall = false + config.Secret = generateRandomSecretKey() + // check if there is already a config file if _, err := os.Stat("config/config.yml"); err != nil { config = collectUserInput(reader) - + loadVersions(&config) - config.DoCrowdsecInstall = false - config.Secret = generateRandomSecretKey() - + if err := createConfigFiles(config); err != nil { fmt.Printf("Error creating config files: %v\n", err) os.Exit(1) diff --git a/server/auth/actions.ts b/server/auth/actions.ts index 7c6d1a4..d974f03 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -109,7 +109,7 @@ export async function checkUserActionPermission( try { 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) { const userOrgRoles = await db .select({ roleId: userOrgs.roleId }) diff --git a/server/index.ts b/server/index.ts index b932f51..7dacae1 100644 --- a/server/index.ts +++ b/server/index.ts @@ -5,8 +5,7 @@ import { createApiServer } from "./apiServer"; import { createNextServer } from "./nextServer"; import { createInternalServer } from "./internalServer"; import { ApiKey, ApiKeyOrg, Session, User, UserOrg } from "./db/schemas"; -import { createIntegrationApiServer } from "./integrationApiServer"; -import config from "@server/lib/config"; +// import { createIntegrationApiServer } from "./integrationApiServer"; async function startServers() { await runSetupFunctions(); @@ -17,9 +16,7 @@ async function startServers() { const nextServer = await createNextServer(); let integrationServer; - if (config.getRawConfig().flags?.enable_integration_api) { - integrationServer = createIntegrationApiServer(); - } + // integrationServer = createIntegrationApiServer(); return { apiServer, diff --git a/server/integrationApiServer.ts b/server/integrationApiServer.ts deleted file mode 100644 index f3dfbbe..0000000 --- a/server/integrationApiServer.ts +++ /dev/null @@ -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" }] - }); -} diff --git a/server/lib/config.ts b/server/lib/config.ts index c0c1f8b..1937d41 100644 --- a/server/lib/config.ts +++ b/server/lib/config.ts @@ -25,12 +25,9 @@ const configSchema = z.object({ .optional() .pipe(z.string().url()) .transform((url) => url.toLowerCase()), - log_level: z - .enum(["debug", "info", "warn", "error"]) - .optional() - .default("info"), - save_logs: z.boolean().optional().default(false), - log_failed_attempts: z.boolean().optional().default(false) + log_level: z.enum(["debug", "info", "warn", "error"]), + save_logs: z.boolean(), + log_failed_attempts: z.boolean().optional() }), domains: z .record( @@ -40,8 +37,8 @@ const configSchema = z.object({ .string() .nonempty("base_domain must not be empty") .transform((url) => url.toLowerCase()), - cert_resolver: z.string().optional().default("letsencrypt"), - prefer_wildcard_cert: z.boolean().optional().default(false) + cert_resolver: z.string().optional(), + prefer_wildcard_cert: z.boolean().optional() }) ) .refine( @@ -61,42 +58,19 @@ const configSchema = z.object({ server: z.object({ integration_port: portSchema .optional() - .default(3003) .transform(stoi) .pipe(portSchema.optional()), - external_port: portSchema - .optional() - .default(3000) - .transform(stoi) - .pipe(portSchema), - internal_port: portSchema - .optional() - .default(3001) - .transform(stoi) - .pipe(portSchema), - next_port: portSchema - .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"), + external_port: portSchema.optional().transform(stoi).pipe(portSchema), + internal_port: portSchema.optional().transform(stoi).pipe(portSchema), + next_port: portSchema.optional().transform(stoi).pipe(portSchema), + internal_hostname: z.string().transform((url) => url.toLowerCase()), + session_cookie_name: z.string(), + resource_access_token_param: z.string(), + resource_access_token_headers: z.object({ + id: z.string(), + token: z.string() + }), + resource_session_request_param: z.string(), dashboard_session_length_hours: z .number() .positive() @@ -124,61 +98,35 @@ const configSchema = z.object({ .transform(getEnvOrYaml("SERVER_SECRET")) .pipe(z.string().min(8)) }), - traefik: z - .object({ - http_entrypoint: z.string().optional().default("web"), - https_entrypoint: z.string().optional().default("websecure"), - additional_middlewares: z.array(z.string()).optional() - }) - .optional() - .default({}), - gerbil: z - .object({ - start_port: portSchema - .optional() - .default(51820) - .transform(stoi) - .pipe(portSchema), - base_endpoint: z - .string() - .optional() - .pipe(z.string()) - .transform((url) => url.toLowerCase()), - use_subdomain: z.boolean().optional().default(false), - subnet_group: z.string().optional().default("100.89.137.0/20"), - block_size: z.number().positive().gt(0).optional().default(24), - site_block_size: z.number().positive().gt(0).optional().default(30) - }) - .optional() - .default({}), - rate_limits: z - .object({ - 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 - .object({ - window_minutes: z.number().positive().gt(0), - max_requests: z.number().positive().gt(0) - }) - .optional() - }) - .optional() - .default({}), + traefik: z.object({ + http_entrypoint: z.string(), + https_entrypoint: z.string().optional(), + additional_middlewares: z.array(z.string()).optional() + }), + gerbil: z.object({ + start_port: portSchema.optional().transform(stoi).pipe(portSchema), + base_endpoint: z + .string() + .optional() + .pipe(z.string()) + .transform((url) => url.toLowerCase()), + use_subdomain: z.boolean(), + subnet_group: z.string(), + block_size: z.number().positive().gt(0), + site_block_size: z.number().positive().gt(0) + }), + rate_limits: z.object({ + global: z.object({ + window_minutes: z.number().positive().gt(0), + max_requests: z.number().positive().gt(0) + }), + auth: z + .object({ + window_minutes: z.number().positive().gt(0), + max_requests: z.number().positive().gt(0) + }) + .optional() + }), email: z .object({ smtp_host: z.string().optional(), @@ -212,8 +160,7 @@ const configSchema = z.object({ disable_user_create_org: z.boolean().optional(), allow_raw_resources: z.boolean().optional(), allow_base_domain_resources: z.boolean().optional(), - allow_local_sites: z.boolean().optional(), - enable_integration_api: z.boolean().optional() + allow_local_sites: z.boolean().optional() }) .optional() }); diff --git a/server/lib/consts.ts b/server/lib/consts.ts index ed61e8c..94d2716 100644 --- a/server/lib/consts.ts +++ b/server/lib/consts.ts @@ -2,7 +2,7 @@ import path from "path"; import { fileURLToPath } from "url"; // 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 __DIRNAME = path.dirname(__FILENAME); diff --git a/server/lib/validators.ts b/server/lib/validators.ts index 50ff567..e33c918 100644 --- a/server/lib/validators.ts +++ b/server/lib/validators.ts @@ -9,10 +9,6 @@ export function isValidIP(ip: string): boolean { } export function isValidUrlGlobPattern(pattern: string): boolean { - if (pattern === "/") { - return true; - } - // Remove leading slash if present pattern = pattern.startsWith("/") ? pattern.slice(1) : pattern; diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts index bee1c37..6dbdcd6 100644 --- a/server/middlewares/index.ts +++ b/server/middlewares/index.ts @@ -16,6 +16,6 @@ export * from "./verifyUserInRole"; export * from "./verifyAccessTokenAccess"; export * from "./verifyUserIsServerAdmin"; export * from "./verifyIsLoggedInUser"; -export * from "./integration"; +// export * from "./integration"; export * from "./verifyUserHasAction"; -export * from "./verifyApiKeyAccess"; +// export * from "./verifyApiKeyAccess"; diff --git a/server/middlewares/integration/index.ts b/server/middlewares/integration/index.ts deleted file mode 100644 index 19bf128..0000000 --- a/server/middlewares/integration/index.ts +++ /dev/null @@ -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"; diff --git a/server/middlewares/integration/verifyAccessTokenAccess.ts b/server/middlewares/integration/verifyAccessTokenAccess.ts deleted file mode 100644 index e9069ba..0000000 --- a/server/middlewares/integration/verifyAccessTokenAccess.ts +++ /dev/null @@ -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" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKey.ts b/server/middlewares/integration/verifyApiKey.ts deleted file mode 100644 index 0b0602e..0000000 --- a/server/middlewares/integration/verifyApiKey.ts +++ /dev/null @@ -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 { - 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" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts b/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts deleted file mode 100644 index 435f01d..0000000 --- a/server/middlewares/integration/verifyApiKeyApiKeyAccess.ts +++ /dev/null @@ -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" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeyHasAction.ts b/server/middlewares/integration/verifyApiKeyHasAction.ts deleted file mode 100644 index 35f4398..0000000 --- a/server/middlewares/integration/verifyApiKeyHasAction.ts +++ /dev/null @@ -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 { - 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" - ) - ); - } - }; -} diff --git a/server/middlewares/integration/verifyApiKeyIsRoot.ts b/server/middlewares/integration/verifyApiKeyIsRoot.ts deleted file mode 100644 index 2ce9c84..0000000 --- a/server/middlewares/integration/verifyApiKeyIsRoot.ts +++ /dev/null @@ -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 { - 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" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeyOrgAccess.ts b/server/middlewares/integration/verifyApiKeyOrgAccess.ts deleted file mode 100644 index 902ccf5..0000000 --- a/server/middlewares/integration/verifyApiKeyOrgAccess.ts +++ /dev/null @@ -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" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeyResourceAccess.ts b/server/middlewares/integration/verifyApiKeyResourceAccess.ts deleted file mode 100644 index f4e3ed0..0000000 --- a/server/middlewares/integration/verifyApiKeyResourceAccess.ts +++ /dev/null @@ -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" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeyRoleAccess.ts b/server/middlewares/integration/verifyApiKeyRoleAccess.ts deleted file mode 100644 index 4d76941..0000000 --- a/server/middlewares/integration/verifyApiKeyRoleAccess.ts +++ /dev/null @@ -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" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeySetResourceUsers.ts b/server/middlewares/integration/verifyApiKeySetResourceUsers.ts deleted file mode 100644 index 1c3b5b1..0000000 --- a/server/middlewares/integration/verifyApiKeySetResourceUsers.ts +++ /dev/null @@ -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" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeySiteAccess.ts b/server/middlewares/integration/verifyApiKeySiteAccess.ts deleted file mode 100644 index 2c83ead..0000000 --- a/server/middlewares/integration/verifyApiKeySiteAccess.ts +++ /dev/null @@ -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" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeyTargetAccess.ts b/server/middlewares/integration/verifyApiKeyTargetAccess.ts deleted file mode 100644 index 7da1f29..0000000 --- a/server/middlewares/integration/verifyApiKeyTargetAccess.ts +++ /dev/null @@ -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" - ) - ); - } -} diff --git a/server/middlewares/integration/verifyApiKeyUserAccess.ts b/server/middlewares/integration/verifyApiKeyUserAccess.ts deleted file mode 100644 index 69f27e9..0000000 --- a/server/middlewares/integration/verifyApiKeyUserAccess.ts +++ /dev/null @@ -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" - ) - ); - } -} diff --git a/server/middlewares/verifyApiKeyAccess.ts b/server/middlewares/verifyApiKeyAccess.ts deleted file mode 100644 index d4d6032..0000000 --- a/server/middlewares/verifyApiKeyAccess.ts +++ /dev/null @@ -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" - ) - ); - } -} diff --git a/server/routers/apiKeys/createOrgApiKey.ts b/server/routers/apiKeys/createOrgApiKey.ts deleted file mode 100644 index bf8ff8c..0000000 --- a/server/routers/apiKeys/createOrgApiKey.ts +++ /dev/null @@ -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; - -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 { - 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(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" - ) - ); - } -} diff --git a/server/routers/apiKeys/createRootApiKey.ts b/server/routers/apiKeys/createRootApiKey.ts deleted file mode 100644 index 7a5d2d8..0000000 --- a/server/routers/apiKeys/createRootApiKey.ts +++ /dev/null @@ -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; - -export type CreateRootApiKeyResponse = { - apiKeyId: string; - name: string; - apiKey: string; - lastChars: string; - createdAt: string; -}; - -export async function createRootApiKey( - req: Request, - res: Response, - next: NextFunction -): Promise { - 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(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" - ) - ); - } -} diff --git a/server/routers/apiKeys/deleteApiKey.ts b/server/routers/apiKeys/deleteApiKey.ts deleted file mode 100644 index e1a74a4..0000000 --- a/server/routers/apiKeys/deleteApiKey.ts +++ /dev/null @@ -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 { - 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") - ); - } -} diff --git a/server/routers/apiKeys/deleteOrgApiKey.ts b/server/routers/apiKeys/deleteOrgApiKey.ts deleted file mode 100644 index dbaf47f..0000000 --- a/server/routers/apiKeys/deleteOrgApiKey.ts +++ /dev/null @@ -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 { - 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") - ); - } -} diff --git a/server/routers/apiKeys/getApiKey.ts b/server/routers/apiKeys/getApiKey.ts deleted file mode 100644 index e0354cf..0000000 --- a/server/routers/apiKeys/getApiKey.ts +++ /dev/null @@ -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>[0] ->; - -export async function getApiKey( - req: Request, - res: Response, - next: NextFunction -): Promise { - 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(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") - ); - } -} diff --git a/server/routers/apiKeys/index.ts b/server/routers/apiKeys/index.ts deleted file mode 100644 index 62ede75..0000000 --- a/server/routers/apiKeys/index.ts +++ /dev/null @@ -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"; diff --git a/server/routers/apiKeys/listApiKeyActions.ts b/server/routers/apiKeys/listApiKeyActions.ts deleted file mode 100644 index 5bd1441..0000000 --- a/server/routers/apiKeys/listApiKeyActions.ts +++ /dev/null @@ -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>; - 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 { - 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(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") - ); - } -} diff --git a/server/routers/apiKeys/listOrgApiKeys.ts b/server/routers/apiKeys/listOrgApiKeys.ts deleted file mode 100644 index 9833ef0..0000000 --- a/server/routers/apiKeys/listOrgApiKeys.ts +++ /dev/null @@ -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>; - 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 { - 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(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") - ); - } -} diff --git a/server/routers/apiKeys/listRootApiKeys.ts b/server/routers/apiKeys/listRootApiKeys.ts deleted file mode 100644 index c639ce5..0000000 --- a/server/routers/apiKeys/listRootApiKeys.ts +++ /dev/null @@ -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>; - pagination: { total: number; limit: number; offset: number }; -}; - -export async function listRootApiKeys( - req: Request, - res: Response, - next: NextFunction -): Promise { - 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(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") - ); - } -} diff --git a/server/routers/apiKeys/setApiKeyActions.ts b/server/routers/apiKeys/setApiKeyActions.ts deleted file mode 100644 index 602c779..0000000 --- a/server/routers/apiKeys/setApiKeyActions.ts +++ /dev/null @@ -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 { - 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") - ); - } -} diff --git a/server/routers/apiKeys/setApiKeyOrgs.ts b/server/routers/apiKeys/setApiKeyOrgs.ts deleted file mode 100644 index c42046d..0000000 --- a/server/routers/apiKeys/setApiKeyOrgs.ts +++ /dev/null @@ -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 { - 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") - ); - } -} diff --git a/server/routers/badger/verifySession.test.ts b/server/routers/badger/verifySession.test.ts index b0ad987..0a459dc 100644 --- a/server/routers/badger/verifySession.test.ts +++ b/server/routers/badger/verifySession.test.ts @@ -1,136 +1,61 @@ +import { isPathAllowed } from './verifySession'; 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() { console.log('Running path matching tests...'); - + // Test exact matching assertEquals(isPathAllowed('foo', 'foo'), true, 'Exact match should be allowed'); assertEquals(isPathAllowed('foo', 'bar'), false, 'Different segments should not match'); assertEquals(isPathAllowed('foo/bar', 'foo/bar'), true, 'Exact multi-segment match should be allowed'); assertEquals(isPathAllowed('foo/bar', 'foo/baz'), false, 'Partial multi-segment match should not be allowed'); - + // Test with leading and trailing slashes assertEquals(isPathAllowed('/foo', 'foo'), true, 'Pattern with leading slash should match'); assertEquals(isPathAllowed('foo/', 'foo'), true, 'Pattern with trailing slash should match'); assertEquals(isPathAllowed('/foo/', 'foo'), true, 'Pattern with both leading and trailing slashes should match'); assertEquals(isPathAllowed('foo', '/foo/'), true, 'Path with leading and trailing slashes should match'); - + // Test simple wildcard matching assertEquals(isPathAllowed('*', 'foo'), true, 'Single wildcard should match any single segment'); assertEquals(isPathAllowed('*', 'foo/bar'), true, 'Single wildcard should match multiple segments'); assertEquals(isPathAllowed('*/bar', 'foo/bar'), true, 'Wildcard prefix should match'); assertEquals(isPathAllowed('foo/*', 'foo/bar'), true, 'Wildcard suffix should match'); assertEquals(isPathAllowed('foo/*/baz', 'foo/bar/baz'), true, 'Wildcard in middle should match'); - + // Test multiple wildcards assertEquals(isPathAllowed('*/*', 'foo/bar'), true, 'Multiple wildcards should match corresponding segments'); assertEquals(isPathAllowed('*/*/*', 'foo/bar/baz'), true, 'Three wildcards should match three segments'); assertEquals(isPathAllowed('foo/*/*', 'foo/bar/baz'), true, 'Specific prefix with wildcards should match'); assertEquals(isPathAllowed('*/*/baz', 'foo/bar/baz'), true, 'Wildcards with specific suffix should match'); - + // Test wildcard consumption behavior assertEquals(isPathAllowed('*', ''), true, 'Wildcard should optionally consume segments'); assertEquals(isPathAllowed('foo/*', 'foo'), true, 'Trailing wildcard should be optional'); assertEquals(isPathAllowed('*/*', 'foo'), true, 'Multiple wildcards can match fewer segments'); assertEquals(isPathAllowed('*/*/*', 'foo/bar'), true, 'Extra wildcards can be skipped'); - + // Test complex nested paths assertEquals(isPathAllowed('api/*/users', 'api/v1/users'), true, 'API versioning pattern should match'); assertEquals(isPathAllowed('api/*/users/*', 'api/v1/users/123'), true, 'API resource pattern should match'); assertEquals(isPathAllowed('api/*/users/*/profile', 'api/v1/users/123/profile'), true, 'Nested API pattern should match'); - + // Test for the requested padbootstrap* pattern assertEquals(isPathAllowed('padbootstrap*', 'padbootstrap'), true, 'padbootstrap* should match padbootstrap'); assertEquals(isPathAllowed('padbootstrap*', 'padbootstrapv1'), true, 'padbootstrap* should match padbootstrapv1'); assertEquals(isPathAllowed('padbootstrap*', 'padbootstrap/files'), false, 'padbootstrap* should not match padbootstrap/files'); assertEquals(isPathAllowed('padbootstrap*/*', 'padbootstrap/files'), true, 'padbootstrap*/* should match padbootstrap/files'); assertEquals(isPathAllowed('padbootstrap*/files', 'padbootstrapv1/files'), true, 'padbootstrap*/files should not match padbootstrapv1/files (wildcard is segment-based, not partial)'); - + // Test wildcard edge cases assertEquals(isPathAllowed('*/*/*/*/*/*', 'a/b'), true, 'Many wildcards can match few segments'); assertEquals(isPathAllowed('a/*/b/*/c', 'a/anything/b/something/c'), true, 'Multiple wildcards in pattern should match corresponding segments'); - + // Test patterns with partial segment matches assertEquals(isPathAllowed('padbootstrap*', 'padbootstrap-123'), true, 'Wildcards in isPathAllowed should be segment-based, not character-based'); 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('/', '/'), true, 'Root path should match root path'); - assertEquals(isPathAllowed('/', '/test'), false, 'Root path should not match non-root path'); - console.log('All tests passed!'); } @@ -139,4 +64,4 @@ try { runTests(); } catch (error) { console.error('Test failed:', error); -} +} \ No newline at end of file diff --git a/server/routers/external.ts b/server/routers/external.ts index 40503db..96f569b 100644 --- a/server/routers/external.ts +++ b/server/routers/external.ts @@ -10,7 +10,7 @@ import * as auth from "./auth"; import * as role from "./role"; import * as accessToken from "./accessToken"; import * as idp from "./idp"; -import * as apiKeys from "./apiKeys"; +// import * as apiKeys from "./apiKeys"; import HttpCode from "@server/types/HttpCode"; import { verifyAccessTokenAccess, @@ -26,8 +26,8 @@ import { verifyUserAccess, getUserOrgs, verifyUserIsServerAdmin, - verifyIsLoggedInUser, - verifyApiKeyAccess, + verifyIsLoggedInUser + // verifyApiKeyAccess } from "@server/middlewares"; import { verifyUserHasAction } from "../middlewares/verifyUserHasAction"; 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/:idpId", verifyUserIsServerAdmin, idp.getIdp); +/* authenticated.get( `/api-key/:apiKeyId`, verifyUserIsServerAdmin, @@ -636,6 +637,7 @@ authenticated.get( verifyUserHasAction(ActionsEnum.getApiKey), apiKeys.getApiKey ); +*/ // Auth routes export const authRouter = Router(); diff --git a/server/routers/idp/validateOidcCallback.ts b/server/routers/idp/validateOidcCallback.ts index a53ddef..274350d 100644 --- a/server/routers/idp/validateOidcCallback.ts +++ b/server/routers/idp/validateOidcCallback.ts @@ -6,12 +6,8 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { - idp, - idpOidcConfig, - users -} from "@server/db/schemas"; -import { and, eq } from "drizzle-orm"; +import { idp, idpOidcConfig, users } from "@server/db/schemas"; +import { and, eq, inArray } from "drizzle-orm"; import * as arctic from "arctic"; import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl"; import jmespath from "jmespath"; @@ -163,9 +159,7 @@ export async function validateOidcCallback( ); const idToken = tokens.idToken(); - logger.debug("ID token", { idToken }); const claims = arctic.decodeIdToken(idToken); - logger.debug("ID token claims", { claims }); const userIdentifier = jmespath.search( claims, diff --git a/server/routers/integration.ts b/server/routers/integration.ts deleted file mode 100644 index 8fa5c25..0000000 --- a/server/routers/integration.ts +++ /dev/null @@ -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 -); diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index 9198bb8..a857e10 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -318,8 +318,8 @@ async function updateHttpResource( domainId: updatePayload.domainId, enabled: updatePayload.enabled, stickySession: updatePayload.stickySession, - tlsServerName: updatePayload.tlsServerName, - setHostHeader: updatePayload.setHostHeader, + tlsServerName: updatePayload.tlsServerName || null, + setHostHeader: updatePayload.setHostHeader || null, fullDomain: updatePayload.fullDomain }) .where(eq(resources.resourceId, resource.resourceId)) diff --git a/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx b/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx index 5c4858e..ca327d6 100644 --- a/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx +++ b/src/app/[orgId]/settings/access/users/[userId]/access-controls/page.tsx @@ -35,7 +35,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext"; import { Tag, TagInput } from "@app/components/tags/tag-input"; const formSchema = z.object({ - username: z.string(), + email: z.string().email({ message: "Please enter a valid email" }), roles: z .array( z.object({ @@ -64,7 +64,7 @@ export default function AccessControlsPage() { const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: { - username: user.username!, + email: user.email!, roles: [] } }); diff --git a/src/app/[orgId]/settings/api-keys/OrgApiKeysDataTable.tsx b/src/app/[orgId]/settings/api-keys/OrgApiKeysDataTable.tsx deleted file mode 100644 index c10a82d..0000000 --- a/src/app/[orgId]/settings/api-keys/OrgApiKeysDataTable.tsx +++ /dev/null @@ -1,28 +0,0 @@ -"use client"; - -import { DataTable } from "@app/components/ui/data-table"; -import { ColumnDef } from "@tanstack/react-table"; - -interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; - addApiKey?: () => void; -} - -export function OrgApiKeysDataTable({ - addApiKey, - columns, - data -}: DataTableProps) { - return ( - - ); -} diff --git a/src/app/[orgId]/settings/api-keys/OrgApiKeysTable.tsx b/src/app/[orgId]/settings/api-keys/OrgApiKeysTable.tsx deleted file mode 100644 index 893f6d7..0000000 --- a/src/app/[orgId]/settings/api-keys/OrgApiKeysTable.tsx +++ /dev/null @@ -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(null); - const [rows, setRows] = useState(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[] = [ - { - id: "dots", - cell: ({ row }) => { - const apiKeyROw = row.original; - const router = useRouter(); - - return ( - - - - - - { - setSelected(apiKeyROw); - }} - > - View settings - - { - setSelected(apiKeyROw); - setIsDeleteModalOpen(true); - }} - > - Delete - - - - ); - } - }, - { - accessorKey: "name", - header: ({ column }) => { - return ( - - ); - } - }, - { - accessorKey: "key", - header: "Key", - cell: ({ row }) => { - const r = row.original; - return {r.key}; - } - }, - { - accessorKey: "createdAt", - header: "Created At", - cell: ({ row }) => { - const r = row.original; - return {moment(r.createdAt).format("lll")} ; - } - }, - { - id: "actions", - cell: ({ row }) => { - const r = row.original; - return ( -
- - - -
- ); - } - } - ]; - - return ( - <> - {selected && ( - { - setIsDeleteModalOpen(val); - setSelected(null); - }} - dialog={ -
-

- Are you sure you want to remove the API key{" "} - {selected?.name || selected?.id} from the - organization? -

- -

- - Once removed, the API key will no longer be - able to be used. - -

- -

- To confirm, please type the name of the API key - below. -

-
- } - buttonText="Confirm Delete API Key" - onConfirm={async () => deleteSite(selected!.id)} - string={selected.name} - title="Delete API Key" - /> - )} - - { - router.push(`/${orgId}/settings/api-keys/create`); - }} - /> - - ); -} diff --git a/src/app/[orgId]/settings/api-keys/[apiKeyId]/layout.tsx b/src/app/[orgId]/settings/api-keys/[apiKeyId]/layout.tsx deleted file mode 100644 index 4a7b319..0000000 --- a/src/app/[orgId]/settings/api-keys/[apiKeyId]/layout.tsx +++ /dev/null @@ -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>( - `/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 ( - <> - - - - {children} - - - ); -} diff --git a/src/app/[orgId]/settings/api-keys/[apiKeyId]/page.tsx b/src/app/[orgId]/settings/api-keys/[apiKeyId]/page.tsx deleted file mode 100644 index e54f442..0000000 --- a/src/app/[orgId]/settings/api-keys/[apiKeyId]/page.tsx +++ /dev/null @@ -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`); -} diff --git a/src/app/[orgId]/settings/api-keys/[apiKeyId]/permissions/page.tsx b/src/app/[orgId]/settings/api-keys/[apiKeyId]/permissions/page.tsx deleted file mode 100644 index 624d13a..0000000 --- a/src/app/[orgId]/settings/api-keys/[apiKeyId]/permissions/page.tsx +++ /dev/null @@ -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(true); - const [selectedPermissions, setSelectedPermissions] = useState< - Record - >({}); - const [loadingSavePermissions, setLoadingSavePermissions] = - useState(false); - - useEffect(() => { - async function load() { - setLoadingPage(true); - - const res = await api - .get< - AxiosResponse - >(`/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 && ( - - - - - Permissions - - - Determine what this API key can do - - - - - - - - - - - - )} - - ); -} diff --git a/src/app/[orgId]/settings/api-keys/create/page.tsx b/src/app/[orgId]/settings/api-keys/create/page.tsx deleted file mode 100644 index d3e7e34..0000000 --- a/src/app/[orgId]/settings/api-keys/create/page.tsx +++ /dev/null @@ -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; - -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; - -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(null); - const [selectedPermissions, setSelectedPermissions] = useState< - Record - >({}); - - const form = useForm({ - resolver: zodResolver(createFormSchema), - defaultValues: { - name: "" - } - }); - - const copiedForm = useForm({ - 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 - >(`/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 ( - <> -
- - -
- - {!loadingPage && ( -
- - {!apiKey && ( - <> - - - - API Key Information - - - - -
- - ( - - - Name - - - - - - - )} - /> - - -
-
-
- - - - - Permissions - - - Determine what this API key can do - - - - - - - - )} - - {apiKey && ( - - - - Your API Key - - - - - - - Name - - - - - - - - Created - - - {moment( - apiKey.createdAt - ).format("lll")} - - - - - - - - Save Your API Key - - - You will only be able to see this - once. Make sure to copy it to a - secure place. - - - -

- Your API key is: -

- - - -
- - ( - -
- { - copiedForm.setValue( - "copied", - e as boolean - ); - }} - /> - -
- -
- )} - /> - - -
-
- )} -
- -
- {!apiKey && ( - - )} - {!apiKey && ( - - )} - - {apiKey && ( - - )} -
-
- )} - - ); -} diff --git a/src/app/[orgId]/settings/api-keys/page.tsx b/src/app/[orgId]/settings/api-keys/page.tsx deleted file mode 100644 index c6e48d2..0000000 --- a/src/app/[orgId]/settings/api-keys/page.tsx +++ /dev/null @@ -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>( - `/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 ( - <> - - - - - ); -} diff --git a/src/app/[orgId]/settings/resources/ResourcesDataTable.tsx b/src/app/[orgId]/settings/resources/ResourcesDataTable.tsx index ce6133d..a9db3e7 100644 --- a/src/app/[orgId]/settings/resources/ResourcesDataTable.tsx +++ b/src/app/[orgId]/settings/resources/ResourcesDataTable.tsx @@ -1,6 +1,8 @@ "use client"; -import { ColumnDef } from "@tanstack/react-table"; +import { + ColumnDef, +} from "@tanstack/react-table"; import { DataTable } from "@app/components/ui/data-table"; interface DataTableProps { @@ -23,10 +25,6 @@ export function ResourcesDataTable({ searchColumn="name" onAdd={createResource} addButtonText="Add Resource" - defaultSort={{ - id: "name", - desc: false - }} /> ); } diff --git a/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx index ddf255e..90e05ff 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/proxy/page.tsx @@ -320,10 +320,8 @@ export default function ReverseProxyTargets(props: { AxiosResponse >(`/resource/${params.resourceId}/target`, data); target.targetId = res.data.data.targetId; - target.new = false; } else if (target.updated) { await api.post(`/target/${target.targetId}`, data); - target.updated = false; } } @@ -798,12 +796,6 @@ export default function ReverseProxyTargets(props: { type="submit" variant="outlinePrimary" className="mt-6" - disabled={ - !( - addTargetForm.getValues("ip") && - addTargetForm.getValues("port") - ) - } > Add Target diff --git a/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx b/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx index f7b3914..2a9fa00 100644 --- a/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx +++ b/src/app/[orgId]/settings/resources/[resourceId]/rules/page.tsx @@ -64,6 +64,7 @@ import { InfoSections, InfoSectionTitle } from "@app/components/InfoSection"; +import { Separator } from "@app/components/ui/separator"; import { InfoPopup } from "@app/components/ui/info-popup"; import { isValidCIDR, diff --git a/src/app/[orgId]/settings/sites/SitesDataTable.tsx b/src/app/[orgId]/settings/sites/SitesDataTable.tsx index 76beca6..08d9795 100644 --- a/src/app/[orgId]/settings/sites/SitesDataTable.tsx +++ b/src/app/[orgId]/settings/sites/SitesDataTable.tsx @@ -1,6 +1,8 @@ "use client"; -import { ColumnDef } from "@tanstack/react-table"; +import { + ColumnDef, +} from "@tanstack/react-table"; import { DataTable } from "@app/components/ui/data-table"; interface DataTableProps { @@ -23,10 +25,6 @@ export function SitesDataTable({ searchColumn="name" onAdd={createSite} addButtonText="Add Site" - defaultSort={{ - id: "name", - desc: false - }} /> ); } diff --git a/src/app/admin/api-keys/ApiKeysDataTable.tsx b/src/app/admin/api-keys/ApiKeysDataTable.tsx deleted file mode 100644 index b7e2ed0..0000000 --- a/src/app/admin/api-keys/ApiKeysDataTable.tsx +++ /dev/null @@ -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 { - columns: ColumnDef[]; - data: TData[]; - addApiKey?: () => void; -} - -export function ApiKeysDataTable({ - addApiKey, - columns, - data -}: DataTableProps) { - return ( - - ); -} diff --git a/src/app/admin/api-keys/ApiKeysTable.tsx b/src/app/admin/api-keys/ApiKeysTable.tsx deleted file mode 100644 index a89157b..0000000 --- a/src/app/admin/api-keys/ApiKeysTable.tsx +++ /dev/null @@ -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(null); - const [rows, setRows] = useState(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[] = [ - { - id: "dots", - cell: ({ row }) => { - const apiKeyROw = row.original; - const router = useRouter(); - - return ( - - - - - - { - setSelected(apiKeyROw); - }} - > - View settings - - { - setSelected(apiKeyROw); - setIsDeleteModalOpen(true); - }} - > - Delete - - - - ); - } - }, - { - accessorKey: "name", - header: ({ column }) => { - return ( - - ); - } - }, - { - accessorKey: "key", - header: "Key", - cell: ({ row }) => { - const r = row.original; - return {r.key}; - } - }, - { - accessorKey: "createdAt", - header: "Created At", - cell: ({ row }) => { - const r = row.original; - return {moment(r.createdAt).format("lll")} ; - } - }, - { - id: "actions", - cell: ({ row }) => { - const r = row.original; - return ( -
- - - -
- ); - } - } - ]; - - return ( - <> - {selected && ( - { - setIsDeleteModalOpen(val); - setSelected(null); - }} - dialog={ -
-

- Are you sure you want to remove the API key{" "} - {selected?.name || selected?.id}? -

- -

- - Once removed, the API key will no longer be - able to be used. - -

- -

- To confirm, please type the name of the API key - below. -

-
- } - buttonText="Confirm Delete API Key" - onConfirm={async () => deleteSite(selected!.id)} - string={selected.name} - title="Delete API Key" - /> - )} - - { - router.push(`/admin/api-keys/create`); - }} - /> - - ); -} diff --git a/src/app/admin/api-keys/[apiKeyId]/layout.tsx b/src/app/admin/api-keys/[apiKeyId]/layout.tsx deleted file mode 100644 index 768ad30..0000000 --- a/src/app/admin/api-keys/[apiKeyId]/layout.tsx +++ /dev/null @@ -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>( - `/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 ( - <> - - - - {children} - - - ); -} diff --git a/src/app/admin/api-keys/[apiKeyId]/page.tsx b/src/app/admin/api-keys/[apiKeyId]/page.tsx deleted file mode 100644 index 910d1b5..0000000 --- a/src/app/admin/api-keys/[apiKeyId]/page.tsx +++ /dev/null @@ -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`); -} diff --git a/src/app/admin/api-keys/[apiKeyId]/permissions/page.tsx b/src/app/admin/api-keys/[apiKeyId]/permissions/page.tsx deleted file mode 100644 index 70c2c55..0000000 --- a/src/app/admin/api-keys/[apiKeyId]/permissions/page.tsx +++ /dev/null @@ -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(true); - const [selectedPermissions, setSelectedPermissions] = useState< - Record - >({}); - const [loadingSavePermissions, setLoadingSavePermissions] = - useState(false); - - useEffect(() => { - async function load() { - setLoadingPage(true); - - const res = await api - .get< - AxiosResponse - >(`/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 && ( - - - - - Permissions - - - Determine what this API key can do - - - - - - - - - - - - )} - - ); -} diff --git a/src/app/admin/api-keys/create/page.tsx b/src/app/admin/api-keys/create/page.tsx deleted file mode 100644 index c762fed..0000000 --- a/src/app/admin/api-keys/create/page.tsx +++ /dev/null @@ -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; - -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; - -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(null); - const [selectedPermissions, setSelectedPermissions] = useState< - Record - >({}); - - const form = useForm({ - resolver: zodResolver(createFormSchema), - defaultValues: { - name: "" - } - }); - - const copiedForm = useForm({ - resolver: zodResolver(copiedFormSchema), - defaultValues: { - copied: false - } - }); - - async function onSubmit(data: CreateFormValues) { - setCreateLoading(true); - - let payload: CreateOrgApiKeyBody = { - name: data.name - }; - - const res = await api - .put>(`/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 ( - <> -
- - -
- - {!loadingPage && ( -
- - {!apiKey && ( - <> - - - - API Key Information - - - - -
- - ( - - - Name - - - - - - - )} - /> - - -
-
-
- - - - - Permissions - - - Determine what this API key can do - - - - - - - - )} - - {apiKey && ( - - - - Your API Key - - - - - - - Name - - - - - - - - Created - - - {moment( - apiKey.createdAt - ).format("lll")} - - - - - - - - Save Your API Key - - - You will only be able to see this - once. Make sure to copy it to a - secure place. - - - -

- Your API key is: -

- - - -
- - ( - -
- { - copiedForm.setValue( - "copied", - e as boolean - ); - }} - /> - -
- -
- )} - /> - - -
-
- )} -
- -
- {!apiKey && ( - - )} - {!apiKey && ( - - )} - - {apiKey && ( - - )} -
-
- )} - - ); -} diff --git a/src/app/admin/api-keys/page.tsx b/src/app/admin/api-keys/page.tsx deleted file mode 100644 index 077c9be..0000000 --- a/src/app/admin/api-keys/page.tsx +++ /dev/null @@ -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>( - `/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 ( - <> - - - - - ); -} diff --git a/src/app/admin/idp/[idpId]/layout.tsx b/src/app/admin/idp/[idpId]/layout.tsx index 5da9a7c..559c87e 100644 --- a/src/app/admin/idp/[idpId]/layout.tsx +++ b/src/app/admin/idp/[idpId]/layout.tsx @@ -33,7 +33,7 @@ export default async function SettingsLayout(props: SettingsLayoutProps) { }, { title: "Organization Policies", - href: `/admin/idp/${params.idpId}/policies` + href: `/admin/idp/${params.idpId}/policies`, } ]; diff --git a/src/app/admin/idp/[idpId]/policies/EditPolicyForm.tsx b/src/app/admin/idp/[idpId]/policies/EditPolicyForm.tsx new file mode 100644 index 0000000..b967fc8 --- /dev/null +++ b/src/app/admin/idp/[idpId]/policies/EditPolicyForm.tsx @@ -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>({ + resolver: zodResolver(formSchema), + defaultValues, + // @ts-ignore + values: policyToEdit + ? { + orgId: policyToEdit.orgId, + roleMapping: policyToEdit.roleMapping || "", + orgMapping: policyToEdit.orgMapping || "" + } + : defaultValues + }); + + async function onSubmit(values: z.infer) { + setLoading(true); + + if (policyToEdit) { + const res = await api + .post>( + `/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>( + `/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 ( + { + setOpen(val); + setLoading(false); + setOrgsPopoverOpen(false); + form.reset(); + }} + > + + + + {policyToEdit ? "Edit" : "Create"} Organization Policy + + + Configure access for an organization + + + +
+ + ( + + Organization + {policyToEdit ? ( + + ) : ( + + + + + + + + + + + + No site found. + + + {orgs.map( + (org) => { + if ( + policies.find( + ( + p + ) => + p.orgId === + org.orgId + ) + ) { + return undefined; + } + return ( + { + form.setValue( + "orgId", + org.orgId + ); + setOrgsPopoverOpen( + false + ); + }} + > + + { + org.name + } + + ); + } + )} + + + + + + )} + + + )} + /> + ( + + + Role Mapping Path (Optional) + + + + + + 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. + + + + )} + /> + ( + + + Organization Mapping Path (Optional) + + + + + + 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. + + + + )} + /> + + +
+ + + + + + +
+
+ ); +} diff --git a/src/app/admin/idp/[idpId]/policies/PolicyDataTable.tsx b/src/app/admin/idp/[idpId]/policies/PolicyDataTable.tsx index 2873b80..73ca2ff 100644 --- a/src/app/admin/idp/[idpId]/policies/PolicyDataTable.tsx +++ b/src/app/admin/idp/[idpId]/policies/PolicyDataTable.tsx @@ -6,7 +6,7 @@ import { DataTable } from "@app/components/ui/data-table"; interface DataTableProps { columns: ColumnDef[]; data: TData[]; - onAdd: () => void; + onAdd?: () => void; } export function PolicyDataTable({ @@ -18,11 +18,11 @@ export function PolicyDataTable({ ); } diff --git a/src/app/admin/idp/[idpId]/policies/PolicyTable.tsx b/src/app/admin/idp/[idpId]/policies/PolicyTable.tsx index 90f3b07..f1c8fb2 100644 --- a/src/app/admin/idp/[idpId]/policies/PolicyTable.tsx +++ b/src/app/admin/idp/[idpId]/policies/PolicyTable.tsx @@ -1,62 +1,78 @@ "use client"; import { ColumnDef } from "@tanstack/react-table"; -import { Button } from "@app/components/ui/button"; -import { - ArrowUpDown, - MoreHorizontal, -} from "lucide-react"; -import { PolicyDataTable } from "./PolicyDataTable"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } 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 { orgId: string; - roleMapping?: string; - orgMapping?: string; + roleMapping: string | null; + orgMapping: string | null; } -interface Props { +type PolicyTableProps = { policies: PolicyRow[]; - onDelete: (orgId: string) => 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[] = [ { - id: "dots", + id: "actions", cell: ({ row }) => { - const r = row.original; + const policyRow = row.original; return ( - - - - - - { - onDelete(r.orgId); - }} - > - Delete - - - + <> +
+ + + + + + onEdit(policyRow)} + > + Edit Policy + + onDelete(policyRow)} + > + + Delete Policy + + + + +
+ ); } }, { + id: "orgId", accessorKey: "orgId", header: ({ column }) => { return ( @@ -74,74 +90,24 @@ export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props }, { accessorKey: "roleMapping", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - const mapping = row.original.roleMapping; - return mapping ? ( - 50 ? `${mapping.substring(0, 50)}...` : mapping} - info={mapping} - /> - ) : ( - "--" - ); - } + header: "Role Mapping" }, { accessorKey: "orgMapping", - header: ({ column }) => { - return ( - - ); - }, - cell: ({ row }) => { - const mapping = row.original.orgMapping; - return mapping ? ( - 50 ? `${mapping.substring(0, 50)}...` : mapping} - info={mapping} - /> - ) : ( - "--" - ); - } + header: "Organization Mapping" }, { - id: "actions", - cell: ({ row }) => { - const policy = row.original; - return ( -
- -
- ); - } + id: "edit", + cell: ({ row }) => ( +
+ +
+ ) } ]; diff --git a/src/app/admin/idp/[idpId]/policies/page.tsx b/src/app/admin/idp/[idpId]/policies/page.tsx index ba10806..7114011 100644 --- a/src/app/admin/idp/[idpId]/policies/page.tsx +++ b/src/app/admin/idp/[idpId]/policies/page.tsx @@ -1,22 +1,8 @@ "use client"; -import { useEffect, useState } from "react"; -import { useParams, useRouter } from "next/navigation"; -import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { useEnvContext } from "@app/hooks/useEnvContext"; -import { toast } from "@app/hooks/useToast"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; 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 { Form, FormControl, @@ -26,33 +12,10 @@ import { FormLabel, FormMessage } from "@app/components/ui/form"; +import { Input } from "@app/components/ui/input"; import { useForm } from "react-hook-form"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; -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 { toast } from "@app/hooks/useToast"; +import { useRouter, useParams } from "next/navigation"; import { SettingsContainer, SettingsSection, @@ -60,64 +23,51 @@ import { SettingsSectionTitle, SettingsSectionDescription, SettingsSectionBody, - SettingsSectionFooter, - SettingsSectionForm + SettingsSectionFooter } 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 = { - 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({ +const DefaultMappingsFormSchema = z.object({ defaultRoleMapping: z.string().optional(), defaultOrgMapping: z.string().optional() }); -type PolicyFormValues = z.infer; -type DefaultMappingsValues = z.infer; +type DefaultMappingsFormValues = z.infer; export default function PoliciesPage() { const { env } = useEnvContext(); const api = createApiClient({ env }); const router = useRouter(); const { idpId } = useParams(); - - const [pageLoading, setPageLoading] = useState(true); - const [addPolicyLoading, setAddPolicyLoading] = useState(false); - const [editPolicyLoading, setEditPolicyLoading] = useState(false); - const [deletePolicyLoading, setDeletePolicyLoading] = useState(false); + const [loading, setLoading] = useState(false); const [updateDefaultMappingsLoading, setUpdateDefaultMappingsLoading] = useState(false); const [policies, setPolicies] = useState([]); - const [organizations, setOrganizations] = useState([]); - const [showAddDialog, setShowAddDialog] = useState(false); - const [editingPolicy, setEditingPolicy] = useState(null); + const [editPolicyFormOpen, setEditPolicyFormOpen] = useState(false); + const [policyToEdit, setPolicyToEdit] = useState(null); + const [orgs, setOrgs] = useState([]); - const form = useForm({ - resolver: zodResolver(policyFormSchema), - defaultValues: { - orgId: "", - roleMapping: "", - orgMapping: "" - } + const defaultMappingsForm = useForm({ + resolver: zodResolver(DefaultMappingsFormSchema), + defaultValues: { defaultRoleMapping: "", defaultOrgMapping: "" } }); - const defaultMappingsForm = useForm({ - resolver: zodResolver(defaultMappingsSchema), - defaultValues: { - defaultRoleMapping: "", - defaultOrgMapping: "" - } - }); - - const loadIdp = async () => { + async function loadIdp() { try { const res = await api.get>( `/idp/${idpId}` @@ -136,11 +86,13 @@ export default function PoliciesPage() { variant: "destructive" }); } - }; + } - const loadPolicies = async () => { + async function loadIdpOrgPolicies() { try { - const res = await api.get(`/idp/${idpId}/org`); + const res = await api.get< + AxiosResponse + >(`/idp/${idpId}/org`); if (res.status === 200) { setPolicies(res.data.data.policies); } @@ -151,17 +103,13 @@ export default function PoliciesPage() { variant: "destructive" }); } - }; + } - const loadOrganizations = async () => { + async function loadOrgs() { try { - const res = await api.get>("/orgs"); + const res = await api.get>(`/orgs`); if (res.status === 200) { - const existingOrgIds = policies.map((p) => p.orgId); - const availableOrgs = res.data.data.orgs.filter( - (org) => !existingOrgIds.includes(org.orgId) - ); - setOrganizations(availableOrgs); + setOrgs(res.data.data.orgs); } } catch (e) { toast({ @@ -170,121 +118,19 @@ export default function PoliciesPage() { variant: "destructive" }); } - }; + } useEffect(() => { - async function load() { - setPageLoading(true); - await loadPolicies(); - await loadIdp(); - setPageLoading(false); - } + const load = async () => { + setLoading(true); + await Promise.all([loadIdp(), loadIdpOrgPolicies(), loadOrgs()]); + setLoading(false); + }; + load(); - }, [idpId]); + }, [idpId, api, router]); - const onAddPolicy = async (data: PolicyFormValues) => { - 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) => { + async function onDefaultMappingsSubmit(data: DefaultMappingsFormValues) { setUpdateDefaultMappingsLoading(true); try { const res = await api.post(`/idp/${idpId}/oidc`, { @@ -306,26 +152,60 @@ export default function PoliciesPage() { } finally { setUpdateDefaultMappingsLoading(false); } - }; + } - if (pageLoading) { - return null; + // Button clicks + + 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 ( <> - + About Organization Policies - Organization policies are used to control access to - organizations based on the user's ID token. You can - specify JMESPath expressions to extract role and - organization information from the ID token. For more - information, see{" "} + Organization policies are used to configure access + control for a specific organization based on the user's + ID token. For more information, see{" "} - The default mappings are used when when there is not - an organization policy defined for an organization. - You can specify the default role and organization + The default mappings are used when there is no + organization policy defined for an organization. You + can specify the default role and organization mappings to fall back to here. @@ -354,10 +234,10 @@ export default function PoliciesPage() {
- The result of this + JMESPath to extract role + information from the ID + token. The result of this expression must return the - role name as defined in the - organization as a string. + role name(s) as defined in + the organization as a + string/list of strings. )} /> - - This expression must return - thr org ID or true for the - user to be allowed to access - the organization. + 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. @@ -420,212 +305,22 @@ export default function PoliciesPage() { { - loadOrganizations(); - form.reset({ - orgId: "", - roleMapping: "", - orgMapping: "" - }); - setEditingPolicy(null); - setShowAddDialog(true); - }} - onEdit={(policy) => { - setEditingPolicy(policy); - form.reset({ - orgId: policy.orgId, - roleMapping: policy.roleMapping || "", - orgMapping: policy.orgMapping || "" - }); - setShowAddDialog(true); - }} + onAdd={onAdd} + onEdit={onEdit} + onDelete={onDelete} + /> + + - - { - setShowAddDialog(val); - setEditingPolicy(null); - form.reset(); - }} - > - - - - {editingPolicy - ? "Edit Organization Policy" - : "Add Organization Policy"} - - - Configure access for an organization - - - - - - ( - - Organization - {editingPolicy ? ( - - ) : ( - - - - - - - - - - - - No org - found. - - - {organizations.map( - ( - org - ) => ( - { - form.setValue( - "orgId", - org.orgId - ); - }} - > - - { - org.name - } - - ) - )} - - - - - - )} - - - )} - /> - - ( - - - Role Mapping Path (Optional) - - - - - - The result of this expression - must return the role name as - defined in the organization as a - string. - - - - )} - /> - - ( - - - Organization Mapping Path - (Optional) - - - - - - This expression must return the - org ID or true for the user to - be allowed to access the - organization. - - - - )} - /> - - - - - - - - - - - ); } diff --git a/src/app/admin/idp/create/page.tsx b/src/app/admin/idp/create/page.tsx index 2aa763d..58e6667 100644 --- a/src/app/admin/idp/create/page.tsx +++ b/src/app/admin/idp/create/page.tsx @@ -410,7 +410,7 @@ export default function Page() { - The path to the user + The JMESPath to the user identifier in the ID token @@ -431,7 +431,7 @@ export default function Page() { - The path to the + The JMESPath to the user's email in the ID token @@ -452,7 +452,7 @@ export default function Page() { - The path to the + The JMESPath to the user's name in the ID token diff --git a/src/app/auth/layout.tsx b/src/app/auth/layout.tsx index fbd5b59..9a149f7 100644 --- a/src/app/auth/layout.tsx +++ b/src/app/auth/layout.tsx @@ -1,8 +1,6 @@ import ProfileIcon from "@app/components/ProfileIcon"; -import { Separator } from "@app/components/ui/separator"; import { verifySession } from "@app/lib/auth/verifySession"; import UserProvider from "@app/providers/UserProvider"; -import { ExternalLink } from "lucide-react"; import { Metadata } from "next"; import { cache } from "react"; @@ -23,52 +21,17 @@ export default async function AuthLayout({ children }: AuthLayoutProps) {
{user && ( -
+
)}
-
{children}
-
- - +
); } diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index 15c1271..821f12c 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -8,6 +8,7 @@ import { Combine, Fingerprint, KeyRound, + TicketCheck } from "lucide-react"; export const orgLangingNavItems: SidebarNavItem[] = [ @@ -64,11 +65,14 @@ export const orgNavItems: SidebarNavItem[] = [ href: "/{orgId}/settings/share-links", icon: }, + /* + TODO: { title: "API Keys", href: "/{orgId}/settings/api-keys", - icon: + icon: , }, + */ { title: "Settings", href: "/{orgId}/settings/general", @@ -82,11 +86,14 @@ export const adminNavItems: SidebarNavItem[] = [ href: "/admin/users", icon: }, + /* + TODO: { title: "API Keys", href: "/admin/api-keys", - icon: + icon: , }, + */ { title: "Identity Providers", href: "/admin/idp", diff --git a/src/components/AuthFooter.tsx b/src/components/AuthFooter.tsx deleted file mode 100644 index a1c0795..0000000 --- a/src/components/AuthFooter.tsx +++ /dev/null @@ -1,3 +0,0 @@ -"use client"; - -export function AuthFooter() {} diff --git a/src/components/PermissionsSelectBox.tsx b/src/components/PermissionsSelectBox.tsx deleted file mode 100644 index c0b9a4e..0000000 --- a/src/components/PermissionsSelectBox.tsx +++ /dev/null @@ -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; - onChange: (updated: Record) => void; -}; - -function getActionsCategories(root: boolean) { - const actionsByCategory: Record> = { - 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) => { - return Object.values(actions).every( - (action) => selectedPermissions[action] - ); - }; - - const toggleAllInCategory = ( - actions: Record, - 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 = {}; - allActions.forEach((action) => { - updated[action] = checked; - }); - onChange(updated); - }; - - return ( - <> -
- - toggleAllPermissions(checked as boolean) - } - /> -
- - {Object.entries(actionsByCategory).map( - ([category, actions]) => { - const allChecked = areAllCheckedInCategory(actions); - return ( - - {category} - -
- - toggleAllInCategory( - actions, - checked as boolean - ) - } - /> - {Object.entries(actions).map( - ([label, value]) => ( - - togglePermission( - value, - checked as boolean - ) - } - /> - ) - )} -
-
-
- ); - } - )} -
- - ); -} diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx index 410d309..7fa689f 100644 --- a/src/components/Settings.tsx +++ b/src/components/Settings.tsx @@ -55,7 +55,7 @@ export function SettingsSectionFooter({ }: { children: React.ReactNode; }) { - return
{children}
; + return
{children}
; } export function SettingsSectionGrid({ diff --git a/src/components/ui/data-table.tsx b/src/components/ui/data-table.tsx index c955605..b544b75 100644 --- a/src/components/ui/data-table.tsx +++ b/src/components/ui/data-table.tsx @@ -31,7 +31,7 @@ import { CardTitle } from "@app/components/ui/card"; -type DataTableProps = { +interface DataTableProps { columns: ColumnDef[]; data: TData[]; title?: string; @@ -39,11 +39,7 @@ type DataTableProps = { onAdd?: () => void; searchPlaceholder?: string; searchColumn?: string; - defaultSort?: { - id: string; - desc: boolean; - }; -}; +} export function DataTable({ columns, @@ -52,12 +48,9 @@ export function DataTable({ addButtonText, onAdd, searchPlaceholder = "Search...", - searchColumn = "name", - defaultSort + searchColumn = "name" }: DataTableProps) { - const [sorting, setSorting] = useState( - defaultSort ? [defaultSort] : [] - ); + const [sorting, setSorting] = useState([]); const [columnFilters, setColumnFilters] = useState([]); const [globalFilter, setGlobalFilter] = useState([]); diff --git a/src/contexts/apiKeyContext.ts b/src/contexts/apiKeyContext.ts deleted file mode 100644 index 58b091f..0000000 --- a/src/contexts/apiKeyContext.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { GetApiKeyResponse } from "@server/routers/apiKeys"; -import { createContext } from "react"; - -interface ApiKeyContextType { - apiKey: GetApiKeyResponse; - updateApiKey: (updatedApiKey: Partial) => void; -} - -const ApiKeyContext = createContext(undefined); - -export default ApiKeyContext; diff --git a/src/hooks/useApikeyContext.ts b/src/hooks/useApikeyContext.ts deleted file mode 100644 index 6c9a829..0000000 --- a/src/hooks/useApikeyContext.ts +++ /dev/null @@ -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; -} diff --git a/src/providers/ApiKeyProvider.tsx b/src/providers/ApiKeyProvider.tsx deleted file mode 100644 index 43a2a9b..0000000 --- a/src/providers/ApiKeyProvider.tsx +++ /dev/null @@ -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(ak); - - const updateApiKey = (updatedApiKey: Partial) => { - if (!apiKey) { - throw new Error("No API key to update"); - } - setApiKey((prev) => { - if (!prev) { - return prev; - } - return { - ...prev, - ...updatedApiKey - }; - }); - }; - - return ( - - {children} - - ); -} - -export default ApiKeyProvider;