Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23150b68db | ||
|
|
e5c46010a4 | ||
|
|
b2bf68d7a4 | ||
|
|
31cec4f5c9 | ||
|
|
84a0ad8dfb | ||
|
|
69466fd43b | ||
|
|
324e078c85 | ||
|
|
7385fc4529 | ||
|
|
66d1fa427f | ||
|
|
bee396a433 | ||
|
|
8727a52931 | ||
|
|
ed5c076a5a | ||
|
|
db5c575ae0 | ||
|
|
2a0f482578 | ||
|
|
c041cf371a | ||
|
|
c6daf09cd2 | ||
|
|
7296e2e012 | ||
|
|
43bf176ef7 |
@@ -55,6 +55,7 @@ OTEL_SERVICE_NAME="langfuse"
|
||||
|
||||
# Auth, optional configuration
|
||||
# AUTH_DOMAINS_WITH_SSO_ENFORCEMENT=domain1.com,domain2.com
|
||||
# AUTH_IGNORE_ACCOUNT_FIELDS=foo,bar
|
||||
# AUTH_DISABLE_USERNAME_PASSWORD=true
|
||||
# AUTH_DISABLE_SIGNUP=true
|
||||
# AUTH_SESSION_MAX_AGE=43200 # 30 days in minutes (default)
|
||||
@@ -67,6 +68,10 @@ OTEL_SERVICE_NAME="langfuse"
|
||||
# AUTH_GITHUB_CLIENT_ID=
|
||||
# AUTH_GITHUB_CLIENT_SECRET=
|
||||
# AUTH_GITHUB_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_GITHUB_ENTERPRISE_CLIENT_ID=
|
||||
# AUTH_GITHUB_ENTERPRISE_CLIENT_SECRET=
|
||||
# AUTH_GITHUB_ENTERPRISE_BASE_URL=
|
||||
# AUTH_GITHUB_ENTERPRISE_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_GITLAB_CLIENT_ID=
|
||||
# AUTH_GITLAB_CLIENT_SECRET=
|
||||
# AUTH_GITLAB_ALLOW_ACCOUNT_LINKING=false
|
||||
@@ -87,6 +92,10 @@ OTEL_SERVICE_NAME="langfuse"
|
||||
# AUTH_COGNITO_CLIENT_SECRET=
|
||||
# AUTH_COGNITO_ISSUER=
|
||||
# AUTH_COGNITO_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_KEYCLOAK_CLIENT_ID=
|
||||
# AUTH_KEYCLOAK_CLIENT_SECRET=
|
||||
# AUTH_KEYCLOAK_ISSUER=
|
||||
# AUTH_KEYCLOAK_ALLOW_ACCOUNT_LINKING=false
|
||||
# AUTH_CUSTOM_CLIENT_ID=
|
||||
# AUTH_CUSTOM_CLIENT_SECRET=
|
||||
# AUTH_CUSTOM_ISSUER=
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0",
|
||||
"axios": "^1.7.7",
|
||||
"next": "^14.2.15",
|
||||
"next-auth": "^4.24.7",
|
||||
"next-auth": "^4.24.11",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "langfuse",
|
||||
"version": "2.92.0",
|
||||
"version": "2.93.7",
|
||||
"author": "engineering@langfuse.com",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
"langchain": "^0.3.6",
|
||||
"langfuse-langchain": "3.30.3",
|
||||
"lodash": "^4.17.21",
|
||||
"next-auth": "^4.24.7",
|
||||
"next-auth": "^4.24.11",
|
||||
"nodemailer": "^6.9.15",
|
||||
"prisma-extension-kysely": "^2.1.0",
|
||||
"uuid": "^9.0.1",
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import type { OAuthConfig, OAuthUserConfig } from "next-auth/providers/oauth";
|
||||
import type { GithubProfile, GithubEmail } from "next-auth/providers/github";
|
||||
|
||||
export function GitHubEnterpriseProvider<P extends GithubProfile>(
|
||||
options: OAuthUserConfig<P> & {
|
||||
enterprise?: {
|
||||
baseUrl?: string;
|
||||
};
|
||||
}
|
||||
): OAuthConfig<P> {
|
||||
const baseUrl = options?.enterprise?.baseUrl ?? "https://github.com"
|
||||
const apiBaseUrl = options?.enterprise?.baseUrl
|
||||
? `${options?.enterprise?.baseUrl}/api/v3`
|
||||
: "https://api.github.com"
|
||||
|
||||
return {
|
||||
id: "github-enterprise",
|
||||
name: "GitHub Enterprise",
|
||||
type: "oauth",
|
||||
authorization: {
|
||||
url: `${baseUrl}/login/oauth/authorize`,
|
||||
params: { scope: "read:user user:email" },
|
||||
},
|
||||
token: `${baseUrl}/login/oauth/access_token`,
|
||||
userinfo: {
|
||||
url: `${apiBaseUrl}/user`,
|
||||
async request({ client, tokens }) {
|
||||
const profile = await client.userinfo(tokens.access_token!)
|
||||
|
||||
if (!profile.email) {
|
||||
// If the user does not have a public email, get another via the GitHub API
|
||||
// See https://docs.github.com/en/rest/users/emails#list-email-addresses-for-the-authenticated-user
|
||||
const res = await fetch(`${apiBaseUrl}/user/emails`, {
|
||||
headers: { Authorization: `token ${tokens.access_token}` },
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
const emails: GithubEmail[] = await res.json()
|
||||
profile.email = (emails.find((e) => e.primary) ?? emails[0]).email
|
||||
}
|
||||
}
|
||||
|
||||
return profile
|
||||
},
|
||||
},
|
||||
profile(profile) {
|
||||
return {
|
||||
id: profile.id.toString(),
|
||||
name: profile.name ?? profile.login,
|
||||
email: profile.email,
|
||||
image: profile.avatar_url,
|
||||
}
|
||||
},
|
||||
style: {
|
||||
logo: "https://raw.githubusercontent.com/nextauthjs/next-auth/main/packages/next-auth/provider-logos/github.svg",
|
||||
logoDark:
|
||||
"https://raw.githubusercontent.com/nextauthjs/next-auth/main/packages/next-auth/provider-logos/github-dark.svg",
|
||||
bg: "#fff",
|
||||
bgDark: "#000",
|
||||
text: "#000",
|
||||
textDark: "#fff",
|
||||
},
|
||||
options,
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ export * from "./services/PromptService";
|
||||
export * from "./services/traces-ui-table-service";
|
||||
export * from "./auth/apiKeys";
|
||||
export * from "./auth/customSsoProvider";
|
||||
export * from "./auth/gitHubEnterpriseProvider";
|
||||
export * from "./llm/fetchLLMCompletion";
|
||||
export * from "./llm/types";
|
||||
export * from "./utils/DatabaseReadStream";
|
||||
|
||||
Generated
+245
-356
File diff suppressed because it is too large
Load Diff
+39
-20
@@ -28,6 +28,9 @@ const cspHeader = `
|
||||
${env.SENTRY_CSP_REPORT_URI ? `report-uri ${env.SENTRY_CSP_REPORT_URI}; report-to csp-endpoint;` : ""}
|
||||
`;
|
||||
|
||||
// Match rules for Hugging Face
|
||||
const huggingFaceHosts = ["huggingface.co", ".*\\.hf\\.space$"];
|
||||
|
||||
const reportToHeader = {
|
||||
key: "Report-To",
|
||||
value: JSON.stringify({
|
||||
@@ -78,10 +81,6 @@ const nextConfig = {
|
||||
{
|
||||
source: "/:path*",
|
||||
headers: [
|
||||
{
|
||||
key: "x-frame-options",
|
||||
value: "SAMEORIGIN",
|
||||
},
|
||||
{
|
||||
key: "X-Content-Type-Options",
|
||||
value: "nosniff",
|
||||
@@ -97,6 +96,21 @@ const nextConfig = {
|
||||
...(env.SENTRY_CSP_REPORT_URI ? [reportToHeader] : []),
|
||||
],
|
||||
},
|
||||
{
|
||||
source: "/:path*",
|
||||
headers: [
|
||||
{
|
||||
key: "x-frame-options",
|
||||
value: "SAMEORIGIN",
|
||||
},
|
||||
],
|
||||
// Disable x-frame-options on Hugging Face to allow for embedded use of Langfuse
|
||||
missing: huggingFaceHosts.map((host) => ({
|
||||
type: "host",
|
||||
value: host,
|
||||
})),
|
||||
},
|
||||
// CSP header
|
||||
{
|
||||
source: "/:path((?!api).*)*",
|
||||
headers: [
|
||||
@@ -105,26 +119,31 @@ const nextConfig = {
|
||||
value: cspHeader.replace(/\n/g, ""),
|
||||
},
|
||||
],
|
||||
// Disable CSP on Hugging Face to allow for embedded use of Langfuse
|
||||
missing: huggingFaceHosts.map((host) => ({
|
||||
type: "host",
|
||||
value: host,
|
||||
})),
|
||||
},
|
||||
// Required to check authentication status from langfuse.com
|
||||
...(env.NEXT_PUBLIC_LANGFUSE_CLOUD_REGION !== undefined
|
||||
? [
|
||||
{
|
||||
source: "/api/auth/session",
|
||||
headers: [
|
||||
{
|
||||
key: "Access-Control-Allow-Origin",
|
||||
value: "https://langfuse.com",
|
||||
},
|
||||
{ key: "Access-Control-Allow-Credentials", value: "true" },
|
||||
{ key: "Access-Control-Allow-Methods", value: "GET,POST" },
|
||||
{
|
||||
key: "Access-Control-Allow-Headers",
|
||||
value: "Content-Type, Authorization",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
{
|
||||
source: "/api/auth/session",
|
||||
headers: [
|
||||
{
|
||||
key: "Access-Control-Allow-Origin",
|
||||
value: "https://langfuse.com",
|
||||
},
|
||||
{ key: "Access-Control-Allow-Credentials", value: "true" },
|
||||
{ key: "Access-Control-Allow-Methods", value: "GET,POST" },
|
||||
{
|
||||
key: "Access-Control-Allow-Headers",
|
||||
value: "Content-Type, Authorization",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
: []),
|
||||
// all files in /public/generated are public and can be accessed from any origin, e.g. to render an API reference based on our openapi schema
|
||||
{
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "2.92.0",
|
||||
"version": "2.93.7",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -117,7 +117,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.447.0",
|
||||
"next": "^14.2.15",
|
||||
"next-auth": "^4.24.7",
|
||||
"next-auth": "^4.24.11",
|
||||
"next-query-params": "^5.0.1",
|
||||
"next-themes": "^0.3.0",
|
||||
"posthog-js": "^1.176.0",
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 34 KiB |
@@ -57,6 +57,7 @@ const unauthenticatedPaths: string[] = [
|
||||
"/auth/sign-in",
|
||||
"/auth/sign-up",
|
||||
"/auth/error",
|
||||
"/auth/hf-spaces",
|
||||
];
|
||||
// auth or unauthed
|
||||
const publishablePaths: string[] = [
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v2.92.0";
|
||||
export const VERSION = "v2.93.7";
|
||||
|
||||
@@ -28,6 +28,20 @@ export const GithubProviderSchema = base.extend({
|
||||
.nullish(),
|
||||
});
|
||||
|
||||
export const GithubEnterpriseProviderSchema = base.extend({
|
||||
authProvider: z.literal("github-enterprise"),
|
||||
authConfig: z
|
||||
.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
enterprise: z.object({
|
||||
baseUrl: z.string().url(),
|
||||
}),
|
||||
allowDangerousEmailAccountLinking: z.boolean().optional().default(false),
|
||||
})
|
||||
.nullish(),
|
||||
});
|
||||
|
||||
export const GitlabProviderSchema = base.extend({
|
||||
authProvider: z.literal("gitlab"),
|
||||
authConfig: z
|
||||
@@ -88,6 +102,18 @@ export const CognitoProviderSchema = base.extend({
|
||||
.nullish(),
|
||||
});
|
||||
|
||||
export const KeycloakProviderSchema = base.extend({
|
||||
authProvider: z.literal("keycloak"),
|
||||
authConfig: z
|
||||
.object({
|
||||
clientId: z.string(),
|
||||
clientSecret: z.string(),
|
||||
issuer: z.string(),
|
||||
allowDangerousEmailAccountLinking: z.boolean().optional().default(false),
|
||||
})
|
||||
.nullish(),
|
||||
});
|
||||
|
||||
export const CustomProviderSchema = base.extend({
|
||||
authProvider: z.literal("custom"),
|
||||
authConfig: z
|
||||
@@ -104,21 +130,25 @@ export const CustomProviderSchema = base.extend({
|
||||
|
||||
export type GoogleProviderSchema = z.infer<typeof GoogleProviderSchema>;
|
||||
export type GithubProviderSchema = z.infer<typeof GithubProviderSchema>;
|
||||
export type GithubEnterpriseProviderSchema = z.infer<typeof GithubEnterpriseProviderSchema>;
|
||||
export type GitlabProviderSchema = z.infer<typeof GitlabProviderSchema>;
|
||||
export type Auth0ProviderSchema = z.infer<typeof Auth0ProviderSchema>;
|
||||
export type OktaProviderSchema = z.infer<typeof OktaProviderSchema>;
|
||||
export type AzureAdProviderSchema = z.infer<typeof AzureAdProviderSchema>;
|
||||
export type CognitoProviderSchema = z.infer<typeof CognitoProviderSchema>;
|
||||
export type KeycloakProviderSchema = z.infer<typeof KeycloakProviderSchema>;
|
||||
export type CustomProviderSchema = z.infer<typeof CustomProviderSchema>;
|
||||
|
||||
export const SsoProviderSchema = z.discriminatedUnion("authProvider", [
|
||||
GoogleProviderSchema,
|
||||
GithubProviderSchema,
|
||||
GithubEnterpriseProviderSchema,
|
||||
GitlabProviderSchema,
|
||||
Auth0ProviderSchema,
|
||||
OktaProviderSchema,
|
||||
AzureAdProviderSchema,
|
||||
CognitoProviderSchema,
|
||||
KeycloakProviderSchema,
|
||||
CustomProviderSchema,
|
||||
]);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import GitHubProvider from "next-auth/providers/github";
|
||||
import GitLabProvider from "next-auth/providers/gitlab";
|
||||
import OktaProvider from "next-auth/providers/okta";
|
||||
import CognitoProvider from "next-auth/providers/cognito";
|
||||
import KeycloakProvider from "next-auth/providers/keycloak";
|
||||
import Auth0Provider from "next-auth/providers/auth0";
|
||||
import AzureADProvider from "next-auth/providers/azure-ad";
|
||||
import { isEeEnabled } from "@/src/ee/utils/isEeEnabled";
|
||||
@@ -12,6 +13,7 @@ import { decrypt } from "@langfuse/shared/encryption";
|
||||
import { SsoProviderSchema } from "./types";
|
||||
import {
|
||||
CustomSSOProvider,
|
||||
GitHubEnterpriseProvider,
|
||||
logger,
|
||||
traceException,
|
||||
} from "@langfuse/shared/src/server";
|
||||
@@ -188,6 +190,12 @@ const dbToNextAuthProvider = (provider: SsoProviderSchema): Provider | null => {
|
||||
...provider.authConfig,
|
||||
clientSecret: decrypt(provider.authConfig.clientSecret),
|
||||
});
|
||||
else if (provider.authProvider === "keycloak")
|
||||
return KeycloakProvider({
|
||||
id: getAuthProviderIdForSsoConfig(provider), // use the domain as the provider id as we use domain-specific credentials
|
||||
...provider.authConfig,
|
||||
clientSecret: decrypt(provider.authConfig.clientSecret),
|
||||
});
|
||||
else if (provider.authProvider === "custom")
|
||||
return CustomSSOProvider({
|
||||
id: getAuthProviderIdForSsoConfig(provider), // use the domain as the provider id as we use domain-specific credentials
|
||||
@@ -197,6 +205,15 @@ const dbToNextAuthProvider = (provider: SsoProviderSchema): Provider | null => {
|
||||
params: { scope: provider.authConfig.scope ?? "openid email profile" },
|
||||
},
|
||||
});
|
||||
else if (provider.authProvider === "github-enterprise")
|
||||
return GitHubEnterpriseProvider({
|
||||
id: getAuthProviderIdForSsoConfig(provider), // use the domain as the provider id as we use domain-specific credentials
|
||||
...provider.authConfig,
|
||||
clientSecret: decrypt(provider.authConfig.clientSecret),
|
||||
enterprise: {
|
||||
baseUrl: provider.authConfig.enterprise.baseUrl,
|
||||
},
|
||||
});
|
||||
else {
|
||||
// Type check to ensure we handle all providers
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
+22
-1
@@ -46,7 +46,7 @@ export const env = createEnv({
|
||||
LANGFUSE_DEFAULT_PROJECT_ROLE: z
|
||||
.enum(["OWNER", "ADMIN", "MEMBER", "VIEWER"])
|
||||
.optional(),
|
||||
LANGFUSE_CSP_ENFORCE_HTTPS: z.enum(["true", "false"]).optional(),
|
||||
LANGFUSE_CSP_ENFORCE_HTTPS: z.enum(["true", "false"]).optional().default("false"),
|
||||
// Telemetry
|
||||
TELEMETRY_ENABLED: z.enum(["true", "false"]).optional(),
|
||||
// AUTH
|
||||
@@ -57,6 +57,10 @@ export const env = createEnv({
|
||||
AUTH_GITHUB_CLIENT_ID: z.string().optional(),
|
||||
AUTH_GITHUB_CLIENT_SECRET: z.string().optional(),
|
||||
AUTH_GITHUB_ALLOW_ACCOUNT_LINKING: z.enum(["true", "false"]).optional(),
|
||||
AUTH_GITHUB_ENTERPRISE_CLIENT_ID: z.string().optional(),
|
||||
AUTH_GITHUB_ENTERPRISE_CLIENT_SECRET: z.string().optional(),
|
||||
AUTH_GITHUB_ENTERPRISE_BASE_URL: z.string().optional(),
|
||||
AUTH_GITHUB_ENTERPRISE_ALLOW_ACCOUNT_LINKING: z.enum(["true", "false"]).optional(),
|
||||
AUTH_GITLAB_CLIENT_ID: z.string().optional(),
|
||||
AUTH_GITLAB_CLIENT_SECRET: z.string().optional(),
|
||||
AUTH_GITLAB_ALLOW_ACCOUNT_LINKING: z.enum(["true", "false"]).optional(),
|
||||
@@ -77,6 +81,10 @@ export const env = createEnv({
|
||||
AUTH_COGNITO_CLIENT_SECRET: z.string().optional(),
|
||||
AUTH_COGNITO_ISSUER: z.string().url().optional(),
|
||||
AUTH_COGNITO_ALLOW_ACCOUNT_LINKING: z.enum(["true", "false"]).optional(),
|
||||
AUTH_KEYCLOAK_CLIENT_ID: z.string().optional(),
|
||||
AUTH_KEYCLOAK_CLIENT_SECRET: z.string().optional(),
|
||||
AUTH_KEYCLOAK_ISSUER: z.string().optional(),
|
||||
AUTH_KEYCLOAK_ALLOW_ACCOUNT_LINKING: z.enum(["true", "false"]).optional(),
|
||||
AUTH_CUSTOM_CLIENT_ID: z.string().optional(),
|
||||
AUTH_CUSTOM_CLIENT_SECRET: z.string().optional(),
|
||||
AUTH_CUSTOM_ISSUER: z.string().url().optional(),
|
||||
@@ -84,6 +92,7 @@ export const env = createEnv({
|
||||
AUTH_CUSTOM_SCOPE: z.string().optional(),
|
||||
AUTH_CUSTOM_ALLOW_ACCOUNT_LINKING: z.enum(["true", "false"]).optional(),
|
||||
AUTH_DOMAINS_WITH_SSO_ENFORCEMENT: z.string().optional(),
|
||||
AUTH_IGNORE_ACCOUNT_FIELDS: z.string().optional(),
|
||||
AUTH_DISABLE_USERNAME_PASSWORD: z.enum(["true", "false"]).optional(),
|
||||
AUTH_DISABLE_SIGNUP: z.enum(["true", "false"]).optional(),
|
||||
AUTH_SESSION_MAX_AGE: z.coerce
|
||||
@@ -291,6 +300,11 @@ export const env = createEnv({
|
||||
AUTH_GITHUB_CLIENT_SECRET: process.env.AUTH_GITHUB_CLIENT_SECRET,
|
||||
AUTH_GITHUB_ALLOW_ACCOUNT_LINKING:
|
||||
process.env.AUTH_GITHUB_ALLOW_ACCOUNT_LINKING,
|
||||
AUTH_GITHUB_ENTERPRISE_CLIENT_ID: process.env.AUTH_GITHUB_ENTERPRISE_CLIENT_ID,
|
||||
AUTH_GITHUB_ENTERPRISE_CLIENT_SECRET: process.env.AUTH_GITHUB_ENTERPRISE_CLIENT_SECRET,
|
||||
AUTH_GITHUB_ENTERPRISE_BASE_URL: process.env.AUTH_GITHUB_ENTERPRISE_BASE_URL,
|
||||
AUTH_GITHUB_ENTERPRISE_ALLOW_ACCOUNT_LINKING:
|
||||
process.env.AUTH_GITHUB_ENTERPRISE_ALLOW_ACCOUNT_LINKING,
|
||||
AUTH_GITLAB_ISSUER: process.env.AUTH_GITLAB_ISSUER,
|
||||
AUTH_GITLAB_CLIENT_ID: process.env.AUTH_GITLAB_CLIENT_ID,
|
||||
AUTH_GITLAB_CLIENT_SECRET: process.env.AUTH_GITLAB_CLIENT_SECRET,
|
||||
@@ -316,6 +330,11 @@ export const env = createEnv({
|
||||
AUTH_COGNITO_ISSUER: process.env.AUTH_COGNITO_ISSUER,
|
||||
AUTH_COGNITO_ALLOW_ACCOUNT_LINKING:
|
||||
process.env.AUTH_COGNITO_ALLOW_ACCOUNT_LINKING,
|
||||
AUTH_KEYCLOAK_CLIENT_ID: process.env.AUTH_KEYCLOAK_CLIENT_ID,
|
||||
AUTH_KEYCLOAK_CLIENT_SECRET: process.env.AUTH_KEYCLOAK_CLIENT_SECRET,
|
||||
AUTH_KEYCLOAK_ISSUER: process.env.AUTH_KEYCLOAK_ISSUER,
|
||||
AUTH_KEYCLOAK_ALLOW_ACCOUNT_LINKING:
|
||||
process.env.AUTH_KEYCLOAK_ALLOW_ACCOUNT_LINKING,
|
||||
AUTH_CUSTOM_CLIENT_ID: process.env.AUTH_CUSTOM_CLIENT_ID,
|
||||
AUTH_CUSTOM_CLIENT_SECRET: process.env.AUTH_CUSTOM_CLIENT_SECRET,
|
||||
AUTH_CUSTOM_ISSUER: process.env.AUTH_CUSTOM_ISSUER,
|
||||
@@ -323,6 +342,8 @@ export const env = createEnv({
|
||||
AUTH_CUSTOM_SCOPE: process.env.AUTH_CUSTOM_SCOPE,
|
||||
AUTH_CUSTOM_ALLOW_ACCOUNT_LINKING:
|
||||
process.env.AUTH_CUSTOM_ALLOW_ACCOUNT_LINKING,
|
||||
AUTH_IGNORE_ACCOUNT_FIELDS:
|
||||
process.env.AUTH_IGNORE_ACCOUNT_FIELDS,
|
||||
AUTH_DOMAINS_WITH_SSO_ENFORCEMENT:
|
||||
process.env.AUTH_DOMAINS_WITH_SSO_ENFORCEMENT,
|
||||
AUTH_DISABLE_USERNAME_PASSWORD: process.env.AUTH_DISABLE_USERNAME_PASSWORD,
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* When running Langfuse in HuggingFace Spaces, the app needs to be opened in a new tab.
|
||||
* Otherwise, the app will not be able to access the session cookie.
|
||||
*/
|
||||
|
||||
import { Button } from "@/src/components/ui/button";
|
||||
import { LangfuseIcon } from "@/src/components/LangfuseLogo";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { type GetServerSideProps } from "next";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { CodeView } from "@/src/components/ui/CodeJsonViewer";
|
||||
|
||||
type PageProps = {
|
||||
deploymentDomain: string;
|
||||
};
|
||||
|
||||
export const getServerSideProps: GetServerSideProps<PageProps> = async () => {
|
||||
// remove /api/auth from the URL as it needs to be added for custom base url
|
||||
const deploymentDomain = env.NEXTAUTH_URL?.replace("/api/auth", "");
|
||||
return {
|
||||
props: {
|
||||
deploymentDomain,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default function HfSpaces({ deploymentDomain }: PageProps) {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Langfuse on Hugging Face</title>
|
||||
</Head>
|
||||
<div className="flex flex-1 flex-col py-6 sm:min-h-full sm:justify-center sm:px-6 sm:py-12 lg:px-8">
|
||||
<div className="sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<LangfuseIcon />
|
||||
<PlusIcon size={12} className="ml-1" />
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src="/assets/huggingface-logo.svg"
|
||||
alt="Hugging Face Logo"
|
||||
width={36}
|
||||
height={36}
|
||||
/>
|
||||
</div>
|
||||
<h2 className="mt-4 text-center text-2xl font-bold leading-9 tracking-tight text-primary">
|
||||
Langfuse on Hugging Face
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="mt-14 bg-background px-6 py-10 shadow sm:mx-auto sm:w-full sm:max-w-[480px] sm:rounded-lg sm:px-10">
|
||||
<div className="space-y-8">
|
||||
<CodeView content={deploymentDomain} title="HF Space Host" />
|
||||
|
||||
<Button className="w-full" asChild>
|
||||
<Link
|
||||
href={deploymentDomain}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Open in new tab
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import { env } from "@/src/env.mjs";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { FcGoogle } from "react-icons/fc";
|
||||
import { FaGithub, FaGitlab } from "react-icons/fa";
|
||||
import { SiOkta, SiAuth0, SiAmazoncognito } from "react-icons/si";
|
||||
import { SiOkta, SiAuth0, SiAmazoncognito, SiKeycloak } from "react-icons/si";
|
||||
import { TbBrandAzure, TbBrandOauth } from "react-icons/tb";
|
||||
import { signIn } from "next-auth/react";
|
||||
import Head from "next/head";
|
||||
@@ -46,11 +46,13 @@ export type PageProps = {
|
||||
credentials: boolean;
|
||||
google: boolean;
|
||||
github: boolean;
|
||||
githubEnterprise: boolean;
|
||||
gitlab: boolean;
|
||||
okta: boolean;
|
||||
azureAd: boolean;
|
||||
auth0: boolean;
|
||||
cognito: boolean;
|
||||
keycloak: boolean;
|
||||
custom:
|
||||
| {
|
||||
name: string;
|
||||
@@ -58,6 +60,7 @@ export type PageProps = {
|
||||
| false;
|
||||
sso: boolean;
|
||||
};
|
||||
runningOnHuggingFaceSpaces: boolean;
|
||||
signUpDisabled: boolean;
|
||||
};
|
||||
|
||||
@@ -74,6 +77,10 @@ export const getServerSideProps: GetServerSideProps<PageProps> = async () => {
|
||||
github:
|
||||
env.AUTH_GITHUB_CLIENT_ID !== undefined &&
|
||||
env.AUTH_GITHUB_CLIENT_SECRET !== undefined,
|
||||
githubEnterprise:
|
||||
env.AUTH_GITHUB_ENTERPRISE_CLIENT_ID !== undefined &&
|
||||
env.AUTH_GITHUB_ENTERPRISE_CLIENT_SECRET !== undefined &&
|
||||
env.AUTH_GITHUB_ENTERPRISE_BASE_URL !== undefined,
|
||||
gitlab:
|
||||
env.AUTH_GITLAB_CLIENT_ID !== undefined &&
|
||||
env.AUTH_GITLAB_CLIENT_SECRET !== undefined,
|
||||
@@ -94,6 +101,10 @@ export const getServerSideProps: GetServerSideProps<PageProps> = async () => {
|
||||
env.AUTH_COGNITO_CLIENT_ID !== undefined &&
|
||||
env.AUTH_COGNITO_CLIENT_SECRET !== undefined &&
|
||||
env.AUTH_COGNITO_ISSUER !== undefined,
|
||||
keycloak:
|
||||
env.AUTH_KEYCLOAK_CLIENT_ID !== undefined &&
|
||||
env.AUTH_KEYCLOAK_CLIENT_SECRET !== undefined &&
|
||||
env.AUTH_KEYCLOAK_ISSUER !== undefined,
|
||||
custom:
|
||||
env.AUTH_CUSTOM_CLIENT_ID !== undefined &&
|
||||
env.AUTH_CUSTOM_CLIENT_SECRET !== undefined &&
|
||||
@@ -104,6 +115,10 @@ export const getServerSideProps: GetServerSideProps<PageProps> = async () => {
|
||||
sso,
|
||||
},
|
||||
signUpDisabled: env.AUTH_DISABLE_SIGNUP === "true",
|
||||
runningOnHuggingFaceSpaces: env.NEXTAUTH_URL?.replace(
|
||||
"/api/auth",
|
||||
"",
|
||||
).endsWith(".hf.space"),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -165,6 +180,16 @@ export function SSOButtons({
|
||||
Github
|
||||
</Button>
|
||||
)}
|
||||
{authProviders.githubEnterprise && (
|
||||
<Button
|
||||
onClick={() => handleSignIn("github-enterprise")}
|
||||
variant="secondary"
|
||||
loading={providerSigningIn === "github-enterprise"}
|
||||
>
|
||||
<FaGithub className="mr-3" size={18} />
|
||||
Github Enterprise
|
||||
</Button>
|
||||
)}
|
||||
{authProviders.gitlab && (
|
||||
<Button
|
||||
onClick={() => handleSignIn("gitlab")}
|
||||
@@ -215,6 +240,18 @@ export function SSOButtons({
|
||||
Cognito
|
||||
</Button>
|
||||
)}
|
||||
{authProviders.keycloak && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
capture("sign_in:button_click", { provider: "keycloak" });
|
||||
void signIn("keycloak");
|
||||
}}
|
||||
variant="secondary"
|
||||
>
|
||||
<SiKeycloak className="mr-3" size={18} />
|
||||
Keycloak
|
||||
</Button>
|
||||
)}
|
||||
{authProviders.custom && (
|
||||
<Button
|
||||
onClick={() => handleSignIn("custom")}
|
||||
@@ -231,6 +268,33 @@ export function SSOButtons({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to HuggingFace Spaces auth page (/auth/hf-spaces) if running in an iframe on a HuggingFace host.
|
||||
* The iframe detection needs to happen client-side since window/document objects are not available during SSR.
|
||||
* @param runningOnHuggingFaceSpaces - whether the app is running on a HuggingFace spaces, needs to be checked server-side
|
||||
*/
|
||||
export function useHuggingFaceRedirect(runningOnHuggingFaceSpaces: boolean) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const isInIframe = () => {
|
||||
try {
|
||||
return window.self !== window.top;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
if (
|
||||
runningOnHuggingFaceSpaces &&
|
||||
typeof window !== "undefined" &&
|
||||
isInIframe()
|
||||
) {
|
||||
void router.push("/auth/hf-spaces");
|
||||
}
|
||||
}, [router, runningOnHuggingFaceSpaces]);
|
||||
}
|
||||
|
||||
const signInErrors = [
|
||||
{
|
||||
code: "OAuthAccountNotLinked",
|
||||
@@ -239,8 +303,13 @@ const signInErrors = [
|
||||
},
|
||||
];
|
||||
|
||||
export default function SignIn({ authProviders, signUpDisabled }: PageProps) {
|
||||
export default function SignIn({
|
||||
authProviders,
|
||||
signUpDisabled,
|
||||
runningOnHuggingFaceSpaces,
|
||||
}: PageProps) {
|
||||
const router = useRouter();
|
||||
useHuggingFaceRedirect(runningOnHuggingFaceSpaces);
|
||||
|
||||
// handle NextAuth error codes: https://next-auth.js.org/configuration/pages#sign-in-page
|
||||
const nextAuthError =
|
||||
|
||||
@@ -20,7 +20,11 @@ import { useState } from "react";
|
||||
import { LangfuseIcon } from "@/src/components/LangfuseLogo";
|
||||
import { CloudPrivacyNotice } from "@/src/features/auth/components/AuthCloudPrivacyNotice";
|
||||
import { CloudRegionSwitch } from "@/src/features/auth/components/AuthCloudRegionSwitch";
|
||||
import { SSOButtons, type PageProps } from "@/src/pages/auth/sign-in";
|
||||
import {
|
||||
SSOButtons,
|
||||
useHuggingFaceRedirect,
|
||||
type PageProps,
|
||||
} from "@/src/pages/auth/sign-in";
|
||||
import { PasswordInput } from "@/src/components/ui/password-input";
|
||||
import { Divider } from "@tremor/react";
|
||||
import { Turnstile } from "@marsidev/react-turnstile";
|
||||
@@ -28,7 +32,12 @@ import { Turnstile } from "@marsidev/react-turnstile";
|
||||
// Use the same getServerSideProps function as src/pages/auth/sign-in.tsx
|
||||
export { getServerSideProps } from "@/src/pages/auth/sign-in";
|
||||
|
||||
export default function SignIn({ authProviders }: PageProps) {
|
||||
export default function SignIn({
|
||||
authProviders,
|
||||
runningOnHuggingFaceSpaces,
|
||||
}: PageProps) {
|
||||
useHuggingFaceRedirect(runningOnHuggingFaceSpaces);
|
||||
|
||||
const [turnstileToken, setTurnstileToken] = useState<string>();
|
||||
// Used to refresh turnstile as the token can only be used once
|
||||
const [turnstileCData, setTurnstileCData] = useState<string>(
|
||||
|
||||
+65
-2
@@ -11,7 +11,6 @@ import { verifyPassword } from "@/src/features/auth-credentials/lib/credentialsS
|
||||
import { parseFlags } from "@/src/features/feature-flags/utils";
|
||||
import { env } from "@/src/env.mjs";
|
||||
import { createProjectMembershipsOnSignup } from "@/src/features/auth/lib/createProjectMembershipsOnSignup";
|
||||
import { type Adapter } from "next-auth/adapters";
|
||||
|
||||
// Providers
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
@@ -23,6 +22,7 @@ import EmailProvider from "next-auth/providers/email";
|
||||
import Auth0Provider from "next-auth/providers/auth0";
|
||||
import CognitoProvider from "next-auth/providers/cognito";
|
||||
import AzureADProvider from "next-auth/providers/azure-ad";
|
||||
import KeycloakProvider from "next-auth/providers/keycloak";
|
||||
import { type Provider } from "next-auth/providers/index";
|
||||
import { getCookieName, getCookieOptions } from "./utils/cookies";
|
||||
import {
|
||||
@@ -33,6 +33,7 @@ import { z } from "zod";
|
||||
import { CloudConfigSchema } from "@langfuse/shared";
|
||||
import {
|
||||
CustomSSOProvider,
|
||||
GitHubEnterpriseProvider,
|
||||
traceException,
|
||||
sendResetPasswordVerificationRequest,
|
||||
instrumentAsync,
|
||||
@@ -40,6 +41,11 @@ import {
|
||||
} from "@langfuse/shared/src/server";
|
||||
import { getOrganizationPlan } from "@/src/features/entitlements/server/getOrganizationPlan";
|
||||
import { projectRoleAccessRights } from "@/src/features/rbac/constants/projectAccessRights";
|
||||
import {
|
||||
type AdapterUser,
|
||||
type Adapter,
|
||||
type AdapterAccount,
|
||||
} from "next-auth/adapters";
|
||||
|
||||
function canCreateOrganizations(userEmail: string | null): boolean {
|
||||
// if no allowlist is set or no active EE key, allow all users to create organizations
|
||||
@@ -227,6 +233,22 @@ if (env.AUTH_GITHUB_CLIENT_ID && env.AUTH_GITHUB_CLIENT_SECRET)
|
||||
}),
|
||||
);
|
||||
|
||||
if (
|
||||
env.AUTH_GITHUB_ENTERPRISE_CLIENT_ID &&
|
||||
env.AUTH_GITHUB_ENTERPRISE_CLIENT_SECRET &&
|
||||
env.AUTH_GITHUB_ENTERPRISE_BASE_URL
|
||||
) {
|
||||
staticProviders.push(
|
||||
GitHubEnterpriseProvider({
|
||||
clientId: env.AUTH_GITHUB_ENTERPRISE_CLIENT_ID,
|
||||
clientSecret: env.AUTH_GITHUB_ENTERPRISE_CLIENT_SECRET,
|
||||
enterprise: { baseUrl: env.AUTH_GITHUB_ENTERPRISE_BASE_URL },
|
||||
allowDangerousEmailAccountLinking:
|
||||
env.AUTH_GITHUB_ENTERPRISE_ALLOW_ACCOUNT_LINKING === "true",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (env.AUTH_GITLAB_CLIENT_ID && env.AUTH_GITLAB_CLIENT_SECRET)
|
||||
staticProviders.push(
|
||||
GitLabProvider({
|
||||
@@ -263,16 +285,33 @@ if (
|
||||
clientId: env.AUTH_COGNITO_CLIENT_ID,
|
||||
clientSecret: env.AUTH_COGNITO_CLIENT_SECRET,
|
||||
issuer: env.AUTH_COGNITO_ISSUER,
|
||||
checks: "nonce",
|
||||
allowDangerousEmailAccountLinking:
|
||||
env.AUTH_COGNITO_ALLOW_ACCOUNT_LINKING === "true",
|
||||
}),
|
||||
);
|
||||
|
||||
if (
|
||||
env.AUTH_KEYCLOAK_CLIENT_ID &&
|
||||
env.AUTH_KEYCLOAK_CLIENT_SECRET &&
|
||||
env.AUTH_KEYCLOAK_ISSUER
|
||||
)
|
||||
staticProviders.push(
|
||||
KeycloakProvider({
|
||||
clientId: env.AUTH_KEYCLOAK_CLIENT_ID,
|
||||
clientSecret: env.AUTH_KEYCLOAK_CLIENT_SECRET,
|
||||
issuer: env.AUTH_KEYCLOAK_ISSUER,
|
||||
allowDangerousEmailAccountLinking:
|
||||
env.AUTH_KEYCLOAK_ALLOW_ACCOUNT_LINKING === "true",
|
||||
}),
|
||||
);
|
||||
|
||||
// Extend Prisma Adapter
|
||||
const prismaAdapter = PrismaAdapter(prisma);
|
||||
const ignoredAccountFields = env.AUTH_IGNORE_ACCOUNT_FIELDS?.split(",") ?? [];
|
||||
const extendedPrismaAdapter: Adapter = {
|
||||
...prismaAdapter,
|
||||
async createUser(profile) {
|
||||
async createUser(profile: Omit<AdapterUser, "id">) {
|
||||
if (!prismaAdapter.createUser)
|
||||
throw new Error("createUser not implemented");
|
||||
if (
|
||||
@@ -294,6 +333,30 @@ const extendedPrismaAdapter: Adapter = {
|
||||
|
||||
return user;
|
||||
},
|
||||
|
||||
async linkAccount(data: AdapterAccount) {
|
||||
if (!prismaAdapter.linkAccount)
|
||||
throw new Error("NextAuth: prismaAdapter.linkAccount not implemented");
|
||||
|
||||
// Keycloak returns incompatible data with the nextjs-auth schema
|
||||
// (refresh_expires_in and not-before-policy in).
|
||||
// So, we need to remove this data from the payload before linking an account.
|
||||
// https://github.com/nextauthjs/next-auth/issues/7655
|
||||
if (data.provider === "keycloak") {
|
||||
delete data["refresh_expires_in"];
|
||||
delete data["not-before-policy"];
|
||||
}
|
||||
|
||||
// Optionally, remove fields returned by the provider that cause issues with the adapter
|
||||
// Configure via AUTH_IGNORE_ACCOUNT_FIELDS
|
||||
for (const ignoredField of ignoredAccountFields) {
|
||||
if (ignoredField in data) {
|
||||
delete data[ignoredField];
|
||||
}
|
||||
}
|
||||
|
||||
await prismaAdapter.linkAccount(data);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "worker",
|
||||
"version": "2.92.0",
|
||||
"version": "2.93.7",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const VERSION = "v2.92.0";
|
||||
export const VERSION = "v2.93.7";
|
||||
|
||||
Reference in New Issue
Block a user