From f526c23e20ce38c0cfcc0c34a305396fbc4caff7 Mon Sep 17 00:00:00 2001 From: Ayush8923 <80516839+Ayush8923@users.noreply.github.com> Date: Fri, 10 Apr 2026 00:03:02 +0530 Subject: [PATCH 1/7] feat(*): login via email flow --- app/(auth)/invite/page.tsx | 220 ++------------------- app/(auth)/verify/page.tsx | 42 ++++ app/api/auth/magic-link/route.ts | 22 +++ app/api/auth/magic-link/verify/route.ts | 35 ++++ app/components/Field.tsx | 4 +- app/components/auth/LoginModal.tsx | 126 ++++++++++-- app/components/auth/TokenVerifyPage.tsx | 240 +++++++++++++++++++++++ app/components/auth/index.ts | 1 + app/components/icons/common/MailIcon.tsx | 21 ++ app/components/icons/index.tsx | 1 + 10 files changed, 496 insertions(+), 216 deletions(-) create mode 100644 app/(auth)/verify/page.tsx create mode 100644 app/api/auth/magic-link/route.ts create mode 100644 app/api/auth/magic-link/verify/route.ts create mode 100644 app/components/auth/TokenVerifyPage.tsx create mode 100644 app/components/icons/common/MailIcon.tsx diff --git a/app/(auth)/invite/page.tsx b/app/(auth)/invite/page.tsx index d807ccde..762eaefe 100644 --- a/app/(auth)/invite/page.tsx +++ b/app/(auth)/invite/page.tsx @@ -1,211 +1,29 @@ "use client"; -import { useEffect, useState, Suspense } from "react"; -import { useSearchParams, useRouter } from "next/navigation"; -import { useAuth } from "@/app/lib/context/AuthContext"; -import { InviteVerifyResponse } from "@/app/lib/types/auth"; -import { - CheckCircleIcon, - WarningIcon, - SpinnerIcon, -} from "@/app/components/icons"; -import { Button } from "@/app/components"; - -type Status = "verifying" | "success" | "error"; +import { Suspense } from "react"; +import { useSearchParams } from "next/navigation"; +import { SpinnerIcon } from "@/app/components/icons"; +import TokenVerifyPage from "@/app/components/auth/TokenVerifyPage"; function InviteContent() { const searchParams = useSearchParams(); - const router = useRouter(); - const { loginWithToken } = useAuth(); - const [status, setStatus] = useState("verifying"); - const [error, setError] = useState(""); - const [progress, setProgress] = useState(0); - - useEffect(() => { - const token = searchParams.get("token"); - - if (!token) { - setStatus("error"); - setError("Invalid invitation link. No token found."); - return; - } - - let cancelled = false; - - (async () => { - try { - const res = await fetch( - `/api/auth/invite?token=${encodeURIComponent(token)}`, - { credentials: "include" }, - ); - - const data: InviteVerifyResponse = await res.json(); - - if (cancelled) return; - - if (!res.ok || !data.success || !data.data) { - setStatus("error"); - setError(data.error || "Invitation link is invalid or has expired."); - return; - } - - loginWithToken(data.data.access_token, data.data.user); - setStatus("success"); - } catch { - if (!cancelled) { - setStatus("error"); - setError("Failed to verify invitation. Please try again."); - } - } - })(); - - return () => { - cancelled = true; - }; - }, [searchParams, loginWithToken]); - - useEffect(() => { - if (status !== "success") return; - - const duration = 2000; - const interval = 30; - let elapsed = 0; - - const timer = setInterval(() => { - elapsed += interval; - const pct = Math.min((elapsed / duration) * 100, 100); - setProgress(pct); - - if (elapsed >= duration) { - clearInterval(timer); - router.push("/evaluations"); - } - }, interval); - - return () => clearInterval(timer); - }, [status, router]); return ( -
-
-
-
-
- -
-
-

- Kaapi Konsole -

-

by Tech4Dev

-
- -
-
- -
-
-
- {status === "verifying" && ( - - )} - {status === "success" && ( - - )} - {status === "error" && ( - - )} -
-
- - {/* Title */} -

- {status === "verifying" && "Verifying invitation"} - {status === "success" && "Welcome aboard!"} - {status === "error" && "Something went wrong"} -

- -

- {status === "verifying" && - "Please wait while we verify your invitation and set up your account."} - {status === "success" && - "Your account has been activated. Redirecting you to the dashboard..."} - {status === "error" && error} -

- - {status === "success" && ( -
-
-
-
-
- )} - - {status === "verifying" && ( -
- {[0, 1, 2].map((i) => ( -
- ))} -
- )} - - {status === "error" && ( -
- - -
- )} -
-
- - {status === "error" && ( -

- If this keeps happening, please contact your organization - administrator for a new invitation link. -

- )} -
-
+ ); } diff --git a/app/(auth)/verify/page.tsx b/app/(auth)/verify/page.tsx new file mode 100644 index 00000000..20caf117 --- /dev/null +++ b/app/(auth)/verify/page.tsx @@ -0,0 +1,42 @@ +"use client"; + +import { Suspense } from "react"; +import { useSearchParams } from "next/navigation"; +import { SpinnerIcon } from "@/app/components/icons"; +import TokenVerifyPage from "@/app/components/auth/TokenVerifyPage"; + +function VerifyContent() { + const searchParams = useSearchParams(); + + return ( + + ); +} + +export default function VerifyPage() { + return ( + + +
+ } + > + + + ); +} diff --git a/app/api/auth/magic-link/route.ts b/app/api/auth/magic-link/route.ts new file mode 100644 index 00000000..b88f67b8 --- /dev/null +++ b/app/api/auth/magic-link/route.ts @@ -0,0 +1,22 @@ +import { NextRequest, NextResponse } from "next/server"; +import { apiClient } from "@/app/lib/apiClient"; + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { status, data } = await apiClient( + request, + "/api/v1/auth/magic-link", + { + method: "POST", + body: JSON.stringify(body), + }, + ); + return NextResponse.json(data, { status }); + } catch { + return NextResponse.json( + { success: false, error: "Failed to connect to backend" }, + { status: 500 }, + ); + } +} diff --git a/app/api/auth/magic-link/verify/route.ts b/app/api/auth/magic-link/verify/route.ts new file mode 100644 index 00000000..fc436f55 --- /dev/null +++ b/app/api/auth/magic-link/verify/route.ts @@ -0,0 +1,35 @@ +import { NextRequest, NextResponse } from "next/server"; +import { apiClient } from "@/app/lib/apiClient"; + +export async function GET(request: NextRequest) { + try { + const { searchParams } = new URL(request.url); + const token = searchParams.get("token"); + + if (!token) { + return NextResponse.json( + { success: false, error: "Missing login token" }, + { status: 400 }, + ); + } + + const { status, data, headers } = await apiClient( + request, + `/api/v1/auth/magic-link/verify?token=${encodeURIComponent(token)}`, + ); + + const res = NextResponse.json(data, { status }); + + const setCookies = headers.getSetCookie?.() ?? []; + for (const cookie of setCookies) { + res.headers.append("Set-Cookie", cookie); + } + + return res; + } catch { + return NextResponse.json( + { success: false, error: "Failed to connect to backend" }, + { status: 500 }, + ); + } +} diff --git a/app/components/Field.tsx b/app/components/Field.tsx index eb32b1a2..f6a3a990 100644 --- a/app/components/Field.tsx +++ b/app/components/Field.tsx @@ -11,6 +11,7 @@ interface FieldProps { error?: string; type?: string; disabled?: boolean; + className?: string; } export default function Field({ @@ -21,6 +22,7 @@ export default function Field({ error, type = "text", disabled = false, + className = "", }: FieldProps) { const [showPassword, setShowPassword] = useState(false); const isPassword = type === "password"; @@ -40,7 +42,7 @@ export default function Field({ disabled={disabled} className={`w-full px-3 py-2 rounded-lg border text-sm text-text-primary bg-white placeholder:text-neutral-400 focus:outline-none focus:ring-accent-primary/20 focus:border-accent-primary transition-colors ${ isPassword ? "pr-10" : "" - } ${error ? "border-red-400" : "border-border"} ${disabled ? "opacity-50 cursor-not-allowed" : ""}`} + } ${error ? "border-red-400" : "border-border"} ${disabled ? "opacity-50 cursor-not-allowed" : ""} ${className}`} /> {isPassword && ( -

+
+
+ OR +
+
+ + {linkSent ? ( +
+
+ +
+

+ Check your email +

+

+ We sent a login link to{" "} + {email}. + Click the link in the email to sign in. +

+ +
+ ) : ( +
+ { + setEmail(val); + if (emailError) setEmailError(""); + }} + placeholder="Email address" + error={emailError} + className="rounded-full! px-5! py-3!" + /> + +
+ )} + + {!linkSent && ( +

+ Want to use an X-API key instead?{" "} + +

+ )}
); diff --git a/app/components/auth/TokenVerifyPage.tsx b/app/components/auth/TokenVerifyPage.tsx new file mode 100644 index 00000000..685b8acd --- /dev/null +++ b/app/components/auth/TokenVerifyPage.tsx @@ -0,0 +1,240 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { useAuth } from "@/app/lib/context/AuthContext"; +import { AuthTokenResponse } from "@/app/lib/types/auth"; +import { + CheckCircleIcon, + WarningIcon, + SpinnerIcon, +} from "@/app/components/icons"; +import { Button } from "@/app/components"; + +type Status = "verifying" | "success" | "error"; + +interface TokenVerifyPageProps { + token: string | null; + apiUrl: string; + title: { + verifying: string; + success: string; + error: string; + }; + description: { + verifying: string; + success: string; + }; + errorFallback: string; + helpText?: string; +} + +export default function TokenVerifyPage({ + token, + apiUrl, + title, + description, + errorFallback, + helpText, +}: TokenVerifyPageProps) { + const router = useRouter(); + const { loginWithToken } = useAuth(); + const [status, setStatus] = useState("verifying"); + const [error, setError] = useState(""); + const [progress, setProgress] = useState(0); + + useEffect(() => { + if (!token) { + setStatus("error"); + setError("Invalid link. No token found."); + return; + } + + let cancelled = false; + + (async () => { + try { + const res = await fetch( + `${apiUrl}?token=${encodeURIComponent(token)}`, + { credentials: "include" }, + ); + + const data: AuthTokenResponse & { + data?: { + user?: { + id: number; + email: string; + full_name: string; + is_active: boolean; + is_superuser: boolean; + }; + }; + } = await res.json(); + + if (cancelled) return; + + if (!res.ok || !data.success || !data.data) { + setStatus("error"); + setError(data.error || errorFallback); + return; + } + + loginWithToken(data.data.access_token, data.data.user); + setStatus("success"); + } catch { + if (!cancelled) { + setStatus("error"); + setError("Failed to verify. Please try again."); + } + } + })(); + + return () => { + cancelled = true; + }; + }, [token, apiUrl, loginWithToken, errorFallback]); + + // Drive progress bar and redirect on success + useEffect(() => { + if (status !== "success") return; + + const duration = 2000; + const interval = 30; + let elapsed = 0; + + const timer = setInterval(() => { + elapsed += interval; + const pct = Math.min((elapsed / duration) * 100, 100); + setProgress(pct); + + if (elapsed >= duration) { + clearInterval(timer); + router.push("/evaluations"); + } + }, interval); + + return () => clearInterval(timer); + }, [status, router]); + + return ( +
+
+
+
+
+ +
+
+

+ Kaapi Konsole +

+

by Tech4Dev

+
+ +
+
+ +
+ {/* Icon */} +
+
+ {status === "verifying" && ( + + )} + {status === "success" && ( + + )} + {status === "error" && ( + + )} +
+
+ +

+ {status === "verifying" && title.verifying} + {status === "success" && title.success} + {status === "error" && title.error} +

+ +

+ {status === "verifying" && description.verifying} + {status === "success" && description.success} + {status === "error" && error} +

+ + {/* Progress bar */} + {status === "success" && ( +
+
+
+
+
+ )} + + {/* Pulsing dots */} + {status === "verifying" && ( +
+ {[0, 1, 2].map((i) => ( +
+ ))} +
+ )} + + {status === "error" && ( +
+ + +
+ )} +
+
+ + {status === "error" && helpText && ( +

+ {helpText} +

+ )} +
+
+ ); +} diff --git a/app/components/auth/index.ts b/app/components/auth/index.ts index 366a852e..3cf3b466 100644 --- a/app/components/auth/index.ts +++ b/app/components/auth/index.ts @@ -1,3 +1,4 @@ export { default as LoginModal } from "./LoginModal"; export { default as FeatureGateModal } from "./FeatureGateModal"; export { default as ProtectedPage } from "./ProtectedPage"; +export { default as TokenVerifyPage } from "./TokenVerifyPage"; diff --git a/app/components/icons/common/MailIcon.tsx b/app/components/icons/common/MailIcon.tsx new file mode 100644 index 00000000..1b8a834f --- /dev/null +++ b/app/components/icons/common/MailIcon.tsx @@ -0,0 +1,21 @@ +export default function MailIcon({ + className = "w-5 h-5", +}: { + className?: string; +}) { + return ( + + + + ); +} diff --git a/app/components/icons/index.tsx b/app/components/icons/index.tsx index b8d3dfef..ce9e2f54 100644 --- a/app/components/icons/index.tsx +++ b/app/components/icons/index.tsx @@ -10,6 +10,7 @@ export { default as WarningTriangleIcon } from "./common/WarningTriangleIcon"; export { default as PlusIcon } from "./common/PlusIcon"; export { default as SearchIcon } from "./common/SearchIcon"; export { default as SidebarToggleIcon } from "./common/SidebarToggleIcon"; +export { default as MailIcon } from "./common/MailIcon"; // Evaluations Icons export { default as ChevronUpIcon } from "./evaluations/ChevronUpIcon"; From 8ee896636e1542d2a1bbd56e1c6f14d827d9450c Mon Sep 17 00:00:00 2001 From: Ayush8923 <80516839+Ayush8923@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:24:33 +0530 Subject: [PATCH 2/7] fix(*): handle the error message --- app/components/auth/LoginModal.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/components/auth/LoginModal.tsx b/app/components/auth/LoginModal.tsx index 11bac08a..e8e548cb 100644 --- a/app/components/auth/LoginModal.tsx +++ b/app/components/auth/LoginModal.tsx @@ -70,14 +70,22 @@ export default function LoginModal({ open, onClose }: LoginModalProps) { setIsSendingLink(true); try { - await fetch("/api/auth/magic-link", { + const res = await fetch("/api/auth/magic-link", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: email.trim() }), }); + + const data = await res.json(); + + if (!res.ok || !data.success) { + setEmailError(data.error || "Failed to send login link."); + return; + } + setLinkSent(true); } catch { - toast.error("Failed to send login link. Please try again."); + toast.error("Failed to connect to server. Please try again."); } finally { setIsSendingLink(false); } From fb939ccfad37cffee5a824f92560f08abbc821c3 Mon Sep 17 00:00:00 2001 From: Ayush8923 <80516839+Ayush8923@users.noreply.github.com> Date: Fri, 10 Apr 2026 20:50:19 +0530 Subject: [PATCH 3/7] fix(*): remove the unwanted js comments --- app/components/auth/TokenVerifyPage.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/components/auth/TokenVerifyPage.tsx b/app/components/auth/TokenVerifyPage.tsx index 685b8acd..7d63f131 100644 --- a/app/components/auth/TokenVerifyPage.tsx +++ b/app/components/auth/TokenVerifyPage.tsx @@ -153,7 +153,6 @@ export default function TokenVerifyPage({ />
- {/* Icon */}
- {/* Progress bar */} {status === "success" && (
@@ -200,7 +198,6 @@ export default function TokenVerifyPage({
)} - {/* Pulsing dots */} {status === "verifying" && (
{[0, 1, 2].map((i) => ( From a7db40a39da2e2a9b6f19b0c87696c17795c35f2 Mon Sep 17 00:00:00 2001 From: Ayush8923 <80516839+Ayush8923@users.noreply.github.com> Date: Fri, 10 Apr 2026 20:53:06 +0530 Subject: [PATCH 4/7] fix(*): centralize the app name and remove the unwanted js comment --- app/components/auth/TokenVerifyPage.tsx | 3 ++- app/components/user-menu/Branding.tsx | 4 +++- app/layout.tsx | 3 ++- app/lib/constants.ts | 3 ++- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/components/auth/TokenVerifyPage.tsx b/app/components/auth/TokenVerifyPage.tsx index 7d63f131..cb314520 100644 --- a/app/components/auth/TokenVerifyPage.tsx +++ b/app/components/auth/TokenVerifyPage.tsx @@ -10,6 +10,7 @@ import { SpinnerIcon, } from "@/app/components/icons"; import { Button } from "@/app/components"; +import { APP_NAME } from "@/app/lib/constants"; type Status = "verifying" | "success" | "error"; @@ -126,7 +127,7 @@ export default function TokenVerifyPage({

- Kaapi Konsole + {APP_NAME}

by Tech4Dev

diff --git a/app/components/user-menu/Branding.tsx b/app/components/user-menu/Branding.tsx index 19353d42..55c259ea 100644 --- a/app/components/user-menu/Branding.tsx +++ b/app/components/user-menu/Branding.tsx @@ -1,8 +1,10 @@ +import { APP_NAME } from "@/app/lib/constants"; + const Branding = () => { return (

- Kaapi Konsole + {APP_NAME}

Tech4Dev

diff --git a/app/layout.tsx b/app/layout.tsx index 77bc11b8..b743fd11 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,6 +2,7 @@ import type { Metadata } from "next"; import { Inter, JetBrains_Mono } from "next/font/google"; import "./globals.css"; import { Providers } from "@/app/components/providers"; +import { APP_NAME } from "@/app/lib/constants"; const inter = Inter({ variable: "--font-sans", @@ -16,7 +17,7 @@ const jetbrainsMono = JetBrains_Mono({ }); export const metadata: Metadata = { - title: "Kaapi Konsole", + title: APP_NAME, description: "", }; diff --git a/app/lib/constants.ts b/app/lib/constants.ts index 274db8d8..5fdbd869 100644 --- a/app/lib/constants.ts +++ b/app/lib/constants.ts @@ -4,7 +4,8 @@ import { ConfigBlob } from "@/app/lib/types/promptEditor"; -// localStorage keys +export const APP_NAME = "Kaapi Konsole"; + export const STORAGE_KEYS = { API_KEYS: "kaapi_api_keys", SESSION: "kaapi_session", From 53eeaa26e5647645e9b8c85e72669fb45bd263f8 Mon Sep 17 00:00:00 2001 From: Ayush8923 <80516839+Ayush8923@users.noreply.github.com> Date: Sun, 12 Apr 2026 00:48:41 +0530 Subject: [PATCH 5/7] fix(*): configuration library ui --- app/(main)/configurations/page.tsx | 13 +++++++++---- app/components/ConfigCard.tsx | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/(main)/configurations/page.tsx b/app/(main)/configurations/page.tsx index d9243f0b..2a68c202 100644 --- a/app/(main)/configurations/page.tsx +++ b/app/(main)/configurations/page.tsx @@ -271,7 +271,10 @@ export default function ConfigLibraryPage() {
-
+
{isLoading ? ( ) : error ? ( @@ -341,11 +344,13 @@ export default function ConfigLibraryPage() { ) : ( <>
{columns.map((col, colIdx) => ( -
+
{col.map((config) => (

{latestVersion.instructions || "No instructions set"} From a748136b71c0de2fbef92701afe90fa84d857aae Mon Sep 17 00:00:00 2001 From: Ayush <80516839+Ayush8923@users.noreply.github.com> Date: Thu, 16 Apr 2026 16:14:38 +0530 Subject: [PATCH 6/7] Onboarding: Credentials Management (#117) --- .../configurations/prompt-editor/page.tsx | 6 +- app/(main)/settings/credentials/page.tsx | 70 +---- app/(main)/settings/onboarding/page.tsx | 83 ++++-- app/api/auth/google/route.ts | 5 + app/api/auth/invite/route.ts | 5 + app/api/auth/logout/route.ts | 3 + app/api/auth/magic-link/verify/route.ts | 5 + .../[projectId]/provider/[provider]/route.ts | 40 +++ .../org/[orgId]/[projectId]/route.ts | 56 ++++ app/api/users/me/route.ts | 9 +- app/components/Field.tsx | 2 +- app/components/PageHeader.tsx | 24 +- app/components/Toast.tsx | 278 +++++++----------- .../icons/common/ErrorCircleIcon.tsx | 23 ++ app/components/icons/document/CloseIcon.tsx | 4 +- app/components/icons/index.tsx | 1 + .../icons/prompt-editor/CheckCircleIcon.tsx | 4 +- app/components/prompt-editor/Header.tsx | 45 +-- app/components/settings/ProviderSidebar.tsx | 52 ++++ app/components/settings/SettingsSidebar.tsx | 6 +- .../settings/credentials/CredentialForm.tsx | 187 ++---------- .../credentials/CredentialFormPanel.tsx | 116 ++++++++ .../settings/credentials/ProviderList.tsx | 79 ----- app/components/settings/credentials/index.ts | 2 + .../onboarding/OnboardingCredentials.tsx | 204 +++++++++++++ .../settings/onboarding/OrganizationList.tsx | 3 +- .../settings/onboarding/UserList.tsx | 58 ++-- app/components/settings/onboarding/index.ts | 1 + app/globals.css | 26 +- app/hooks/useToast.ts | 1 + app/lib/authCookie.ts | 49 +++ app/lib/constants.ts | 47 ++- app/lib/navConfig.ts | 2 +- app/lib/types/onboarding.ts | 4 +- app/lib/types/toast.ts | 18 ++ middleware.ts | 68 +++++ 36 files changed, 974 insertions(+), 612 deletions(-) create mode 100644 app/api/credentials/org/[orgId]/[projectId]/provider/[provider]/route.ts create mode 100644 app/api/credentials/org/[orgId]/[projectId]/route.ts create mode 100644 app/components/icons/common/ErrorCircleIcon.tsx create mode 100644 app/components/settings/ProviderSidebar.tsx create mode 100644 app/components/settings/credentials/CredentialFormPanel.tsx delete mode 100644 app/components/settings/credentials/ProviderList.tsx create mode 100644 app/components/settings/credentials/index.ts create mode 100644 app/components/settings/onboarding/OnboardingCredentials.tsx create mode 100644 app/hooks/useToast.ts create mode 100644 app/lib/authCookie.ts create mode 100644 app/lib/types/toast.ts create mode 100644 middleware.ts diff --git a/app/(main)/configurations/prompt-editor/page.tsx b/app/(main)/configurations/prompt-editor/page.tsx index f4596aea..19677150 100644 --- a/app/(main)/configurations/prompt-editor/page.tsx +++ b/app/(main)/configurations/prompt-editor/page.tsx @@ -380,10 +380,7 @@ function PromptEditorContent() { }; return ( -

+
) : (
- {/* Split View: Prompt (left) + Config (right) */}
>({}); const [isActive, setIsActive] = useState(true); - const [visibleFields, setVisibleFields] = useState>(new Set()); const [existingCredential, setExistingCredential] = useState(null); - // Load credentials once we have an API key + // Load credentials once authenticated useEffect(() => { if (!isAuthenticated) return; loadCredentials(); - }, [apiKeys]); + }, [isAuthenticated, apiKeys]); // Re-populate form when provider or credentials change useEffect(() => { @@ -64,7 +63,6 @@ export default function CredentialsPage() { }); setFormValues(blank); } - setVisibleFields(new Set()); }, [selectedProvider, credentials]); const loadCredentials = async () => { @@ -151,7 +149,6 @@ export default function CredentialsPage() { setFormValues(blank); setIsActive(true); } - setVisibleFields(new Set()); }; const handleDelete = async () => { @@ -178,68 +175,29 @@ export default function CredentialsPage() { setFormValues((prev) => ({ ...prev, [key]: value })); }; - const handleToggleVisibility = (key: string) => { - setVisibleFields((prev) => { - const next = new Set(prev); - if (next.has(key)) { - next.delete(key); - } else { - next.add(key); - } - return next; - }); - }; - return ( -
+
-
-
-

- Credentials -

-

- Manage provider credentials -

-
-
+
-
{!isAuthenticated ? ( -
+
Please log in to manage credentials.
) : ( @@ -251,10 +209,8 @@ export default function CredentialsPage() { isLoading={isLoading} isSaving={isSaving} isDeleting={isDeleting} - visibleFields={visibleFields} onChange={handleFieldChange} onActiveChange={setIsActive} - onToggleVisibility={handleToggleVisibility} onSave={handleSave} onCancel={handleCancel} onDelete={handleDelete} diff --git a/app/(main)/settings/onboarding/page.tsx b/app/(main)/settings/onboarding/page.tsx index f453e60c..b7e2197c 100644 --- a/app/(main)/settings/onboarding/page.tsx +++ b/app/(main)/settings/onboarding/page.tsx @@ -1,7 +1,6 @@ "use client"; import { useState, useEffect, useCallback } from "react"; -import { useRouter } from "next/navigation"; import SettingsSidebar from "@/app/components/settings/SettingsSidebar"; import PageHeader from "@/app/components/PageHeader"; import { useAuth } from "@/app/lib/context/AuthContext"; @@ -13,6 +12,7 @@ import { ProjectList, StepIndicator, UserList, + OnboardingCredentials, } from "@/app/components/settings/onboarding"; import { Organization, @@ -21,9 +21,14 @@ import { OnboardResponseData, } from "@/app/lib/types/onboarding"; import { apiFetch } from "@/app/lib/apiClient"; -import { colors } from "@/app/lib/colors"; import { ArrowLeftIcon } from "@/app/components/icons"; import { DEFAULT_PAGE_LIMIT } from "@/app/lib/constants"; +import TabNavigation from "@/app/components/TabNavigation"; + +const PROJECT_TABS = [ + { id: "users", label: "Users" }, + { id: "credentials", label: "Credentials" }, +]; type View = "loading" | "list" | "projects" | "users" | "form" | "success"; @@ -59,8 +64,7 @@ function OrganizationListSkeleton() { } export default function OnboardingPage() { - const router = useRouter(); - const { activeKey, currentUser, isHydrated, isAuthenticated } = useAuth(); + const { activeKey } = useAuth(); const [view, setView] = useState("loading"); const [selectedOrg, setSelectedOrg] = useState(null); const [selectedProject, setSelectedProject] = useState(null); @@ -69,6 +73,7 @@ export default function OnboardingPage() { const [onboardData, setOnboardData] = useState( null, ); + const [activeProjectTab, setActiveProjectTab] = useState("users"); const { items: organizations, @@ -97,18 +102,6 @@ export default function OnboardingPage() { } }, [isLoadingOrgs, organizations.length]); - // Redirect if no API key or not a superuser - useEffect(() => { - if (!isHydrated) return; - if (!isAuthenticated) { - router.replace("/"); - return; - } - if (currentUser && !currentUser.is_superuser) { - router.replace("/settings/credentials"); - } - }, [isHydrated, activeKey, currentUser, router]); - const fetchProjects = useCallback( async (org: Organization) => { setSelectedOrg(org); @@ -172,20 +165,17 @@ export default function OnboardingPage() { }; return ( -
+
-
+
{view === "loading" && } @@ -195,7 +185,6 @@ export default function OnboardingPage() { isLoadingMore={isLoadingMore} onNewOrg={() => setView("form")} onSelectOrg={fetchProjects} - scrollRef={scrollRef} /> )} @@ -211,11 +200,47 @@ export default function OnboardingPage() { )} {view === "users" && selectedOrg && selectedProject && ( - +
+ + +
+
+

+ {selectedProject.name} +

+

+ {selectedOrg.name} +

+
+
+ + + + {activeProjectTab === "users" && ( + + )} + + {activeProjectTab === "credentials" && ( + + )} +
)} {view === "form" && ( diff --git a/app/api/auth/google/route.ts b/app/api/auth/google/route.ts index 7dc39ad3..b9d1bd04 100644 --- a/app/api/auth/google/route.ts +++ b/app/api/auth/google/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from "next/server"; import { apiClient } from "@/app/lib/apiClient"; +import { setRoleCookieFromBody } from "@/app/lib/authCookie"; /** Proxy Google login token to backend. Forwards Set-Cookie headers back to the browser. */ export async function POST(request: Request) { @@ -31,6 +32,10 @@ export async function POST(request: Request) { }); } + if (status >= 200 && status < 300) { + setRoleCookieFromBody(response, data); + } + return response; } catch { return NextResponse.json( diff --git a/app/api/auth/invite/route.ts b/app/api/auth/invite/route.ts index 96984c15..e1fd42d4 100644 --- a/app/api/auth/invite/route.ts +++ b/app/api/auth/invite/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { apiClient } from "@/app/lib/apiClient"; +import { setRoleCookieFromBody } from "@/app/lib/authCookie"; export async function GET(request: NextRequest) { try { @@ -25,6 +26,10 @@ export async function GET(request: NextRequest) { res.headers.append("Set-Cookie", cookie); } + if (status >= 200 && status < 300) { + setRoleCookieFromBody(res, data); + } + return res; } catch { return NextResponse.json( diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts index 3b5994f7..5111c38a 100644 --- a/app/api/auth/logout/route.ts +++ b/app/api/auth/logout/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { apiClient } from "@/app/lib/apiClient"; +import { clearRoleCookie } from "@/app/lib/authCookie"; export async function POST(request: NextRequest) { const { status, data, headers } = await apiClient( @@ -15,5 +16,7 @@ export async function POST(request: NextRequest) { res.headers.append("Set-Cookie", cookie); } + clearRoleCookie(res); + return res; } diff --git a/app/api/auth/magic-link/verify/route.ts b/app/api/auth/magic-link/verify/route.ts index fc436f55..70475a48 100644 --- a/app/api/auth/magic-link/verify/route.ts +++ b/app/api/auth/magic-link/verify/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; import { apiClient } from "@/app/lib/apiClient"; +import { setRoleCookieFromBody } from "@/app/lib/authCookie"; export async function GET(request: NextRequest) { try { @@ -25,6 +26,10 @@ export async function GET(request: NextRequest) { res.headers.append("Set-Cookie", cookie); } + if (status >= 200 && status < 300) { + setRoleCookieFromBody(res, data); + } + return res; } catch { return NextResponse.json( diff --git a/app/api/credentials/org/[orgId]/[projectId]/provider/[provider]/route.ts b/app/api/credentials/org/[orgId]/[projectId]/provider/[provider]/route.ts new file mode 100644 index 00000000..583ed969 --- /dev/null +++ b/app/api/credentials/org/[orgId]/[projectId]/provider/[provider]/route.ts @@ -0,0 +1,40 @@ +import { apiClient } from "@/app/lib/apiClient"; +import { NextResponse, NextRequest } from "next/server"; + +type Params = { + params: Promise<{ orgId: string; projectId: string; provider: string }>; +}; + +export async function GET(request: NextRequest, { params }: Params) { + const { orgId, projectId, provider } = await params; + try { + const { status, data } = await apiClient( + request, + `/api/v1/credentials/${orgId}/${projectId}/provider/${provider}`, + ); + return NextResponse.json(data, { status }); + } catch (e: unknown) { + return NextResponse.json( + { error: e instanceof Error ? e.message : String(e) }, + { status: 500 }, + ); + } +} + +export async function DELETE(request: NextRequest, { params }: Params) { + const { orgId, projectId, provider } = await params; + try { + const { status, data } = await apiClient( + request, + `/api/v1/credentials/${orgId}/${projectId}/provider/${provider}`, + { method: "DELETE" }, + ); + if (status === 204) return new NextResponse(null, { status: 204 }); + return NextResponse.json(data, { status }); + } catch (e: unknown) { + return NextResponse.json( + { error: e instanceof Error ? e.message : String(e) }, + { status: 500 }, + ); + } +} diff --git a/app/api/credentials/org/[orgId]/[projectId]/route.ts b/app/api/credentials/org/[orgId]/[projectId]/route.ts new file mode 100644 index 00000000..d98638ba --- /dev/null +++ b/app/api/credentials/org/[orgId]/[projectId]/route.ts @@ -0,0 +1,56 @@ +import { apiClient } from "@/app/lib/apiClient"; +import { NextResponse, NextRequest } from "next/server"; + +type Params = { params: Promise<{ orgId: string; projectId: string }> }; + +export async function GET(request: NextRequest, { params }: Params) { + const { orgId, projectId } = await params; + try { + const { status, data } = await apiClient( + request, + `/api/v1/credentials/${orgId}/${projectId}`, + ); + return NextResponse.json(data, { status }); + } catch (e: unknown) { + return NextResponse.json( + { error: e instanceof Error ? e.message : String(e) }, + { status: 500 }, + ); + } +} + +export async function PATCH(request: NextRequest, { params }: Params) { + const { orgId, projectId } = await params; + try { + const body = await request.json(); + const { status, data } = await apiClient( + request, + `/api/v1/credentials/${orgId}/${projectId}`, + { method: "PATCH", body: JSON.stringify(body) }, + ); + return NextResponse.json(data, { status }); + } catch (e: unknown) { + return NextResponse.json( + { error: e instanceof Error ? e.message : String(e) }, + { status: 500 }, + ); + } +} + +export async function DELETE(request: NextRequest, { params }: Params) { + const { orgId, projectId } = await params; + try { + const { status, data } = await apiClient( + request, + `/api/v1/credentials/${orgId}/${projectId}`, + { method: "DELETE" }, + ); + if (status === 204) return new NextResponse(null, { status: 204 }); + return NextResponse.json(data, { status }); + } catch (e: unknown) { + return NextResponse.json( + { error: e instanceof Error ? e.message : String(e) }, + { status: 500 }, + ); + } +} diff --git a/app/api/users/me/route.ts b/app/api/users/me/route.ts index f910e1c3..6b9fe0f2 100644 --- a/app/api/users/me/route.ts +++ b/app/api/users/me/route.ts @@ -1,10 +1,17 @@ import { NextRequest, NextResponse } from "next/server"; import { apiClient } from "@/app/lib/apiClient"; +import { setRoleCookieFromBody } from "@/app/lib/authCookie"; export async function GET(request: NextRequest) { try { const { status, data } = await apiClient(request, "/api/v1/users/me"); - return NextResponse.json(data, { status }); + const res = NextResponse.json(data, { status }); + + if (status >= 200 && status < 300) { + setRoleCookieFromBody(res, data); + } + + return res; } catch { return NextResponse.json( { error: "Failed to connect to backend" }, diff --git a/app/components/Field.tsx b/app/components/Field.tsx index f6a3a990..7d90b0b8 100644 --- a/app/components/Field.tsx +++ b/app/components/Field.tsx @@ -48,7 +48,7 @@ export default function Field({ + +
+
+
); } - -function getToastStyles(type: ToastType) { - switch (type) { - case "success": - return { - bg: "#f0fdf4", - border: "#86efac", - text: "#15803d", - icon: "#16a34a", - }; - case "error": - return { - bg: "#fef2f2", - border: "#fca5a5", - text: "#b91c1c", - icon: "#dc2626", - }; - case "warning": - return { - bg: "#fffbeb", - border: "#fcd34d", - text: "#b45309", - icon: "#f59e0b", - }; - case "info": - default: - return { - bg: "#eff6ff", - border: "#93c5fd", - text: "#1e40af", - icon: "#3b82f6", - }; - } -} diff --git a/app/components/icons/common/ErrorCircleIcon.tsx b/app/components/icons/common/ErrorCircleIcon.tsx new file mode 100644 index 00000000..a0f604e5 --- /dev/null +++ b/app/components/icons/common/ErrorCircleIcon.tsx @@ -0,0 +1,23 @@ +interface IconProps { + className?: string; + style?: React.CSSProperties; +} + +export default function ErrorCircleIcon({ className, style }: IconProps) { + return ( + + + + ); +} diff --git a/app/components/icons/document/CloseIcon.tsx b/app/components/icons/document/CloseIcon.tsx index af939c24..59b151c5 100644 --- a/app/components/icons/document/CloseIcon.tsx +++ b/app/components/icons/document/CloseIcon.tsx @@ -1,8 +1,9 @@ interface IconProps { className?: string; + style?: React.CSSProperties; } -export default function CloseIcon({ className }: IconProps) { +export default function CloseIcon({ className, style }: IconProps) { return ( { - const params = new URLSearchParams(); - if (currentConfigId && currentConfigVersion) { - params.set("config", currentConfigId); - params.set("version", currentConfigVersion.toString()); - } - if (datasetId) params.set("dataset", datasetId); - if (experimentName) params.set("experiment", experimentName); - - router.push(`/evaluations?${params.toString()}`); - }; - return ( - - {fromEvaluations ? : } - {fromEvaluations ? "Back to Evaluation" : "Run Evaluation"} - - } - > +
+
+ +
+
+ ); +} diff --git a/app/components/settings/SettingsSidebar.tsx b/app/components/settings/SettingsSidebar.tsx index e2cc8893..e0598e52 100644 --- a/app/components/settings/SettingsSidebar.tsx +++ b/app/components/settings/SettingsSidebar.tsx @@ -6,6 +6,7 @@ import Image from "next/image"; import { ArrowLeftIcon, KeyIcon, SlidersIcon } from "@/app/components/icons"; import { SETTINGS_NAV } from "@/app/lib/navConfig"; import { useAuth } from "@/app/lib/context/AuthContext"; +import { useApp } from "@/app/lib/context/AppContext"; import { Branding, UserMenuPopover } from "@/app/components/user-menu"; const iconMap: Record = { @@ -17,6 +18,7 @@ export default function SettingsSidebar() { const router = useRouter(); const pathname = usePathname(); const { currentUser, googleProfile, isAuthenticated, logout } = useAuth(); + const { sidebarCollapsed } = useApp(); const [showUserMenu, setShowUserMenu] = useState(false); const userMenuRef = useRef(null); @@ -41,7 +43,9 @@ export default function SettingsSidebar() { .toUpperCase(); return ( -