Skip to content

Commit 60c4fa5

Browse files
committed
feat: new readme
1 parent 344e49a commit 60c4fa5

File tree

7 files changed

+181
-101
lines changed

7 files changed

+181
-101
lines changed

docs/deployment/authentication/azuread-auth.mdx

+4-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ This feature is a part of Keep Enterprise.
88
Talk to us to get access: https://www.keephq.dev/meet-keep
99
</Tip>
1010

11-
Keep supports enterprise authentication through Azure Active Directory (Azure AD), enabling organizations to use their existing Microsoft identity platform for secure access management.
11+
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.
1212

1313
## When to Use
1414

@@ -49,9 +49,9 @@ We use "Web" platform instead of "Single Page Application (SPA)" because Keep's
4949
</Info>
5050

5151
<Tip>
52-
For localhost, the redirect would be http://localhost:3000/api/auth/callback/azure-ad
52+
For localhost, the redirect would be http://localhost:3000/api/auth/callback/microsoft-entra-id
5353

54-
For production, it should be something like http://your_keep_frontend_domain/api/auth/callback/azure-ad
54+
For production, it should be something like http://your_keep_frontend_domain/api/auth/callback/microsoft-entra-id
5555

5656
</Tip>
5757

@@ -177,7 +177,7 @@ How to get:
177177
| AUTH_TYPE | Set to 'AZUREAD' for Azure AD authentication | Yes | - |
178178
| KEEP_AZUREAD_CLIENT_ID | Your Azure AD application (client) ID | Yes | - |
179179
| KEEP_AZUREAD_CLIENT_SECRET | Your client secret | Yes | - |
180-
| KEEP_AZUREAD_TENANT_ID | Your Azure AD tenant ID | Yes | - |
180+
| KEEP_AZUREAD_ISSUER | Your Azure AD tenant ID | Yes | - |
181181
| NEXTAUTH_URL | Your Keep application URL | Yes | - |
182182
| NEXTAUTH_SECRET | Random string for NextAuth.js | Yes | - |
183183

docs/images/azuread_3.png

-81.1 KB
Loading

keep-ui/app/(keep)/alerts/alert-table.tsx

+77-56
Original file line numberDiff line numberDiff line change
@@ -265,15 +265,20 @@ export function AlertTable({
265265
};
266266

267267
return (
268-
<div className="flex flex-col gap-4 h-full">
269-
<TitleAndFilters
270-
table={table}
271-
alerts={alerts}
272-
presetName={presetName}
273-
onThemeChange={handleThemeChange}
274-
/>
275-
<div className="min-h-10">
276-
{/* Setting min-h-10 to avoid jumping when actions are shown */}
268+
// Add h-screen to make it full height and remove the default flex-col gap
269+
<div className="h-screen flex flex-col">
270+
{/* Add padding to account for any top nav/header */}
271+
<div className="pt-4 px-4 flex-none">
272+
<TitleAndFilters
273+
table={table}
274+
alerts={alerts}
275+
presetName={presetName}
276+
onThemeChange={handleThemeChange}
277+
/>
278+
</div>
279+
280+
{/* Make actions/presets section fixed height */}
281+
<div className="h-12 px-4 flex-none">
277282
{selectedRowIds.length ? (
278283
<AlertActions
279284
selectedRowIds={selectedRowIds}
@@ -292,61 +297,77 @@ export function AlertTable({
292297
/>
293298
)}
294299
</div>
295-
<div className="flex gap-6">
296-
<div className="w-32 min-w-[12rem]">
297-
<AlertFacets
298-
className="sticky top-0"
299-
alerts={alerts}
300-
facetFilters={facetFilters}
301-
setFacetFilters={setFacetFilters}
302-
dynamicFacets={dynamicFacets}
303-
setDynamicFacets={setDynamicFacets}
304-
onDelete={handleFacetDelete}
305-
table={table}
306-
showSkeleton={showSkeleton}
307-
/>
308-
</div>
309-
<div className="flex flex-col gap-4 min-w-0">
310-
<Card className="flex-grow h-full flex flex-col p-0">
311-
<div className="flex-grow">
312-
{/* For dynamic preset, add alert tabs*/}
313-
{!presetStatic && (
314-
<AlertTabs
315-
presetId={presetId}
316-
tabs={tabs}
317-
setTabs={setTabs}
318-
selectedTab={selectedTab}
319-
setSelectedTab={setSelectedTab}
320-
/>
321-
)}
322-
<div ref={a11yContainerRef} className="sr-only" />
323-
<Table className="[&>table]:table-fixed [&>table]:w-full">
324-
<AlertsTableHeaders
325-
columns={columns}
326-
table={table}
327-
presetName={presetName}
328-
a11yContainerRef={a11yContainerRef}
329-
/>
330-
<AlertsTableBody
331-
table={table}
332-
showSkeleton={showSkeleton}
333-
showEmptyState={showEmptyState}
334-
theme={theme}
335-
onRowClick={handleRowClick}
336-
presetName={presetName}
337-
/>
338-
</Table>
339-
</div>
340-
</Card>
300+
301+
{/* Main content area - uses flex-grow to fill remaining space */}
302+
<div className="flex-grow overflow-hidden px-4 pb-4">
303+
<div className="h-full flex gap-6">
304+
{/* Facets sidebar */}
305+
<div className="w-32 min-w-[12rem] overflow-y-auto">
306+
<AlertFacets
307+
className="sticky top-0"
308+
alerts={alerts}
309+
facetFilters={facetFilters}
310+
setFacetFilters={setFacetFilters}
311+
dynamicFacets={dynamicFacets}
312+
setDynamicFacets={setDynamicFacets}
313+
onDelete={handleFacetDelete}
314+
table={table}
315+
showSkeleton={showSkeleton}
316+
/>
317+
</div>
318+
319+
{/* Table section */}
320+
<div className="flex-1 flex flex-col min-w-0">
321+
<Card className="h-full flex flex-col p-0">
322+
<div className="flex-grow flex flex-col overflow-hidden">
323+
{!presetStatic && (
324+
<div className="flex-none">
325+
<AlertTabs
326+
presetId={presetId}
327+
tabs={tabs}
328+
setTabs={setTabs}
329+
selectedTab={selectedTab}
330+
setSelectedTab={setSelectedTab}
331+
/>
332+
</div>
333+
)}
334+
335+
<div ref={a11yContainerRef} className="sr-only" />
336+
337+
{/* Make table wrapper scrollable */}
338+
<div className="flex-grow overflow-auto">
339+
<Table className="[&>table]:table-fixed [&>table]:w-full">
340+
<AlertsTableHeaders
341+
columns={columns}
342+
table={table}
343+
presetName={presetName}
344+
a11yContainerRef={a11yContainerRef}
345+
/>
346+
<AlertsTableBody
347+
table={table}
348+
showSkeleton={showSkeleton}
349+
showEmptyState={showEmptyState}
350+
theme={theme}
351+
onRowClick={handleRowClick}
352+
presetName={presetName}
353+
/>
354+
</Table>
355+
</div>
356+
</div>
357+
</Card>
358+
</div>
341359
</div>
342360
</div>
343-
<div className="mt-2 mb-8 pl-[14rem]">
361+
362+
{/* Pagination footer - fixed height */}
363+
<div className="h-16 px-4 flex-none pl-[14rem]">
344364
<AlertPagination
345365
table={table}
346366
presetName={presetName}
347367
isRefreshAllowed={isRefreshAllowed}
348368
/>
349369
</div>
370+
350371
<AlertSidebar
351372
isOpen={isSidebarOpen}
352373
toggle={() => setIsSidebarOpen(false)}

keep-ui/app/(keep)/alerts/alerts-table-body.tsx

+9-7
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,15 @@ export function AlertsTableBody({
3535
if (showEmptyState) {
3636
return (
3737
<>
38-
<div className="flex flex-col justify-center items-center h-96 w-full absolute p-4">
39-
<EmptyStateCard
40-
title="No alerts to display"
41-
description="It is because you have not connected any data source yet or there are no alerts matching the filter."
42-
buttonText="Add Alert"
43-
onClick={handleModalOpen}
44-
/>
38+
<div className="flex items-center h-full w-full absolute -mt-20">
39+
<div className="flex flex-col justify-center items-center w-full p-4">
40+
<EmptyStateCard
41+
title="No alerts to display"
42+
description="It is because you have not connected any data source yet or there are no alerts matching the filter."
43+
buttonText="Add Alert"
44+
onClick={handleModalOpen}
45+
/>
46+
</div>
4547
</div>
4648
{modalOpen && (
4749
<PushAlertToServerModal

keep-ui/app/(signin)/signin/SignInForm.tsx

+3-7
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export interface Providers {
2121
auth0?: Provider;
2222
credentials?: Provider;
2323
keycloak?: Provider;
24-
"azure-ad"?: Provider;
24+
"microsoft-entra-id"?: Provider;
2525
}
2626

2727
interface SignInFormInputs {
@@ -42,15 +42,11 @@ export default function SignInForm({ params }: { params?: { amt: string } }) {
4242
} = useForm<SignInFormInputs>();
4343

4444
useEffect(() => {
45-
console.log("Fetching providers");
4645
async function fetchProviders() {
47-
console.log("Fetching providers 2");
4846
const response = await getProviders();
4947
setProviders(response as Providers);
5048
}
51-
console.log("Fetching providers 3");
5249
fetchProviders();
53-
console.log("Fetching providers 4");
5450
}, []);
5551

5652
useEffect(() => {
@@ -69,9 +65,9 @@ export default function SignInForm({ params }: { params?: { amt: string } }) {
6965
} else if (providers.keycloak) {
7066
console.log("Signing in with keycloak provider");
7167
signIn("keycloak", { callbackUrl: "/" });
72-
} else if (providers["azure-ad"]) {
68+
} else if (providers["microsoft-entra-id"]) {
7369
console.log("Signing in with Azure AD provider");
74-
signIn("azure-ad", { callbackUrl: "/" });
70+
signIn("microsoft-entra-id", { callbackUrl: "/" });
7571
} else if (
7672
providers.credentials &&
7773
providers.credentials.name == "NoAuth"

keep-ui/auth.ts

+68-27
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@ import MicrosoftEntraID from "@auth/core/providers/microsoft-entra-id";
88
import { AuthError } from "next-auth";
99
import { AuthenticationError, AuthErrorCodes } from "@/errors";
1010
import type { JWT } from "@auth/core/jwt";
11-
12-
// see https://authjs.dev/guides/corporate-proxy
13-
import { ProxyAgent, fetch as undici } from "undici";
11+
// https://github.com/nextauthjs/next-auth/issues/11028
12+
import { initProxyFetch } from "./proxyFetch";
1413

1514
export class BackendRefusedError extends AuthError {
1615
static type = "BackendRefusedError";
@@ -38,22 +37,61 @@ const authType =
3837
? AuthType.NOAUTH
3938
: (authTypeEnv as AuthType);
4039

41-
// Proxy configuration
4240
const proxyUrl =
4341
process.env.HTTP_PROXY ||
4442
process.env.HTTPS_PROXY ||
4543
process.env.http_proxy ||
4644
process.env.https_proxy;
47-
const dispatcher = proxyUrl ? new ProxyAgent(proxyUrl) : undefined;
4845

49-
function proxyFetch(
50-
...args: Parameters<typeof fetch>
51-
): ReturnType<typeof fetch> {
52-
if (!dispatcher) return fetch(...args);
53-
// @ts-expect-error `undici` has a `duplex` option
54-
return undici(args[0], { ...args[1], dispatcher });
46+
// Helper function to dynamically import proxyFetch only when needed
47+
async function getProxyFetch() {
48+
if (typeof window === "undefined" && !process.env.NEXT_RUNTIME) {
49+
// Only import in Node.js environment, not Edge
50+
try {
51+
const { initProxyFetch } = await import("./proxyFetch");
52+
return initProxyFetch();
53+
} catch (e) {
54+
console.warn("Failed to load proxy fetch:", e);
55+
return null;
56+
}
57+
}
58+
return null;
5559
}
5660

61+
// Create Azure AD provider configuration
62+
const createAzureADProvider = () => {
63+
const baseConfig = {
64+
clientId: process.env.KEEP_AZUREAD_CLIENT_ID!,
65+
clientSecret: process.env.KEEP_AZUREAD_CLIENT_SECRET!,
66+
// The issuer is the URL of the identity provider
67+
issuer: `https://login.microsoftonline.com/${process.env
68+
.KEEP_AZUREAD_TENANT_ID!}/v2.0`,
69+
authorization: {
70+
params: {
71+
scope: `api://${process.env
72+
.KEEP_AZUREAD_CLIENT_ID!}/default openid profile email`,
73+
},
74+
},
75+
client: {
76+
token_endpoint_auth_method: "client_secret_post",
77+
},
78+
};
79+
80+
if (!proxyUrl) {
81+
console.log("Using built-in fetch for Azure AD provider");
82+
return MicrosoftEntraID(baseConfig);
83+
}
84+
85+
console.log("Using proxy fetch for Azure AD provider");
86+
return MicrosoftEntraID({
87+
...baseConfig,
88+
async [customFetch](...args) {
89+
const proxyFetch = await getProxyFetch();
90+
return proxyFetch ? proxyFetch(...args) : fetch(...args);
91+
},
92+
});
93+
};
94+
5795
async function refreshAccessToken(token: any) {
5896
const issuerUrl = process.env.KEYCLOAK_ISSUER;
5997
const refreshTokenUrl = `${issuerUrl}/protocol/openid-connect/token`;
@@ -179,21 +217,7 @@ const providerConfigs = {
179217
authorization: { params: { scope: "openid email profile roles" } },
180218
}),
181219
],
182-
[AuthType.AZUREAD]: [
183-
MicrosoftEntraID({
184-
clientId: process.env.KEEP_AZUREAD_CLIENT_ID!,
185-
clientSecret: process.env.KEEP_AZUREAD_CLIENT_SECRET!,
186-
issuer: process.env.KEEP_AZUREAD_TENANT_ID!,
187-
authorization: {
188-
params: {
189-
scope: `api://${process.env
190-
.KEEP_AZUREAD_CLIENT_ID!}/default openid profile email`,
191-
},
192-
},
193-
// see https://authjs.dev/guides/corporate-proxy
194-
...(proxyFetch && { [customFetch]: proxyFetch }),
195-
}),
196-
],
220+
[AuthType.AZUREAD]: [createAzureADProvider()],
197221
};
198222

199223
// Create the config
@@ -225,7 +249,24 @@ const config = {
225249
let tenantId: string | undefined = user.tenantId;
226250
let role: string | undefined = user.role;
227251
// Ensure we always have an accessToken
228-
if (authType == AuthType.AUTH0) {
252+
if (authType === AuthType.AZUREAD) {
253+
// Properly handle Azure AD tokens
254+
accessToken = account.access_token;
255+
// You might want to extract additional claims from the id_token if needed
256+
if (account.id_token) {
257+
try {
258+
// Basic JWT decode (you might want to use a proper JWT library here)
259+
const payload = JSON.parse(
260+
Buffer.from(account.id_token.split(".")[1], "base64").toString()
261+
);
262+
// Extract any additional claims you need
263+
role = payload.roles?.[0] || "user";
264+
tenantId = payload.tid || undefined;
265+
} catch (e) {
266+
console.warn("Failed to decode id_token:", e);
267+
}
268+
}
269+
} else if (authType == AuthType.AUTH0) {
229270
accessToken = account.id_token;
230271
if ((profile as any)?.keep_tenant_id) {
231272
tenantId = (profile as any).keep_tenant_id;

keep-ui/proxyFetch.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// proxyFetch.ts
2+
let proxyFetch: typeof fetch | undefined;
3+
4+
export async function initProxyFetch() {
5+
const proxyUrl =
6+
process.env.HTTP_PROXY ||
7+
process.env.HTTPS_PROXY ||
8+
process.env.http_proxy ||
9+
process.env.https_proxy;
10+
11+
if (proxyUrl && typeof window === "undefined") {
12+
const { ProxyAgent, fetch: undici } = await import("undici");
13+
const dispatcher = new ProxyAgent(proxyUrl);
14+
return (...args: Parameters<typeof fetch>): ReturnType<typeof fetch> => {
15+
// @ts-expect-error `undici` has a `duplex` option
16+
return undici(args[0], { ...args[1], dispatcher });
17+
};
18+
}
19+
return undefined;
20+
}

0 commit comments

Comments
 (0)