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"