Merge pull request #4339 from supabase/km/migrate-to-gotrue

Fix: Migrate dashboard from auth0 to gotrue
This commit is contained in:
Kang Ming authored and GitHub committed 2021-12-17 14:22:49 +08:00
commit 73ca8e8051
14 files changed
+13975 -166

No files matched your search

+34 -5
View File
@@ -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'}>
+42 -15
View File
@@ -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
}
+51
View File
@@ -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
}
+4 -8
View File
@@ -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,
})
+4 -8
View File
@@ -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)
+4 -8
View File
@@ -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,
})
+4 -8
View File
@@ -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)
+36
View File
@@ -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 }
}
}
+2 -2
View File
@@ -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'
+13755 -102
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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">