mirror of
https://github.com/supabase/supabase.git
synced 2026-10-11 12:25:05 +03:00
feat: use local gotrue for studio auth in self-hosted
This commit is contained in:
1 parent
2b6aa930d3
commit
d9e3cdb5d2
17 files changed
+157
-36
No files matched your search
@@ -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
|
||||
|
||||
@@ -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 */}
|
||||
<Link
|
||||
href={forgotPasswordUrl}
|
||||
className="absolute top-0 right-0 text-sm text-foreground-lighter"
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
{/* Hide forgot password link for self-hosted - users must be managed manually */}
|
||||
{IS_PLATFORM && (
|
||||
<Link
|
||||
href={forgotPasswordUrl}
|
||||
className="absolute top-0 right-0 text-sm text-foreground-lighter"
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="self-center">
|
||||
|
||||
@@ -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<T>(
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<AuthProviderInternal alwaysLoggedIn={!IS_PLATFORM}>
|
||||
<AuthProviderInternal alwaysLoggedIn={!useRealAuth}>
|
||||
<AuthErrorToaster>{children}</AuthErrorToaster>
|
||||
</AuthProviderInternal>
|
||||
)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
<div className="flex flex-col gap-4">
|
||||
|
||||
@@ -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()
|
||||
}, [])
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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 <SignInPartner />
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <UnknownInterface fullHeight={false} urlBack="/sign-in" />
|
||||
}
|
||||
|
||||
@@ -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 && <SignInForm />}
|
||||
</div>
|
||||
|
||||
{signUpEnabled && (
|
||||
{/* Hide signup link for self-hosted - users must be added manually */}
|
||||
{IS_PLATFORM && signUpEnabled && (
|
||||
<div className="self-center my-8 text-sm">
|
||||
<div>
|
||||
<span className="text-foreground-light">Don’t have an account?</span>{' '}
|
||||
<span className="text-foreground-light">Don't have an account?</span>{' '}
|
||||
<Link
|
||||
href={{
|
||||
pathname: '/sign-up',
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import { SignInWithGitHub } from 'components/interfaces/SignIn/SignInWithGitHub'
|
||||
import { SignUpForm } from 'components/interfaces/SignIn/SignUpForm'
|
||||
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 SignUpPage: NextPageWithLayout = () => {
|
||||
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 <UnknownInterface fullHeight={false} urlBack="/sign-in" />
|
||||
}
|
||||
|
||||
@@ -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 <email> <password>
|
||||
# Leave empty for default Kong basic-auth (DASHBOARD_USERNAME/DASHBOARD_PASSWORD)
|
||||
NEXT_PUBLIC_STUDIO_AUTH_MODE=
|
||||
|
||||
|
||||
############
|
||||
# Functions - Configuration for Functions
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user