diff --git a/.github/workflows/test-docs.yml b/.github/workflows/test-docs.yml
index 685908bb7..439994b74 100644
--- a/.github/workflows/test-docs.yml
+++ b/.github/workflows/test-docs.yml
@@ -2,11 +2,11 @@ name: Test docs
on:
push:
paths:
- - 'keep/poviders/**'
+ - 'keep/providers/**'
- 'docs/**'
pull_request:
paths:
- - 'keep/poviders/**'
+ - 'keep/providers/**'
- 'docs/**'
workflow_dispatch:
concurrency:
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/docs/providers/documentation/gke-provider.mdx b/docs/providers/documentation/gke-provider.mdx
index 416d6301e..b395d399d 100644
--- a/docs/providers/documentation/gke-provider.mdx
+++ b/docs/providers/documentation/gke-provider.mdx
@@ -1,7 +1,7 @@
---
-title: "GKE"
-sidebarTitle: "GKE Provider"
-description: "GKE provider allows managing Google Kubernetes Engine clusters and related resources."
+title: "Google Kubernetes Engine"
+sidebarTitle: "Google Kubernetes Engine Provider"
+description: "Google Kubernetes Engine provider allows managing Google Kubernetes Engine clusters and related resources."
---
## Inputs
diff --git a/docs/providers/documentation/teams-provider.mdx b/docs/providers/documentation/teams-provider.mdx
index 2a907da42..673b9dc07 100644
--- a/docs/providers/documentation/teams-provider.mdx
+++ b/docs/providers/documentation/teams-provider.mdx
@@ -1,7 +1,7 @@
---
-title: "Teams Provider"
-sidebarTitle: "Teams Provider"
-description: "Teams Provider is a provider that allows to notify alerts to Microsoft Teams chats."
+title: "Microsoft Teams Provider"
+sidebarTitle: "Microsoft Teams Provider"
+description: "Microsoft Teams Provider is a provider that allows to notify alerts to Microsoft Teams chats."
---
## Inputs
diff --git a/docs/providers/overview.mdx b/docs/providers/overview.mdx
index 74e1ddc87..3e8767f60 100644
--- a/docs/providers/overview.mdx
+++ b/docs/providers/overview.mdx
@@ -45,7 +45,7 @@ By leveraging Keep Providers, users are able to deeply integrate Keep with the t
/>
@@ -653,7 +653,7 @@ By leveraging Keep Providers, users are able to deeply integrate Keep with the t
>
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/app/api/auth/[...nextauth]/route.ts b/keep-ui/app/api/auth/[...nextauth]/route.ts
index 7c62e2db1..be6543d31 100644
--- a/keep-ui/app/api/auth/[...nextauth]/route.ts
+++ b/keep-ui/app/api/auth/[...nextauth]/route.ts
@@ -1,2 +1,23 @@
import { handlers } from "@/auth";
-export const { GET, POST } = handlers;
+import { NextRequest } from "next/server";
+
+const reqWithTrustedOrigin = (req: NextRequest): NextRequest => {
+ if (process.env.AUTH_TRUST_HOST !== "true") return req;
+ const proto = req.headers.get("x-forwarded-proto");
+ const host = req.headers.get("x-forwarded-host");
+ if (!proto || !host) {
+ console.warn("Missing x-forwarded-proto or x-forwarded-host headers.");
+ return req;
+ }
+ const envOrigin = `${proto}://${host}`;
+ const { href, origin } = req.nextUrl;
+ return new NextRequest(href.replace(origin, envOrigin), req);
+};
+
+export const GET = (req: NextRequest) => {
+ return handlers.GET(reqWithTrustedOrigin(req));
+};
+
+export const POST = (req: NextRequest) => {
+ return handlers.POST(reqWithTrustedOrigin(req));
+};
diff --git a/keep-ui/auth.config.ts b/keep-ui/auth.config.ts
new file mode 100644
index 000000000..5ec968059
--- /dev/null
+++ b/keep-ui/auth.config.ts
@@ -0,0 +1,274 @@
+import type { NextAuthConfig } 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 "next-auth/providers/microsoft-entra-id";
+import { AuthError } from "next-auth";
+import { AuthenticationError, AuthErrorCodes } from "@/errors";
+import type { JWT } from "next-auth/jwt";
+import type { User } from "next-auth";
+import { getApiURL } from "@/utils/apiUrl";
+import {
+ AuthType,
+ MULTI_TENANT,
+ SINGLE_TENANT,
+ NO_AUTH,
+ NoAuthUserEmail,
+ NoAuthTenant,
+} from "@/utils/authenticationType";
+
+export class BackendRefusedError extends AuthError {
+ static type = "BackendRefusedError";
+}
+
+// Determine auth type with backward compatibility
+const authTypeEnv = process.env.AUTH_TYPE;
+export const authType =
+ authTypeEnv === MULTI_TENANT
+ ? AuthType.AUTH0
+ : authTypeEnv === SINGLE_TENANT
+ ? AuthType.DB
+ : authTypeEnv === NO_AUTH
+ ? AuthType.NOAUTH
+ : (authTypeEnv as AuthType);
+
+async function refreshAccessToken(token: any) {
+ const issuerUrl = process.env.KEYCLOAK_ISSUER;
+ const refreshTokenUrl = `${issuerUrl}/protocol/openid-connect/token`;
+
+ try {
+ const response = await fetch(refreshTokenUrl, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ body: new URLSearchParams({
+ client_id: process.env.KEYCLOAK_ID!,
+ client_secret: process.env.KEYCLOAK_SECRET!,
+ grant_type: "refresh_token",
+ refresh_token: token.refreshToken,
+ }),
+ });
+
+ const refreshedTokens = await response.json();
+
+ if (!response.ok) {
+ throw new Error(
+ `Refresh token failed: ${response.status} ${response.statusText}`
+ );
+ }
+
+ return {
+ ...token,
+ accessToken: refreshedTokens.access_token,
+ accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
+ refreshToken: refreshedTokens.refresh_token ?? token.refreshToken,
+ };
+ } catch (error) {
+ console.error("Error refreshing access token:", error);
+ return {
+ ...token,
+ error: "RefreshAccessTokenError",
+ };
+ }
+}
+
+// Base provider configurations without AzureAD
+const baseProviderConfigs = {
+ [AuthType.AUTH0]: [
+ Auth0({
+ clientId: process.env.AUTH0_CLIENT_ID!,
+ clientSecret: process.env.AUTH0_CLIENT_SECRET!,
+ issuer: process.env.AUTH0_ISSUER!,
+ authorization: {
+ params: {
+ prompt: "login",
+ },
+ },
+ }),
+ ],
+ [AuthType.DB]: [
+ Credentials({
+ name: "Credentials",
+ credentials: {
+ username: { label: "Username", type: "text", placeholder: "keep" },
+ password: { label: "Password", type: "password", placeholder: "keep" },
+ },
+ async authorize(credentials): Promise {
+ try {
+ const response = await fetch(`${getApiURL()}/signin`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(credentials),
+ });
+
+ if (!response.ok) {
+ const errorData = await response.json().catch(() => ({}));
+ console.error("Authentication failed:", errorData);
+ throw new AuthenticationError(AuthErrorCodes.INVALID_CREDENTIALS);
+ }
+
+ const user = await response.json();
+ if (!user.accessToken) return null;
+
+ return {
+ id: user.id,
+ name: user.name,
+ email: user.email,
+ accessToken: user.accessToken,
+ tenantId: user.tenantId,
+ role: user.role,
+ };
+ } catch (error) {
+ if (error instanceof TypeError && error.message === "fetch failed") {
+ throw new AuthenticationError(AuthErrorCodes.CONNECTION_REFUSED);
+ }
+
+ if (error instanceof AuthenticationError) {
+ throw error;
+ }
+
+ throw new AuthenticationError(AuthErrorCodes.SERVICE_UNAVAILABLE);
+ }
+ },
+ }),
+ ],
+ [AuthType.NOAUTH]: [
+ Credentials({
+ name: "NoAuth",
+ credentials: {},
+ async authorize(): Promise {
+ return {
+ id: "keep-user-for-no-auth-purposes",
+ name: "Keep",
+ email: NoAuthUserEmail,
+ accessToken: "keep-token-for-no-auth-purposes",
+ tenantId: NoAuthTenant,
+ role: "user",
+ };
+ },
+ }),
+ ],
+ [AuthType.KEYCLOAK]: [
+ Keycloak({
+ clientId: process.env.KEYCLOAK_ID!,
+ clientSecret: process.env.KEYCLOAK_SECRET!,
+ issuer: process.env.KEYCLOAK_ISSUER,
+ 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: `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",
+ },
+ }),
+ ],
+};
+
+export const config = {
+ trustHost: true,
+ providers:
+ baseProviderConfigs[authType as keyof typeof baseProviderConfigs] ||
+ baseProviderConfigs[AuthType.NOAUTH],
+ pages: {
+ signIn: "/signin",
+ },
+ session: {
+ strategy: "jwt" as const,
+ maxAge: 30 * 24 * 60 * 60, // 30 days
+ },
+ callbacks: {
+ authorized({ auth, request: { nextUrl } }) {
+ const isLoggedIn = !!auth?.user;
+ const isOnDashboard = nextUrl.pathname.startsWith("/dashboard");
+ if (isOnDashboard) {
+ if (isLoggedIn) return true;
+ return false;
+ }
+ return true;
+ },
+ jwt: async ({ token, user, account, profile }): Promise => {
+ if (account && user) {
+ let accessToken: string | undefined;
+ let tenantId: string | undefined = user.tenantId;
+ let role: string | undefined = user.role;
+
+ if (authType === AuthType.AZUREAD) {
+ accessToken = account.access_token;
+ if (account.id_token) {
+ try {
+ const payload = JSON.parse(
+ Buffer.from(account.id_token.split(".")[1], "base64").toString()
+ );
+ 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;
+ }
+ if ((profile as any)?.keep_role) {
+ role = (profile as any).keep_role;
+ }
+ } else {
+ accessToken =
+ user.accessToken || account.access_token || account.id_token;
+ }
+ if (!accessToken) {
+ throw new Error("No access token available");
+ }
+
+ token.accessToken = accessToken;
+ token.tenantId = tenantId;
+ token.role = role;
+
+ if (authType === AuthType.KEYCLOAK) {
+ token.refreshToken = account.refresh_token;
+ token.accessTokenExpires =
+ Date.now() + (account.expires_in as number) * 1000;
+ }
+ } else if (
+ authType === AuthType.KEYCLOAK &&
+ token.accessTokenExpires &&
+ typeof token.accessTokenExpires === "number" &&
+ Date.now() < token.accessTokenExpires
+ ) {
+ token = await refreshAccessToken(token);
+ if (!token.accessToken) {
+ throw new Error("Failed to refresh access token");
+ }
+ }
+
+ return token;
+ },
+ session: async ({ session, token, user }) => {
+ return {
+ ...session,
+ accessToken: token.accessToken as string,
+ tenantId: token.tenantId as string,
+ userRole: token.role as string,
+ user: {
+ ...session.user,
+ accessToken: token.accessToken as string,
+ tenantId: token.tenantId as string,
+ role: token.role as string,
+ },
+ };
+ },
+ },
+} satisfies NextAuthConfig;
diff --git a/keep-ui/auth.ts b/keep-ui/auth.ts
index 159de7439..6c2cb25a6 100644
--- a/keep-ui/auth.ts
+++ b/keep-ui/auth.ts
@@ -1,264 +1,103 @@
import NextAuth from "next-auth";
-import type { NextAuthConfig } 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 { AuthError } from "next-auth";
-import { AuthenticationError, AuthErrorCodes } from "@/errors";
-import type { JWT } from "@auth/core/jwt";
+import { customFetch } from "next-auth";
+import { config, authType } from "@/auth.config";
+import { ProxyAgent, fetch as undici } from "undici";
+import MicrosoftEntraID from "next-auth/providers/microsoft-entra-id";
+import { AuthType } from "@/utils/authenticationType";
+
+const proxyUrl =
+ process.env.HTTP_PROXY ||
+ process.env.HTTPS_PROXY ||
+ process.env.http_proxy ||
+ process.env.https_proxy;
+
+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,
+ });
+ }
-export class BackendRefusedError extends AuthError {
- static type = "BackendRefusedError";
+ // @ts-expect-error `undici` has a `duplex` option
+ return undici(args[0], { ...(args[1] || {}), dispatcher });
}
-import { getApiURL } from "@/utils/apiUrl";
-import {
- AuthType,
- MULTI_TENANT,
- SINGLE_TENANT,
- NO_AUTH,
- NoAuthUserEmail,
- NoAuthTenant,
-} from "@/utils/authenticationType";
-import type { User } from "next-auth";
-
-// Determine auth type with backward compatibility
-const authTypeEnv = process.env.AUTH_TYPE;
-const authType =
- authTypeEnv === MULTI_TENANT
- ? AuthType.AUTH0
- : authTypeEnv === SINGLE_TENANT
- ? AuthType.DB
- : authTypeEnv === NO_AUTH
- ? AuthType.NOAUTH
- : (authTypeEnv as AuthType);
+// Modify the config if using Azure AD with proxy
+if (authType === AuthType.AZUREAD && proxyUrl) {
+ const provider = config.providers[0] as ReturnType;
-async function refreshAccessToken(token: any) {
- const issuerUrl = process.env.KEYCLOAK_ISSUER;
- const refreshTokenUrl = `${issuerUrl}/protocol/openid-connect/token`;
-
- try {
- const response = await fetch(refreshTokenUrl, {
- method: "POST",
- headers: {
- "Content-Type": "application/x-www-form-urlencoded",
- },
- body: new URLSearchParams({
- client_id: process.env.KEYCLOAK_ID!,
- client_secret: process.env.KEYCLOAK_SECRET!,
- grant_type: "refresh_token",
- refresh_token: token.refreshToken,
- }),
- });
+ if (!proxyUrl) {
+ console.log("Proxy is not enabled for Azure AD");
+ } else {
+ console.log("Proxy is enabled for Azure AD:", proxyUrl);
+ }
- const refreshedTokens = await response.json();
+ // 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 = provider.issuer?.match(tenantRe)?.[1] ?? "common";
+ const issuer = json.issuer.replace("{tenantid}", tenantId);
+ console.log("Modified issuer:", issuer);
+ return Response.json({ ...json, issuer });
+ }
- if (!response.ok) {
- throw new Error(
- `Refresh token failed: ${response.status} ${response.statusText}`
- );
+ // Fallback for all other requests
+ return proxyFetch(...args);
+ };
+
+ // Override profile since it uses fetch without customFetch
+ provider.profile = async (profile, tokens) => {
+ const profilePhotoSize = 48;
+ 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);
+ }
}
return {
- ...token,
- accessToken: refreshedTokens.access_token,
- accessTokenExpires: Date.now() + refreshedTokens.expires_in * 1000,
- refreshToken: refreshedTokens.refresh_token ?? token.refreshToken,
- };
- } catch (error) {
- console.error("Error refreshing access token:", error);
- return {
- ...token,
- error: "RefreshAccessTokenError",
+ id: profile.sub,
+ name: profile.name,
+ email: profile.email,
+ image: image ?? null,
+ accessToken: tokens.access_token ?? "",
};
- }
+ };
}
-// Define provider configurations
-const providerConfigs = {
- [AuthType.AUTH0]: [
- Auth0({
- clientId: process.env.AUTH0_CLIENT_ID!,
- clientSecret: process.env.AUTH0_CLIENT_SECRET!,
- issuer: process.env.AUTH0_ISSUER!,
- authorization: {
- params: {
- prompt: "login",
- },
- },
- }),
- ],
- [AuthType.DB]: [
- Credentials({
- name: "Credentials",
- credentials: {
- username: { label: "Username", type: "text", placeholder: "keep" },
- password: { label: "Password", type: "password", placeholder: "keep" },
- },
- async authorize(credentials): Promise {
- try {
- const response = await fetch(`${getApiURL()}/signin`, {
- method: "POST",
- headers: { "Content-Type": "application/json" },
- body: JSON.stringify(credentials),
- });
-
- if (!response.ok) {
- const errorData = await response.json().catch(() => ({}));
- console.error("Authentication failed:", errorData);
- throw new AuthenticationError(AuthErrorCodes.INVALID_CREDENTIALS);
- }
-
- const user = await response.json();
- if (!user.accessToken) return null;
-
- return {
- id: user.id,
- name: user.name,
- email: user.email,
- accessToken: user.accessToken,
- tenantId: user.tenantId,
- role: user.role,
- };
- } catch (error) {
- if (error instanceof TypeError && error.message === "fetch failed") {
- throw new AuthenticationError(AuthErrorCodes.CONNECTION_REFUSED);
- }
-
- if (error instanceof AuthenticationError) {
- throw error;
- }
-
- throw new AuthenticationError(AuthErrorCodes.SERVICE_UNAVAILABLE);
- }
- },
- }),
- ],
- [AuthType.NOAUTH]: [
- Credentials({
- name: "NoAuth",
- credentials: {},
- async authorize(): Promise {
- return {
- id: "keep-user-for-no-auth-purposes",
- name: "Keep",
- email: NoAuthUserEmail,
- accessToken: "keep-token-for-no-auth-purposes",
- tenantId: NoAuthTenant,
- role: "user",
- };
- },
- }),
- ],
- [AuthType.KEYCLOAK]: [
- Keycloak({
- clientId: process.env.KEYCLOAK_ID!,
- clientSecret: process.env.KEYCLOAK_SECRET!,
- issuer: process.env.KEYCLOAK_ISSUER,
- 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`,
- },
- },
- }),
- ],
-};
-
-// Create the config
-const config = {
- trustHost: true,
- providers:
- providerConfigs[authType as keyof typeof providerConfigs] ||
- providerConfigs[AuthType.NOAUTH],
- pages: {
- signIn: "/signin",
- },
- session: {
- strategy: "jwt" as const,
- maxAge: 30 * 24 * 60 * 60, // 30 days
- },
- callbacks: {
- authorized({ auth, request: { nextUrl } }) {
- const isLoggedIn = !!auth?.user;
- const isOnDashboard = nextUrl.pathname.startsWith("/dashboard");
- if (isOnDashboard) {
- if (isLoggedIn) return true;
- return false;
- }
- return true;
- },
- jwt: async ({ token, user, account, profile }): Promise => {
- if (account && user) {
- let accessToken: string | undefined;
- let tenantId: string | undefined = user.tenantId;
- let role: string | undefined = user.role;
- // Ensure we always have an accessToken
- if (authType == AuthType.AUTH0) {
- accessToken = account.id_token;
- if ((profile as any)?.keep_tenant_id) {
- tenantId = (profile as any).keep_tenant_id;
- }
- if ((profile as any)?.keep_role) {
- role = (profile as any).keep_role;
- }
- } else {
- accessToken =
- user.accessToken || account.access_token || account.id_token;
- }
- if (!accessToken) {
- throw new Error("No access token available");
- }
-
- token.accessToken = accessToken;
- token.tenantId = tenantId;
- token.role = role;
-
- if (authType === AuthType.KEYCLOAK) {
- token.refreshToken = account.refresh_token;
- token.accessTokenExpires =
- Date.now() + (account.expires_in as number) * 1000;
- }
- } else if (
- authType === AuthType.KEYCLOAK &&
- token.accessTokenExpires &&
- typeof token.accessTokenExpires === "number" &&
- Date.now() < token.accessTokenExpires
- ) {
- token = await refreshAccessToken(token);
- if (!token.accessToken) {
- throw new Error("Failed to refresh access token");
- }
- }
-
- return token;
- },
- session: async ({ session, token, user }) => {
- return {
- ...session,
- accessToken: token.accessToken as string,
- tenantId: token.tenantId as string,
- userRole: token.role as string,
- user: {
- ...session.user,
- accessToken: token.accessToken as string,
- tenantId: token.tenantId as string,
- role: token.role as string,
- },
- };
- },
- },
-} satisfies NextAuthConfig;
-
console.log("Starting Keep frontend with auth type:", authType);
export const { handlers, auth, signIn, signOut } = NextAuth(config);
diff --git a/keep-ui/middleware.tsx b/keep-ui/middleware.tsx
index 052c05792..ccdaa8355 100644
--- a/keep-ui/middleware.tsx
+++ b/keep-ui/middleware.tsx
@@ -1,15 +1,21 @@
-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";
+import { config as authConfig } from "@/auth.config";
+import NextAuth from "next-auth";
-// Use auth as a wrapper for middleware logic
-export default auth(async (req) => {
- const { pathname, searchParams } = req.nextUrl;
+const { auth } = NextAuth(authConfig);
- // Keep it on header so it can be used in server components
- const requestHeaders = new Headers(req.headers);
- requestHeaders.set("x-url", req.url);
+export async function middleware(request: NextRequest) {
+ const { pathname, searchParams } = request.nextUrl;
+ const session = await auth();
+ const role = session?.userRole;
+ const isAuthenticated = !!session;
+ // Keep it on header so it can be used in server components
+ const requestHeaders = new Headers(request.headers);
+ requestHeaders.set("x-url", request.url);
// Handle legacy /backend/ redirects
if (pathname.startsWith("/backend/")) {
const apiUrl = getApiURL();
@@ -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 (!isAuthenticated && !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 incidents
+ if (isAuthenticated && 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 (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/proxy/squid.conf b/proxy/squid.conf
index e36a34687..2e569360f 100644
--- a/proxy/squid.conf
+++ b/proxy/squid.conf
@@ -1,26 +1,31 @@
# Port configurations
http_port 3128
-dns_nameservers 8.8.8.8 8.8.4.4 # Google DNS servers, adjust as needed
# DNS configurations
+dns_nameservers 8.8.8.8 8.8.4.4
dns_v4_first on
-dns_timeout 5 seconds
-positive_dns_ttl 24 hours
-negative_dns_ttl 1 minutes
-# Allow all clients in our Docker network
-acl localnet src 172.16.0.0/12
+# ACL definitions
+acl SSL_ports port 443
+acl Safe_ports port 80 # http
+acl Safe_ports port 443 # https
+acl Safe_ports port 1025-65535 # unprivileged ports
+acl CONNECT method CONNECT
+acl localnet src 172.16.0.0/12 # Docker network
+
+# Access rules - order is important
+http_access deny !Safe_ports
+http_access deny CONNECT !SSL_ports
+http_access allow localnet
http_access allow all
-# Basic settings
+# Logging
+debug_options ALL,1 28,3
+
+# Cache settings
cache_dir ufs /var/spool/squid 100 16 256
coredump_dir /var/spool/squid
-# DNS cache settings
-ipcache_size 1024
-ipcache_low 90
-ipcache_high 95
-
# Refresh patterns
refresh_pattern ^ftp: 1440 20% 10080
refresh_pattern ^gopher: 1440 0% 1440
diff --git a/pyproject.toml b/pyproject.toml
index 85a037ae5..83eeea92c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "keep"
-version = "0.29.3"
+version = "0.29.5"
description = "Alerting. for developers, by developers."
authors = ["Keep Alerting LTD"]
packages = [{include = "keep"}]
diff --git a/scripts/docs_get_providers_list.py b/scripts/docs_get_providers_list.py
index e2459c12c..caee00c79 100644
--- a/scripts/docs_get_providers_list.py
+++ b/scripts/docs_get_providers_list.py
@@ -14,9 +14,8 @@
LOGO_DEV_PUBLISHABLE_KEY = "pk_dfXfZBoKQMGDTIgqu7LvYg"
-NON_DOCUMENTED_PROVIDERS = (
- []
-) # known not documented providers https://github.com/keephq/keep/issues/2033
+NON_DOCUMENTED_PROVIDERS = [
+]
def validate_overview_is_complete(documented_providers):
@@ -49,6 +48,7 @@ def validate_all_providers_are_documented(documented_providers):
if (
provider_name not in documented_providers
and provider_name not in NON_DOCUMENTED_PROVIDERS
+ and not provider.coming_soon
):
raise Exception(
f"""Provider "{provider_name}" is not documented in the docs/providers/documentation folder,