diff --git a/README.md b/README.md
index f3074d4..83fd83c 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,7 @@ This app is intended to run alongside the Seamless Auth API as part of a self-ho
- filter and investigate authentication events
- review suspicious activity and anomaly signals
- edit system configuration
+- configure allowed login methods and OAuth providers
- operate with runtime config injection in containerized environments
## Tech Stack
@@ -68,6 +69,55 @@ That runtime-config flow is intentional. The dashboard is designed to be reconfi
### System Configuration
- manage available roles and auth settings
+- enable or disable login methods such as passkeys, magic links, OTP, and OAuth
+- configure OAuth providers without entering provider client secrets in the browser
+
+#### OAuth Provider Configuration
+
+The dashboard edits the Seamless Auth API `oauth_providers` system config. OAuth is enabled by
+turning on the `OAuth` login method and adding one or more provider records.
+
+Each provider record includes:
+
+- provider id, such as `google` or `github`
+- display name
+- client id
+- `clientSecretEnv`, the name of the server environment variable holding the client secret
+- authorization URL
+- token URL
+- userinfo URL
+- requested scopes
+- JSON paths used to read provider subject, email, and name from the userinfo response
+- optional redirect URI
+- signup policy
+
+The dashboard intentionally does **not** collect provider client-secret values. Store those secrets
+on the Seamless Auth API host and reference them by environment variable name, for example
+`GOOGLE_CLIENT_SECRET`.
+
+Example provider configuration:
+
+```json
+{
+ "id": "google",
+ "name": "Google",
+ "enabled": true,
+ "clientId": "google-oauth-client-id",
+ "clientSecretEnv": "GOOGLE_CLIENT_SECRET",
+ "authorizationUrl": "https://accounts.google.com/o/oauth2/v2/auth",
+ "tokenUrl": "https://oauth2.googleapis.com/token",
+ "userInfoUrl": "https://openidconnect.googleapis.com/v1/userinfo",
+ "scopes": ["openid", "email", "profile"],
+ "redirectUri": "https://app.example.com/oauth/callback",
+ "subjectJsonPath": "sub",
+ "emailJsonPath": "email",
+ "nameJsonPath": "name",
+ "allowSignup": true
+}
+```
+
+After saving config, clients can discover enabled providers with `GET /oauth/providers` and start
+login with `POST /oauth/:providerId/start`.
### Appearance
diff --git a/src/components/PieChart.tsx b/src/components/PieChart.tsx
index d17975a..47f1f5b 100644
--- a/src/components/PieChart.tsx
+++ b/src/components/PieChart.tsx
@@ -19,6 +19,7 @@ import { buildEventQuery } from "../lib/eventNavigation";
type PieChartDatum = {
type: string;
+ label?: string;
count: number;
};
@@ -29,11 +30,15 @@ function generateColor(index: number) {
"var(--primary)",
"var(--accent)",
"var(--highlight)",
- "#8C6A5D",
- "#5A7D7C",
- "#D4A373",
- "#7F5539",
- "#6B9080",
+ "color-mix(in srgb, var(--primary) 70%, var(--accent))",
+ "color-mix(in srgb, var(--accent) 70%, var(--highlight))",
+ "color-mix(in srgb, var(--highlight) 70%, var(--primary))",
+ "color-mix(in srgb, var(--primary) 55%, var(--surface-alt))",
+ "color-mix(in srgb, var(--accent) 55%, var(--surface-alt))",
+ "color-mix(in srgb, var(--highlight) 55%, var(--surface-alt))",
+ "color-mix(in srgb, var(--primary) 65%, var(--text-muted))",
+ "color-mix(in srgb, var(--accent) 65%, var(--text-muted))",
+ "color-mix(in srgb, var(--highlight) 65%, var(--text-muted))",
];
return palette[index % palette.length];
@@ -43,15 +48,29 @@ function generateColor(index: number) {
export default function PieChart({ data }: { data: PieChartDatum[] }) {
const navigate = useNavigate();
+ const chartData = data
+ .filter((item) => item.count > 0)
+ .map((item) => ({
+ ...item,
+ label: item.label ?? item.type,
+ }));
+
+ if (chartData.length === 0) {
+ return (
+
+ No event data yet
+
+ );
+ }
return (
-
+
- {data.map((_, i) => (
+ {chartData.map((_, i) => (
|
diff --git a/src/hooks/useGroupedEvents.ts b/src/hooks/useGroupedEvents.ts
index bd062cf..f7e2ffa 100644
--- a/src/hooks/useGroupedEvents.ts
+++ b/src/hooks/useGroupedEvents.ts
@@ -6,16 +6,34 @@
import { useQuery } from "@tanstack/react-query";
import { apiFetch } from "../lib/api";
+import { categorizeEventSummary } from "../lib/eventCategories";
-export interface groupedEvents {
+type EventSummaryResponse = {
summary: {
type: string;
count: number;
}[];
+};
+
+export interface GroupedEvents {
+ summary: {
+ type: string;
+ label: string;
+ count: number;
+ }[];
}
+
export function useGroupedEvents() {
return useQuery({
queryKey: ["grouped-events"],
- queryFn: () => apiFetch("/internal/auth-events/grouped"),
+ queryFn: async (): Promise => {
+ const data = await apiFetch(
+ "/internal/auth-events/summary",
+ );
+
+ return {
+ summary: categorizeEventSummary(data.summary),
+ };
+ },
});
}
diff --git a/src/hooks/useSystemConfig.ts b/src/hooks/useSystemConfig.ts
index f51766f..a005de6 100644
--- a/src/hooks/useSystemConfig.ts
+++ b/src/hooks/useSystemConfig.ts
@@ -8,7 +8,29 @@
import { useQuery } from "@tanstack/react-query";
import { apiFetch } from "../lib/api";
-export type LoginMethod = "passkey" | "magic_link" | "email_otp" | "phone_otp";
+export type LoginMethod =
+ | "passkey"
+ | "magic_link"
+ | "email_otp"
+ | "phone_otp"
+ | "oauth";
+
+export type OAuthProviderConfig = {
+ id: string;
+ name: string;
+ enabled: boolean;
+ clientId: string;
+ clientSecretEnv: string;
+ authorizationUrl: string;
+ tokenUrl: string;
+ userInfoUrl: string;
+ scopes: string[];
+ redirectUri?: string;
+ subjectJsonPath: string;
+ emailJsonPath: string;
+ nameJsonPath?: string;
+ allowSignup: boolean;
+};
export type SystemConfig = {
app_name: string;
@@ -20,6 +42,7 @@ export type SystemConfig = {
delay_after: number;
login_methods: LoginMethod[];
passkey_login_fallback_enabled: boolean;
+ oauth_providers: OAuthProviderConfig[];
rpid: string;
origins: string[];
};
diff --git a/src/hooks/useUpdateSystemConfig.ts b/src/hooks/useUpdateSystemConfig.ts
index 255babc..32000d8 100644
--- a/src/hooks/useUpdateSystemConfig.ts
+++ b/src/hooks/useUpdateSystemConfig.ts
@@ -6,7 +6,7 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiFetch } from "../lib/api";
-import type { LoginMethod } from "./useSystemConfig";
+import type { LoginMethod, OAuthProviderConfig } from "./useSystemConfig";
export type SystemConfig = {
app_name: string;
@@ -18,6 +18,7 @@ export type SystemConfig = {
delay_after: number;
login_methods: LoginMethod[];
passkey_login_fallback_enabled: boolean;
+ oauth_providers: OAuthProviderConfig[];
rpid: string;
origins: string[];
};
diff --git a/src/lib/eventCategories.test.ts b/src/lib/eventCategories.test.ts
new file mode 100644
index 0000000..e0fb38b
--- /dev/null
+++ b/src/lib/eventCategories.test.ts
@@ -0,0 +1,39 @@
+/*
+ * Copyright © 2026 Fells Code, LLC
+ * Licensed under the GNU Affero General Public License v3.0
+ * See LICENSE file in the project root for full license information
+ */
+
+import { describe, expect, it } from "vitest";
+import { categorizeEventSummary, getEventCategory } from "./eventCategories";
+
+describe("eventCategories", () => {
+ it("classifies concrete API event types into operator categories", () => {
+ expect(getEventCategory("oauth_login_success").value).toBe("oauth");
+ expect(getEventCategory("webauthn_login_success").value).toBe("webauthn");
+ expect(getEventCategory("refresh_token_failed").value).toBe("token");
+ expect(getEventCategory("service_token_success").value).toBe(
+ "serviceToken",
+ );
+ expect(getEventCategory("step_up_challenge").value).toBe("stepUp");
+ expect(getEventCategory("login_suspicious").value).toBe("security");
+ });
+
+ it("groups raw event summaries and only keeps populated categories", () => {
+ expect(
+ categorizeEventSummary([
+ { type: "login_success", count: 8 },
+ { type: "refresh_token_failed", count: 5 },
+ { type: "service_token_success", count: 3 },
+ { type: "oauth_login_failed", count: 4 },
+ { type: "unknown_backend_event", count: 2 },
+ ]),
+ ).toEqual([
+ { type: "login", label: "Login", count: 8 },
+ { type: "token", label: "Session Tokens", count: 5 },
+ { type: "oauth", label: "OAuth", count: 4 },
+ { type: "serviceToken", label: "Service Tokens", count: 3 },
+ { type: "other", label: "Other", count: 2 },
+ ]);
+ });
+});
diff --git a/src/lib/eventCategories.ts b/src/lib/eventCategories.ts
new file mode 100644
index 0000000..81656c5
--- /dev/null
+++ b/src/lib/eventCategories.ts
@@ -0,0 +1,170 @@
+/*
+ * Copyright © 2026 Fells Code, LLC
+ * Licensed under the GNU Affero General Public License v3.0
+ * See LICENSE file in the project root for full license information
+ */
+
+export type EventCategory = {
+ label: string;
+ value: string;
+ match: (type: string) => boolean;
+};
+
+export type EventCount = {
+ type: string;
+ count: number;
+};
+
+export type EventCategoryCount = {
+ type: string;
+ label: string;
+ count: number;
+};
+
+const exact = (types: string[]) => {
+ const values = new Set(types);
+
+ return (type: string) => values.has(type);
+};
+
+const startsWithAny = (prefixes: string[]) => (type: string) =>
+ prefixes.some((prefix) => type.startsWith(prefix));
+
+const isSuspicious = (type: string) => type.includes("suspicious");
+
+export const eventCategories: EventCategory[] = [
+ {
+ label: "Security",
+ value: "security",
+ match: (type) => isSuspicious(type) || type === "request_suspicious",
+ },
+ {
+ label: "Login",
+ value: "login",
+ match: exact(["login_challenge", "login_failed", "login_success"]),
+ },
+ {
+ label: "OAuth",
+ value: "oauth",
+ match: startsWithAny(["oauth_"]),
+ },
+ {
+ label: "Passkeys",
+ value: "webauthn",
+ match: startsWithAny(["webauthn_"]),
+ },
+ {
+ label: "Magic Links",
+ value: "magicLink",
+ match: startsWithAny(["magic_link_"]),
+ },
+ {
+ label: "OTP",
+ value: "otp",
+ match: startsWithAny(["otp_", "recovery_otp_", "verify_otp_"]),
+ },
+ {
+ label: "TOTP / MFA",
+ value: "totp",
+ match: startsWithAny(["mfa_otp_", "totp_"]),
+ },
+ {
+ label: "Step-Up",
+ value: "stepUp",
+ match: startsWithAny(["step_up_"]),
+ },
+ {
+ label: "Registration",
+ value: "registration",
+ match: startsWithAny(["registration_"]),
+ },
+ {
+ label: "Session Tokens",
+ value: "token",
+ match: startsWithAny(["bearer_token_", "cookie_token_", "refresh_token_"]),
+ },
+ {
+ label: "Service Tokens",
+ value: "serviceToken",
+ match: startsWithAny(["service_token_"]),
+ },
+ {
+ label: "Logout",
+ value: "logout",
+ match: startsWithAny(["logout_"]),
+ },
+ {
+ label: "User Admin",
+ value: "user",
+ match: exact([
+ "credentials_deleted",
+ "internal_user_updated_by_owner",
+ "user_created",
+ "user_data_failed",
+ "user_data_success",
+ "user_deleted",
+ ]),
+ },
+ {
+ label: "System Config",
+ value: "system",
+ match: startsWithAny(["system_config_"]),
+ },
+ {
+ label: "Bootstrap",
+ value: "bootstrap",
+ match: startsWithAny(["bootstrap_admin_"]),
+ },
+ {
+ label: "JWKS",
+ value: "jwks",
+ match: startsWithAny(["jwks_"]),
+ },
+ {
+ label: "Notifications",
+ value: "notification",
+ match: exact(["notification_sent", "notication_sent"]),
+ },
+ {
+ label: "Operations",
+ value: "operation",
+ match: exact(["auth_action_incremented", "informational"]),
+ },
+];
+
+export const otherEventCategory: EventCategory = {
+ label: "Other",
+ value: "other",
+ match: () => false,
+};
+
+export function getEventCategory(type: string) {
+ return (
+ eventCategories.find((category) => category.match(type)) ??
+ otherEventCategory
+ );
+}
+
+export function categorizeEventSummary(
+ summary: EventCount[],
+): EventCategoryCount[] {
+ const counts = new Map();
+
+ for (const category of [...eventCategories, otherEventCategory]) {
+ counts.set(category.value, 0);
+ }
+
+ for (const item of summary) {
+ const category = getEventCategory(item.type);
+ counts.set(category.value, (counts.get(category.value) ?? 0) + item.count);
+ }
+
+ return [...eventCategories, otherEventCategory]
+ .map((category) => ({
+ type: category.value,
+ label: category.label,
+ count: counts.get(category.value) ?? 0,
+ }))
+ .filter((item) => item.count > 0)
+ .sort((a, b) => b.count - a.count);
+}
diff --git a/src/lib/eventGroups.test.ts b/src/lib/eventGroups.test.ts
index 6c1027f..e3609e6 100644
--- a/src/lib/eventGroups.test.ts
+++ b/src/lib/eventGroups.test.ts
@@ -11,11 +11,24 @@ describe("eventGroups", () => {
it("includes the expected top-level quick filters", () => {
expect(eventGroups.map((group) => group.value)).toEqual([
"",
+ "security",
"login",
+ "oauth",
"webauthn",
+ "magicLink",
"otp",
+ "totp",
+ "stepUp",
+ "registration",
"token",
- "security",
+ "serviceToken",
+ "logout",
+ "user",
+ "system",
+ "bootstrap",
+ "jwks",
+ "notification",
+ "operation",
]);
});
@@ -24,6 +37,7 @@ describe("eventGroups", () => {
expect(loginGroup?.match("login_success")).toBe(true);
expect(loginGroup?.match("login_suspicious")).toBe(false);
+ expect(loginGroup?.match("oauth_login_success")).toBe(false);
});
it("matches security-only suspicious activity", () => {
@@ -34,4 +48,16 @@ describe("eventGroups", () => {
expect(securityGroup?.match("request_suspicious")).toBe(true);
expect(securityGroup?.match("login_success")).toBe(false);
});
+
+ it("matches infrastructure-specific event families", () => {
+ const tokenGroup = eventGroups.find((group) => group.value === "token");
+ const serviceTokenGroup = eventGroups.find(
+ (group) => group.value === "serviceToken",
+ );
+ const stepUpGroup = eventGroups.find((group) => group.value === "stepUp");
+
+ expect(tokenGroup?.match("refresh_token_failed")).toBe(true);
+ expect(serviceTokenGroup?.match("service_token_success")).toBe(true);
+ expect(stepUpGroup?.match("step_up_challenge")).toBe(true);
+ });
});
diff --git a/src/lib/eventGroups.ts b/src/lib/eventGroups.ts
index bbec8dd..b5aeed5 100644
--- a/src/lib/eventGroups.ts
+++ b/src/lib/eventGroups.ts
@@ -5,8 +5,7 @@
*/
// src/lib/eventGroups.ts
-
-const isSuspicious = (t: string) => t.includes("suspicious");
+import { eventCategories } from "./eventCategories";
export const eventGroups = [
{
@@ -14,34 +13,5 @@ export const eventGroups = [
value: "",
match: () => true,
},
-
- {
- label: "Login",
- value: "login",
- match: (t: string) => t.startsWith("login") && !isSuspicious(t),
- },
-
- {
- label: "WebAuthn",
- value: "webauthn",
- match: (t: string) => t.startsWith("webauthn") && !isSuspicious(t),
- },
-
- {
- label: "OTP",
- value: "otp",
- match: (t: string) => t.includes("otp") && !isSuspicious(t),
- },
-
- {
- label: "Tokens",
- value: "token",
- match: (t: string) => t.includes("token") && !isSuspicious(t),
- },
-
- {
- label: "Security",
- value: "security",
- match: (t: string) => isSuspicious(t) || t === "request_suspicious",
- },
+ ...eventCategories,
];
diff --git a/src/lib/eventMapping.test.ts b/src/lib/eventMapping.test.ts
index e3485ea..1a7c0d9 100644
--- a/src/lib/eventMapping.test.ts
+++ b/src/lib/eventMapping.test.ts
@@ -19,12 +19,19 @@ describe("collapseTypes", () => {
it("collapses magic link and suspicious groups", () => {
expect(collapseTypes(["magic_link_requested"])).toBe("magicLink");
- expect(collapseTypes(["request_suspicious"])).toBe("suspicious");
+ expect(collapseTypes(["request_suspicious"])).toBe("security");
+ });
+
+ it("collapses infrastructure event families", () => {
+ expect(collapseTypes(["refresh_token_success"])).toBe("token");
+ expect(collapseTypes(["service_token_success"])).toBe("serviceToken");
+ expect(collapseTypes(["oauth_login_success"])).toBe("oauth");
+ expect(collapseTypes(["step_up_success"])).toBe("stepUp");
});
it("falls back to the first concrete type", () => {
- expect(collapseTypes(["token_refreshed", "token_issued"])).toBe(
- "token_refreshed",
+ expect(collapseTypes(["unknown_event", "unknown_other"])).toBe(
+ "unknown_event",
);
});
diff --git a/src/lib/eventMapping.ts b/src/lib/eventMapping.ts
index 9910785..0172598 100644
--- a/src/lib/eventMapping.ts
+++ b/src/lib/eventMapping.ts
@@ -5,35 +5,14 @@
*/
// src/lib/eventTypeMapping.ts
+import { getEventCategory } from "./eventCategories";
export function collapseTypes(types: string[]): string {
if (!types || types.length === 0) return "";
- if (types.includes("login_success") || types.includes("login_failed")) {
- return "login";
- }
+ const matchedCategory = types
+ .map((type) => getEventCategory(type))
+ .find((category) => category.value !== "other");
- if (types.includes("otp_success") || types.includes("otp_failed")) {
- return "otp";
- }
-
- if (
- types.includes("webauthn_login_success") ||
- types.includes("webauthn_login_failed")
- ) {
- return "webauthn";
- }
-
- if (
- types.includes("magic_link_success") ||
- types.includes("magic_link_requested")
- ) {
- return "magicLink";
- }
-
- if (types.some((t) => t.includes("suspicious"))) {
- return "suspicious";
- }
-
- return types[0]; // fallback
+ return matchedCategory?.value ?? types[0];
}
diff --git a/src/pages/Overview.tsx b/src/pages/Overview.tsx
index d9157ec..9be6be9 100644
--- a/src/pages/Overview.tsx
+++ b/src/pages/Overview.tsx
@@ -47,11 +47,8 @@ export default function Overview() {
}, grouped.summary[0])
: undefined;
- const securitySignalCount = grouped?.summary.length
- ? grouped.summary
- .filter((item) => item.type.includes("suspicious"))
- .reduce((sum, item) => sum + item.count, 0)
- : 0;
+ const securitySignalCount =
+ grouped?.summary.find((item) => item.type === "security")?.count ?? 0;
return (
@@ -90,7 +87,7 @@ export default function Overview() {
/>
diff --git a/src/pages/SystemConfig.test.tsx b/src/pages/SystemConfig.test.tsx
index 277a87f..81e6748 100644
--- a/src/pages/SystemConfig.test.tsx
+++ b/src/pages/SystemConfig.test.tsx
@@ -32,6 +32,7 @@ const baseConfig = {
delay_after: 10,
login_methods: ["passkey", "magic_link"],
passkey_login_fallback_enabled: true,
+ oauth_providers: [],
rpid: "example.com",
origins: ["https://example.com"],
};
@@ -65,4 +66,53 @@ describe("SystemConfigPage", () => {
}),
);
});
+
+ it("adds OAuth provider configuration without a secret value", () => {
+ render(
);
+
+ fireEvent.change(screen.getByLabelText(/provider id/i), {
+ target: { value: "google" },
+ });
+ fireEvent.change(screen.getByLabelText(/display name/i), {
+ target: { value: "Google" },
+ });
+ fireEvent.change(screen.getByLabelText(/client id/i), {
+ target: { value: "client-id" },
+ });
+ fireEvent.change(screen.getByLabelText(/client secret env/i), {
+ target: { value: "GOOGLE_CLIENT_SECRET" },
+ });
+ fireEvent.change(screen.getByLabelText(/authorization url/i), {
+ target: { value: "https://accounts.google.com/o/oauth2/v2/auth" },
+ });
+ fireEvent.change(screen.getByLabelText(/token url/i), {
+ target: { value: "https://oauth2.googleapis.com/token" },
+ });
+ fireEvent.change(screen.getByLabelText(/user info url/i), {
+ target: { value: "https://openidconnect.googleapis.com/v1/userinfo" },
+ });
+ fireEvent.change(screen.getByLabelText(/redirect uri/i), {
+ target: { value: "https://example.com/oauth/callback" },
+ });
+ fireEvent.change(screen.getByLabelText(/scopes/i), {
+ target: { value: "openid, email, profile" },
+ });
+
+ fireEvent.click(screen.getByRole("button", { name: /add provider/i }));
+ fireEvent.click(screen.getByRole("button", { name: /save changes/i }));
+
+ expect(mocks.mutate).toHaveBeenCalledWith(
+ expect.objectContaining({
+ oauth_providers: [
+ expect.objectContaining({
+ id: "google",
+ name: "Google",
+ clientId: "client-id",
+ clientSecretEnv: "GOOGLE_CLIENT_SECRET",
+ scopes: ["openid", "email", "profile"],
+ }),
+ ],
+ }),
+ );
+ });
});
diff --git a/src/pages/SystemConfig.tsx b/src/pages/SystemConfig.tsx
index 47e91bb..b4dd717 100644
--- a/src/pages/SystemConfig.tsx
+++ b/src/pages/SystemConfig.tsx
@@ -9,6 +9,7 @@ import { KeyRound, ShieldCheck, TimerReset, Waypoints } from "lucide-react";
import {
useSystemConfig,
type LoginMethod,
+ type OAuthProviderConfig,
type SystemConfig,
} from "../hooks/useSystemConfig";
import { useUpdateSystemConfig } from "../hooks/useUpdateSystemConfig";
@@ -43,6 +44,11 @@ const LOGIN_METHOD_OPTIONS: {
label: "SMS OTP",
description: "One-time SMS codes after login initiation.",
},
+ {
+ value: "oauth",
+ label: "OAuth",
+ description: "External identity providers such as Google or GitHub.",
+ },
];
export default function SystemConfigPage() {
@@ -139,6 +145,10 @@ export default function SystemConfigPage() {
label="Login methods"
value={`${form.login_methods.length}`}
/>
+
@@ -278,6 +288,16 @@ export default function SystemConfigPage() {
+
+ updateField("oauth_providers", value)}
+ />
+
+
onChange(e.target.value)}
className="w-full rounded-md border border-subtle bg-surface-alt px-3 py-2 text-sm outline-none transition focus:border-[var(--primary)] focus:ring-1 focus:ring-[var(--primary)]"
@@ -569,6 +590,203 @@ function CheckboxField({
);
}
+const emptyOAuthProvider: OAuthProviderConfig = {
+ id: "",
+ name: "",
+ enabled: true,
+ clientId: "",
+ clientSecretEnv: "",
+ authorizationUrl: "",
+ tokenUrl: "",
+ userInfoUrl: "",
+ scopes: [],
+ redirectUri: "",
+ subjectJsonPath: "sub",
+ emailJsonPath: "email",
+ nameJsonPath: "name",
+ allowSignup: true,
+};
+
+function OAuthProvidersEditor({
+ providers,
+ setProviders,
+}: {
+ providers: OAuthProviderConfig[];
+ setProviders: (providers: OAuthProviderConfig[]) => void;
+}) {
+ const [draft, setDraft] = useState(emptyOAuthProvider);
+
+ const addProvider = () => {
+ if (!draft.id || !draft.name || !draft.clientId || !draft.clientSecretEnv) {
+ return;
+ }
+
+ setProviders([
+ ...providers.filter((provider) => provider.id !== draft.id),
+ {
+ ...draft,
+ scopes: draft.scopes.filter(Boolean),
+ redirectUri: draft.redirectUri || undefined,
+ nameJsonPath: draft.nameJsonPath || undefined,
+ },
+ ]);
+ setDraft(emptyOAuthProvider);
+ };
+
+ const updateProvider = (
+ index: number,
+ updates: Partial,
+ ) => {
+ setProviders(
+ providers.map((provider, currentIndex) =>
+ currentIndex === index ? { ...provider, ...updates } : provider,
+ ),
+ );
+ };
+
+ const removeProvider = (index: number) => {
+ setProviders(providers.filter((_, currentIndex) => currentIndex !== index));
+ };
+
+ return (
+
+
+
setDraft({ ...draft, id: value })}
+ />
+
setDraft({ ...draft, name: value })}
+ />
+
setDraft({ ...draft, clientId: value })}
+ />
+
setDraft({ ...draft, clientSecretEnv: value })}
+ />
+
setDraft({ ...draft, authorizationUrl: value })}
+ />
+
setDraft({ ...draft, tokenUrl: value })}
+ />
+
setDraft({ ...draft, userInfoUrl: value })}
+ />
+
setDraft({ ...draft, redirectUri: value })}
+ />
+
+ setDraft({
+ ...draft,
+ scopes: value
+ .split(",")
+ .map((scope) => scope.trim())
+ .filter(Boolean),
+ })
+ }
+ />
+
setDraft({ ...draft, subjectJsonPath: value })}
+ />
+
setDraft({ ...draft, emailJsonPath: value })}
+ />
+
setDraft({ ...draft, nameJsonPath: value })}
+ />
+
+
+
+
+
+
+ {providers.map((provider, index) => (
+
+
+
+
{provider.name}
+
{provider.id}
+
+ Secret env: {provider.clientSecretEnv}
+
+
+
+
+
+
+
+
+
+
+ Client ID: {provider.clientId}
+
+ Scopes: {provider.scopes.join(", ") || "None"}
+
+
+ Authorization: {provider.authorizationUrl}
+
+
+ User info: {provider.userInfoUrl}
+
+
+
+ ))}
+
+ {providers.length === 0 && (
+
+ No OAuth providers configured.
+
+ )}
+
+
+ );
+}
+
function OriginsEditor({
origins,
setOrigins,
diff --git a/src/types/authEventTypes.ts b/src/types/authEventTypes.ts
index 305a09a..e9e26c2 100644
--- a/src/types/authEventTypes.ts
+++ b/src/types/authEventTypes.ts
@@ -8,23 +8,22 @@
import { z } from "zod";
export const AuthEventTypeEnum = z.enum([
- "login",
- "magicLink",
- "suspicious",
- "otp",
- "webauthn",
"auth_action_incremented",
"bearer_token_failed",
"bearer_token_success",
"bearer_token_suspicious",
+ "bootstrap_admin_check_skipped",
+ "bootstrap_admin_granted",
"cookie_token_failed",
"cookie_token_success",
"cookie_token_suspicious",
+ "credentials_deleted",
"informational",
"internal_user_updated_by_owner",
"jwks_failed",
"jwks_success",
"jwks_suspicious",
+ "login_challenge",
"login_failed",
"login_success",
"login_suspicious",
@@ -37,7 +36,10 @@ export const AuthEventTypeEnum = z.enum([
"mfa_otp_failed",
"mfa_otp_success",
"mfa_otp_suspicious",
- "notication_sent",
+ "notification_sent",
+ "oauth_login_failed",
+ "oauth_login_started",
+ "oauth_login_success",
"otp_failed",
"otp_success",
"otp_suspicious",
@@ -50,17 +52,29 @@ export const AuthEventTypeEnum = z.enum([
"registration_failed",
"registration_success",
"registration_suspicious",
+ "request_suspicious",
"service_token_failed",
"service_token_rotated",
"service_token_success",
"service_token_suspicious",
+ "step_up_challenge",
+ "step_up_failed",
+ "step_up_success",
+ "step_up_suspicious",
"system_config_error",
"system_config_read",
"system_config_updated",
+ "totp_disabled",
+ "totp_enrollment_started",
+ "totp_enrollment_success",
+ "totp_failed",
+ "totp_success",
+ "totp_suspicious",
"user_created",
"user_data_failed",
"user_data_success",
"user_data_suspicious",
+ "user_deleted",
"verify_otp_failed",
"verify_otp_success",
"verify_otp_suspicious",