Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENG-0000 fix(portal,1ui): Fix Privy auth issues #939

Merged
merged 11 commits into from
Nov 21, 2024
9 changes: 7 additions & 2 deletions apps/portal/app/.client/privy-login-button.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { useState } from 'react'
import { useEffect, useState } from 'react'

import { Button, Icon } from '@0xintuition/1ui'

import logger from '@lib/utils/logger'
import { useLogin, User } from '@privy-io/react-auth'
import { useLogin, useLogout, User } from '@privy-io/react-auth'

interface PrivyLoginButtonProps {
handleLogin: (
Expand All @@ -16,6 +16,7 @@ interface PrivyLoginButtonProps {
export default function PrivyLoginButton({
handleLogin,
}: PrivyLoginButtonProps) {
const { logout } = useLogout()
const [loading, setLoading] = useState(false)
const { login } = useLogin({
onComplete: (user, isNewUser, wasAlreadyAuthenticated) => {
Expand All @@ -31,6 +32,10 @@ export default function PrivyLoginButton({
setLoading(true)
login()
}
useEffect(() => {
// ensure privy knows user is logged out
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this would force the logout to ensure it's cleared fully before logging in?

logout()
}, [])

return (
<Button
Expand Down
63 changes: 51 additions & 12 deletions apps/portal/app/.client/privy-refresh.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,61 @@
import { useEffect } from 'react'

import { useCallback, useEffect, useState } from 'react'
import logger from '@lib/utils/logger'
import LoadingLogo from '@components/loading-logo'
import { usePrivy } from '@privy-io/react-auth'
import { useRevalidator } from '@remix-run/react'

export default function PrivyRefresh() {
export default function PrivyRefresh({
refreshPath,
redirectTo,
}: {
refreshPath: string
redirectTo: string
}) {
const { ready, getAccessToken } = usePrivy()
const { revalidate } = useRevalidator()
const [accessToken, setAccessToken] = useState<string | null>(null)

useEffect(() => {
async function refresh() {
if (ready) {
await getAccessToken()
revalidate()
const refresh = useCallback(async () => {
try {
if (!ready) {
return
}

logger('Getting access token...')
const idToken = await getAccessToken()
logger('Access token:', idToken)
setAccessToken(idToken)
} catch (error) {
console.error('Failed to refresh session:', error)
}
}, [ready])

useEffect(() => {
refresh()
}, [ready, revalidate])
}, [refresh])
useEffect(() => {
if (accessToken) {
// instead of revalidating, redirect to same route and replace true
logger('Redirecting to', `${refreshPath}?redirectTo=${redirectTo}`)
window.location.replace(`${refreshPath}?redirectTo=${redirectTo}`)
}
}, [accessToken])

return <div />
return (
<div className="fixed inset-0 flex items-center justify-center">
<div
0xjojikun marked this conversation as resolved.
Show resolved Hide resolved
role="status"
className="flex flex-col items-center gap-4"
aria-label="Refreshing session"
>
<LoadingLogo size={50} />
<div className="flex flex-col items-center gap-1">
<p className="text-base text-foreground/70 font-medium">
Reconnecting your session...
</p>
<p className="text-sm text-muted-foreground">
This will only take a moment.
</p>
</div>
</div>
</div>
)
}
45 changes: 37 additions & 8 deletions apps/portal/app/.server/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { RedirectOptions } from 'app/types'

import {
getPrivyAccessToken,
getPrivyClient,
getPrivySessionToken,
getPrivyUserById,
isOAuthInProgress,
verifyPrivyAccessToken,
} from './privy'
Expand All @@ -21,13 +21,38 @@ export async function getUserId(request: Request): Promise<string | null> {
}

export async function getUser(request: Request): Promise<User | null> {
const userId = await getUserId(request)
return userId ? await getPrivyUserById(userId) : null
const privyIdToken = getPrivyAccessToken(request)
const privyClient = getPrivyClient()

if (!privyIdToken) {
logger('No Privy ID token found')
return null
}

try {
// First verify the token is valid
const verifiedClaims = await verifyPrivyAccessToken(request)
if (!verifiedClaims) {
logger('Invalid Privy token')
return null
}

// Then get the full user object directly using the verified user ID
const user = await privyClient.getUserById(verifiedClaims.userId)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

logger('Successfully fetched user by ID', user.wallet?.address)
return user
} catch (error) {
logger('Error fetching user', error)
return null
}
}

export async function getUserWallet(request: Request): Promise<string | null> {
const user = await getUser(request)
return user?.wallet?.address ?? null
if (!user) {
return null
}
return user.wallet?.address ?? null
}

export async function requireUserId(
Expand Down Expand Up @@ -99,15 +124,19 @@ export async function handlePrivyRedirect({
const accessToken = getPrivyAccessToken(request)
const sessionToken = getPrivySessionToken(request)
const isOAuth = await isOAuthInProgress(request.url)

if (isOAuth) {
// Do not redirect or interrupt the flow.
return
} else if (!accessToken || !sessionToken) {
return null
}

if (!accessToken || !sessionToken) {
const redirectUrl = await getRedirectToUrl(request, path, options)
throw redirect(redirectUrl)
}
logger('Hit end of handlePrivyRedirect', accessToken, sessionToken, isOAuth)
return

// Explicitly return null when we reach the end
return null
}

export async function setupAPI(request: Request) {
Expand Down
6 changes: 3 additions & 3 deletions apps/portal/app/.server/privy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ export const verifyPrivyAccessToken = async (
}
}

// takes user privy DID (e.g. authCheck().userId)
export const getPrivyUserById = async (id: string): Promise<User> => {
export const getPrivyUserById = async (idToken: string): Promise<User> => {
const privy = getPrivyClient()
const user = await privy.getUser(id)
const user = await privy.getUser({ idToken })
logger('Successfully fetched getPrivyUserById')
return user
}

Expand Down
3 changes: 3 additions & 0 deletions apps/portal/app/assets/intuition-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions apps/portal/app/components/loading-logo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react'

interface LoadingLogoProps {
size?: number
}

const LoadingLogo: React.FC<LoadingLogoProps> = ({ size = 300 }) => {
return (
<div className="animate-pulse-slow">
<svg
width={size}
height={size}
viewBox="0 0 300 300"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="animate-spin-slow"
>
<g>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M160.133 0.342656C161.316 0.422794 162.196 1.46156 162.099 2.64387L161.591 8.82711C161.494 10.0094 160.457 10.8874 159.273 10.8086C139.849 9.51442 120.36 12.2998 102.063 18.995C83.0936 25.9362 65.8577 36.9143 51.5472 51.1704C37.2366 65.4264 26.1927 82.6202 19.179 101.563C12.4141 119.834 9.55422 139.312 10.7742 158.742C10.8485 159.926 9.96653 160.959 8.78386 161.052L2.59873 161.536C1.41605 161.629 0.380659 160.745 0.305042 159.561C-1.03277 138.616 2.03942 117.614 9.33229 97.9174C16.8739 77.5486 28.749 59.0606 44.1367 43.7316C59.5245 28.4025 78.0577 16.5981 98.4551 9.13438C118.18 1.91679 139.193 -1.07515 160.133 0.342656ZM239.534 32.3403C240.253 31.3963 240.071 30.0472 239.116 29.3424C219.597 14.9257 196.871 5.45943 172.889 1.75665C171.717 1.57563 170.631 2.3965 170.466 3.57137L169.608 9.71569C169.443 10.8906 170.263 11.9744 171.435 12.1567C193.634 15.6087 214.672 24.3715 232.757 37.6992C233.712 38.403 235.059 38.2215 235.777 37.2775L239.534 32.3403ZM282.833 219.68C282.282 220.731 280.976 221.117 279.934 220.551L274.481 217.59C273.439 217.024 273.054 215.721 273.604 214.67C288.233 186.71 292.96 154.593 286.973 123.567C280.985 92.5419 264.648 64.4899 240.665 43.9811C239.764 43.2101 239.636 41.8569 240.393 40.9436L244.352 36.167C245.109 35.2537 246.464 35.1262 247.367 35.8963C273.221 57.9579 290.834 88.1641 297.282 121.578C303.731 154.991 298.622 189.583 282.833 219.68ZM1.27663 169.528C1.12219 168.352 1.96743 167.285 3.14571 167.148L9.30789 166.428C10.4862 166.29 11.5512 167.134 11.7069 168.31C14.0852 186.273 19.9402 203.6 28.9445 219.324C29.534 220.353 29.1992 221.67 28.1789 222.276L22.8433 225.441C21.8231 226.047 20.5039 225.711 19.9132 224.682C10.1588 207.691 3.82726 188.953 1.27663 169.528ZM232.933 272.402C233.598 273.384 233.342 274.721 232.351 275.373C216.191 285.987 198.125 293.381 179.146 297.141C159.465 301.039 139.201 300.95 119.555 296.878C99.9087 292.805 81.279 284.833 64.7687 273.433C48.8478 262.439 35.2097 248.473 24.6004 232.31C23.9495 231.318 24.2461 229.99 25.247 229.353L30.4818 226.023C31.4828 225.387 32.809 225.683 33.461 226.674C43.3158 241.653 55.9698 254.597 70.7349 264.792C86.0895 275.395 103.415 282.809 121.686 286.596C139.957 290.383 158.802 290.467 177.106 286.841C194.707 283.354 211.464 276.506 226.461 266.679C227.453 266.029 228.787 266.284 229.453 267.266L232.933 272.402ZM277.015 229.794C277.646 228.789 277.323 227.467 276.31 226.85L271.01 223.626C269.996 223.009 268.676 223.332 268.044 224.336C259.058 238.605 247.559 251.128 234.105 261.295C233.159 262.01 232.949 263.353 233.65 264.31L237.314 269.317C238.014 270.274 239.359 270.484 240.306 269.769C254.871 258.787 267.311 245.24 277.015 229.794ZM257.757 150C257.757 209.512 209.512 257.757 150 257.757C90.4879 257.757 42.2437 209.512 42.2437 150C42.2437 90.4879 90.4879 42.2437 150 42.2437C209.512 42.2437 257.757 90.4879 257.757 150ZM280.668 150C280.668 222.166 222.166 280.668 150 280.668C77.8342 280.668 19.332 222.166 19.332 150C19.332 77.8342 77.8342 19.332 150 19.332C222.166 19.332 280.668 77.8342 280.668 150Z"
className="fill-foreground/50 "
/>
</g>
</svg>
</div>
)
}

export default LoadingLogo
23 changes: 23 additions & 0 deletions apps/portal/app/lib/hooks/usePageVisibility.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { useEffect, useState } from 'react'

const usePageVisibility = () => {
const [isVisible, setIsVisible] = useState(!document.hidden)

useEffect(() => {
const handleVisibilityChange = () => {
setIsVisible(!document.hidden)
}

// Add event listener
document.addEventListener('visibilitychange', handleVisibilityChange)

// Cleanup
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange)
}
}, [])

return isVisible
}

export default usePageVisibility
38 changes: 25 additions & 13 deletions apps/portal/app/routes/_index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import PrivyRefresh from '@client/privy-refresh'
import logger from '@lib/utils/logger'
import { getMaintenanceMode } from '@lib/utils/maintenance'
import { json, LoaderFunctionArgs, redirect } from '@remix-run/node'
import { onboardingModalCookie } from '@server/onboarding'
import { getPrivyTokens } from '@server/privy'
import { getPrivyTokens, verifyPrivyAccessToken } from '@server/privy'
import { PATHS } from 'app/consts'

export async function loader({ request }: LoaderFunctionArgs) {
Expand All @@ -23,24 +22,37 @@ export async function loader({ request }: LoaderFunctionArgs) {
if (!cookie) {
throw redirect('/intro')
}

const { accessToken, sessionToken } = getPrivyTokens(request)

// If we have an access token, verify it's still valid
if (accessToken) {
logger('accessToken', accessToken)
if (redirectTo) {
throw redirect(redirectTo)
const verifiedClaims = await verifyPrivyAccessToken(request)
if (verifiedClaims) {
logger('[Loader] User is authenticated, redirecting to destination')
throw redirect(redirectTo || PATHS.HOME)
}
throw redirect(PATHS.HOME)
// Token exists but is invalid - continue to refresh flow
}

if (!sessionToken) {
throw redirect('/login')
} else {
// if there is no access token, but there is a session token
// Load the client to refresh the user's session if it's valid
return json({})
// If we have a session token but no valid access token,
// redirect to refresh route to handle token refresh
if (sessionToken) {
logger(
'[Loader] Session token present but no valid access token, redirecting to refresh',
)
const refreshUrl = new URL('/refresh', request.url)
if (redirectTo) {
refreshUrl.searchParams.set('redirectTo', redirectTo)
}
throw redirect(refreshUrl.toString())
}

// No tokens at all, redirect to login
logger('[Loader] No tokens present, redirecting to login')
throw redirect('/login')
}

export default function Index() {
return <PrivyRefresh />
return null
}
8 changes: 2 additions & 6 deletions apps/portal/app/routes/app+/profile+/_index+/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,13 @@ import {
useRevalidator,
} from '@remix-run/react'
import { fetchWrapper } from '@server/api'
import { requireUser, requireUserWallet } from '@server/auth'
import { requireUser } from '@server/auth'
import { getVaultDetails } from '@server/multivault'
import { getRelicCount } from '@server/relics'
import {
BLOCK_EXPLORER_URL,
CURRENT_ENV,
MULTIVAULT_CONTRACT_ADDRESS,
NO_WALLET_ERROR,
PATHS,
userIdentityRouteOptions,
} from 'app/consts'
Expand All @@ -91,9 +90,6 @@ export async function loader({ request }: LoaderFunctionArgs) {
invariant(user.wallet?.address, 'User wallet not found')
const userWallet = user.wallet?.address

const wallet = await requireUserWallet(request)
invariant(wallet, NO_WALLET_ERROR)

// TODO: Remove this relic hold/mint count and points calculation when it is stored in BE.
const relicHoldCount = await getRelicCount(userWallet as `0x${string}`)

Expand All @@ -114,7 +110,7 @@ export async function loader({ request }: LoaderFunctionArgs) {

const { identity: userIdentity, isPending } = await getIdentityOrPending(
request,
wallet,
userWallet,
)

invariant(userIdentity, 'No user identity found')
Expand Down
Loading
Loading