diff --git a/docs/deployment/authentication/azuread-auth.mdx b/docs/deployment/authentication/azuread-auth.mdx index 2ee4fd365..3dae091ab 100644 --- a/docs/deployment/authentication/azuread-auth.mdx +++ b/docs/deployment/authentication/azuread-auth.mdx @@ -8,7 +8,7 @@ This feature is a part of Keep Enterprise. Talk to us to get access: https://www.keephq.dev/meet-keep -Keep supports enterprise authentication through Azure Active Directory (Azure AD), enabling organizations to use their existing Microsoft identity platform for secure access management. +Keep supports enterprise authentication through Azure Entre ID (formerly known as Azure AD), enabling organizations to use their existing Microsoft identity platform for secure access management. ## When to Use @@ -49,9 +49,9 @@ We use "Web" platform instead of "Single Page Application (SPA)" because Keep's -For localhost, the redirect would be http://localhost:3000/api/auth/callback/azure-ad +For localhost, the redirect would be http://localhost:3000/api/auth/callback/microsoft-entra-id -For production, it should be something like http://your_keep_frontend_domain/api/auth/callback/azure-ad +For production, it should be something like http://your_keep_frontend_domain/api/auth/callback/microsoft-entra-id diff --git a/docs/images/azuread_3.png b/docs/images/azuread_3.png index c7466ca36..4e91c7263 100644 Binary files a/docs/images/azuread_3.png and b/docs/images/azuread_3.png differ diff --git a/keep-ui/app/(keep)/alerts/alert-table.tsx b/keep-ui/app/(keep)/alerts/alert-table.tsx index d61cb2167..5a27259da 100644 --- a/keep-ui/app/(keep)/alerts/alert-table.tsx +++ b/keep-ui/app/(keep)/alerts/alert-table.tsx @@ -264,15 +264,20 @@ export function AlertTable({ }; return ( -
- -
- {/* Setting min-h-10 to avoid jumping when actions are shown */} + // Add h-screen to make it full height and remove the default flex-col gap +
+ {/* Add padding to account for any top nav/header */} +
+ +
+ + {/* Make actions/presets section fixed height */} +
{selectedRowIds.length ? ( )}
-
-
- -
-
- -
- {/* For dynamic preset, add alert tabs*/} - {!presetStatic && ( - - )} -
- - - -
-
- + + {/* Main content area - uses flex-grow to fill remaining space */} +
+
+ {/* Facets sidebar */} +
+ +
+ + {/* Table section */} +
+ +
+ {!presetStatic && ( +
+ +
+ )} + +
+ + {/* Make table wrapper scrollable */} +
+ + + +
+
+
+ +
-
+ + {/* Pagination footer - fixed height */} +
+ setIsSidebarOpen(false)} diff --git a/keep-ui/app/(keep)/alerts/alerts-table-body.tsx b/keep-ui/app/(keep)/alerts/alerts-table-body.tsx index 0ee55b8a0..08a4c61bf 100644 --- a/keep-ui/app/(keep)/alerts/alerts-table-body.tsx +++ b/keep-ui/app/(keep)/alerts/alerts-table-body.tsx @@ -35,13 +35,15 @@ export function AlertsTableBody({ if (showEmptyState) { return ( <> -
- +
+
+ +
{modalOpen && ( (); useEffect(() => { - console.log("Fetching providers"); async function fetchProviders() { - console.log("Fetching providers 2"); const response = await getProviders(); setProviders(response as Providers); } - console.log("Fetching providers 3"); fetchProviders(); - console.log("Fetching providers 4"); }, []); useEffect(() => { @@ -69,9 +65,9 @@ export default function SignInForm({ params }: { params?: { amt: string } }) { } else if (providers.keycloak) { console.log("Signing in with keycloak provider"); signIn("keycloak", { callbackUrl: "/" }); - } else if (providers["azure-ad"]) { + } else if (providers["microsoft-entra-id"]) { console.log("Signing in with Azure AD provider"); - signIn("azure-ad", { callbackUrl: "/" }); + signIn("microsoft-entra-id", { callbackUrl: "/" }); } else if ( providers.credentials && providers.credentials.name == "NoAuth" diff --git a/keep-ui/auth.ts b/keep-ui/auth.ts index 159de7439..f0b470971 100644 --- a/keep-ui/auth.ts +++ b/keep-ui/auth.ts @@ -1,12 +1,14 @@ import NextAuth from "next-auth"; import type { NextAuthConfig } from "next-auth"; +import { customFetch } from "next-auth"; import Credentials from "next-auth/providers/credentials"; import Keycloak from "next-auth/providers/keycloak"; import Auth0 from "next-auth/providers/auth0"; -import MicrosoftEntraID from "@auth/core/providers/microsoft-entra-id"; +import MicrosoftEntraID from "next-auth/providers/microsoft-entra-id"; import { AuthError } from "next-auth"; import { AuthenticationError, AuthErrorCodes } from "@/errors"; -import type { JWT } from "@auth/core/jwt"; +import type { JWT } from "next-auth/jwt"; +// https://github.com/nextauthjs/next-auth/issues/11028 export class BackendRefusedError extends AuthError { static type = "BackendRefusedError"; @@ -34,6 +36,127 @@ const authType = ? AuthType.NOAUTH : (authTypeEnv as AuthType); +const proxyUrl = + process.env.HTTP_PROXY || + process.env.HTTPS_PROXY || + process.env.http_proxy || + process.env.https_proxy; + +import { ProxyAgent, fetch as undici } from "undici"; +function proxyFetch( + ...args: Parameters +): ReturnType { + console.log( + "Proxy called for URL:", + args[0] instanceof Request ? args[0].url : args[0] + ); + const dispatcher = new ProxyAgent(proxyUrl!); + + if (args[0] instanceof Request) { + const request = args[0]; + // @ts-expect-error `undici` has a `duplex` option + return undici(request.url, { + ...args[1], + method: request.method, + headers: request.headers as HeadersInit, + body: request.body, + dispatcher, + }); + } + + // @ts-expect-error `undici` has a `duplex` option + return undici(args[0], { ...(args[1] || {}), dispatcher }); +} + +/** + * Creates a Microsoft Entra ID provider configuration and overrides the customFetch. + * + * SHAHAR: this is a workaround to override the customFetch symbol in the provider + * because in Microsoft entra it already has a customFetch symbol and we need to override it.s + */ +export const createAzureADProvider = () => { + if (!proxyUrl) { + console.log("Proxy is not enabled"); + } else { + console.log("Proxy is enabled:", proxyUrl); + } + + // Step 1: Create the base provider + const baseConfig = { + clientId: process.env.KEEP_AZUREAD_CLIENT_ID!, + clientSecret: process.env.KEEP_AZUREAD_CLIENT_SECRET!, + issuer: `https://login.microsoftonline.com/${process.env + .KEEP_AZUREAD_TENANT_ID!}/v2.0`, + authorization: { + params: { + scope: `api://${process.env + .KEEP_AZUREAD_CLIENT_ID!}/default openid profile email`, + }, + }, + client: { + token_endpoint_auth_method: "client_secret_post", + }, + }; + + const provider = MicrosoftEntraID(baseConfig); + // if not proxyUrl, return the provider + if (!proxyUrl) return provider; + + // Step 2: Override the `customFetch` symbol in the provider + provider[customFetch] = async (...args: Parameters) => { + const url = new URL(args[0] instanceof Request ? args[0].url : args[0]); + console.log("Custom Fetch Intercepted:", url.toString()); + + // Handle `.well-known/openid-configuration` logic + if (url.pathname.endsWith(".well-known/openid-configuration")) { + console.log("Intercepting .well-known/openid-configuration"); + const response = await proxyFetch(...args); + const json = await response.clone().json(); + const tenantRe = /microsoftonline\.com\/(\w+)\/v2\.0/; + const tenantId = baseConfig.issuer?.match(tenantRe)?.[1] ?? "common"; + const issuer = json.issuer.replace("{tenantid}", tenantId); + console.log("Modified issuer:", issuer); + return Response.json({ ...json, issuer }); + } + + // Fallback for all other requests + return proxyFetch(...args); + }; + + // Step 3: override profile since it use fetch without customFetch + provider.profile = async (profile, tokens) => { + const profilePhotoSize = 48; // Default or custom size + console.log("Fetching profile photo via proxy"); + + const response = await proxyFetch( + `https://graph.microsoft.com/v1.0/me/photos/${profilePhotoSize}x${profilePhotoSize}/$value`, + { headers: { Authorization: `Bearer ${tokens.access_token}` } } + ); + + let image: string | null = null; + if (response.ok && typeof Buffer !== "undefined") { + try { + const pictureBuffer = await response.arrayBuffer(); + const pictureBase64 = Buffer.from(pictureBuffer).toString("base64"); + image = `data:image/jpeg;base64,${pictureBase64}`; + } catch (error) { + console.error("Error processing profile photo:", error); + } + } + + // Ensure the returned object matches the User interface + return { + id: profile.sub, + name: profile.name, + email: profile.email, + image: image ?? null, + accessToken: tokens.access_token ?? "", // Provide empty string as fallback + }; + }; + + return provider; +}; + async function refreshAccessToken(token: any) { const issuerUrl = process.env.KEYCLOAK_ISSUER; const refreshTokenUrl = `${issuerUrl}/protocol/openid-connect/token`; @@ -159,19 +282,7 @@ const providerConfigs = { authorization: { params: { scope: "openid email profile roles" } }, }), ], - [AuthType.AZUREAD]: [ - MicrosoftEntraID({ - clientId: process.env.KEEP_AZUREAD_CLIENT_ID!, - clientSecret: process.env.KEEP_AZUREAD_CLIENT_SECRET!, - issuer: process.env.KEEP_AZUREAD_TENANT_ID!, - authorization: { - params: { - scope: `api://${process.env - .KEEP_AZUREAD_CLIENT_ID!}/default openid profile email`, - }, - }, - }), - ], + [AuthType.AZUREAD]: [createAzureADProvider()], }; // Create the config @@ -203,7 +314,25 @@ const config = { let tenantId: string | undefined = user.tenantId; let role: string | undefined = user.role; // Ensure we always have an accessToken - if (authType == AuthType.AUTH0) { + // https://github.com/nextauthjs/next-auth/discussions/4255 + if (authType === AuthType.AZUREAD) { + // Properly handle Azure AD tokens + accessToken = account.access_token; + // You might want to extract additional claims from the id_token if needed + if (account.id_token) { + try { + // Basic JWT decode (you might want to use a proper JWT library here) + const payload = JSON.parse( + Buffer.from(account.id_token.split(".")[1], "base64").toString() + ); + // Extract any additional claims you need + role = payload.roles?.[0] || "user"; + tenantId = payload.tid || undefined; + } catch (e) { + console.warn("Failed to decode id_token:", e); + } + } + } else if (authType == AuthType.AUTH0) { accessToken = account.id_token; if ((profile as any)?.keep_tenant_id) { tenantId = (profile as any).keep_tenant_id; diff --git a/keep-ui/middleware.tsx b/keep-ui/middleware.tsx index 052c05792..78e82ef4b 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"; +import { getToken } from "next-auth/jwt"; +import type { JWT } 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 with the correct type + const token = (await getToken({ + req: request, + secret: process.env.NEXTAUTH_SECRET, + })) as JWT | null; // 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?.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..6d4ace32f 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: [ @@ -29,12 +52,7 @@ const nextConfig = { ], }, compiler: { - removeConsole: - process.env.NODE_ENV === "production" - ? { - exclude: ["error"], - } - : process.env.REMOVE_CONSOLE === "true", + removeConsole: false, }, output: "standalone", productionBrowserSourceMaps: diff --git a/keep-ui/package-lock.json b/keep-ui/package-lock.json index 11e155b6a..c6e0749ac 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", @@ -48,6 +49,7 @@ "eslint-scope": "^7.2.0", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.4.1", + "https-proxy-agent": "^7.0.5", "js-yaml": "^4.1.0", "lodash.debounce": "^4.0.8", "lucide-react": "^0.460.0", @@ -97,6 +99,7 @@ "swr": "^2.2.5", "tailwind-merge": "^1.12.0", "tailwindcss": "^3.4.1", + "undici": "^6.21.0", "uuid": "^8.3.2", "yaml": "^2.2.2", "zustand": "^5.0.1" @@ -196,18 +199,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", @@ -226,16 +227,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", @@ -6964,6 +6955,31 @@ "node": ">=10" } }, + "node_modules/@sentry/cli/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@sentry/cli/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/@sentry/core": { "version": "8.38.0", "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.38.0.tgz", @@ -8681,14 +8697,15 @@ } }, "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "license": "MIT", "dependencies": { - "debug": "4" + "debug": "^4.3.4" }, "engines": { - "node": ">= 6.0.0" + "node": ">= 14" } }, "node_modules/agentkeepalive": { @@ -12119,6 +12136,31 @@ "node": ">=12" } }, + "node_modules/gaxios/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/gaxios/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/gcp-metadata": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", @@ -12860,27 +12902,17 @@ "node": ">= 14" } }, - "node_modules/http-proxy-agent/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "license": "MIT", "dependencies": { - "agent-base": "6", + "agent-base": "^7.0.2", "debug": "4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/humanize-ms": { @@ -13692,29 +13724,6 @@ } } }, - "node_modules/jsdom/node_modules/agent-base": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", - "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", - "dependencies": { - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/jsdom/node_modules/https-proxy-agent": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", - "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", - "dependencies": { - "agent-base": "^7.0.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/jsesc": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", @@ -15783,6 +15792,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", @@ -16723,13 +16785,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" } @@ -20011,6 +20070,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.0.tgz", + "integrity": "sha512-BUgJXc752Kou3oOIuU1i+yZZypyZRqNPW0vqoMPl8VaoalSfeR0D8/t4iAS3yirs79SSMTxTag+ZC86uswv+Cw==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/keep-ui/package.json b/keep-ui/package.json index 172d2dfd2..4678a06dc 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", @@ -49,6 +50,7 @@ "eslint-scope": "^7.2.0", "eslint-utils": "^3.0.0", "eslint-visitor-keys": "^3.4.1", + "https-proxy-agent": "^7.0.5", "js-yaml": "^4.1.0", "lodash.debounce": "^4.0.8", "lucide-react": "^0.460.0", @@ -98,6 +100,7 @@ "swr": "^2.2.5", "tailwind-merge": "^1.12.0", "tailwindcss": "^3.4.1", + "undici": "^6.21.0", "uuid": "^8.3.2", "yaml": "^2.2.2", "zustand": "^5.0.1" 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 new file mode 100644 index 000000000..d0d1c936e --- /dev/null +++ b/keep-ui/proxyFetch.ts @@ -0,0 +1,11 @@ +// proxyFetch.ts + +// We only export the type from this file +export type ProxyFetchFn = ( + ...args: Parameters +) => ReturnType; + +// 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/keep-ui/types/auth.d.ts b/keep-ui/types/auth.d.ts index 22237903d..6dd72458c 100644 --- a/keep-ui/types/auth.d.ts +++ b/keep-ui/types/auth.d.ts @@ -1,6 +1,7 @@ -import type { DefaultSession } from "@auth/core/types"; +import type { DefaultSession } from "next-auth"; +import type { JWT } from "next-auth/jwt"; -declare module "@auth/core/types" { +declare module "next-auth" { interface Session { accessToken: string; tenantId?: string; @@ -13,7 +14,7 @@ declare module "@auth/core/types" { accessToken: string; tenantId?: string; role?: string; - }; + } & DefaultSession["user"]; } interface User { @@ -26,9 +27,9 @@ declare module "@auth/core/types" { } } -declare module "@auth/core/jwt" { +declare module "next-auth/jwt" { interface JWT { - accessToken: string; // Changed to required + accessToken: string; tenantId?: string; role?: string; } 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 diff --git a/pyproject.toml b/pyproject.toml index 5002c864b..b9494f570 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "keep" -version = "0.29.3" +version = "0.29.4" description = "Alerting. for developers, by developers." authors = ["Keep Alerting LTD"] readme = "README.md"