mirror of
https://github.com/supabase/supabase.git
synced 2026-10-16 22:59:11 +03:00
Merge pull request #4339 from supabase/km/migrate-to-gotrue
Fix: Migrate dashboard from auth0 to gotrue
This commit is contained in:
14 files changed
+13975
-166
No files matched your search
@@ -2,15 +2,32 @@ import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import Head from 'next/head'
|
||||
import { observer } from 'mobx-react-lite'
|
||||
import { Button, Typography } from '@supabase/ui'
|
||||
import { Button, IconGitHub, Typography } from '@supabase/ui'
|
||||
|
||||
import { API_URL } from 'lib/constants'
|
||||
import { useStore } from 'hooks'
|
||||
import { auth, GOTRUE_ENABLED } from 'lib/gotrue'
|
||||
|
||||
const Landing = () => {
|
||||
const { ui } = useStore()
|
||||
const { theme } = ui
|
||||
|
||||
async function handleGithubSignIn() {
|
||||
try {
|
||||
const { error } = await auth.signIn(
|
||||
{
|
||||
provider: 'github',
|
||||
},
|
||||
{
|
||||
redirectTo: process.env.NEXT_PUBLIC_SITE_URL,
|
||||
}
|
||||
)
|
||||
if (error) throw error
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="z-10 relative h-screen flex flex-col">
|
||||
<Head>
|
||||
@@ -38,7 +55,13 @@ const Landing = () => {
|
||||
type="success"
|
||||
className="ml-8 font-medium hover:text-gray-300 focus:outline-none focus:text-gray-300 transition duration-150 ease-in-out"
|
||||
>
|
||||
<Link href={`${API_URL}/login`}>Sign In</Link>
|
||||
{GOTRUE_ENABLED ? (
|
||||
<Button onClick={handleGithubSignIn} icon={<IconGitHub />}>
|
||||
Sign In With Github
|
||||
</Button>
|
||||
) : (
|
||||
<Link href={`${API_URL}/login`}>Sign In</Link>
|
||||
)}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -55,9 +78,15 @@ const Landing = () => {
|
||||
</p>
|
||||
</Typography.Text>
|
||||
<div className="mt-5 sm:mt-8 sm:flex sm:justify-center space-x-3">
|
||||
<Link href={`${API_URL}/login`}>
|
||||
<Button size="large">Sign In</Button>
|
||||
</Link>
|
||||
{GOTRUE_ENABLED ? (
|
||||
<Button onClick={handleGithubSignIn} size="large" icon={<IconGitHub />}>
|
||||
Sign In With Github
|
||||
</Button>
|
||||
) : (
|
||||
<Link href={`${API_URL}/login`}>
|
||||
<Button size="large">Sign In</Button>
|
||||
</Link>
|
||||
)}
|
||||
<Link href="https://supabase.com/docs">
|
||||
<Button size="large" type="default">
|
||||
Docs
|
||||
|
||||
@@ -5,6 +5,7 @@ import { observer } from 'mobx-react-lite'
|
||||
import { API_URL, IS_PLATFORM } from 'lib/constants'
|
||||
import { useStore } from 'hooks'
|
||||
import WithSidebar from './WithSidebar'
|
||||
import { auth, GOTRUE_ENABLED } from 'lib/gotrue'
|
||||
|
||||
/**
|
||||
* layout for dashboard homepage, account and org settings
|
||||
@@ -19,6 +20,22 @@ const AccountLayout = ({ children, title, breadcrumbs }: any) => {
|
||||
|
||||
const { app, ui } = useStore()
|
||||
|
||||
const onClickLogout = async () => {
|
||||
await auth.signOut()
|
||||
router.reload()
|
||||
}
|
||||
|
||||
let baseLogoutLink = {
|
||||
icon: '/icons/feather/power.svg',
|
||||
label: 'Logout',
|
||||
href: `${API_URL}/logout`,
|
||||
key: `${API_URL}/logout`,
|
||||
}
|
||||
|
||||
let logoutLink = GOTRUE_ENABLED
|
||||
? { ...baseLogoutLink, href: undefined, onClick: onClickLogout }
|
||||
: baseLogoutLink
|
||||
|
||||
const organizationsLinks = app.organizations
|
||||
.list()
|
||||
.map((x: any) => ({
|
||||
@@ -36,7 +53,6 @@ const AccountLayout = ({ children, title, breadcrumbs }: any) => {
|
||||
isActive: router.pathname == '/',
|
||||
label: 'All projects',
|
||||
href: '/',
|
||||
as: `/`,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -60,12 +76,7 @@ const AccountLayout = ({ children, title, breadcrumbs }: any) => {
|
||||
href: `/account/me`,
|
||||
key: `/account/me`,
|
||||
},
|
||||
{
|
||||
icon: '/img/power.svg',
|
||||
label: 'Logout',
|
||||
href: `${API_URL}/logout`,
|
||||
key: `${API_URL}/logout`,
|
||||
},
|
||||
logoutLink,
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Link from 'next/link'
|
||||
import { CSSProperties, FC, ReactNode } from 'react'
|
||||
import { Menu, Typography, IconArrowUpRight, Badge } from '@supabase/ui'
|
||||
import { isUndefined } from 'lodash'
|
||||
import LayoutHeader from '../ProjectLayout/LayoutHeader'
|
||||
|
||||
interface Props {
|
||||
@@ -142,7 +143,24 @@ const LinksWithoutHeaders: FC<any> = ({ links, subitems, subitemsParentKey }) =>
|
||||
)
|
||||
}
|
||||
|
||||
const SidebarItem: FC<any> = ({ id, label, as, href, isActive, isSubitem, onClick, external }) => {
|
||||
const SidebarItem: FC<any> = ({ id, label, href, isActive, isSubitem, onClick, external }) => {
|
||||
if (isUndefined(href)) {
|
||||
return (
|
||||
<Menu.Item
|
||||
rounded
|
||||
key={id}
|
||||
style={{
|
||||
marginLeft: isSubitem && '.5rem',
|
||||
}}
|
||||
active={isActive ? true : false}
|
||||
onClick={onClick || (() => {})}
|
||||
icon={external && <IconArrowUpRight size={'tiny'} />}
|
||||
>
|
||||
{isSubitem ? <Typography.Text small>{label}</Typography.Text> : label}
|
||||
</Menu.Item>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={href || ''}>
|
||||
<a className="block" target={external ? '_blank' : '_self'}>
|
||||
|
||||
@@ -4,6 +4,8 @@ import { readOnly } from './supabaseClient'
|
||||
import { SupaResponse, User } from 'types'
|
||||
import { auth0 } from './auth0'
|
||||
import { flattenNamespaceOnUser } from './apiHelpers'
|
||||
import { config } from 'process'
|
||||
import { getAuth0Id, getAuthUser, getIdentity, GOTRUE_ENABLED } from 'lib/gotrue'
|
||||
|
||||
/**
|
||||
* Use this method on api routes to check if user is authenticated and having required permissions.
|
||||
@@ -32,7 +34,7 @@ export async function apiAuthenticate(
|
||||
try {
|
||||
// Check that they are logged in
|
||||
// If no error throw from getAccessToken. That's good, we can continue
|
||||
await auth0.getAccessToken(req, res)
|
||||
if (!(GOTRUE_ENABLED)) await auth0.getAccessToken(req, res)
|
||||
|
||||
const user = await fetchUser(req, res)
|
||||
if (!user) {
|
||||
@@ -53,16 +55,41 @@ export async function apiAuthenticate(
|
||||
* user with only id prop or detail object. It depends on requireUserDetail config
|
||||
*/
|
||||
async function fetchUser(req: NextApiRequest, res: NextApiResponse): Promise<any> {
|
||||
const session = auth0.getSession(req, res)
|
||||
let user_id_supabase = null
|
||||
let user_id_auth0 = null
|
||||
let gotrue_id = null
|
||||
let email = null
|
||||
if (GOTRUE_ENABLED) {
|
||||
const token = req.headers.authorization
|
||||
if (!token) {
|
||||
return res.status(401).end('Unauthorized: missing access token')
|
||||
}
|
||||
let { user: gotrue_user, error: authError } = await getAuthUser(token)
|
||||
if (authError) {
|
||||
throw authError
|
||||
}
|
||||
if (gotrue_user !== null) {
|
||||
gotrue_id = gotrue_user?.id
|
||||
email = gotrue_user.email
|
||||
|
||||
if (!session) {
|
||||
return null
|
||||
let { identity, error } = getIdentity(gotrue_user)
|
||||
if (error) throw error
|
||||
if (identity?.provider !== undefined) {
|
||||
user_id_auth0 = getAuth0Id(identity?.provider, identity?.id)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const session = auth0.getSession(req, res)
|
||||
if (!session) {
|
||||
return null
|
||||
}
|
||||
const { user: auth0_user } = session
|
||||
const flattened = flattenNamespaceOnUser('https://supabase.io', auth0_user)
|
||||
user_id_supabase = flattened.user_id_supabase
|
||||
user_id_auth0 = flattened.user_id_auth0
|
||||
email = flattened.email
|
||||
}
|
||||
|
||||
const { user: auth0_user } = session
|
||||
const flattened = flattenNamespaceOnUser('https://supabase.io', auth0_user)
|
||||
const { user_id_supabase, user_id_auth0, email } = flattened
|
||||
|
||||
if (user_id_supabase) {
|
||||
return {
|
||||
id: user_id_supabase,
|
||||
@@ -70,15 +97,15 @@ async function fetchUser(req: NextApiRequest, res: NextApiResponse): Promise<any
|
||||
}
|
||||
}
|
||||
|
||||
const { data } = await readOnly
|
||||
.from('users')
|
||||
.select(
|
||||
`
|
||||
const query = readOnly.from('users').select(
|
||||
`
|
||||
id, auth0_id, primary_email, username, first_name, last_name, mobile, is_alpha_user
|
||||
`
|
||||
)
|
||||
.eq('auth0_id', user_id_auth0)
|
||||
.single()
|
||||
)
|
||||
|
||||
const { data } = GOTRUE_ENABLED
|
||||
? await query.eq('gotrue_id', gotrue_id).single()
|
||||
: await await query.eq('auth0_id', user_id_auth0).single()
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { GOTRUE_ENABLED } from 'lib/gotrue'
|
||||
import { tryParseJson } from 'lib/helpers'
|
||||
import { SupaResponse } from 'types/base'
|
||||
|
||||
export function handleError<T>(e: any, requestId: string): SupaResponse<T> {
|
||||
@@ -65,3 +67,52 @@ export async function handleResponseError<T = unknown>(
|
||||
return { error } as unknown as SupaResponse<T>
|
||||
}
|
||||
}
|
||||
|
||||
export function getAccessToken() {
|
||||
// ignore if server-side
|
||||
if (typeof window === 'undefined') return ''
|
||||
|
||||
const tokenData = window?.localStorage['supabase.auth.token']
|
||||
if (!tokenData) {
|
||||
// try to get from url fragment
|
||||
const access_token = getParameterByName('access_token')
|
||||
if (access_token) return access_token
|
||||
else return undefined
|
||||
}
|
||||
const tokenObj = tryParseJson(tokenData)
|
||||
if (tokenObj === false) {
|
||||
return ''
|
||||
}
|
||||
return tokenObj.currentSession.access_token
|
||||
}
|
||||
|
||||
// get param from URL fragment
|
||||
function getParameterByName(name: string, url?: string) {
|
||||
// ignore if server-side
|
||||
if (typeof window === 'undefined') return ''
|
||||
|
||||
if (!url) url = window?.location?.href || ''
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
name = name.replace(/[\[\]]/g, '\\$&')
|
||||
const regex = new RegExp('[?&#]' + name + '(=([^&#]*)|&|#|$)'),
|
||||
results = regex.exec(url)
|
||||
if (!results) return null
|
||||
if (!results[2]) return ''
|
||||
return decodeURIComponent(results[2].replace(/\+/g, ' '))
|
||||
}
|
||||
|
||||
export function constructHeaders(requestId: string, optionHeaders?: { [prop: string]: any }) {
|
||||
let headers: { [prop: string]: any } = {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'X-Request-Id': requestId,
|
||||
...optionHeaders,
|
||||
}
|
||||
|
||||
if (GOTRUE_ENABLED) {
|
||||
const accessToken = getAccessToken()
|
||||
if (accessToken) headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { handleError, handleResponse, handleResponseError } from './base'
|
||||
import { handleError, handleResponse, handleResponseError, constructHeaders } from './base'
|
||||
import { uuidv4 } from '../../helpers'
|
||||
import { SupaResponse } from 'types/base'
|
||||
|
||||
@@ -8,19 +8,15 @@ export async function delete_<T = any>(
|
||||
options?: { [prop: string]: any }
|
||||
): Promise<SupaResponse<T>> {
|
||||
const requestId = uuidv4()
|
||||
const { headers, ...otherOptions } = options ?? {}
|
||||
try {
|
||||
const { headers: optionHeaders, ...otherOptions } = options ?? {}
|
||||
const headers = constructHeaders(requestId, optionHeaders)
|
||||
const response = await fetch(url, {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include',
|
||||
referrerPolicy: 'no-referrer-when-downgrade',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'X-Request-Id': requestId,
|
||||
...headers,
|
||||
},
|
||||
headers,
|
||||
...otherOptions,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { handleError, handleResponse, handleResponseError } from './base'
|
||||
import { handleError, handleResponse, handleResponseError, constructHeaders } from './base'
|
||||
import { uuidv4 } from '../../helpers'
|
||||
import { SupaResponse } from 'types/base'
|
||||
|
||||
@@ -8,17 +8,13 @@ export async function get<T = any>(
|
||||
): Promise<SupaResponse<T>> {
|
||||
const requestId = uuidv4()
|
||||
try {
|
||||
const { headers, ...otherOptions } = options ?? {}
|
||||
const { headers: optionHeaders, ...otherOptions } = options ?? {}
|
||||
const headers = constructHeaders(requestId, optionHeaders)
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
referrerPolicy: 'no-referrer-when-downgrade',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'X-Request-Id': requestId,
|
||||
...headers,
|
||||
},
|
||||
headers,
|
||||
...otherOptions,
|
||||
})
|
||||
if (!response.ok) return handleResponseError(response, requestId)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { handleError, handleResponse, handleResponseError } from './base'
|
||||
import { constructHeaders, handleError, handleResponse, handleResponseError } from './base'
|
||||
import { uuidv4 } from '../../helpers'
|
||||
import { SupaResponse } from 'types/base'
|
||||
|
||||
@@ -9,18 +9,14 @@ export async function patch<T = any>(
|
||||
): Promise<SupaResponse<T>> {
|
||||
const requestId = uuidv4()
|
||||
try {
|
||||
const { headers, ...otherOptions } = options ?? {}
|
||||
const { headers: optionHeaders, ...otherOptions } = options ?? {}
|
||||
const headers = constructHeaders(requestId, optionHeaders)
|
||||
const response = await fetch(url, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include',
|
||||
referrerPolicy: 'no-referrer-when-downgrade',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'X-Request-Id': requestId,
|
||||
...headers,
|
||||
},
|
||||
headers,
|
||||
...otherOptions,
|
||||
})
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { handleError, handleResponse, handleResponseError } from './base'
|
||||
import { handleError, handleResponse, handleResponseError, constructHeaders } from './base'
|
||||
import { uuidv4 } from 'lib/helpers'
|
||||
import { SupaResponse } from 'types/base'
|
||||
|
||||
@@ -9,18 +9,14 @@ export async function post<T = any>(
|
||||
): Promise<SupaResponse<T>> {
|
||||
const requestId = uuidv4()
|
||||
try {
|
||||
const { headers, ...otherOptions } = options ?? {}
|
||||
const { headers: optionHeaders, ...otherOptions } = options ?? {}
|
||||
const headers = constructHeaders(requestId, optionHeaders)
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
credentials: 'include',
|
||||
referrerPolicy: 'no-referrer-when-downgrade',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'X-Request-Id': requestId,
|
||||
...headers,
|
||||
},
|
||||
headers,
|
||||
...otherOptions,
|
||||
})
|
||||
if (!response.ok) return handleResponseError(response, requestId)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { GoTrueClient, User } from '@supabase/gotrue-js'
|
||||
|
||||
export const GOTRUE_ENABLED = (process.env.NEXT_PUBLIC_GOTRUE_ENABLED === 'true') || false
|
||||
export const GOTRUE_URL = process.env.NEXT_PUBLIC_GOTRUE_URL || 'http://localhost:8000/auth/v1'
|
||||
|
||||
export const auth = new GoTrueClient({
|
||||
url: GOTRUE_URL,
|
||||
autoRefreshToken: true,
|
||||
})
|
||||
|
||||
export const getAuthUser = async (token: String): Promise<any> => {
|
||||
try {
|
||||
const { data: user, error } = await auth.api.getUser(token.replace('Bearer ', ''))
|
||||
if (error) throw error
|
||||
|
||||
return { user, error: null }
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
return { user: null, error: err }
|
||||
}
|
||||
}
|
||||
|
||||
export const getAuth0Id = (provider: String, providerId: String): String => {
|
||||
return `${provider}|${providerId}`
|
||||
}
|
||||
|
||||
export const getIdentity = (gotrueUser: User) => {
|
||||
try {
|
||||
if (gotrueUser !== undefined && gotrueUser.identities !== undefined) {
|
||||
return { identity: gotrueUser.identities[0], error: null }
|
||||
}
|
||||
throw 'Missing identity'
|
||||
} catch (err) {
|
||||
return { identity: null, error: err }
|
||||
}
|
||||
}
|
||||
@@ -45,8 +45,8 @@ export const timeout = (ms: number) => {
|
||||
|
||||
export const getURL = () => {
|
||||
const url =
|
||||
process?.env?.SITE_URL && process.env.SITE_URL !== ''
|
||||
? process.env.SITE_URL
|
||||
process?.env?.NEXT_PUBLIC_SITE_URL && process.env.NEXT_PUBLIC_SITE_URL !== ''
|
||||
? process.env.NEXT_PUBLIC_SITE_URL
|
||||
: process?.env?.VERCEL_URL && process.env.VERCEL_URL !== ''
|
||||
? process.env.VERCEL_URL
|
||||
: 'https://app.supabase.io'
|
||||
|
||||
Generated
+13755
-102
File diff suppressed because it is too large.
Load diff
+1
-1
@@ -20,7 +20,7 @@
|
||||
"@supabase/grid": "^2.3.9",
|
||||
"@supabase/postgres-meta": "^0.26.1",
|
||||
"@supabase/react-data-grid": "^7.1.0-beta.7",
|
||||
"@supabase/supabase-js": "^1.27.0",
|
||||
"@supabase/supabase-js": "^1.28.0",
|
||||
"@supabase/ui": "^0.35.0",
|
||||
"ajv": "^8.6.3",
|
||||
"analytics-node": "^3.5.0",
|
||||
|
||||
@@ -20,7 +20,7 @@ const Error404: NextPage = ({}) => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="w-full h-screen relative flex flex-col items-center justify-center mx-auto relative">
|
||||
<div className="w-full h-screen relative flex flex-col items-center justify-center mx-auto">
|
||||
<div className="absolute top-0 w-full max-w-7xl mx-auto pt-6 px-8 sm:px-6 lg:px-8">
|
||||
<nav className="relative flex items-center justify-between sm:h-10">
|
||||
<div className="flex items-center flex-grow flex-shrink-0 lg:flex-grow-0">
|
||||
|
||||
Reference in new issue
Block a user