From b190931d01401162a6cd1de2b6e8f79e86b69f5d Mon Sep 17 00:00:00 2001
From: Viet Nguyen <3805254+vnugent@users.noreply.github.com>
Date: Sat, 16 Nov 2024 10:51:58 -0800
Subject: [PATCH] refactor: initialize user data db in once (#1224)
* refactor: initialize user profile in db once
* refactor: migrate next-auth handler to next 13
---
src/app/(default)/layout.tsx | 2 -
src/app/(maps)/components/ProfileMenu.tsx | 2 -
.../api/auth/[...nextauth]/route.ts} | 20 +++++--
src/app/api/user/me/route.ts | 33 ++++++++++++
src/components/auth/OnboardingCheck.tsx | 14 -----
src/js/auth/initializeUserInDb.ts | 44 ++++++++++++++++
src/js/auth/withUserAuth.ts | 3 +-
src/js/hooks/useUserProfileCmd.tsx | 2 +-
src/js/hooks/useUsernameCheck.ts | 52 -------------------
src/js/types/User.ts | 4 ++
src/pages/_app.tsx | 2 -
src/pages/api/basecamp/user.ts | 2 +-
src/pages/api/basecamp/userRoles.ts | 2 +-
src/pages/api/basecamp/users.ts | 2 +-
src/pages/api/media/get-signed-url.ts | 2 +-
src/pages/api/user/me.ts | 33 ------------
src/pages/api/withAuth.ts | 2 +-
17 files changed, 105 insertions(+), 116 deletions(-)
rename src/{pages/api/auth/[...nextauth].ts => app/api/auth/[...nextauth]/route.ts} (86%)
create mode 100644 src/app/api/user/me/route.ts
delete mode 100644 src/components/auth/OnboardingCheck.tsx
create mode 100644 src/js/auth/initializeUserInDb.ts
delete mode 100644 src/js/hooks/useUsernameCheck.ts
delete mode 100644 src/pages/api/user/me.ts
diff --git a/src/app/(default)/layout.tsx b/src/app/(default)/layout.tsx
index 6a8f7e8c0..bcfa7ed93 100644
--- a/src/app/(default)/layout.tsx
+++ b/src/app/(default)/layout.tsx
@@ -8,7 +8,6 @@ import { PageFooter } from './components/PageFooter'
import { NextAuthProvider } from '@/components/auth/NextAuthProvider'
import { ReactToastifyProvider } from './components/ReactToastifyProvider'
import { BlockingAlertUploadingInProgress } from './components/ui/GlobalAlerts'
-import { OnboardingCheck } from '@/components/auth/OnboardingCheck'
export const metadata: Metadata = {
title: 'OpenBeta',
@@ -36,7 +35,6 @@ export default function RootLayout ({
{children}
-
diff --git a/src/app/(maps)/components/ProfileMenu.tsx b/src/app/(maps)/components/ProfileMenu.tsx
index 6a4c7627b..e880ab792 100644
--- a/src/app/(maps)/components/ProfileMenu.tsx
+++ b/src/app/(maps)/components/ProfileMenu.tsx
@@ -3,7 +3,6 @@ import Link from 'next/link'
import { SessionProvider } from 'next-auth/react'
import { House } from '@phosphor-icons/react/dist/ssr'
import AuthenticatedProfileNavButton from '@/components/AuthenticatedProfileNavButton'
-import { OnboardingCheck } from '@/components/auth/OnboardingCheck'
export const ProfileMenu: React.FC = () => {
return (
@@ -14,7 +13,6 @@ export const ProfileMenu: React.FC = () => {
-
)
}
diff --git a/src/pages/api/auth/[...nextauth].ts b/src/app/api/auth/[...nextauth]/route.ts
similarity index 86%
rename from src/pages/api/auth/[...nextauth].ts
rename to src/app/api/auth/[...nextauth]/route.ts
index be03eb6cb..efa420bc3 100644
--- a/src/pages/api/auth/[...nextauth].ts
+++ b/src/app/api/auth/[...nextauth]/route.ts
@@ -3,8 +3,9 @@ import axios from 'axios'
import type { NextAuthOptions } from 'next-auth'
import Auth0Provider from 'next-auth/providers/auth0'
-import { AUTH_CONFIG_SERVER } from '../../../Config'
-import { IUserMetadata, UserRole } from '../../../js/types/User'
+import { AUTH_CONFIG_SERVER } from '../../../../Config'
+import { IUserMetadata, UserRole } from '../../../../js/types/User'
+import { initializeUserInDB } from '@/js/auth/initializeUserInDb'
const CustomClaimsNS = 'https://tacos.openbeta.io/'
const CustomClaimUserMetadata = CustomClaimsNS + 'user_metadata'
@@ -24,7 +25,6 @@ export const authOptions: NextAuthOptions = {
clientSecret,
issuer,
authorization: { params: { audience: 'https://api.openbeta.io', scope: 'offline_access access_token_authz openid email profile read:current_user create:current_user_metadata update:current_user_metadata read:stats update:area_attrs' } },
-
client: {
token_endpoint_auth_method: clientSecret.length === 0 ? 'none' : 'client_secret_basic'
}
@@ -84,6 +84,17 @@ export const authOptions: NextAuthOptions = {
throw new Error('Invalid auth data')
}
+ if (!(token.userMetadata?.initializedDb ?? false)) {
+ const { userMetadata, email, picture: avatar, id: auth0UserId } = token
+ const { nick: username, uuid: userUuid } = userMetadata
+ const { accessToken } = token
+
+ const success = await initializeUserInDB({ auth0UserId, accessToken, username, userUuid, avatar, email })
+ if (success) {
+ token.userMetadata.initializedDb = true
+ }
+ }
+
if ((token.expiresAt as number) < (Date.now() / 1000)) {
const { accessToken, refreshToken, expiresAt } = await refreshAccessTokenSilently(token.refreshToken as string)
token.accessToken = accessToken
@@ -109,7 +120,8 @@ export const authOptions: NextAuthOptions = {
}
}
-export default NextAuth(authOptions)
+const handler = NextAuth(authOptions)
+export { handler as GET, handler as POST }
const refreshAccessTokenSilently = async (refreshToken: string): Promise => {
const response = await axios.request<{
diff --git a/src/app/api/user/me/route.ts b/src/app/api/user/me/route.ts
new file mode 100644
index 000000000..6644166ad
--- /dev/null
+++ b/src/app/api/user/me/route.ts
@@ -0,0 +1,33 @@
+import { withUserAuth } from '@/js/auth/withUserAuth'
+import { NextRequest, NextResponse } from 'next/server'
+import useUserProfileCmd from '@/js/hooks/useUserProfileCmd'
+
+/**
+ * Direct `/api/user/me` to `/u/ => {
+ const uuid = req.headers.get('x-openbeta-user-uuid')
+ const accessToken = req.headers.get('x-auth0-access-token')
+
+ if (accessToken == null || uuid == null) {
+ return NextResponse.json({ status: 500 })
+ }
+
+ const url = req.nextUrl.clone()
+ url.pathname = '/'
+
+ try {
+ const { getUsernameById } = useUserProfileCmd({ accessToken })
+ const usernameInfo = await getUsernameById({ userUuid: uuid })
+ if (usernameInfo?.username == null) {
+ return NextResponse.rewrite(url)
+ } else {
+ url.pathname = `/u/${usernameInfo.username}`
+ return NextResponse.redirect(url)
+ }
+ } catch (e) {
+ return NextResponse.rewrite(url)
+ }
+}
+
+export const GET = withUserAuth(getHandler)
diff --git a/src/components/auth/OnboardingCheck.tsx b/src/components/auth/OnboardingCheck.tsx
deleted file mode 100644
index 08d28c74b..000000000
--- a/src/components/auth/OnboardingCheck.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-'use client'
-import useUsernameCheck, { useUsernameCheckAppDir } from '@/js/hooks/useUsernameCheck'
-
-/**
- * A wrapper component to imperatively check if the user has completed onboarding.
- */
-export const OnboardingCheck: React.FC<{ isAppDir?: boolean }> = ({ isAppDir = true }) => {
- if (isAppDir) {
- useUsernameCheckAppDir()
- } else {
- useUsernameCheck()
- }
- return null
-}
diff --git a/src/js/auth/initializeUserInDb.ts b/src/js/auth/initializeUserInDb.ts
new file mode 100644
index 000000000..d4911741a
--- /dev/null
+++ b/src/js/auth/initializeUserInDb.ts
@@ -0,0 +1,44 @@
+import { getClient } from '../graphql/ServerClient'
+import { MUTATION_UPDATE_PROFILE } from '../graphql/gql/users'
+import { updateUser } from './ManagementClient'
+
+export interface UpdateUsernameInput {
+ userUuid: string
+ username: string
+ email?: string | null
+ avatar?: string | null
+}
+
+interface InitializeUserInDBParams extends UpdateUsernameInput {
+ accessToken: string
+ auth0UserId: string
+}
+
+export const initializeUserInDB = async (params: InitializeUserInDBParams): Promise => {
+ const { auth0UserId, accessToken, userUuid, username, email, avatar } = params
+ const res = await getClient().mutate<{ updateUserProfile?: boolean }, UpdateUsernameInput>({
+ mutation: MUTATION_UPDATE_PROFILE,
+ variables: {
+ userUuid,
+ username,
+ email,
+ avatar
+ },
+ context: {
+ headers: {
+ authorization: `Bearer ${accessToken}`
+ }
+ },
+ fetchPolicy: 'no-cache'
+ })
+ const success = res.data?.updateUserProfile ?? false
+ if (success) {
+ try {
+ await updateUser(auth0UserId, { initializedDb: true })
+ } catch (error) {
+ console.log('Error initializing user in db')
+ return false
+ }
+ }
+ return success
+}
diff --git a/src/js/auth/withUserAuth.ts b/src/js/auth/withUserAuth.ts
index dce265384..04e078bca 100644
--- a/src/js/auth/withUserAuth.ts
+++ b/src/js/auth/withUserAuth.ts
@@ -1,6 +1,6 @@
import { getServerSession } from 'next-auth'
import { NextRequest, NextResponse } from 'next/server'
-import { authOptions } from 'pages/api/auth/[...nextauth]'
+import { authOptions } from '@/app/api/auth/[...nextauth]/route'
type Next13APIHandler = (req: NextRequest) => Promise
@@ -15,6 +15,7 @@ export const withUserAuth = (handler: Next13APIHandler): Next13APIHandler => {
// Passing useful session data downstream
req.headers.set('x-openbeta-user-uuid', session.user.metadata.uuid)
req.headers.set('x-auth0-userid', session.id)
+ req.headers.set('x-auth0-access-token', session.accessToken)
return await handler(req)
} else {
return NextResponse.json({ status: 401 })
diff --git a/src/js/hooks/useUserProfileCmd.tsx b/src/js/hooks/useUserProfileCmd.tsx
index 62ff83164..73847c907 100644
--- a/src/js/hooks/useUserProfileCmd.tsx
+++ b/src/js/hooks/useUserProfileCmd.tsx
@@ -9,7 +9,7 @@ import { UserPublicProfile } from '../types/User'
interface GetUsernameByIdInput {
userUuid: string
}
-interface UpdateUsernameInput {
+export interface UpdateUsernameInput {
userUuid: string
username: string
email?: string
diff --git a/src/js/hooks/useUsernameCheck.ts b/src/js/hooks/useUsernameCheck.ts
deleted file mode 100644
index a44db2564..000000000
--- a/src/js/hooks/useUsernameCheck.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { useEffect } from 'react'
-import { useRouter as useRouterAppDir, usePathname } from 'next/navigation'
-import { useRouter } from 'next/router'
-import { useSession } from 'next-auth/react'
-import useUserProfileCmd from './useUserProfileCmd'
-
-/**
- * A global hook that forces new users to go to the user name screen.
- */
-export default function useUsernameCheck (): void {
- const router = useRouter()
- const { data, status } = useSession()
- const { getUsernameById } = useUserProfileCmd({ accessToken: data?.accessToken as string })
-
- useEffect(() => {
- const uuid = data?.user.metadata.uuid
- if (router.asPath.startsWith('/auth/')) {
- return
- }
- if (status === 'authenticated' && uuid != null) {
- void getUsernameById({ userUuid: uuid }).then(usernameInfo => {
- if (usernameInfo === null) {
- void router.push('/account/changeUsername')
- }
- })
- }
- }, [status])
-}
-
-/**
- * A global hook that forces new users to go to the user name screen. Use this version for pages in the `/app` directory (Next v13).
- */
-export function useUsernameCheckAppDir (): void {
- const router = useRouterAppDir()
- const pathname = usePathname()
- const { data, status } = useSession()
- const { getUsernameById } = useUserProfileCmd({ accessToken: data?.accessToken as string })
-
- useEffect(() => {
- const uuid = data?.user.metadata.uuid
- if (pathname.startsWith('/auth/')) {
- return
- }
- if (status === 'authenticated' && uuid != null) {
- void getUsernameById({ userUuid: uuid }).then(usernameInfo => {
- if (usernameInfo === null) {
- void router.push('/account/changeUsername')
- }
- })
- }
- }, [status])
-}
diff --git a/src/js/types/User.ts b/src/js/types/User.ts
index cbf48f8e6..4564cefc9 100644
--- a/src/js/types/User.ts
+++ b/src/js/types/User.ts
@@ -22,6 +22,10 @@ export interface IWritableUserMetadata {
bio: string
website?: string
ticksImported?: boolean
+ /**
+ * Indicate whether or not we have initialized user account in our own db
+ */
+ initializedDb?: boolean
collections?: {
/** Users can organize entities into their own 'climbing playlists'
* Strictly speaking, this should always be mutated as a SET rather than an
diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx
index 66b90f2ed..9c1b7dc4c 100644
--- a/src/pages/_app.tsx
+++ b/src/pages/_app.tsx
@@ -13,7 +13,6 @@ import '../../public/fonts/fonts.css'
import { useUserGalleryStore } from '../js/stores/useUserGalleryStore'
import { BlockingAlert } from '../components/ui/micro/AlertDialogue'
import { XMarkIcon } from '@heroicons/react/24/outline'
-import { OnboardingCheck } from '@/components/auth/OnboardingCheck'
Router.events.on('routeChangeStart', () => NProgress.start())
Router.events.on('routeChangeComplete', () => NProgress.done())
@@ -42,7 +41,6 @@ export default function MyApp ({ Component, pageProps: { session, ...pageProps }
>
)
}
-
= async (req, res) => {
try {
diff --git a/src/pages/api/basecamp/userRoles.ts b/src/pages/api/basecamp/userRoles.ts
index 806cb4bde..e089c7f57 100644
--- a/src/pages/api/basecamp/userRoles.ts
+++ b/src/pages/api/basecamp/userRoles.ts
@@ -4,7 +4,7 @@ import { getServerSession } from 'next-auth'
import withAuth from '../withAuth'
import { getUserRoles, setUserRoles } from '../../../js/auth/ManagementClient'
import { UserRole } from '../../../js/types'
-import { authOptions } from '../auth/[...nextauth]'
+import { authOptions } from '@/app/api/auth/[...nextauth]/route'
const handler: NextApiHandler = async (req, res) => {
try {
diff --git a/src/pages/api/basecamp/users.ts b/src/pages/api/basecamp/users.ts
index 48f829414..2b61036d3 100644
--- a/src/pages/api/basecamp/users.ts
+++ b/src/pages/api/basecamp/users.ts
@@ -4,7 +4,7 @@ import { getServerSession } from 'next-auth'
import withAuth from '../withAuth'
import { getAllUsersMetadata } from '../../../js/auth/ManagementClient'
import { UserRole } from '../../../js/types'
-import { authOptions } from '../auth/[...nextauth]'
+import { authOptions } from '@/app/api/auth/[...nextauth]/route'
const handler: NextApiHandler = async (req, res) => {
try {
diff --git a/src/pages/api/media/get-signed-url.ts b/src/pages/api/media/get-signed-url.ts
index daab26321..c6afa0674 100644
--- a/src/pages/api/media/get-signed-url.ts
+++ b/src/pages/api/media/get-signed-url.ts
@@ -4,7 +4,7 @@ import { nolookalikesSafe } from 'nanoid-dictionary'
import { extname } from 'path'
import { getServerSession } from 'next-auth'
import withAuth from '../withAuth'
-import { authOptions } from '../auth/[...nextauth]'
+import { authOptions } from '@/app/api/auth/[...nextauth]/route'
import { getSignedUrlForUpload } from '../../../js/media/storageClient'
export interface MediaPreSignedProps {
diff --git a/src/pages/api/user/me.ts b/src/pages/api/user/me.ts
deleted file mode 100644
index 989a89ea8..000000000
--- a/src/pages/api/user/me.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { NextApiHandler } from 'next'
-import { getServerSession } from 'next-auth'
-import { authOptions } from '../auth/[...nextauth]'
-
-import withAuth from '../withAuth'
-import useUserProfileCmd from '../../../js/hooks/useUserProfileCmd'
-
-const handler: NextApiHandler = async (req, res) => {
- const session = await getServerSession(req, res, authOptions)
- const uuid = session?.user.metadata.uuid
-
- if (uuid == null) {
- res.writeHead(307, { Location: '/' }).end()
- return
- }
-
- const isPreview = req.query?.preview != null
-
- try {
- const { getUsernameById } = useUserProfileCmd({ accessToken: uuid })
- const usernameInfo = await getUsernameById({ userUuid: uuid })
- if (usernameInfo?.username == null) {
- res.writeHead(307, { Location: '/' }).end()
- } else {
- const Location = isPreview ? `/u2/${usernameInfo.username}` : `/u/${usernameInfo.username}`
- res.writeHead(307, { Location }).end()
- }
- } catch (e) {
- res.writeHead(307, { Location: '/' }).end()
- }
-}
-
-export default withAuth(handler)
diff --git a/src/pages/api/withAuth.ts b/src/pages/api/withAuth.ts
index 42aa78e81..242462379 100644
--- a/src/pages/api/withAuth.ts
+++ b/src/pages/api/withAuth.ts
@@ -4,7 +4,7 @@
*/
import { NextApiHandler } from 'next'
import { getServerSession } from 'next-auth'
-import { authOptions } from './auth/[...nextauth]'
+import { authOptions } from '@/app/api/auth/[...nextauth]/route'
const withAuth = (handler: NextApiHandler): NextApiHandler => {
return async (req, res) => {