mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-13 16:00:02 +02:00
show sso page in browser for gateway resource
This commit is contained in:
+5
-5
@@ -1699,16 +1699,16 @@
|
|||||||
"virtualApiKeysFilterUnassigned": "Unassigned",
|
"virtualApiKeysFilterUnassigned": "Unassigned",
|
||||||
"myVirtualApiKeysTitle": "Your API Keys",
|
"myVirtualApiKeysTitle": "Your API Keys",
|
||||||
"myVirtualApiKeysDescription": "View your identity key and any virtual API keys attributed to you in this organization",
|
"myVirtualApiKeysDescription": "View your identity key and any virtual API keys attributed to you in this organization",
|
||||||
"myVirtualApiKeysResourceTitle": "Your API Keys for This Resource",
|
"myVirtualApiKeysResourceTitle": "Your API Keys for {resourceName}",
|
||||||
"myVirtualApiKeysResourceDescription": "View your identity key and virtual API keys attributed to you that can access this resource",
|
"myVirtualApiKeysResourceDescription": "View your identity key and virtual API keys attributed to you that can access {resourceName}",
|
||||||
"myVirtualApiKeysIdentityTitle": "Identity Key",
|
"myVirtualApiKeysIdentityTitle": "Identity Key",
|
||||||
"myVirtualApiKeysIdentityHeadline": "Your Personal API Key",
|
"myVirtualApiKeysIdentityHeadline": "Your Personal API Key",
|
||||||
"myVirtualApiKeysIdentityDescription": "Your personal key for this organization. It is unique to your account and used to identify you when calling AI Gateway resources.",
|
"myVirtualApiKeysIdentityDescription": "Your personal key for this organization. It is unique to your account and used to identify you when calling AI Gateway resources.",
|
||||||
"myVirtualApiKeysIdentityResourceHeadline": "Your Personal API Key for This Resource",
|
"myVirtualApiKeysIdentityResourceHeadline": "Your Personal API Key for {resourceName}",
|
||||||
"myVirtualApiKeysIdentityResourceDescription": "Your personal key for this organization. Use it to call this AI Gateway resource.",
|
"myVirtualApiKeysIdentityResourceDescription": "Your personal key for this organization. Use it to call {resourceName}.",
|
||||||
"myVirtualApiKeysManualTitle": "Attributed Keys",
|
"myVirtualApiKeysManualTitle": "Attributed Keys",
|
||||||
"myVirtualApiKeysManualDescription": "Manual virtual API keys an admin associated with your account",
|
"myVirtualApiKeysManualDescription": "Manual virtual API keys an admin associated with your account",
|
||||||
"myVirtualApiKeysManualResourceDescription": "Manual virtual API keys associated with your account that can access this resource",
|
"myVirtualApiKeysManualResourceDescription": "Manual virtual API keys associated with your account that can access {resourceName}",
|
||||||
"myVirtualApiKeysManualEmpty": "No attributed keys yet",
|
"myVirtualApiKeysManualEmpty": "No attributed keys yet",
|
||||||
"myVirtualApiKeysKindUser": "Identity",
|
"myVirtualApiKeysKindUser": "Identity",
|
||||||
"myVirtualApiKeysKindManual": "Manual",
|
"myVirtualApiKeysKindManual": "Manual",
|
||||||
|
|||||||
@@ -329,16 +329,10 @@ export async function verifyResourceSession(
|
|||||||
// Only offer a browser redirect to clients that can actually follow one and log in
|
// Only offer a browser redirect to clients that can actually follow one and log in
|
||||||
// (an interactive browser). Non-browser clients (curl, scripts, bots, etc.) just get
|
// (an interactive browser). Non-browser clients (curl, scripts, bots, etc.) just get
|
||||||
// an unauthorized response from Badger instead of a login redirect URL.
|
// an unauthorized response from Badger instead of a login redirect URL.
|
||||||
// Inference browsers go to the dashboard keys page (not back to the inference host)
|
|
||||||
// so a valid session cannot create a redirect loop.
|
|
||||||
const redirectPath = clientIsBrowser
|
const redirectPath = clientIsBrowser
|
||||||
? mode === "inference"
|
? `/auth/resource/${encodeURIComponent(
|
||||||
? `/${resource.orgId}/resource/${encodeURIComponent(
|
resource.resourceGuid
|
||||||
resource.resourceGuid
|
)}?redirect=${encodeURIComponent(originalRequestURL)}`
|
||||||
)}/keys`
|
|
||||||
: `/auth/resource/${encodeURIComponent(
|
|
||||||
resource.resourceGuid
|
|
||||||
)}?redirect=${encodeURIComponent(originalRequestURL)}`
|
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
// Virtual API keys for public inference resources (provider-style auth headers).
|
// Virtual API keys for public inference resources (provider-style auth headers).
|
||||||
@@ -414,7 +408,7 @@ export async function verifyResourceSession(
|
|||||||
parsedBody.data
|
parsedBody.data
|
||||||
);
|
);
|
||||||
|
|
||||||
return notAllowed(res, redirectPath);
|
return notAllowed(res, redirectPath, resource.orgId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for access token in headers
|
// check for access token in headers
|
||||||
|
|||||||
@@ -123,10 +123,12 @@ export async function listMyVirtualApiKeys(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let resourceId: number | undefined;
|
let resourceId: number | undefined;
|
||||||
|
let resourceName: string | undefined;
|
||||||
if (resourceGuid) {
|
if (resourceGuid) {
|
||||||
const [resource] = await db
|
const [resource] = await db
|
||||||
.select({
|
.select({
|
||||||
resourceId: resources.resourceId
|
resourceId: resources.resourceId,
|
||||||
|
name: resources.name
|
||||||
})
|
})
|
||||||
.from(resources)
|
.from(resources)
|
||||||
.where(
|
.where(
|
||||||
@@ -147,6 +149,7 @@ export async function listMyVirtualApiKeys(
|
|||||||
}
|
}
|
||||||
|
|
||||||
resourceId = resource.resourceId;
|
resourceId = resource.resourceId;
|
||||||
|
resourceName = resource.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { key: userKeyRow } = await getOrCreateUserVirtualApiKey({
|
const { key: userKeyRow } = await getOrCreateUserVirtualApiKey({
|
||||||
@@ -203,7 +206,8 @@ export async function listMyVirtualApiKeys(
|
|||||||
userKey: toKeyWithResources(userKeyRow, resourceIdsByKey),
|
userKey: toKeyWithResources(userKeyRow, resourceIdsByKey),
|
||||||
manualKeys: manualRows.map((row) =>
|
manualKeys: manualRows.map((row) =>
|
||||||
toKeyWithResources(row, resourceIdsByKey)
|
toKeyWithResources(row, resourceIdsByKey)
|
||||||
)
|
),
|
||||||
|
...(resourceName !== undefined ? { resourceName } : {})
|
||||||
},
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export type CreateOrEditVirtualApiKeyResponse = {
|
|||||||
export type ListMyVirtualApiKeysResponse = {
|
export type ListMyVirtualApiKeysResponse = {
|
||||||
userKey: VirtualApiKeyWithResources;
|
userKey: VirtualApiKeyWithResources;
|
||||||
manualKeys: VirtualApiKeyWithResources[];
|
manualKeys: VirtualApiKeyWithResources[];
|
||||||
|
resourceName?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GetMyVirtualApiKeyResponse = {
|
export type GetMyVirtualApiKeyResponse = {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { cache } from "react";
|
|||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
const t = await getTranslations();
|
const t = await getTranslations();
|
||||||
return {
|
return {
|
||||||
title: t("myVirtualApiKeysResourceTitle")
|
title: t("myVirtualApiKeysTitle")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ export default async function ResourceKeysPage(props: ResourceKeysPageProps) {
|
|||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
redirect(
|
redirect(
|
||||||
`/auth/login?redirect=/${orgId}/resource/${resourceGuid}/keys`
|
`/auth/resource/${encodeURIComponent(resourceGuid)}?redirect=${encodeURIComponent(`/${orgId}/resource/${resourceGuid}/keys`)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,11 +113,7 @@ export default async function ResourceKeysPage(props: ResourceKeysPageProps) {
|
|||||||
launcherMode
|
launcherMode
|
||||||
showViewAsAdmin={isAdminOrOwner}
|
showViewAsAdmin={isAdminOrOwner}
|
||||||
>
|
>
|
||||||
<UserVirtualApiKeys
|
<UserVirtualApiKeys orgId={orgId} initialData={keysData} />
|
||||||
orgId={orgId}
|
|
||||||
resourceGuid={resourceGuid}
|
|
||||||
initialData={keysData}
|
|
||||||
/>
|
|
||||||
</Layout>
|
</Layout>
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -163,17 +163,11 @@ export default async function ResourceAuthPage(props: {
|
|||||||
if (user && !user.emailVerified && env.flags.emailVerificationRequired) {
|
if (user && !user.emailVerified && env.flags.emailVerificationRequired) {
|
||||||
redirect(
|
redirect(
|
||||||
`/auth/verify-email?redirect=${encodeURIComponent(
|
`/auth/verify-email?redirect=${encodeURIComponent(
|
||||||
isInference
|
`/auth/resource/${authInfo.resourceGuid}`
|
||||||
? keysPath
|
|
||||||
: `/auth/resource/${authInfo.resourceGuid}`
|
|
||||||
)}`
|
)}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isInference && !user) {
|
|
||||||
redirect(`/auth/login?redirect=${encodeURIComponent(keysPath)}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const cookie = await authCookieHeader();
|
const cookie = await authCookieHeader();
|
||||||
|
|
||||||
// Check org policy compliance before proceeding
|
// Check org policy compliance before proceeding
|
||||||
@@ -192,9 +186,7 @@ export default async function ResourceAuthPage(props: {
|
|||||||
|
|
||||||
// If user is not compliant with org policies, show policy requirements
|
// If user is not compliant with org policies, show policy requirements
|
||||||
if (orgPolicyCheck && !orgPolicyCheck.allowed && orgPolicyCheck.policies) {
|
if (orgPolicyCheck && !orgPolicyCheck.allowed && orgPolicyCheck.policies) {
|
||||||
const resourceAuthPageUrl = isInference
|
const resourceAuthPageUrl = `/auth/resource/${authInfo.resourceGuid}${redirectUrl !== authInfo.url ? `?redirect=${encodeURIComponent(redirectUrl)}` : ""}`;
|
||||||
? keysPath
|
|
||||||
: `/auth/resource/${authInfo.resourceGuid}${redirectUrl !== authInfo.url ? `?redirect=${encodeURIComponent(redirectUrl)}` : ""}`;
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-md">
|
||||||
<OrgPolicyRequired
|
<OrgPolicyRequired
|
||||||
@@ -208,10 +200,14 @@ export default async function ResourceAuthPage(props: {
|
|||||||
|
|
||||||
// Inference resources never establish a resource session on the inference
|
// Inference resources never establish a resource session on the inference
|
||||||
// host. Authenticated users retrieve their virtual API key on the dashboard.
|
// host. Authenticated users retrieve their virtual API key on the dashboard.
|
||||||
if (isInference) {
|
if (isInference && user) {
|
||||||
redirect(keysPath);
|
redirect(keysPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// After password/pincode/SSO, do not send the browser back to the
|
||||||
|
// inference host (session alone cannot pass Badger). Land on keys instead.
|
||||||
|
const postAuthRedirect = isInference ? keysPath : redirectUrl;
|
||||||
|
|
||||||
if (!hasAuth) {
|
if (!hasAuth) {
|
||||||
// no authentication so always go straight to the resource
|
// no authentication so always go straight to the resource
|
||||||
redirect(redirectUrl);
|
redirect(redirectUrl);
|
||||||
@@ -296,7 +292,7 @@ export default async function ResourceAuthPage(props: {
|
|||||||
<AutoLoginHandler
|
<AutoLoginHandler
|
||||||
resourceId={authInfo.resourceId}
|
resourceId={authInfo.resourceId}
|
||||||
skipToIdpId={authInfo.skipToIdpId}
|
skipToIdpId={authInfo.skipToIdpId}
|
||||||
redirectUrl={redirectUrl}
|
redirectUrl={postAuthRedirect}
|
||||||
orgId={build === "saas" ? authInfo.orgId : undefined}
|
orgId={build === "saas" ? authInfo.orgId : undefined}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -334,7 +330,7 @@ export default async function ResourceAuthPage(props: {
|
|||||||
name: authInfo.resourceName,
|
name: authInfo.resourceName,
|
||||||
id: authInfo.resourceId
|
id: authInfo.resourceId
|
||||||
}}
|
}}
|
||||||
redirect={redirectUrl}
|
redirect={postAuthRedirect}
|
||||||
idps={loginIdps}
|
idps={loginIdps}
|
||||||
orgId={build === "saas" ? authInfo.orgId : undefined}
|
orgId={build === "saas" ? authInfo.orgId : undefined}
|
||||||
branding={
|
branding={
|
||||||
|
|||||||
@@ -4,11 +4,9 @@ import { useState } from "react";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { Badge } from "@app/components/ui/badge";
|
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import CopyTextBox from "@app/components/CopyTextBox";
|
import CopyTextBox from "@app/components/CopyTextBox";
|
||||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
|
||||||
import {
|
import {
|
||||||
SettingsContainer,
|
SettingsContainer,
|
||||||
SettingsFormCell,
|
SettingsFormCell,
|
||||||
@@ -34,7 +32,6 @@ import {
|
|||||||
|
|
||||||
type UserVirtualApiKeysProps = {
|
type UserVirtualApiKeysProps = {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
resourceGuid?: string;
|
|
||||||
initialData: ListMyVirtualApiKeysResponse;
|
initialData: ListMyVirtualApiKeysResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -131,12 +128,12 @@ function IdentityKeyCenterpiece({
|
|||||||
orgId,
|
orgId,
|
||||||
virtualApiKeyId,
|
virtualApiKeyId,
|
||||||
lastChars,
|
lastChars,
|
||||||
resourceGuid
|
resourceName
|
||||||
}: {
|
}: {
|
||||||
orgId: string;
|
orgId: string;
|
||||||
virtualApiKeyId: string;
|
virtualApiKeyId: string;
|
||||||
lastChars: string;
|
lastChars: string;
|
||||||
resourceGuid?: string;
|
resourceName?: string | null;
|
||||||
}) {
|
}) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const preview = formatVirtualApiKeyPreview(virtualApiKeyId, lastChars);
|
const preview = formatVirtualApiKeyPreview(virtualApiKeyId, lastChars);
|
||||||
@@ -145,11 +142,11 @@ function IdentityKeyCenterpiece({
|
|||||||
virtualApiKeyId
|
virtualApiKeyId
|
||||||
);
|
);
|
||||||
const displayValue = credential ?? preview;
|
const displayValue = credential ?? preview;
|
||||||
const headline = resourceGuid
|
const headline = resourceName
|
||||||
? t("myVirtualApiKeysIdentityResourceHeadline")
|
? t("myVirtualApiKeysIdentityResourceHeadline", { resourceName })
|
||||||
: t("myVirtualApiKeysIdentityHeadline");
|
: t("myVirtualApiKeysIdentityHeadline");
|
||||||
const description = resourceGuid
|
const description = resourceName
|
||||||
? t("myVirtualApiKeysIdentityResourceDescription")
|
? t("myVirtualApiKeysIdentityResourceDescription", { resourceName })
|
||||||
: t("myVirtualApiKeysIdentityDescription");
|
: t("myVirtualApiKeysIdentityDescription");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -220,17 +217,10 @@ function ManualKeyRow({
|
|||||||
|
|
||||||
export default function UserVirtualApiKeys({
|
export default function UserVirtualApiKeys({
|
||||||
orgId,
|
orgId,
|
||||||
resourceGuid,
|
|
||||||
initialData
|
initialData
|
||||||
}: UserVirtualApiKeysProps) {
|
}: UserVirtualApiKeysProps) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
const resourceName = initialData.resourceName;
|
||||||
const title = resourceGuid
|
|
||||||
? t("myVirtualApiKeysResourceTitle")
|
|
||||||
: t("myVirtualApiKeysTitle");
|
|
||||||
const description = resourceGuid
|
|
||||||
? t("myVirtualApiKeysResourceDescription")
|
|
||||||
: t("myVirtualApiKeysDescription");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -239,7 +229,7 @@ export default function UserVirtualApiKeys({
|
|||||||
orgId={orgId}
|
orgId={orgId}
|
||||||
virtualApiKeyId={initialData.userKey.virtualApiKeyId}
|
virtualApiKeyId={initialData.userKey.virtualApiKeyId}
|
||||||
lastChars={initialData.userKey.lastChars}
|
lastChars={initialData.userKey.lastChars}
|
||||||
resourceGuid={resourceGuid}
|
resourceName={resourceName}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{initialData.manualKeys.length > 0 ? (
|
{initialData.manualKeys.length > 0 ? (
|
||||||
@@ -249,9 +239,10 @@ export default function UserVirtualApiKeys({
|
|||||||
{t("myVirtualApiKeysManualTitle")}
|
{t("myVirtualApiKeysManualTitle")}
|
||||||
</SectionTitle>
|
</SectionTitle>
|
||||||
<SettingsSectionDescription>
|
<SettingsSectionDescription>
|
||||||
{resourceGuid
|
{resourceName
|
||||||
? t(
|
? t(
|
||||||
"myVirtualApiKeysManualResourceDescription"
|
"myVirtualApiKeysManualResourceDescription",
|
||||||
|
{ resourceName }
|
||||||
)
|
)
|
||||||
: t("myVirtualApiKeysManualDescription")}
|
: t("myVirtualApiKeysManualDescription")}
|
||||||
</SettingsSectionDescription>
|
</SettingsSectionDescription>
|
||||||
|
|||||||
@@ -202,7 +202,12 @@ export function LauncherInferenceApiKeysSection({
|
|||||||
</SettingsSubsectionTitle>
|
</SettingsSubsectionTitle>
|
||||||
<SettingsSubsectionDescription>
|
<SettingsSubsectionDescription>
|
||||||
{t(
|
{t(
|
||||||
"myVirtualApiKeysManualResourceDescription"
|
"myVirtualApiKeysManualResourceDescription",
|
||||||
|
{
|
||||||
|
resourceName:
|
||||||
|
data.resourceName ??
|
||||||
|
t("resource")
|
||||||
|
}
|
||||||
)}
|
)}
|
||||||
</SettingsSubsectionDescription>
|
</SettingsSubsectionDescription>
|
||||||
</SettingsSubsectionHeader>
|
</SettingsSubsectionHeader>
|
||||||
|
|||||||
Reference in New Issue
Block a user