diff --git a/apps/studio/Dockerfile b/apps/studio/Dockerfile index bae1a6b3b1c..ff99415656c 100644 --- a/apps/studio/Dockerfile +++ b/apps/studio/Dockerfile @@ -51,6 +51,9 @@ CMD ["pnpm", "dev:studio"] # Compile Next.js FROM dev AS builder +ARG NEXT_PUBLIC_STUDIO_AUTH_MODE +ENV NEXT_PUBLIC_STUDIO_AUTH_MODE=$NEXT_PUBLIC_STUDIO_AUTH_MODE + RUN pnpm --filter studio exec next build # Copy only compiled code and dependencies diff --git a/apps/studio/components/interfaces/SignIn/SignInForm.tsx b/apps/studio/components/interfaces/SignIn/SignInForm.tsx index a09d53769e2..291363fa3a6 100644 --- a/apps/studio/components/interfaces/SignIn/SignInForm.tsx +++ b/apps/studio/components/interfaces/SignIn/SignInForm.tsx @@ -13,6 +13,7 @@ import { useAddLoginEvent } from 'data/misc/audit-login-mutation' import { getMfaAuthenticatorAssuranceLevel } from 'data/profile/mfa-authenticator-assurance-level-query' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import { useLastSignIn } from 'hooks/misc/useLastSignIn' +import { IS_PLATFORM } from 'lib/constants' import { captureCriticalError } from 'lib/error-reporting' import { auth, buildPathWithParams, getReturnToPath } from 'lib/gotrue' import { Button, Form_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, Input_Shadcn_ } from 'ui' @@ -86,15 +87,18 @@ export const SignInForm = () => { } toast.success(`Signed in successfully!`, { id: toastId }) - sendEvent({ - action: 'sign_in', - properties: { category: 'account', method: 'email' }, - }) - addLoginEvent({}) + if (IS_PLATFORM) { + sendEvent({ + action: 'sign_in', + properties: { category: 'account', method: 'email' }, + }) + addLoginEvent({}) + } await queryClient.resetQueries() // since we're already on the /sign-in page, prevent redirect loops - let redirectPath = '/organizations' + // Use /project/default for self-hosted, /organizations for platform + let redirectPath = IS_PLATFORM ? '/organizations' : '/project/default' if (returnTo && returnTo !== '/sign-in') { redirectPath = returnTo } @@ -175,12 +179,15 @@ export const SignInForm = () => { /> {/* positioned using absolute instead of labelOptional prop so tabbing between inputs works smoothly */} - - Forgot password? - + {/* Hide forgot password link for self-hosted - users must be managed manually */} + {IS_PLATFORM && ( + + Forgot password? + + )}
diff --git a/apps/studio/hooks/misc/withAuth.tsx b/apps/studio/hooks/misc/withAuth.tsx index 98dc741cc96..49b4d6429a0 100644 --- a/apps/studio/hooks/misc/withAuth.tsx +++ b/apps/studio/hooks/misc/withAuth.tsx @@ -7,7 +7,7 @@ import { SessionTimeoutModal } from 'components/interfaces/SignIn/SessionTimeout import { usePermissionsQuery } from 'data/permissions/permissions-query' import { useAuthenticatorAssuranceLevelQuery } from 'data/profile/mfa-authenticator-assurance-level-query' import { useSignOut } from 'lib/auth' -import { BASE_PATH, IS_PLATFORM } from 'lib/constants' +import { BASE_PATH, IS_PLATFORM, STUDIO_AUTH_ENABLED } from 'lib/constants' import { isNextPageWithLayout, type NextPageWithLayout } from 'types' const MAX_TIMEOUT = 10000 // 10 seconds @@ -26,8 +26,8 @@ export function withAuth( useHighestAAL: boolean } = { useHighestAAL: true } ) { - // ignore auth in self-hosted - if (!IS_PLATFORM) { + // ignore auth in self-hosted unless studio auth mode is enabled + if (!IS_PLATFORM && !STUDIO_AUTH_ENABLED) { return WrappedComponent } diff --git a/apps/studio/lib/auth.tsx b/apps/studio/lib/auth.tsx index 7ac8442b23a..ae3e01164d5 100644 --- a/apps/studio/lib/auth.tsx +++ b/apps/studio/lib/auth.tsx @@ -10,7 +10,7 @@ import { useAuthError, } from 'common' import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' -import { GOTRUE_ERRORS, IS_PLATFORM } from './constants' +import { GOTRUE_ERRORS, IS_PLATFORM, STUDIO_AUTH_ENABLED } from './constants' const AuthErrorToaster = ({ children }: PropsWithChildren) => { const error = useAuthError() @@ -33,8 +33,11 @@ const AuthErrorToaster = ({ children }: PropsWithChildren) => { } export const AuthProvider = ({ children }: PropsWithChildren) => { + // Use real auth if platform OR if studio auth mode is enabled for self-hosted + const useRealAuth = IS_PLATFORM || STUDIO_AUTH_ENABLED + return ( - + {children} ) diff --git a/apps/studio/lib/constants/index.ts b/apps/studio/lib/constants/index.ts index 3bf961dec49..647a6d65477 100644 --- a/apps/studio/lib/constants/index.ts +++ b/apps/studio/lib/constants/index.ts @@ -3,6 +3,7 @@ export * from './infrastructure' export const IS_PLATFORM = process.env.NEXT_PUBLIC_IS_PLATFORM === 'true' +export const STUDIO_AUTH_ENABLED = process.env.NEXT_PUBLIC_STUDIO_AUTH_MODE === 'supabase' export const API_URL = (() => { if (process.env.NODE_ENV === 'test') return 'http://localhost:3000/api' diff --git a/apps/studio/lib/gotrue.ts b/apps/studio/lib/gotrue.ts index a80ab433d6e..d8bd487c606 100644 --- a/apps/studio/lib/gotrue.ts +++ b/apps/studio/lib/gotrue.ts @@ -1,10 +1,12 @@ import type { JwtPayload } from '@supabase/supabase-js' import { type User } from 'common/auth' import { gotrueClient } from 'common/gotrue' +import { IS_PLATFORM } from './constants' export const auth = gotrueClient -export const DEFAULT_FALLBACK_PATH = '/organizations' +// Use /project/default for self-hosted, /organizations for platform +export const DEFAULT_FALLBACK_PATH = IS_PLATFORM ? '/organizations' : '/project/default' export const validateReturnTo = ( returnTo: string, diff --git a/apps/studio/pages/forgot-password-mfa.tsx b/apps/studio/pages/forgot-password-mfa.tsx index 801d9507323..e954dfd06c1 100644 --- a/apps/studio/pages/forgot-password-mfa.tsx +++ b/apps/studio/pages/forgot-password-mfa.tsx @@ -7,6 +7,7 @@ import { toast } from 'sonner' import { getAccessToken } from 'common' import { SignInMfaForm } from 'components/interfaces/SignIn/SignInMfaForm' import ForgotPasswordLayout from 'components/layouts/SignInLayout/ForgotPasswordLayout' +import { IS_PLATFORM } from 'lib/constants' import { auth, buildPathWithParams, getReturnToPath } from 'lib/gotrue' import type { NextPageWithLayout } from 'types' import { LogoLoader } from 'ui' @@ -17,6 +18,13 @@ const ForgotPasswordMfa: NextPageWithLayout = () => { const [loading, setLoading] = useState(true) + useEffect(() => { + // Forgot password flow is disabled for self-hosted + if (!IS_PLATFORM) { + router.replace('/project/default') + } + }, [router]) + // This useEffect redirects the user to MFA if they're already halfway signed in useEffect(() => { auth diff --git a/apps/studio/pages/forgot-password.tsx b/apps/studio/pages/forgot-password.tsx index 8952cd12cc6..a26e640874d 100644 --- a/apps/studio/pages/forgot-password.tsx +++ b/apps/studio/pages/forgot-password.tsx @@ -1,10 +1,22 @@ import Link from 'next/link' +import { useRouter } from 'next/router' +import { useEffect } from 'react' import { ForgotPasswordWizard } from 'components/interfaces/SignIn/ForgotPasswordWizard' import ForgotPasswordLayout from 'components/layouts/SignInLayout/ForgotPasswordLayout' +import { IS_PLATFORM } from 'lib/constants' import type { NextPageWithLayout } from 'types' const ForgotPasswordPage: NextPageWithLayout = () => { + const router = useRouter() + + useEffect(() => { + // No self-service password reset for self-hosted - users must be managed manually + if (!IS_PLATFORM) { + router.replace('/project/default') + } + }, [router]) + return ( <>
diff --git a/apps/studio/pages/logout.tsx b/apps/studio/pages/logout.tsx index 8be367a62dd..fb930557bf7 100644 --- a/apps/studio/pages/logout.tsx +++ b/apps/studio/pages/logout.tsx @@ -3,6 +3,7 @@ import { useEffect } from 'react' import { toast } from 'sonner' import { useSignOut } from 'lib/auth' +import { IS_PLATFORM, STUDIO_AUTH_ENABLED } from 'lib/constants' import type { NextPageWithLayout } from 'types' import { LogoLoader } from 'ui' @@ -14,7 +15,9 @@ const LogoutPage: NextPageWithLayout = () => { const logout = async () => { await signOut() toast('Successfully logged out') - await router.push('/sign-in') + // Redirect to sign-in if using real auth, otherwise to project page + const redirectPath = IS_PLATFORM || STUDIO_AUTH_ENABLED ? '/sign-in' : '/project/default' + await router.push(redirectPath) } logout() }, []) diff --git a/apps/studio/pages/sign-in-fly-tos.tsx b/apps/studio/pages/sign-in-fly-tos.tsx index d700355fb78..6c8ef65cf8e 100644 --- a/apps/studio/pages/sign-in-fly-tos.tsx +++ b/apps/studio/pages/sign-in-fly-tos.tsx @@ -7,7 +7,7 @@ import { useEffect, useState } from 'react' import { useIsLoggedIn } from 'common' import { useOrganizationByFlyOrgIdMutation } from 'data/organizations/organization-by-fly-organization-id-mutation' import { useProjectByFlyExtensionIdMutation } from 'data/projects/project-by-fly-extension-id-mutation' -import { API_URL, BASE_PATH } from 'lib/constants' +import { API_URL, BASE_PATH, IS_PLATFORM } from 'lib/constants' import { Button } from 'ui' const SignInFlyTos = () => { @@ -18,6 +18,13 @@ const SignInFlyTos = () => { isReady, query: { fly_extension_id, fly_organization_id }, } = router + + useEffect(() => { + // Fly.io integration is platform-only feature + if (!IS_PLATFORM) { + router.replace('/project/default') + } + }, [router]) const { resolvedTheme } = useTheme() const { mutateAsync: getProjectByFlyExtensionId } = useProjectByFlyExtensionIdMutation({ onSuccess: (res) => { diff --git a/apps/studio/pages/sign-in-mfa.tsx b/apps/studio/pages/sign-in-mfa.tsx index 33a09078d4e..ef9e65dd259 100644 --- a/apps/studio/pages/sign-in-mfa.tsx +++ b/apps/studio/pages/sign-in-mfa.tsx @@ -10,6 +10,7 @@ import SignInLayout from 'components/layouts/SignInLayout/SignInLayout' import { useAddLoginEvent } from 'data/misc/audit-login-mutation' import { useSendEventMutation } from 'data/telemetry/send-event-mutation' import useLatest from 'hooks/misc/useLatest' +import { IS_PLATFORM } from 'lib/constants' import { auth, buildPathWithParams, getReturnToPath } from 'lib/gotrue' import type { NextPageWithLayout } from 'types' import { LogoLoader } from 'ui' @@ -54,14 +55,16 @@ const SignInMfaPage: NextPageWithLayout = () => { } if (data.currentLevel === data.nextLevel) { - sendEvent({ - action: 'sign_in', - properties: { - category: 'account', - method: signInMethodRef.current, - }, - }) - addLoginEvent({}) + if (IS_PLATFORM) { + sendEvent({ + action: 'sign_in', + properties: { + category: 'account', + method: signInMethodRef.current, + }, + }) + addLoginEvent({}) + } await queryClient.resetQueries() router.push(getReturnToPath()) diff --git a/apps/studio/pages/sign-in-partner.tsx b/apps/studio/pages/sign-in-partner.tsx index b51320a2d39..8721d149231 100644 --- a/apps/studio/pages/sign-in-partner.tsx +++ b/apps/studio/pages/sign-in-partner.tsx @@ -1,9 +1,22 @@ +import { useRouter } from 'next/router' +import { useEffect } from 'react' + import { SignInPartner } from 'components/interfaces/SignIn/SignInPartner' import ForgotPasswordLayout from 'components/layouts/SignInLayout/ForgotPasswordLayout' +import { IS_PLATFORM } from 'lib/constants' import type { NextPageWithLayout } from 'types' import { cn } from 'ui' const SignInPartnerPage: NextPageWithLayout = () => { + const router = useRouter() + + useEffect(() => { + // Partner sign-in is platform-only feature + if (!IS_PLATFORM) { + router.replace('/project/default') + } + }, [router]) + return } diff --git a/apps/studio/pages/sign-in-sso.tsx b/apps/studio/pages/sign-in-sso.tsx index 387fd6723c4..fb92939cb8b 100644 --- a/apps/studio/pages/sign-in-sso.tsx +++ b/apps/studio/pages/sign-in-sso.tsx @@ -1,12 +1,24 @@ +import { useRouter } from 'next/router' +import { useEffect } from 'react' + import { SignInSSOForm } from 'components/interfaces/SignIn/SignInSSOForm' import SignInLayout from 'components/layouts/SignInLayout/SignInLayout' import { UnknownInterface } from 'components/ui/UnknownInterface' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' +import { IS_PLATFORM } from 'lib/constants' import type { NextPageWithLayout } from 'types' const SignInSSOPage: NextPageWithLayout = () => { + const router = useRouter() const signInWithSSOEnabled = useIsFeatureEnabled('dashboard_auth:sign_in_with_sso') + useEffect(() => { + // SSO is platform-only feature + if (!IS_PLATFORM) { + router.replace('/project/default') + } + }, [router]) + if (!signInWithSSOEnabled) { return } diff --git a/apps/studio/pages/sign-in.tsx b/apps/studio/pages/sign-in.tsx index f55db7b8011..c9e09ce2cbc 100644 --- a/apps/studio/pages/sign-in.tsx +++ b/apps/studio/pages/sign-in.tsx @@ -11,7 +11,7 @@ import { AuthenticationLayout } from 'components/layouts/AuthenticationLayout' import SignInLayout from 'components/layouts/SignInLayout/SignInLayout' import { useCustomContent } from 'hooks/custom-content/useCustomContent' import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled' -import { IS_PLATFORM } from 'lib/constants' +import { IS_PLATFORM, STUDIO_AUTH_ENABLED } from 'lib/constants' import type { NextPageWithLayout } from 'types' import { Button } from 'ui' @@ -38,8 +38,8 @@ const SignInPage: NextPageWithLayout = () => { (signInWithGithubEnabled || signInWithSsoEnabled || customProvider) && signInWithEmailEnabled useEffect(() => { - if (!IS_PLATFORM) { - // on selfhosted instance just redirect to projects page + // Redirect to projects page on self-hosted unless studio auth mode is enabled + if (!IS_PLATFORM && !STUDIO_AUTH_ENABLED) { router.replace('/project/default') } }, [router]) @@ -83,10 +83,11 @@ const SignInPage: NextPageWithLayout = () => { {signInWithEmailEnabled && }
- {signUpEnabled && ( + {/* Hide signup link for self-hosted - users must be added manually */} + {IS_PLATFORM && signUpEnabled && (
- Don’t have an account?{' '} + Don't have an account?{' '} { + const router = useRouter() + const { dashboardAuthSignUp: signUpEnabled, dashboardAuthSignInWithGithub: signInWithGithubEnabled, } = useIsFeatureEnabled(['dashboard_auth:sign_up', 'dashboard_auth:sign_in_with_github']) + useEffect(() => { + // No self-service signup for self-hosted - users must be added manually + if (!IS_PLATFORM) { + router.replace('/project/default') + } + }, [router]) + if (!signUpEnabled) { return } diff --git a/docker/.env.example b/docker/.env.example index b66761b81ca..2f48cb18aac 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -104,6 +104,12 @@ IMGPROXY_ENABLE_WEBP_DETECTION=true # Add your OpenAI API key to enable SQL Editor Assistant OPENAI_API_KEY= +# Studio Authentication Mode +# Set to 'supabase' to use Supabase Auth (GoTrue) for Studio login instead of Kong basic-auth +# When enabled, you must create users manually via CLI: docker compose exec auth ./auth admin createuser --confirm +# Leave empty for default Kong basic-auth (DASHBOARD_USERNAME/DASHBOARD_PASSWORD) +NEXT_PUBLIC_STUDIO_AUTH_MODE= + ############ # Functions - Configuration for Functions diff --git a/docker/volumes/api/kong.yml b/docker/volumes/api/kong.yml index 168634f5a9a..3299ae81036 100644 --- a/docker/volumes/api/kong.yml +++ b/docker/volumes/api/kong.yml @@ -62,6 +62,33 @@ services: - /auth/v1/authorize plugins: - name: cors + - name: auth-v1-open-token + url: http://auth:9999/token + routes: + - name: auth-v1-open-token + strip_path: true + paths: + - /auth/v1/token + plugins: + - name: cors + - name: auth-v1-open-user + url: http://auth:9999/user + routes: + - name: auth-v1-open-user + strip_path: true + paths: + - /auth/v1/user + plugins: + - name: cors + - name: auth-v1-open-logout + url: http://auth:9999/logout + routes: + - name: auth-v1-open-logout + strip_path: true + paths: + - /auth/v1/logout + plugins: + - name: cors ## Secure Auth routes - name: auth-v1 @@ -278,6 +305,7 @@ services: - / plugins: - name: cors - - name: basic-auth - config: - hide_credentials: true + # Uncomment to use Kong basic-auth instead of Supabase Auth (GoTrue) + # - name: basic-auth + # config: + # hide_credentials: true