diff --git a/keep-ui/auth.ts b/keep-ui/auth.ts index 92f52ae9d..2a56f3abd 100644 --- a/keep-ui/auth.ts +++ b/keep-ui/auth.ts @@ -9,7 +9,6 @@ import { AuthError } from "next-auth"; import { AuthenticationError, AuthErrorCodes } from "@/errors"; import type { JWT } from "@auth/core/jwt"; // https://github.com/nextauthjs/next-auth/issues/11028 -import { initProxyFetch } from "./proxyFetch"; export class BackendRefusedError extends AuthError { static type = "BackendRefusedError"; @@ -43,23 +42,21 @@ const proxyUrl = process.env.http_proxy || process.env.https_proxy; -// Helper function to dynamically import proxyFetch only when needed -async function getProxyFetch() { - if (typeof window === "undefined" && !process.env.NEXT_RUNTIME) { - // Only import in Node.js environment, not Edge - try { - const { initProxyFetch } = await import("./proxyFetch"); - return initProxyFetch(); - } catch (e) { - console.warn("Failed to load proxy fetch:", e); - return null; - } - } - return null; +import { ProxyAgent, fetch as undici } from "undici"; +function proxy(...args: Parameters): ReturnType { + const dispatcher = new ProxyAgent(proxyUrl!); + // @ts-expect-error `undici` has a `duplex` option + return undici(args[0], { ...args[1], dispatcher }); } // Create Azure AD provider configuration const createAzureADProvider = () => { + // check if proxy is enabled + if (!proxyUrl) { + console.log("Proxy is not enabled"); + } else { + console.log("Proxy is enabled - ", proxyUrl); + } const baseConfig = { clientId: process.env.KEEP_AZUREAD_CLIENT_ID!, clientSecret: process.env.KEEP_AZUREAD_CLIENT_SECRET!, @@ -72,24 +69,13 @@ const createAzureADProvider = () => { .KEEP_AZUREAD_CLIENT_ID!}/default openid profile email`, }, }, + [customFetch]: proxyUrl ? proxy : undefined, client: { token_endpoint_auth_method: "client_secret_post", }, }; - if (!proxyUrl) { - console.log("Using built-in fetch for Azure AD provider"); - return MicrosoftEntraID(baseConfig); - } - - console.log("Using proxy fetch for Azure AD provider"); - return MicrosoftEntraID({ - ...baseConfig, - async [customFetch](...args) { - const proxyFetch = await getProxyFetch(); - return proxyFetch ? proxyFetch(...args) : fetch(...args); - }, - }); + return MicrosoftEntraID(baseConfig); }; async function refreshAccessToken(token: any) { diff --git a/keep-ui/middleware.tsx b/keep-ui/middleware.tsx index 052c05792..68cf62d12 100644 --- a/keep-ui/middleware.tsx +++ b/keep-ui/middleware.tsx @@ -1,14 +1,20 @@ -import { auth } from "@/auth"; -import { NextResponse } from "next/server"; +import { NextResponse, type NextRequest } from "next/server"; +// https://github.com/nextauthjs/next-auth/issues/11028#issuecomment-2391497962 +import { getToken } from "next-auth/jwt"; import { getApiURL } from "@/utils/apiUrl"; -// Use auth as a wrapper for middleware logic -export default auth(async (req) => { - const { pathname, searchParams } = req.nextUrl; +export async function middleware(request: NextRequest) { + const { pathname, searchParams } = request.nextUrl; // Keep it on header so it can be used in server components - const requestHeaders = new Headers(req.headers); - requestHeaders.set("x-url", req.url); + const requestHeaders = new Headers(request.headers); + requestHeaders.set("x-url", request.url); + + // Get the token using next-auth/jwt instead of auth wrapper + const token = await getToken({ + req: request, + secret: process.env.NEXTAUTH_SECRET, + }); // Handle legacy /backend/ redirects if (pathname.startsWith("/backend/")) { @@ -25,36 +31,37 @@ export default auth(async (req) => { if (pathname.startsWith("/api/")) { return NextResponse.next(); } + // If not authenticated and not on signin page, redirect to signin - if (!req.auth && !pathname.startsWith("/signin")) { + if (!token && !pathname.startsWith("/signin")) { console.log("Redirecting to signin page because user is not authenticated"); - return NextResponse.redirect(new URL("/signin", req.url)); + return NextResponse.redirect(new URL("/signin", request.url)); } - // else if authenticated and on signin page, redirect to dashboard - if (req.auth && pathname.startsWith("/signin")) { + // If authenticated and on signin page, redirect to dashboard + if (token && pathname.startsWith("/signin")) { console.log( "Redirecting to incidents because user try to get /signin but already authenticated" ); - return NextResponse.redirect(new URL("/incidents", req.url)); + return NextResponse.redirect(new URL("/incidents", request.url)); } // Role-based routing (NOC users) - if (req.auth?.user?.role === "noc" && !pathname.startsWith("/alerts")) { - return NextResponse.redirect(new URL("/alerts/feed", req.url)); + if (token?.user?.role === "noc" && !pathname.startsWith("/alerts")) { + return NextResponse.redirect(new URL("/alerts/feed", request.url)); } // Allow all other authenticated requests console.log("Allowing request to pass through"); - console.log("Request URL: ", req.url); - // console.log("Request headers: ", requestHeaders) + console.log("Request URL: ", request.url); + return NextResponse.next({ request: { // Apply new request headers headers: requestHeaders, }, }); -}); +} // Update the matcher to handle static files and public routes export const config = { diff --git a/keep-ui/next.config.js b/keep-ui/next.config.js index 47f5c2ca4..47e955ce0 100644 --- a/keep-ui/next.config.js +++ b/keep-ui/next.config.js @@ -3,6 +3,29 @@ const { withSentryConfig } = require("@sentry/nextjs"); /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: false, + webpack: ( + config, + { buildId, dev, isServer, defaultLoaders, nextRuntime, webpack } + ) => { + // Only apply proxy configuration for Node.js server runtime + if (isServer && nextRuntime === "nodejs") { + // Add environment variables for proxy at build time + config.plugins.push( + new webpack.DefinePlugin({ + "process.env.IS_NODEJS_RUNTIME": JSON.stringify(true), + }) + ); + } else { + // For edge runtime and client + config.plugins.push( + new webpack.DefinePlugin({ + "process.env.IS_NODEJS_RUNTIME": JSON.stringify(false), + }) + ); + } + + return config; + }, transpilePackages: ["next-auth"], images: { remotePatterns: [ diff --git a/keep-ui/package-lock.json b/keep-ui/package-lock.json index 4701e68a7..0d1311115 100644 --- a/keep-ui/package-lock.json +++ b/keep-ui/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@auth/core": "^0.37.4", "@boiseitguru/cookie-cutter": "^0.2.3", "@copilotkit/react-core": "^1.3.15", "@copilotkit/react-ui": "^1.3.15", @@ -197,18 +198,16 @@ } }, "node_modules/@auth/core": { - "version": "0.37.2", - "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.37.2.tgz", - "integrity": "sha512-kUvzyvkcd6h1vpeMAojK2y7+PAV5H+0Cc9+ZlKYDFhDY31AlvsB+GW5vNO4qE3Y07KeQgvNO9U0QUx/fN62kBw==", + "version": "0.37.4", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.37.4.tgz", + "integrity": "sha512-HOXJwXWXQRhbBDHlMU0K/6FT1v+wjtzdKhsNg0ZN7/gne6XPsIrjZ4daMcFnbq0Z/vsAbYBinQhhua0d77v7qw==", "license": "ISC", "dependencies": { "@panva/hkdf": "^1.2.1", - "@types/cookie": "0.6.0", - "cookie": "0.7.1", - "jose": "^5.9.3", - "oauth4webapi": "^3.0.0", - "preact": "10.11.3", - "preact-render-to-string": "5.2.3" + "jose": "^5.9.6", + "oauth4webapi": "^3.1.1", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" }, "peerDependencies": { "@simplewebauthn/browser": "^9.0.1", @@ -227,16 +226,6 @@ } } }, - "node_modules/@auth/core/node_modules/preact": { - "version": "10.11.3", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", - "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/preact" - } - }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -15769,6 +15758,59 @@ } } }, + "node_modules/next-auth/node_modules/@auth/core": { + "version": "0.37.2", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.37.2.tgz", + "integrity": "sha512-kUvzyvkcd6h1vpeMAojK2y7+PAV5H+0Cc9+ZlKYDFhDY31AlvsB+GW5vNO4qE3Y07KeQgvNO9U0QUx/fN62kBw==", + "license": "ISC", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "@types/cookie": "0.6.0", + "cookie": "0.7.1", + "jose": "^5.9.3", + "oauth4webapi": "^3.0.0", + "preact": "10.11.3", + "preact-render-to-string": "5.2.3" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^6.8.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, + "node_modules/next-auth/node_modules/preact": { + "version": "10.11.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.11.3.tgz", + "integrity": "sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/next-auth/node_modules/preact-render-to-string": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz", + "integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==", + "license": "MIT", + "dependencies": { + "pretty-format": "^3.8.0" + }, + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -16709,13 +16751,10 @@ } }, "node_modules/preact-render-to-string": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz", - "integrity": "sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==", + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", "license": "MIT", - "dependencies": { - "pretty-format": "^3.8.0" - }, "peerDependencies": { "preact": ">=10" } diff --git a/keep-ui/package.json b/keep-ui/package.json index f86360bab..f48566727 100644 --- a/keep-ui/package.json +++ b/keep-ui/package.json @@ -10,6 +10,7 @@ "lint": "next lint" }, "dependencies": { + "@auth/core": "^0.37.4", "@boiseitguru/cookie-cutter": "^0.2.3", "@copilotkit/react-core": "^1.3.15", "@copilotkit/react-ui": "^1.3.15", diff --git a/keep-ui/proxyFetch.node.ts b/keep-ui/proxyFetch.node.ts new file mode 100644 index 000000000..76defb940 --- /dev/null +++ b/keep-ui/proxyFetch.node.ts @@ -0,0 +1,24 @@ +// proxyFetch.node.ts +import { ProxyAgent, fetch as undici } from "undici"; +import type { ProxyFetchFn } from "./proxyFetch"; + +export const createProxyFetch = async (): Promise => { + const proxyUrl = + process.env.HTTP_PROXY || + process.env.HTTPS_PROXY || + process.env.http_proxy || + process.env.https_proxy; + + if (!proxyUrl) { + return undefined; + } + + const dispatcher = new ProxyAgent(proxyUrl); + + return function proxy( + ...args: Parameters + ): ReturnType { + // @ts-expect-error `undici` has a `duplex` option + return undici(args[0], { ...args[1], dispatcher }); + }; +}; diff --git a/keep-ui/proxyFetch.ts b/keep-ui/proxyFetch.ts index 38eae2b84..d0d1c936e 100644 --- a/keep-ui/proxyFetch.ts +++ b/keep-ui/proxyFetch.ts @@ -1,20 +1,11 @@ // proxyFetch.ts -let proxyFetch: typeof fetch | undefined; -export async function initProxyFetch() { - const proxyUrl = - process.env.HTTP_PROXY || - process.env.HTTPS_PROXY || - process.env.http_proxy || - process.env.https_proxy; +// We only export the type from this file +export type ProxyFetchFn = ( + ...args: Parameters +) => ReturnType; - if (proxyUrl && typeof window === "undefined") { - const { ProxyAgent, fetch: undici } = await import("undici"); - const dispatcher = new ProxyAgent(proxyUrl); - return (...args: Parameters): ReturnType => { - // @ts-expect-error `undici` has a `duplex` option - return undici(args[0], { ...args[1], dispatcher }); - }; - } +// This function will be imported dynamically only in Node.js environment +export const createProxyFetch = async (): Promise => { return undefined; -} +}; diff --git a/keep-ui/tsconfig.json b/keep-ui/tsconfig.json index 2087f066f..7aceabb38 100644 --- a/keep-ui/tsconfig.json +++ b/keep-ui/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "sourceMap": true, "target": "es6", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, diff --git a/proxy/README.md b/proxy/README.md new file mode 100644 index 000000000..524e922e5 --- /dev/null +++ b/proxy/README.md @@ -0,0 +1,137 @@ +# Development Proxy Setup + +This directory contains the configuration files and Docker services needed to run Keep with a proxy setup, primarily used for testing and development scenarios requiring proxy configurations (e.g., corporate environments, Azure AD authentication). + +## Directory Structure + +``` +proxy/ +├── docker-compose-proxy.yml # Docker Compose configuration for proxy setup +├── squid.conf # Squid proxy configuration +├── nginx.conf # Nginx reverse proxy configuration +└── README.md # This file +``` + +## Components + +The setup consists of several services: + +- **Squid Proxy**: Acts as a forward proxy for HTTP/HTTPS traffic +- **Nginx**: Serves as a reverse proxy/tunnel +- **Keep Frontend**: The Keep UI service configured to use the proxy +- **Keep Backend**: The Keep API service +- **Keep WebSocket**: The WebSocket server for real-time updates + +## Network Architecture + +The setup uses two Docker networks: + +- `proxy-net`: External network for proxy communication +- `internal`: Internal network with no external access (secure network for inter-service communication) + +## Configuration + +### Environment Variables + +The Keep Frontend service is preconfigured with proxy-related environment variables: + +```env +http_proxy=http://proxy:3128 +https_proxy=http://proxy:3128 +HTTP_PROXY=http://proxy:3128 +HTTPS_PROXY=http://proxy:3128 +npm_config_proxy=http://proxy:3128 +npm_config_https_proxy=http://proxy:3128 +``` + +### Usage + +1. Start the proxy environment: + +```bash +docker compose -f docker-compose-proxy.yml up +``` + +2. To run in detached mode: + +```bash +docker compose -f docker-compose-proxy.yml up -d +``` + +3. To stop all services: + +```bash +docker compose -f docker-compose-proxy.yml down +``` + +### Accessing Services + +- Keep Frontend: http://localhost:3000 +- Keep Backend: http://localhost:8080 +- Squid Proxy: localhost:3128 + +## Custom Configuration + +### Modifying Proxy Settings + +To modify the Squid proxy configuration: + +1. Edit `squid.conf` +2. Restart the proxy service: + +```bash +docker compose -f docker-compose-proxy.yml restart proxy +``` + +### Modifying Nginx Settings + +To modify the Nginx reverse proxy configuration: + +1. Edit `nginx.conf` +2. Restart the nginx service: + +```bash +docker compose -f docker-compose-proxy.yml restart tunnel +``` + +## Troubleshooting + +If you encounter connection issues: + +1. Verify proxy is running: + +```bash +docker compose -f docker-compose-proxy.yml ps +``` + +2. Check proxy logs: + +```bash +docker compose -f docker-compose-proxy.yml logs proxy +``` + +3. Test proxy connection: + +```bash +curl -x http://localhost:3128 https://www.google.com +``` + +## Development Notes + +- The proxy setup is primarily intended for development and testing +- When using Azure AD authentication, ensure the proxy configuration matches your environment's requirements +- SSL certificate validation is disabled by default for development purposes (`npm_config_strict_ssl=false`) + +## Security Considerations + +- This setup is intended for development environments only +- The internal network is isolated from external access for security +- Modify security settings in `squid.conf` and `nginx.conf` according to your requirements + +## Contributing + +When modifying the proxy setup: + +1. Document any changes to configuration files +2. Test the setup with both proxy and non-proxy environments +3. Update this README if adding new features or configurations diff --git a/proxy/docker-compose-proxy.yml b/proxy/docker-compose-proxy.yml index ac4a61689..a75ead324 100644 --- a/proxy/docker-compose-proxy.yml +++ b/proxy/docker-compose-proxy.yml @@ -26,7 +26,7 @@ services: ports: - "3000:3000" extends: - file: docker-compose.common.yml + file: ../docker-compose.common.yml service: keep-frontend-common image: us-central1-docker.pkg.dev/keephq/keep/keep-ui:feature_proxy environment: @@ -49,15 +49,15 @@ services: depends_on: - keep-backend - proxy - # networks: - # - proxy-net - # - internal + networks: + # - proxy-net + - internal keep-backend: ports: - "8080:8080" extends: - file: docker-compose.common.yml + file: ../docker-compose.common.yml service: keep-backend-common image: us-central1-docker.pkg.dev/keephq/keep/keep-api environment: @@ -70,7 +70,7 @@ services: keep-websocket-server: extends: - file: docker-compose.common.yml + file: ../docker-compose.common.yml service: keep-websocket-server-common networks: - internal