diff --git a/.env.example b/.env.example
index 5768ed18..dcf61999 100644
--- a/.env.example
+++ b/.env.example
@@ -8,14 +8,17 @@ NX_POLYGON_API_KEY=
# If using free ngrok account for webhooks
NGROK_AUTH_TOKEN=
-# Required for Auth0 deploy client (see `yarn auth0:deploy` command)
-AUTH0_ENV=development
-NX_AUTH0_MGMT_CLIENT_SECRET=
-NX_AUTH0_CLIENT_SECRET=
-AUTH0_DEPLOY_CLIENT_SECRET=
POSTMARK_SMTP_PASS=
NX_SESSION_SECRET=
+# Generate a new secret using openssl rand -base64 32
+NEXTAUTH_SECRET=
+NEXTAUTH_URL=http://localhost:4200
+NX_NEXTAUTH_URL=http://localhost:4200
+
NX_PLAID_SECRET=
NX_FINICITY_APP_KEY=
NX_FINICITY_PARTNER_SECRET=
+NX_CONVERTKIT_SECRET=
+
+NEXT_PUBLIC_ZAPIER_FEEDBACK_HOOK_URL=
diff --git a/README.md b/README.md
index 03281b11..d2e47717 100644
--- a/README.md
+++ b/README.md
@@ -43,8 +43,17 @@ This is the current state of building the app. You'll hit errors, which we're wo
You'll need Docker installed to run the app locally.
+First, copy the `.env.example` file to `.env`:
+
```
cp .env.example .env
+```
+
+Then, create a new secret using `openssl rand -base64 32` and populate `NEXTAUTH_SECRET` in your `.env` file with it.
+
+Then run the following yarn commands:
+
+```
yarn install
yarn run dev:services
yarn prisma:migrate:dev
diff --git a/apps/client/components/APM.tsx b/apps/client/components/APM.tsx
index 94e858c3..f043261d 100644
--- a/apps/client/components/APM.tsx
+++ b/apps/client/components/APM.tsx
@@ -1,19 +1,19 @@
-import { useAuth0 } from '@auth0/auth0-react'
import { useEffect } from 'react'
import * as Sentry from '@sentry/react'
+import { useSession } from 'next-auth/react'
export default function APM() {
- const { user } = useAuth0()
+ const { data: session } = useSession()
// Identify Sentry user
useEffect(() => {
- if (user) {
+ if (session && session.user) {
Sentry.setUser({
- id: user.sub,
- email: user.email,
+ id: session.user['sub'] ?? undefined,
+ email: session.user['https://maybe.co'] ?? undefined,
})
}
- }, [user])
+ }, [session])
return null
}
diff --git a/apps/client/components/ModalManager.tsx b/apps/client/components/ModalManager.tsx
deleted file mode 100644
index cbf7800a..00000000
--- a/apps/client/components/ModalManager.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import {
- type ModalKey,
- type ModalManagerAction,
- ModalManagerContext,
- useUserApi,
- useLocalStorage,
-} from '@maybe-finance/client/shared'
-import { type PropsWithChildren, useReducer, useEffect } from 'react'
-import { LinkAccountFlow } from '@maybe-finance/client/features'
-
-function reducer(
- state: Record,
- action: ModalManagerAction
-) {
- switch (action.type) {
- case 'open':
- return { ...state, [action.key]: { isOpen: true, props: action.props } }
- case 'close':
- return { ...state, [action.key]: { isOpen: false, props: null } }
- }
-}
-
-/**
- * Manages auto-prompt modals and regular modals to avoid stacking collisions
- */
-export default function ModalManager({ children }: PropsWithChildren) {
- const [accountLinkHidden, setAccountLinkHidden] = useLocalStorage('account-link-hidden', false)
-
- const [state, dispatch] = useReducer(reducer, {
- linkAuth0Accounts: { isOpen: false, props: null },
- })
-
- const allClosed = Object.values(state).every((v) => v.isOpen === false)
-
- const { useAuth0Profile } = useUserApi()
- const auth0Profile = useAuth0Profile()
-
- useEffect(() => {
- const autoPrompt = auth0Profile.data?.autoPromptEnabled === true
- const provider = auth0Profile.data?.suggestedIdentities?.[0]?.provider
-
- if (autoPrompt && provider && allClosed && !accountLinkHidden) {
- dispatch({
- type: 'open',
- key: 'linkAuth0Accounts',
- props: { secondaryProvider: provider },
- })
- }
- }, [auth0Profile.data, dispatch, allClosed, accountLinkHidden])
-
- return (
-
- {children}
-
- {
- dispatch({ type: 'close', key: 'linkAuth0Accounts' })
- setAccountLinkHidden(true)
- }}
- {...state.linkAuth0Accounts.props}
- />
-
- )
-}
diff --git a/apps/client/env.ts b/apps/client/env.ts
index 0e00bd5f..3d38de48 100644
--- a/apps/client/env.ts
+++ b/apps/client/env.ts
@@ -1,13 +1,10 @@
const env = {
NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3333',
- NEXT_PUBLIC_AUTH0_DOMAIN:
- process.env.NEXT_PUBLIC_AUTH0_DOMAIN || 'REPLACE_THIS',
- NEXT_PUBLIC_AUTH0_CLIENT_ID:
- process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID || 'REPLACE_THIS',
+ NEXT_PUBLIC_AUTH0_DOMAIN: process.env.NEXT_PUBLIC_AUTH0_DOMAIN || 'REPLACE_THIS',
+ NEXT_PUBLIC_AUTH0_CLIENT_ID: process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID || 'REPLACE_THIS',
NEXT_PUBLIC_AUTH0_AUDIENCE:
process.env.NEXT_PUBLIC_AUTH0_AUDIENCE || 'https://maybe-finance-api/v1',
- NEXT_PUBLIC_LD_CLIENT_SIDE_ID:
- process.env.NEXT_PUBLIC_LD_CLIENT_SIDE_ID || 'REPLACE_THIS',
+ NEXT_PUBLIC_LD_CLIENT_SIDE_ID: process.env.NEXT_PUBLIC_LD_CLIENT_SIDE_ID || 'REPLACE_THIS',
NEXT_PUBLIC_SENTRY_DSN: process.env.NEXT_PUBLIC_SENTRY_DSN,
NEXT_PUBLIC_SENTRY_ENV: process.env.NEXT_PUBLIC_SENTRY_ENV,
}
diff --git a/apps/client/pages/_app.tsx b/apps/client/pages/_app.tsx
index 4fde4633..d1fb65c9 100644
--- a/apps/client/pages/_app.tsx
+++ b/apps/client/pages/_app.tsx
@@ -1,4 +1,4 @@
-import type { PropsWithChildren, ReactElement } from 'react'
+import { useEffect, type PropsWithChildren, type ReactElement } from 'react'
import type { AppProps } from 'next/app'
import { ErrorBoundary } from 'react-error-boundary'
import { Analytics } from '@vercel/analytics/react'
@@ -8,18 +8,17 @@ import {
ErrorFallback,
LogProvider,
UserAccountContextProvider,
- AuthProvider,
} from '@maybe-finance/client/shared'
-import { AccountsManager } from '@maybe-finance/client/features'
+import { AccountsManager, OnboardingGuard } from '@maybe-finance/client/features'
import { AccountContextProvider } from '@maybe-finance/client/shared'
import * as Sentry from '@sentry/react'
import { BrowserTracing } from '@sentry/tracing'
import env from '../env'
import '../styles.css'
-import { withAuthenticationRequired } from '@auth0/auth0-react'
-import ModalManager from '../components/ModalManager'
+import { SessionProvider, useSession } from 'next-auth/react'
import Meta from '../components/Meta'
import APM from '../components/APM'
+import { useRouter } from 'next/router'
Sentry.init({
dsn: env.NEXT_PUBLIC_SENTRY_DSN,
@@ -33,20 +32,30 @@ Sentry.init({
})
// Providers and components only relevant to a logged-in user
-const WithAuth = withAuthenticationRequired(function ({ children }: PropsWithChildren) {
- return (
-
-
-
- {children}
+const WithAuth = function ({ children }: PropsWithChildren) {
+ const { data: session } = useSession()
+ const router = useRouter()
- {/* Add, edit, delete connections and manual accounts */}
-
-
-
-
- )
-})
+ useEffect(() => {
+ if (!session) {
+ router.push('/login')
+ }
+ }, [session, router])
+
+ if (session) {
+ return (
+
+
+
+ {children}
+
+
+
+
+ )
+ }
+ return null
+}
export default function App({
Component: Page,
@@ -71,7 +80,7 @@ export default function App({
-
+
<>
@@ -82,7 +91,7 @@ export default function App({
)}
>
-
+
diff --git a/apps/client/pages/api/auth/[...nextauth].ts b/apps/client/pages/api/auth/[...nextauth].ts
new file mode 100644
index 00000000..3a18412a
--- /dev/null
+++ b/apps/client/pages/api/auth/[...nextauth].ts
@@ -0,0 +1,145 @@
+import NextAuth from 'next-auth'
+import type { SessionStrategy, NextAuthOptions } from 'next-auth'
+import CredentialsProvider from 'next-auth/providers/credentials'
+import { z } from 'zod'
+import { PrismaClient, type Prisma } from '@prisma/client'
+import { PrismaAdapter } from '@auth/prisma-adapter'
+import type { SharedType } from '@maybe-finance/shared'
+import bcrypt from 'bcrypt'
+
+let prismaInstance: PrismaClient | null = null
+
+function getPrismaInstance() {
+ if (!prismaInstance) {
+ prismaInstance = new PrismaClient()
+ }
+ return prismaInstance
+}
+
+const prisma = getPrismaInstance()
+
+async function createAuthUser(data: Prisma.AuthUserCreateInput) {
+ const authUser = await prisma.authUser.create({ data: { ...data } })
+ return authUser
+}
+
+async function getAuthUserByEmail(email: string) {
+ if (!email) throw new Error('No email provided.')
+ return await prisma.authUser.findUnique({
+ where: { email },
+ })
+}
+
+async function validateCredentials(credentials: any): Promise> {
+ const authSchema = z.object({
+ firstName: z.string().optional(),
+ lastName: z.string().optional(),
+ email: z.string().email({ message: 'Invalid email address.' }),
+ password: z.string().min(6),
+ })
+
+ const parsed = authSchema.safeParse(credentials)
+ if (!parsed.success) {
+ throw new Error(parsed.error.issues.map((issue) => issue.message).join(', '))
+ }
+
+ return parsed.data
+}
+
+async function createNewAuthUser(credentials: {
+ firstName: string
+ lastName: string
+ email: string
+ password: string
+}): Promise {
+ const { firstName, lastName, email, password } = credentials
+
+ if (!firstName || !lastName) {
+ throw new Error('Both first name and last name are required.')
+ }
+
+ const hashedPassword = await bcrypt.hash(password, 10)
+ return createAuthUser({
+ firstName,
+ lastName,
+ name: `${firstName} ${lastName}`,
+ email,
+ password: hashedPassword,
+ })
+}
+
+const authPrisma = {
+ account: prisma.authAccount,
+ user: prisma.authUser,
+ session: prisma.authSession,
+ verificationToken: prisma.authVerificationToken,
+} as unknown as PrismaClient
+
+export const authOptions = {
+ adapter: PrismaAdapter(authPrisma),
+ secret: process.env.NEXTAUTH_SECRET || 'CHANGE_ME',
+ pages: {
+ signIn: '/login',
+ },
+ session: {
+ strategy: 'jwt' as SessionStrategy,
+ maxAge: 1 * 24 * 60 * 60, // 1 Day
+ },
+
+ providers: [
+ CredentialsProvider({
+ name: 'Credentials',
+ type: 'credentials',
+ credentials: {
+ firstName: { label: 'First name', type: 'text', placeholder: 'First name' },
+ lastName: { label: 'Last name', type: 'text', placeholder: 'Last name' },
+ email: { label: 'Email', type: 'email', placeholder: 'hello@maybe.co' },
+ password: { label: 'Password', type: 'password' },
+ },
+ async authorize(credentials) {
+ const { firstName, lastName, email, password } = await validateCredentials(
+ credentials
+ )
+
+ const existingUser = await getAuthUserByEmail(email)
+ if (existingUser) {
+ const isPasswordMatch = await bcrypt.compare(password, existingUser.password!)
+ if (!isPasswordMatch) {
+ throw new Error('Email or password is invalid.')
+ }
+
+ return existingUser
+ }
+
+ if (!firstName || !lastName) {
+ throw new Error('Invalid credentials provided.')
+ }
+
+ return createNewAuthUser({ firstName, lastName, email, password })
+ },
+ }),
+ ],
+ callbacks: {
+ async jwt({ token, user: authUser }: { token: any; user: any }) {
+ if (authUser) {
+ token.sub = authUser.id
+ token['https://maybe.co/email'] = authUser.email
+ token.firstName = authUser.firstName
+ token.lastName = authUser.lastName
+ token.name = authUser.name
+ }
+ return token
+ },
+ async session({ session, token }: { session: any; token: any }) {
+ session.user = token.sub
+ session.sub = token.sub
+ session['https://maybe.co/email'] = token['https://maybe.co/email']
+ session.firstName = token.firstName
+ session.lastName = token.lastName
+ session.name = token.name
+ return session
+ },
+ },
+} as NextAuthOptions
+
+export default NextAuth(authOptions)
diff --git a/apps/client/pages/login.tsx b/apps/client/pages/login.tsx
index a6a393fc..84634495 100644
--- a/apps/client/pages/login.tsx
+++ b/apps/client/pages/login.tsx
@@ -1,20 +1,116 @@
-import { useAuth0 } from '@auth0/auth0-react'
-import { LoadingSpinner } from '@maybe-finance/design-system'
+import { useState, type ReactElement } from 'react'
+import { FullPageLayout } from '@maybe-finance/client/features'
+import { Input, InputPassword, Button } from '@maybe-finance/design-system'
+import { signIn, useSession } from 'next-auth/react'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
+import Script from 'next/script'
+import Link from 'next/link'
export default function LoginPage() {
- const { isAuthenticated } = useAuth0()
+ const [email, setEmail] = useState('')
+ const [password, setPassword] = useState('')
+ const [isValid, setIsValid] = useState(false)
+ const [errorMessage, setErrorMessage] = useState(null)
+
+ const { data: session } = useSession()
const router = useRouter()
useEffect(() => {
- if (isAuthenticated) router.push('/')
- }, [isAuthenticated, router])
+ if (session) {
+ router.push('/')
+ }
+ }, [session, router])
+
+ const onSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setErrorMessage(null)
+ setPassword('')
+
+ const response = await signIn('credentials', {
+ email,
+ password,
+ redirect: false,
+ })
+
+ if (response && response.error) {
+ setErrorMessage(response.error)
+ }
+ }
- // _app.tsx will automatically redirect if not authenticated
return (
-
-
-
+ <>
+
+
+
+
+
+
+
+
+
+ >
)
}
+
+LoginPage.getLayout = function getLayout(page: ReactElement) {
+ return {page}
+}
+
+LoginPage.isPublic = true
diff --git a/apps/client/pages/register.tsx b/apps/client/pages/register.tsx
index fbf2be5e..9192e405 100644
--- a/apps/client/pages/register.tsx
+++ b/apps/client/pages/register.tsx
@@ -1,20 +1,134 @@
-import { useAuth0 } from '@auth0/auth0-react'
-import { LoadingSpinner } from '@maybe-finance/design-system'
+import { useState, type ReactElement } from 'react'
+import { Input, InputPassword, Button } from '@maybe-finance/design-system'
+import { FullPageLayout } from '@maybe-finance/client/features'
+import { signIn, useSession } from 'next-auth/react'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
+import Script from 'next/script'
+import Link from 'next/link'
export default function RegisterPage() {
- const { isAuthenticated } = useAuth0()
+ const [firstName, setFirstName] = useState('')
+ const [lastName, setLastName] = useState('')
+ const [email, setEmail] = useState('')
+ const [password, setPassword] = useState('')
+ const [isValid, setIsValid] = useState(false)
+ const [errorMessage, setErrorMessage] = useState(null)
+
+ const { data: session } = useSession()
const router = useRouter()
useEffect(() => {
- if (isAuthenticated) router.push('/')
- }, [isAuthenticated, router])
+ if (session) router.push('/')
+ }, [session, router])
+
+ const onSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+
+ setErrorMessage(null)
+ setFirstName('')
+ setLastName('')
+ setEmail('')
+ setPassword('')
+
+ const response = await signIn('credentials', {
+ email,
+ password,
+ firstName,
+ lastName,
+ redirect: false,
+ })
+
+ if (response && response.error) {
+ setErrorMessage(response.error)
+ }
+ }
- // _app.tsx will automatically redirect if not authenticated
return (
-
-
-
+ <>
+
+
+
+
+
+
+
+
+
+ >
)
}
+
+RegisterPage.getLayout = function getLayout(page: ReactElement) {
+ return {page}
+}
+
+RegisterPage.isPublic = true
diff --git a/apps/client/pages/settings.tsx b/apps/client/pages/settings.tsx
index b511d426..581a937b 100644
--- a/apps/client/pages/settings.tsx
+++ b/apps/client/pages/settings.tsx
@@ -1,14 +1,13 @@
import type { ReactElement } from 'react'
-import { useQueryParam } from '@maybe-finance/client/shared'
+import { useQueryParam } from '@maybe-finance/client/shared'
import {
AccountSidebar,
BillingPreferences,
- GeneralPreferences,
SecurityPreferences,
UserDetails,
WithSidebarLayout,
} from '@maybe-finance/client/features'
-import { Tab } from '@maybe-finance/design-system'
+import { Tab } from '@maybe-finance/design-system'
import { useRouter } from 'next/router'
import Script from 'next/script'
@@ -35,7 +34,6 @@ export default function SettingsPage() {
>
Details
- Notifications
Security
Billing
@@ -43,11 +41,6 @@ export default function SettingsPage() {
-
-
-
-
-
diff --git a/apps/client/styles.css b/apps/client/styles.css
index 899c9424..788cf552 100644
--- a/apps/client/styles.css
+++ b/apps/client/styles.css
@@ -145,3 +145,20 @@
height: 0;
pointer-events: none;
}
+
+.radial-gradient-background {
+ background-image: radial-gradient(
+ 60% 200% at 50% 50%,
+ rgba(67, 97, 238, 0.5) 0%,
+ transparent 100%
+ );
+}
+
+.radial-gradient-background-dark {
+ background-image: radial-gradient(
+ 100% 100% at clamp(20%, calc(30% + var(--mx) * 0.05), 40%)
+ clamp(50%, calc(50% + var(--my) * 0.05), 60%),
+ #4361ee33 0%,
+ #16161af4 120%
+ );
+}
diff --git a/apps/server/src/app/__tests__/net-worth.integration.spec.ts b/apps/server/src/app/__tests__/net-worth.integration.spec.ts
index ef9fe84d..644ca0b9 100644
--- a/apps/server/src/app/__tests__/net-worth.integration.spec.ts
+++ b/apps/server/src/app/__tests__/net-worth.integration.spec.ts
@@ -4,7 +4,6 @@ import { createLogger, transports } from 'winston'
import { DateTime } from 'luxon'
import { PgService } from '@maybe-finance/server/shared'
import { AccountQueryService, UserService } from '@maybe-finance/server/features'
-import { managementClient } from '../lib/auth0'
import { resetUser } from './utils/user'
jest.mock('plaid')
jest.mock('auth0')
@@ -38,7 +37,6 @@ describe('user net worth', () => {
},
{} as any,
{} as any,
- managementClient,
{} as any
)
diff --git a/apps/server/src/app/__tests__/stripe.integration.spec.ts b/apps/server/src/app/__tests__/stripe.integration.spec.ts
index 5fa5ecd2..9e24f678 100644
--- a/apps/server/src/app/__tests__/stripe.integration.spec.ts
+++ b/apps/server/src/app/__tests__/stripe.integration.spec.ts
@@ -3,7 +3,6 @@ import { PrismaClient } from '@prisma/client'
import { createLogger, transports } from 'winston'
import { AccountQueryService, UserService } from '@maybe-finance/server/features'
import { resetUser } from './utils/user'
-import { managementClient } from '../lib/auth0'
import stripe from '../lib/stripe'
import { PgService } from '@maybe-finance/server/shared'
import { DateTime } from 'luxon'
@@ -18,7 +17,6 @@ const userService = new UserService(
{} as any,
{} as any,
{} as any,
- managementClient,
stripe
)
diff --git a/apps/server/src/app/__tests__/utils/axios.ts b/apps/server/src/app/__tests__/utils/axios.ts
index 8bf1ef02..e5b80ac5 100644
--- a/apps/server/src/app/__tests__/utils/axios.ts
+++ b/apps/server/src/app/__tests__/utils/axios.ts
@@ -23,10 +23,7 @@ export async function getAxiosClient() {
password: 'REPLACE_THIS',
audience: 'https://maybe-finance-api/v1',
scope: '',
- client_id: isCI
- ? 'REPLACE_THIS'
- : 'REPLACE_THIS',
- client_secret: env.NX_AUTH0_CLIENT_SECRET,
+ client_id: isCI ? 'REPLACE_THIS' : 'REPLACE_THIS',
},
})
diff --git a/apps/server/src/app/app.ts b/apps/server/src/app/app.ts
index 0d77d664..a2ae1001 100644
--- a/apps/server/src/app/app.ts
+++ b/apps/server/src/app/app.ts
@@ -19,7 +19,7 @@ import logger from './lib/logger'
import prisma from './lib/prisma'
import {
defaultErrorHandler,
- validateAuth0Jwt,
+ validateAuthJwt,
superjson,
authErrorHandler,
maintenance,
@@ -30,7 +30,6 @@ import {
usersRouter,
accountsRouter,
connectionsRouter,
- adminRouter,
webhooksRouter,
plaidRouter,
accountRollupRouter,
@@ -88,11 +87,10 @@ app.use(express.static(__dirname + '/assets'))
const origin = [env.NX_CLIENT_URL, ...env.NX_CORS_ORIGINS]
logger.info(`CORS origins: ${origin}`)
-app.use(cors({ origin }))
+app.use(cors({ origin, credentials: true }))
app.options('*', cors() as RequestHandler)
app.set('view engine', 'ejs').set('views', __dirname + '/app/admin/views')
-app.use('/admin', adminRouter)
app.use(
morgan(env.NX_MORGAN_LOG_LEVEL, {
@@ -116,7 +114,7 @@ app.use(express.json({ limit: '50mb' })) // Finicity sends large response bodies
app.use(
'/trpc',
- validateAuth0Jwt,
+ validateAuthJwt,
trpcExpress.createExpressMiddleware({
router: appRouter,
createContext: createTRPCContext,
@@ -150,7 +148,7 @@ app.use('/v1', webhooksRouter)
app.use('/v1', publicRouter)
// All routes AFTER this line are protected via OAuth
-app.use('/v1', validateAuth0Jwt)
+app.use('/v1', validateAuthJwt)
// Private routes
app.use('/v1/users', usersRouter)
diff --git a/apps/server/src/app/lib/auth0.ts b/apps/server/src/app/lib/auth0.ts
deleted file mode 100644
index 1c96ffff..00000000
--- a/apps/server/src/app/lib/auth0.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import type { SharedType } from '@maybe-finance/shared'
-import { ManagementClient } from 'auth0'
-import env from '../../env'
-
-/**
- * Management API Documentation
- * - https://auth0.com/docs/api/management/v2
- * - https://auth0.github.io/node-auth0/module-management.ManagementClient.html
- */
-export const managementClient = new ManagementClient<
- SharedType.MaybeAppMetadata,
- SharedType.MaybeUserMetadata
->({
- domain: env.NX_AUTH0_DOMAIN,
- clientId: env.NX_AUTH0_MGMT_CLIENT_ID,
- clientSecret: env.NX_AUTH0_MGMT_CLIENT_SECRET,
- scope: 'read:users update:users delete:users',
-})
diff --git a/apps/server/src/app/lib/endpoint.ts b/apps/server/src/app/lib/endpoint.ts
index 7c813157..535dd45f 100644
--- a/apps/server/src/app/lib/endpoint.ts
+++ b/apps/server/src/app/lib/endpoint.ts
@@ -24,6 +24,7 @@ import Redis from 'ioredis'
import {
AccountService,
AccountConnectionService,
+ AuthUserService,
UserService,
EmailService,
AccountQueryService,
@@ -56,7 +57,6 @@ import plaid, { getPlaidWebhookUrl } from './plaid'
import finicity, { getFinicityTxPushUrl, getFinicityWebhookUrl } from './finicity'
import stripe from './stripe'
import postmark from './postmark'
-import { managementClient } from './auth0'
import defineAbilityFor from './ability'
import env from '../../env'
import logger from '../lib/logger'
@@ -205,6 +205,10 @@ const accountService = new AccountService(
balanceSyncStrategyFactory
)
+// auth-user
+
+const authUserService = new AuthUserService(logger.child({ service: 'AuthUserService' }), prisma)
+
// user
const userService = new UserService(
@@ -214,7 +218,6 @@ const userService = new UserService(
balanceSyncStrategyFactory,
queueService.getQueue('sync-user'),
queueService.getQueue('purge-user'),
- managementClient,
stripe
)
@@ -276,22 +279,23 @@ const stripeWebhooks = new StripeWebhookHandler(
)
// helper function for parsing JWT and loading User record
+// TODO: update this with roles, identity, and metadata
async function getCurrentUser(jwt: NonNullable
) {
if (!jwt.sub) throw new Error(`jwt missing sub`)
if (!jwt['https://maybe.co/email']) throw new Error(`jwt missing email`)
const user =
(await prisma.user.findUnique({
- where: { auth0Id: jwt.sub },
+ where: { authId: jwt.sub },
})) ??
(await prisma.user.upsert({
- where: { auth0Id: jwt.sub },
+ where: { authId: jwt.sub },
create: {
- auth0Id: jwt.sub,
+ authId: jwt.sub,
email: jwt['https://maybe.co/email'],
- picture: jwt[SharedType.Auth0CustomNamespace.Picture],
- firstName: jwt[SharedType.Auth0CustomNamespace.UserMetadata]?.['firstName'],
- lastName: jwt[SharedType.Auth0CustomNamespace.UserMetadata]?.['lastName'],
+ picture: jwt['picture'],
+ firstName: jwt['firstName'],
+ lastName: jwt['lastName'],
},
update: {},
}))
@@ -312,7 +316,6 @@ export async function createContext(req: Request) {
prisma,
plaid,
stripe,
- managementClient,
logger,
user,
ability: defineAbilityFor(user),
@@ -320,6 +323,7 @@ export async function createContext(req: Request) {
transactionService,
holdingService,
accountConnectionService,
+ authUserService,
userService,
valuationService,
institutionService,
diff --git a/apps/server/src/app/middleware/identify-user.ts b/apps/server/src/app/middleware/identify-user.ts
index 6fe763bf..9dc6631e 100644
--- a/apps/server/src/app/middleware/identify-user.ts
+++ b/apps/server/src/app/middleware/identify-user.ts
@@ -3,7 +3,7 @@ import * as Sentry from '@sentry/node'
export const identifySentryUser: ErrorRequestHandler = (err, req, _res, next) => {
Sentry.setUser({
- auth0Id: req.user?.sub,
+ authId: req.user?.sub,
})
next(err)
diff --git a/apps/server/src/app/middleware/index.ts b/apps/server/src/app/middleware/index.ts
index 8e60cf38..4a8ebce8 100644
--- a/apps/server/src/app/middleware/index.ts
+++ b/apps/server/src/app/middleware/index.ts
@@ -2,7 +2,7 @@ export * from './dev-only'
export * from './error-handler'
export * from './auth-error-handler'
export * from './superjson'
-export * from './validate-auth0-jwt'
+export * from './validate-auth-jwt'
export * from './validate-plaid-jwt'
export * from './validate-finicity-signature'
export { default as maintenance } from './maintenance'
diff --git a/apps/server/src/app/middleware/validate-auth-jwt.ts b/apps/server/src/app/middleware/validate-auth-jwt.ts
new file mode 100644
index 00000000..5e46cec8
--- /dev/null
+++ b/apps/server/src/app/middleware/validate-auth-jwt.ts
@@ -0,0 +1,33 @@
+import cookieParser from 'cookie-parser'
+import { decode } from 'next-auth/jwt'
+
+const SECRET = process.env.NEXTAUTH_SECRET ?? 'REPLACE_THIS'
+
+export const validateAuthJwt = async (req, res, next) => {
+ cookieParser(SECRET)(req, res, async (err) => {
+ if (err) {
+ return res.status(500).json({ message: 'Internal Server Error' })
+ }
+
+ if (req.cookies && 'next-auth.session-token' in req.cookies) {
+ try {
+ const token = await decode({
+ token: req.cookies['next-auth.session-token'],
+ secret: SECRET,
+ })
+
+ if (token) {
+ req.user = token
+ return next()
+ } else {
+ return res.status(401).json({ message: 'Unauthorized' })
+ }
+ } catch (error) {
+ console.error('Error in token validation', error)
+ return res.status(500).json({ message: 'Internal Server Error' })
+ }
+ } else {
+ return res.status(401).json({ message: 'Unauthorized' })
+ }
+ })
+}
diff --git a/apps/server/src/app/middleware/validate-auth0-jwt.ts b/apps/server/src/app/middleware/validate-auth0-jwt.ts
deleted file mode 100644
index 6855df1a..00000000
--- a/apps/server/src/app/middleware/validate-auth0-jwt.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import type { GetVerificationKey } from 'express-jwt'
-import { expressjwt as jwt } from 'express-jwt'
-import jwks from 'jwks-rsa'
-import env from '../../env'
-
-/**
- * The user will authenticate on the frontend SPA (React) via Authorization Code Flow with PKCE
- * and receive an access token. This token is passed in HTTP headers and validated on the backend
- * via this middleware
- */
-export const validateAuth0Jwt = jwt({
- requestProperty: 'user',
- secret: jwks.expressJwtSecret({
- cache: true,
- rateLimit: true,
- jwksRequestsPerMinute: 5,
- jwksUri: `https://${env.NX_AUTH0_CUSTOM_DOMAIN}/.well-known/jwks.json`,
- }) as GetVerificationKey,
- audience: env.NX_AUTH0_AUDIENCE, // This is a unique identifier from Auth0 (not a valid URL)
- issuer: `https://${env.NX_AUTH0_CUSTOM_DOMAIN}/`,
- algorithms: ['RS256'],
-})
diff --git a/apps/server/src/app/routes/admin.router.ts b/apps/server/src/app/routes/admin.router.ts
deleted file mode 100644
index f4b7567b..00000000
--- a/apps/server/src/app/routes/admin.router.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import { Router } from 'express'
-import { auth, claimCheck } from 'express-openid-connect'
-import { createBullBoard } from '@bull-board/api'
-import { BullAdapter } from '@bull-board/api/bullAdapter'
-import { ExpressAdapter } from '@bull-board/express'
-import { AuthUtil, BullQueue } from '@maybe-finance/server/shared'
-import { SharedType } from '@maybe-finance/shared'
-import { queueService } from '../lib/endpoint'
-import env from '../../env'
-
-const router = Router()
-
-const serverAdapter = new ExpressAdapter().setBasePath('/admin/bullmq')
-
-createBullBoard({
- queues: queueService.allQueues
- .filter((q): q is BullQueue => q instanceof BullQueue)
- .map((q) => new BullAdapter(q.queue)),
- serverAdapter,
-})
-
-const isProd = process.env.NODE_ENV === 'production' && process.env.IS_PULL_REQUEST !== 'true'
-
-const prodCookieConfig = isProd
- ? {
- session: {
- cookie: {
- domain: '.maybe.co',
- path: '/admin',
- },
- },
- }
- : {}
-
-// This will ensure that only Auth0 users with the "admin" role can visit these pages
-router.use(
- auth({
- authRequired: true,
- idpLogout: true, // Logout of Auth0 provider
- auth0Logout: isProd, // Same as idpLogout, but for custom domain
- secret: env.NX_SESSION_SECRET,
- baseURL: `${env.NX_API_URL}/admin`,
- clientID: env.NX_AUTH0_CLIENT_ID,
- clientSecret: env.NX_AUTH0_CLIENT_SECRET,
- issuerBaseURL: `https://${env.NX_AUTH0_CUSTOM_DOMAIN}`,
- authorizationParams: {
- response_type: 'code',
- audience: env.NX_AUTH0_AUDIENCE,
- scope: 'openid profile email',
- },
- routes: {
- postLogoutRedirect: env.NX_API_URL,
- },
- ...prodCookieConfig,
- })
-)
-
-/**
- * Auth0 requires all custom claims to be namespaced
- * @see https://auth0.com/docs/security/tokens/json-web-tokens/create-namespaced-custom-claims
- *
- * This is the namespace that has been set in the "Rules" section of Maybe's Auth0 dashboard
- *
- * The rule used below is called "Add Roles to ID token", and will attach an array of roles
- * that are assigned to an Auth0 user under the https://maybe.co/roles namespace
- *
- * @see https://auth0.com/docs/authorization/authorization-policies/sample-use-cases-rules-with-authorization#add-user-roles-to-tokens
- */
-
-const adminClaimCheck = claimCheck((_req, claims) => AuthUtil.verifyRoleClaims(claims, 'Admin'))
-
-router.get('/', adminClaimCheck, (req, res) => {
- res.render('pages/dashboard', {
- user: req.oidc.user?.name,
- role: req.oidc.idTokenClaims?.[SharedType.Auth0CustomNamespace.Roles],
- })
-})
-
-// Visit /admin/bullmq to see BullMQ Dashboard
-router.use('/bullmq', adminClaimCheck, serverAdapter.getRouter())
-
-export default router
diff --git a/apps/server/src/app/routes/e2e.router.ts b/apps/server/src/app/routes/e2e.router.ts
index 31004185..0b4bc830 100644
--- a/apps/server/src/app/routes/e2e.router.ts
+++ b/apps/server/src/app/routes/e2e.router.ts
@@ -47,13 +47,13 @@ router.post(
trialLapsed: z.boolean().default(false),
}),
resolve: async ({ ctx, input }) => {
- ctx.logger.debug(`Resetting CI user ${ctx.user!.auth0Id}`)
+ ctx.logger.debug(`Resetting CI user ${ctx.user!.authId}`)
await ctx.prisma.$transaction([
- ctx.prisma.$executeRaw`DELETE FROM "user" WHERE auth0_id=${ctx.user!.auth0Id};`,
+ ctx.prisma.$executeRaw`DELETE FROM "user" WHERE auth_id=${ctx.user!.authId};`,
ctx.prisma.user.create({
data: {
- auth0Id: ctx.user!.auth0Id,
+ authId: ctx.user!.authId,
email: 'REPLACE_THIS',
dob: new Date('1990-01-01'),
linkAccountDismissedAt: new Date(), // ensures our auto-account link doesn't trigger
diff --git a/apps/server/src/app/routes/index.ts b/apps/server/src/app/routes/index.ts
index fd09f81c..135a9a24 100644
--- a/apps/server/src/app/routes/index.ts
+++ b/apps/server/src/app/routes/index.ts
@@ -5,7 +5,6 @@ export { default as usersRouter } from './users.router'
export { default as webhooksRouter } from './webhooks.router'
export { default as plaidRouter } from './plaid.router'
export { default as finicityRouter } from './finicity.router'
-export { default as adminRouter } from './admin.router'
export { default as valuationsRouter } from './valuations.router'
export { default as institutionsRouter } from './institutions.router'
export { default as transactionsRouter } from './transactions.router'
diff --git a/apps/server/src/app/routes/users.router.ts b/apps/server/src/app/routes/users.router.ts
index 15b80f16..2e6c1f88 100644
--- a/apps/server/src/app/routes/users.router.ts
+++ b/apps/server/src/app/routes/users.router.ts
@@ -1,6 +1,4 @@
import { Router } from 'express'
-import axios from 'axios'
-import type { UnlinkAccountsParamsProvider } from 'auth0'
import { subject } from '@casl/ability'
import { z } from 'zod'
import { DateUtil, type SharedType } from '@maybe-finance/shared'
@@ -106,25 +104,10 @@ router.put(
)
router.get(
- '/auth0-profile',
+ '/auth-profile',
endpoint.create({
resolve: async ({ ctx }) => {
- return ctx.userService.getAuth0Profile(ctx.user!)
- },
- })
-)
-
-router.put(
- '/auth0-profile',
- endpoint.create({
- input: z.object({
- enrolled_mfa: z.boolean(),
- }),
- resolve: ({ input, ctx }) => {
- return ctx.managementClient.updateUser(
- { id: ctx.user!.auth0Id },
- { user_metadata: { enrolled_mfa: input.enrolled_mfa } }
- )
+ return ctx.userService.getAuthProfile(ctx.user!.id)
},
})
)
@@ -276,53 +259,20 @@ router.get(
})
)
-router.post(
- '/link-accounts',
- endpoint.create({
- input: z.object({
- secondaryJWT: z.string(),
- secondaryProvider: z.string(),
- }),
- resolve: async ({ input, ctx }) => {
- return ctx.userService.linkAccounts(ctx.user!.auth0Id, input.secondaryProvider, {
- token: input.secondaryJWT,
- domain: env.NX_AUTH0_CUSTOM_DOMAIN,
- audience: env.NX_AUTH0_AUDIENCE,
- })
- },
- })
-)
-
-router.post(
- '/unlink-account',
- endpoint.create({
- input: z.object({
- secondaryAuth0Id: z.string(),
- secondaryProvider: z.string(),
- }),
- resolve: async ({ input, ctx }) => {
- return ctx.userService.unlinkAccounts(
- ctx.user!.auth0Id,
- input.secondaryAuth0Id,
- input.secondaryProvider as UnlinkAccountsParamsProvider
- )
- },
- })
-)
-
+// TODO: Implement verification email using Postmark instead of Auth0
router.post(
'/resend-verification-email',
endpoint.create({
input: z.object({
- auth0Id: z.string().optional(),
+ authId: z.string().optional(),
}),
resolve: async ({ input, ctx }) => {
- const auth0Id = input.auth0Id ?? ctx.user?.auth0Id
- if (!auth0Id) throw new Error('User not found')
+ const authId = input.authId ?? ctx.user?.authId
+ if (!authId) throw new Error('User not found')
- await ctx.managementClient.sendEmailVerification({ user_id: auth0Id })
+ //await ctx.managementClient.sendEmailVerification({ user_id: authId })
- ctx.logger.info(`Sent verification email to ${auth0Id}`)
+ ctx.logger.info(`Sent verification email to ${authId}`)
return { success: true }
},
@@ -341,52 +291,18 @@ router.put(
throw new Error('Unable to update password. No user found.')
}
- const user = await ctx.managementClient.getUser({ id: req.user.sub })
-
const { newPassword, currentPassword } = input
- /**
- * Auth0 doesn't have a verify password endpoint on the Management API, so this is a secure way to
- * verify that the old password was valid before changing it. Why they don't have this feature still? ¯\_(ツ)_/¯
- *
- * @see https://community.auth0.com/t/change-password-validation/8158/10
- */
try {
- // If this succeeds, we know the old password was correct
- await axios.post(
- `https://${env.NX_AUTH0_DOMAIN}/oauth/token`,
- {
- grant_type: 'password',
- username: user.email,
- password: currentPassword,
- audience: env.NX_AUTH0_AUDIENCE,
- client_id: env.NX_AUTH0_CLIENT_ID,
- client_secret: env.NX_AUTH0_CLIENT_SECRET,
- },
- { headers: { 'content-type': 'application/json' } }
- )
+ await ctx.authUserService.updatePassword(req.user.sub, currentPassword, newPassword)
} catch (err) {
- let errMessage = 'Could not reset password'
-
- if (axios.isAxiosError(err)) {
- errMessage =
- err.response?.status === 401
- ? 'Invalid password, please try again'
- : errMessage
- }
-
+ const errMessage = 'Could not reset password'
// Do not log the full error here, the user's password could be in it!
ctx.logger.error('Could not reset password')
return { success: false, error: errMessage }
}
- // https://auth0.com/docs/connections/database/password-change#use-the-management-api
- await ctx.managementClient.updateUser(
- { id: req.user?.sub },
- { password: newPassword, connection: 'Username-Password-Authentication' }
- )
-
return { success: true }
},
})
@@ -426,9 +342,8 @@ router.post(
customer: ctx.user.stripeCustomerId,
}
: {
- customer_email: (
- await ctx.managementClient.getUser({ id: req.user.sub })
- ).email,
+ customer_email:
+ (await ctx.authUserService.get(req.user.sub)).email ?? undefined,
}),
})
diff --git a/apps/server/src/env.ts b/apps/server/src/env.ts
index 05a0c763..e27169ed 100644
--- a/apps/server/src/env.ts
+++ b/apps/server/src/env.ts
@@ -33,15 +33,6 @@ const envSchema = z.object({
NX_NGROK_URL: z.string().default('http://localhost:4551'),
- // Dev doesn't have a custom domain, so replace with the original dev URL
- NX_AUTH0_DOMAIN: z.string().default('REPLACE_THIS'),
- NX_AUTH0_CUSTOM_DOMAIN: z.string().default('REPLACE_THIS'),
- NX_AUTH0_AUDIENCE: z.string().default('https://maybe-finance-api/v1'),
- NX_AUTH0_CLIENT_ID: z.string().default('REPLACE_THIS'),
- NX_AUTH0_CLIENT_SECRET: z.string(),
- NX_AUTH0_MGMT_CLIENT_ID: z.string().default('REPLACE_THIS'),
- NX_AUTH0_MGMT_CLIENT_SECRET: z.string(),
-
NX_PLAID_CLIENT_ID: z.string().default('REPLACE_THIS'),
NX_PLAID_SECRET: z.string(),
NX_PLAID_ENV: z.string().default('sandbox'),
diff --git a/apps/workers/src/app/lib/auth0.ts b/apps/workers/src/app/lib/auth0.ts
deleted file mode 100644
index 1c96ffff..00000000
--- a/apps/workers/src/app/lib/auth0.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import type { SharedType } from '@maybe-finance/shared'
-import { ManagementClient } from 'auth0'
-import env from '../../env'
-
-/**
- * Management API Documentation
- * - https://auth0.com/docs/api/management/v2
- * - https://auth0.github.io/node-auth0/module-management.ManagementClient.html
- */
-export const managementClient = new ManagementClient<
- SharedType.MaybeAppMetadata,
- SharedType.MaybeUserMetadata
->({
- domain: env.NX_AUTH0_DOMAIN,
- clientId: env.NX_AUTH0_MGMT_CLIENT_ID,
- clientSecret: env.NX_AUTH0_MGMT_CLIENT_SECRET,
- scope: 'read:users update:users delete:users',
-})
diff --git a/apps/workers/src/app/lib/di.ts b/apps/workers/src/app/lib/di.ts
index 4c4b027b..55f3341a 100644
--- a/apps/workers/src/app/lib/di.ts
+++ b/apps/workers/src/app/lib/di.ts
@@ -28,14 +28,12 @@ import {
LoanBalanceSyncStrategy,
PlaidETL,
PlaidService,
- PropertyService,
SecurityPricingProcessor,
SecurityPricingService,
TransactionBalanceSyncStrategy,
UserProcessor,
UserService,
ValuationBalanceSyncStrategy,
- VehicleService,
EmailService,
EmailProcessor,
TransactionService,
@@ -58,7 +56,6 @@ import prisma from './prisma'
import plaid from './plaid'
import finicity from './finicity'
import postmark from './postmark'
-import { managementClient } from './auth0'
import stripe from './stripe'
import env from '../../env'
import { BullQueueEventHandler, WorkerErrorHandlerService } from '../services'
@@ -127,10 +124,6 @@ const finicityService = new FinicityService(
env.NX_FINICITY_ENV === 'sandbox'
)
-const propertyService = new PropertyService(logger.child({ service: 'PropertyService' }))
-
-const vehicleService = new VehicleService(logger.child({ service: 'VehicleService' }))
-
// account-connection
const accountConnectionProviderFactory = new AccountConnectionProviderFactory({
@@ -228,7 +221,6 @@ export const userService: IUserService = new UserService(
balanceSyncStrategyFactory,
queueService.getQueue('sync-user'),
queueService.getQueue('purge-user'),
- managementClient,
stripe
)
@@ -282,6 +274,5 @@ export const emailService: IEmailService = new EmailService(
export const emailProcessor: IEmailProcessor = new EmailProcessor(
logger.child({ service: 'EmailProcessor' }),
prisma,
- managementClient,
emailService
)
diff --git a/apps/workers/src/app/services/bull-queue-event-handler.ts b/apps/workers/src/app/services/bull-queue-event-handler.ts
index 9f80c8d1..8e1c1379 100644
--- a/apps/workers/src/app/services/bull-queue-event-handler.ts
+++ b/apps/workers/src/app/services/bull-queue-event-handler.ts
@@ -82,7 +82,7 @@ export class BullQueueEventHandler implements IBullQueueEventHandler {
}
private async getUserFromJob(job: Job) {
- let user: Pick | undefined
+ let user: Pick | undefined
try {
if (job.queue.name === 'sync-account' && 'accountId' in job.data) {
diff --git a/apps/workers/src/env.ts b/apps/workers/src/env.ts
index 8333001d..b9632599 100644
--- a/apps/workers/src/env.ts
+++ b/apps/workers/src/env.ts
@@ -24,25 +24,13 @@ const envSchema = z.object({
NX_POLYGON_API_KEY: z.string().default(''),
- NX_AUTH0_DOMAIN: z.string().default('REPLACE_THIS'),
- NX_AUTH0_MGMT_CLIENT_ID: z.string().default('REPLACE_THIS'),
- NX_AUTH0_MGMT_CLIENT_SECRET: z.string(),
-
NX_POSTMARK_FROM_ADDRESS: z.string().default('account@maybe.co'),
NX_POSTMARK_REPLY_TO_ADDRESS: z.string().default('support@maybe.co'),
NX_POSTMARK_API_TOKEN: z.string().default('REPLACE_THIS'),
- NX_STRIPE_SECRET_KEY: z
- .string()
- .default(
- 'sk_test_REPLACE_THIS'
- ),
+ NX_STRIPE_SECRET_KEY: z.string().default('sk_test_REPLACE_THIS'),
- NX_CDN_PRIVATE_BUCKET: z
- .string()
- .default('REPLACE_THIS'),
- NX_CDN_PUBLIC_BUCKET: z
- .string()
- .default('REPLACE_THIS'),
+ NX_CDN_PRIVATE_BUCKET: z.string().default('REPLACE_THIS'),
+ NX_CDN_PUBLIC_BUCKET: z.string().default('REPLACE_THIS'),
})
const env = envSchema.parse(process.env)
diff --git a/libs/client/features/src/account/AccountMenu.tsx b/libs/client/features/src/account/AccountMenu.tsx
index a7b88e44..66070442 100644
--- a/libs/client/features/src/account/AccountMenu.tsx
+++ b/libs/client/features/src/account/AccountMenu.tsx
@@ -1,21 +1,16 @@
import type { SharedType } from '@maybe-finance/shared'
-import { BrowserUtil, useAccountApi, useAccountContext } from '@maybe-finance/client/shared'
+import { useAccountContext } from '@maybe-finance/client/shared'
import { Menu } from '@maybe-finance/design-system'
-import { RiDeleteBin5Line, RiPencilLine, RiRefreshLine } from 'react-icons/ri'
+import { RiDeleteBin5Line, RiPencilLine } from 'react-icons/ri'
import { useRouter } from 'next/router'
-import { useAuth0 } from '@auth0/auth0-react'
type Props = {
account?: SharedType.AccountDetail
}
export function AccountMenu({ account }: Props) {
- const { user } = useAuth0()
const { editAccount, deleteAccount } = useAccountContext()
- const { useSyncAccount } = useAccountApi()
-
const router = useRouter()
- const syncAccount = useSyncAccount()
if (!account) return null
@@ -28,15 +23,6 @@ export function AccountMenu({ account }: Props) {
} onClick={() => editAccount(account)}>
Edit
- {BrowserUtil.hasRole(user, 'Admin') && (
- }
- destructive
- onClick={() => syncAccount.mutate(account.id)}
- >
- Sync
-
- )}
{!account.accountConnectionId && (
}
diff --git a/libs/client/features/src/index.ts b/libs/client/features/src/index.ts
index 4e025574..ae8ad5c8 100644
--- a/libs/client/features/src/index.ts
+++ b/libs/client/features/src/index.ts
@@ -4,13 +4,11 @@ export * from './holdings-list'
export * from './insights'
export * from './user-billing'
export * from './user-details'
-export * from './user-notifications'
export * from './user-security'
export * from './transactions-list'
export * from './investment-transactions-list'
export * from './layout'
export * from './accounts-manager'
-export * from './user'
export * from './net-worth-insights'
export * from './data-editor'
export * from './loan-details'
diff --git a/libs/client/features/src/layout/DesktopLayout.tsx b/libs/client/features/src/layout/DesktopLayout.tsx
index 1af64799..e61fbdac 100644
--- a/libs/client/features/src/layout/DesktopLayout.tsx
+++ b/libs/client/features/src/layout/DesktopLayout.tsx
@@ -19,14 +19,12 @@ import {
RiMore2Fill,
RiPieChart2Line,
RiFlagLine,
- RiChatPollLine,
RiArrowRightSLine,
} from 'react-icons/ri'
import { Button, Tooltip } from '@maybe-finance/design-system'
-import { useAuth0 } from '@auth0/auth0-react'
import { MenuPopover } from './MenuPopover'
-import { UpgradePrompt } from '../user-billing'
import { SidebarOnboarding } from '../onboarding'
+import { useSession } from 'next-auth/react'
export interface DesktopLayoutProps {
sidebar: React.ReactNode
@@ -95,7 +93,8 @@ export function DesktopLayout({ children, sidebar }: DesktopLayoutProps) {
const [onboardingExpanded, setOnboardingExpanded] = useState(false)
const { popoutContents, close: closePopout } = usePopoutContext()
- const { user } = useAuth0()
+ const { data: session } = useSession()
+ const user = session!.user
const { useOnboarding, useUpdateOnboarding } = useUserApi()
const onboarding = useOnboarding('sidebar')
const updateOnboarding = useUpdateOnboarding()
@@ -270,8 +269,8 @@ export function DesktopLayout({ children, sidebar }: DesktopLayoutProps) {
)}
}
- name={user?.name}
- email={user?.email}
+ name={user?.name ?? ''}
+ email={user?.email ?? ''}
>
{sidebar}
diff --git a/libs/client/features/src/layout/MenuPopover.tsx b/libs/client/features/src/layout/MenuPopover.tsx
index 7a1cbcd1..6413d08f 100644
--- a/libs/client/features/src/layout/MenuPopover.tsx
+++ b/libs/client/features/src/layout/MenuPopover.tsx
@@ -1,4 +1,4 @@
-import { useAuth0 } from '@auth0/auth0-react'
+import { signOut } from 'next-auth/react'
import { Menu } from '@maybe-finance/design-system'
import type { ComponentProps } from 'react'
import {
@@ -16,8 +16,6 @@ export function MenuPopover({
placement?: ComponentProps['placement']
isHeader: boolean
}) {
- const { logout } = useAuth0()
-
return (
{icon}
@@ -31,11 +29,7 @@ export function MenuPopover({
} href="/data-editor">
Fix my data
- }
- destructive={true}
- onClick={() => logout({ logoutParams: { returnTo: window.location.origin } })}
- >
+ } destructive={true} onClick={() => signOut()}>
Log out
diff --git a/libs/client/features/src/layout/MobileLayout.tsx b/libs/client/features/src/layout/MobileLayout.tsx
index f2904cf3..206df618 100644
--- a/libs/client/features/src/layout/MobileLayout.tsx
+++ b/libs/client/features/src/layout/MobileLayout.tsx
@@ -1,7 +1,6 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { motion } from 'framer-motion'
import {
- RiChatPollLine,
RiCloseLine,
RiFlagLine,
RiFolderOpenLine,
@@ -15,7 +14,6 @@ import Link from 'next/link'
import { useRouter } from 'next/router'
import { ProfileCircle } from '@maybe-finance/client/shared'
import { usePopoutContext, LayoutContextProvider } from '@maybe-finance/client/shared'
-import { UpgradePrompt } from '../user-billing'
import classNames from 'classnames'
import type { IconType } from 'react-icons'
diff --git a/libs/client/features/src/onboarding/OnboardingGuard.tsx b/libs/client/features/src/onboarding/OnboardingGuard.tsx
index 76976980..25e71746 100644
--- a/libs/client/features/src/onboarding/OnboardingGuard.tsx
+++ b/libs/client/features/src/onboarding/OnboardingGuard.tsx
@@ -3,7 +3,7 @@ import { MainContentOverlay, useUserApi } from '@maybe-finance/client/shared'
import { LoadingSpinner } from '@maybe-finance/design-system'
import { useRouter } from 'next/router'
import type { SharedType } from '@maybe-finance/shared'
-import { useAuth0 } from '@auth0/auth0-react'
+import { signOut } from 'next-auth/react'
function shouldRedirect(pathname: string, data?: SharedType.OnboardingResponse) {
if (!data) return false
@@ -14,7 +14,6 @@ function shouldRedirect(pathname: string, data?: SharedType.OnboardingResponse)
export function OnboardingGuard({ children }: PropsWithChildren) {
const router = useRouter()
- const { logout } = useAuth0()
const { useOnboarding } = useUserApi()
const onboarding = useOnboarding('main', {
onSuccess(data) {
@@ -29,7 +28,7 @@ export function OnboardingGuard({ children }: PropsWithChildren) {
logout({ logoutParams: { returnTo: window.location.origin } })}
+ onAction={() => signOut()}
>
Contact us if this issue persists.
diff --git a/libs/client/features/src/onboarding/OnboardingNavbar.tsx b/libs/client/features/src/onboarding/OnboardingNavbar.tsx
index f2ac24ce..d88454c2 100644
--- a/libs/client/features/src/onboarding/OnboardingNavbar.tsx
+++ b/libs/client/features/src/onboarding/OnboardingNavbar.tsx
@@ -1,4 +1,4 @@
-import { useAuth0 } from '@auth0/auth0-react'
+import { signOut } from 'next-auth/react'
import { ProfileCircle } from '@maybe-finance/client/shared'
import { Button, Menu } from '@maybe-finance/design-system'
import type { SharedType } from '@maybe-finance/shared'
@@ -15,8 +15,6 @@ type Props = {
}
export function OnboardingNavbar({ steps, currentStep, onBack }: Props) {
- const { logout } = useAuth0()
-
const groups = uniqBy(steps, 'group')
.map((s) => s.group)
.filter((g): g is string => g != null)
@@ -85,9 +83,7 @@ export function OnboardingNavbar({ steps, currentStep, onBack }: Props) {
}
destructive={true}
- onClick={() =>
- logout({ logoutParams: { returnTo: window.location.origin } })
- }
+ onClick={() => signOut()}
>
Log out
diff --git a/libs/client/features/src/onboarding/sidebar/SidebarOnboarding.tsx b/libs/client/features/src/onboarding/sidebar/SidebarOnboarding.tsx
index 07ddc24a..5d8b8e75 100644
--- a/libs/client/features/src/onboarding/sidebar/SidebarOnboarding.tsx
+++ b/libs/client/features/src/onboarding/sidebar/SidebarOnboarding.tsx
@@ -237,7 +237,7 @@ export function SidebarOnboarding({ onClose, onHide }: Props) {
const description = getDescriptionComponent(step.key)
return (
-
+
{({ open }) => (
logout({ logoutParams: { returnTo: window.location.origin } }), 500)
+ setTimeout(() => signOut(), 500)
},
onError() {
toast.error(`Error deleting account`)
diff --git a/libs/client/features/src/onboarding/steps/setup/AddFirstAccount.tsx b/libs/client/features/src/onboarding/steps/setup/AddFirstAccount.tsx
index ae7cb1c1..6cf93491 100644
--- a/libs/client/features/src/onboarding/steps/setup/AddFirstAccount.tsx
+++ b/libs/client/features/src/onboarding/steps/setup/AddFirstAccount.tsx
@@ -59,6 +59,8 @@ export function AddFirstAccount({ title, onNext }: StepProps) {
loader={BrowserUtil.enhancerizerLoader}
src={`financial-institutions/white/${src}.svg`}
alt={name}
+ height={96}
+ width={96}
/>
))}
diff --git a/libs/client/features/src/onboarding/steps/setup/EmailVerification.tsx b/libs/client/features/src/onboarding/steps/setup/EmailVerification.tsx
index a3f12dd6..cb8d1730 100644
--- a/libs/client/features/src/onboarding/steps/setup/EmailVerification.tsx
+++ b/libs/client/features/src/onboarding/steps/setup/EmailVerification.tsx
@@ -8,14 +8,14 @@ import { useUserApi } from '@maybe-finance/client/shared'
import type { StepProps } from '../StepProps'
export function EmailVerification({ title, onNext }: StepProps) {
- const { useAuth0Profile, useResendEmailVerification } = useUserApi()
+ const { useAuthProfile, useResendEmailVerification } = useUserApi()
const emailVerified = useRef(false)
- const profile = useAuth0Profile({
+ const profile = useAuthProfile({
refetchInterval: emailVerified.current ? false : 5_000,
onSuccess: (data) => {
- if (data.email_verified) {
+ if (data.emailVerified) {
emailVerified.current = true
}
},
@@ -70,7 +70,7 @@ export function EmailVerification({ title, onNext }: StepProps) {
'linear-gradient(180deg, rgba(35, 36, 40, 0.2) 0%, rgba(68, 71, 76, 0.2) 100%)',
}}
>
- {profile.data?.email_verified ? (
+ {profile.data?.emailVerified ? (
) : (
@@ -78,10 +78,10 @@ export function EmailVerification({ title, onNext }: StepProps) {
- {profile.data?.email_verified ? 'Email verified' : title}
+ {profile.data?.emailVerified ? 'Email verified' : title}
- {profile.data?.email_verified ? (
+ {profile.data?.emailVerified ? (
You have successfully verified{' '}
{profile.data?.email ?? 'your email'}
@@ -130,7 +130,7 @@ export function EmailVerification({ title, onNext }: StepProps) {
>
)}
- {profile.data?.email_verified && (
+ {profile.data?.emailVerified && (
Continue setup
diff --git a/libs/client/features/src/user-billing/SubscriberGuard.tsx b/libs/client/features/src/user-billing/SubscriberGuard.tsx
index bb570779..85abc75d 100644
--- a/libs/client/features/src/user-billing/SubscriberGuard.tsx
+++ b/libs/client/features/src/user-billing/SubscriberGuard.tsx
@@ -1,4 +1,4 @@
-import { useAuth0 } from '@auth0/auth0-react'
+import { signOut } from 'next-auth/react'
import { MainContentOverlay, useUserApi } from '@maybe-finance/client/shared'
import { LoadingSpinner } from '@maybe-finance/design-system'
import type { SharedType } from '@maybe-finance/shared'
@@ -22,7 +22,6 @@ function shouldRedirect(path: string, data?: SharedType.UserSubscription) {
export function SubscriberGuard({ children }: PropsWithChildren) {
const router = useRouter()
- const { logout } = useAuth0()
const { useSubscription } = useUserApi()
const subscription = useSubscription()
@@ -31,7 +30,7 @@ export function SubscriberGuard({ children }: PropsWithChildren) {
logout({ logoutParams: { returnTo: window.location.origin } })}
+ onAction={() => signOut()}
>
Contact us if this issue persists.
diff --git a/libs/client/features/src/user-details/LinkAccountFlow.tsx b/libs/client/features/src/user-details/LinkAccountFlow.tsx
deleted file mode 100644
index 4f3856b7..00000000
--- a/libs/client/features/src/user-details/LinkAccountFlow.tsx
+++ /dev/null
@@ -1,267 +0,0 @@
-import { useAuth0 } from '@auth0/auth0-react'
-import { BoxIcon, linkAuth0AccountCtx, useUserApi } from '@maybe-finance/client/shared'
-import { Button, DialogV2 } from '@maybe-finance/design-system'
-import { useQueryClient } from '@tanstack/react-query'
-import { useState } from 'react'
-import { RiAppleFill, RiCheckLine, RiLink, RiLinkUnlink, RiLoader4Fill } from 'react-icons/ri'
-
-type Props = {
- secondaryProvider: string
- isOpen: boolean
- onClose(): void
-}
-
-const steps = ['authenticate', 'confirm', 'complete']
-
-export function LinkAccountFlow({ secondaryProvider, isOpen, onClose }: Props) {
- const queryClient = useQueryClient()
-
- const secondaryAuth0 = useAuth0(linkAuth0AccountCtx)
-
- const [stepIdx, setStepIdx] = useState(0)
- const [error, setError] = useState(null)
-
- const { useLinkAccounts } = useUserApi()
- const linkAccounts = useLinkAccounts({
- onSettled() {
- queryClient.invalidateQueries(['users', 'auth0-profile'])
- },
- onSuccess() {
- setStepIdx((prev) => prev + 1)
- },
- onError(err) {
- setError(
- err instanceof Error ? err.message : 'Something went wrong while linking accounts'
- )
- },
- })
-
- const { useUpdateProfile } = useUserApi()
- const updateUser = useUpdateProfile({
- onSettled: () => queryClient.invalidateQueries(['users', 'auth0-profile']),
- onSuccess: undefined,
- })
-
- function completeFlow() {
- updateUser.mutate({ linkAccountDismissedAt: new Date() })
- setError(null)
- setStepIdx(0)
- onClose()
- }
-
- return (
-
- {error ? (
-
- ) : (
- (function () {
- switch (steps[stepIdx]) {
- case 'authenticate':
- return (
- {
- await secondaryAuth0.loginWithPopup({
- authorizationParams: {
- connection:
- secondaryProvider === 'apple'
- ? 'apple'
- : 'Username-Password-Authentication',
- screen_hint:
- secondaryProvider !== 'apple'
- ? 'show-form-only'
- : undefined,
- max_age: 0,
- display: 'page',
- },
- })
-
- setStepIdx((prev) => prev + 1)
- }}
- />
- )
- case 'confirm':
- return (
- {
- const token = await secondaryAuth0.getAccessTokenSilently()
-
- linkAccounts.mutate({
- secondaryJWT: token,
- secondaryProvider,
- })
- }}
- isLoading={linkAccounts.isLoading}
- isReady={secondaryAuth0.isAuthenticated}
- />
- )
- case 'complete':
- return
- default:
- return null
- }
- })()
- )}
-
- )
-}
-
-type StepProps = {
- onCancel(): void
- onNext(): void
-}
-
-function PromptStep({
- secondaryProvider,
- onCancel,
- onNext,
-}: StepProps & { secondaryProvider: string }) {
- return (
- <>
-
-
- Link accounts?
-
-
- We found an {secondaryProvider === 'apple' ? 'Apple ' : ' '} account using the same
- email address as this one in our system. Do you want to link it?
-
-
-
-
- Close
-
- {secondaryProvider === 'apple' ? (
-
- Link with Apple
-
- ) : (
-
- Link accounts
-
- )}
-
- >
- )
-}
-
-function ConfirmStep({
- isLoading,
- isReady,
- onCancel,
- onNext,
-}: StepProps & { isLoading: boolean; isReady: boolean }) {
- if (!isReady) {
- return (
- <>
-
-
- Authentication in progress...
-
-
- Cancel
-
- >
- )
- }
-
- return (
- <>
-
-
-
- {isLoading ? 'Linking accounts ...' : 'Continue linking accounts?'}
-
-
-
- {isLoading ? (
-
- Your accounts are being linked and data is being merged. This may take a few
- seconds.
-
- ) : (
- <>
-
- After linking, both logins will use the data in{' '}
- the current account. Any data you
- have in your secondary account will be archived and no longer available
- to you. If you ever wish to recover that data, you can reverse this
- process by unlinking the account in your settings.{' '}
-
-
-
No data will be deleted.
- >
- )}
-
-
-
-
- Don't link
-
-
-
- {isLoading && (
-
- )}
- {isLoading ? 'Linking...' : 'Continue'}
-
-
- >
- )
-}
-
-function LinkComplete({ onClose }: { onClose(): void }) {
- return (
- <>
-
-
- Accounts linked successfully!
-
-
- Your accounts have been linked and the data has been merged successfully.
-
-
-
-
- Done
-
-
- >
- )
-}
-
-function LinkError({ onClose, error }: { onClose(): void; error: string }) {
- return (
- <>
-
-
- Account linking failed
-
- {error}
-
-
- Please contact us.
-
-
-
-
- Close
-
-
- >
- )
-}
diff --git a/libs/client/features/src/user-details/UserDetails.tsx b/libs/client/features/src/user-details/UserDetails.tsx
index 1fea920c..5098b7d0 100644
--- a/libs/client/features/src/user-details/UserDetails.tsx
+++ b/libs/client/features/src/user-details/UserDetails.tsx
@@ -1,16 +1,14 @@
import { useState } from 'react'
import { Controller, useForm } from 'react-hook-form'
-import { useAuth0 } from '@auth0/auth0-react'
+import { signOut } from 'next-auth/react'
import classNames from 'classnames'
import { AiOutlineLoading3Quarters as LoadingIcon } from 'react-icons/ai'
import {
RiAnticlockwise2Line,
- RiAppleFill,
RiArrowGoBackFill,
RiDownloadLine,
RiShareForwardLine,
} from 'react-icons/ri'
-import { UserIdentityList } from '../user-details/UserIdentityList'
import {
Button,
DatePicker,
@@ -30,10 +28,8 @@ import { DeleteUserButton } from './DeleteUserButton'
import { DateTime } from 'luxon'
export function UserDetails() {
- const { logout } = useAuth0()
- const { useProfile, useAuth0Profile, useUpdateProfile } = useUserApi()
+ const { useProfile, useUpdateProfile } = useUserApi()
- const auth0ProfileQuery = useAuth0Profile()
const updateProfileQuery = useUpdateProfile()
const profileQuery = useProfile()
@@ -77,17 +73,7 @@ export function UserDetails() {
type="text"
/>
- {auth0ProfileQuery.data?.primaryIdentity.provider === 'apple' && (
-
- Apple identity
-
-
- )}
-
- {auth0ProfileQuery.data && (
-
- )}
@@ -101,9 +87,7 @@ export function UserDetails() {
Deleting your account is a permanent action. If you delete your account, you
will no longer be able to sign and all data will be deleted.
- logout({ logoutParams: { returnTo: window.location.origin } })}
- />
+ signOut()} />
)
diff --git a/libs/client/features/src/user-details/UserIdentityCard.tsx b/libs/client/features/src/user-details/UserIdentityCard.tsx
deleted file mode 100644
index c092bfa7..00000000
--- a/libs/client/features/src/user-details/UserIdentityCard.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import type { SharedType } from '@maybe-finance/shared'
-import { Button } from '@maybe-finance/design-system'
-import { RiAppleFill, RiMailLine, RiCheckboxCircleFill } from 'react-icons/ri'
-
-export type UserIdentity =
- | {
- variant: 'primary'
- provider: string
- email: string
- auth0Id?: string
- isLinked?: boolean
- }
- | {
- variant: 'linked'
- provider: string
- email: string
- auth0Id: string
- isLinked?: never
- }
- | {
- variant: 'unlinked'
- provider: string
- email?: string // We might have an email here if we've detected a duplicate account
- auth0Id?: never
- isLinked?: never
- }
-
-export function UserIdentityCard({
- identity,
- onUnlink,
- onLink,
-}: {
- identity: UserIdentity
- onUnlink?(data: SharedType.UnlinkAccount): void
- onLink?(): void
-}) {
- return (
-
-
- {identity.provider === 'apple' ? (
-
- ) : (
-
- )}
-
-
-
-
{identity.email ?? ''}
-
- {!identity.email && (
-
- {identity.provider === 'apple' ? 'Apple account' : 'Email account'}
-
- )}
- {identity.variant === 'primary' && (
-
-
- Main
-
- )}
-
-
-
- {identity.isLinked && (
-
-
- Linked
-
- )}
-
- {identity.variant === 'linked' && (
-
- onUnlink?.({
- secondaryAuth0Id: identity.auth0Id,
- secondaryProvider: identity.provider,
- })
- }
- >
- Unlink
-
- )}
-
- {identity.variant === 'unlinked' && (
-
- Link
-
- )}
-
- )
-}
diff --git a/libs/client/features/src/user-details/UserIdentityList.tsx b/libs/client/features/src/user-details/UserIdentityList.tsx
deleted file mode 100644
index 13324bfe..00000000
--- a/libs/client/features/src/user-details/UserIdentityList.tsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import type { SharedType } from '@maybe-finance/shared'
-import { ConfirmDialog, useModalManager, useUserApi } from '@maybe-finance/client/shared'
-import { useState } from 'react'
-import { UserIdentityCard } from './UserIdentityCard'
-
-export function UserIdentityList({ profile }: { profile: SharedType.Auth0Profile }) {
- const { useUnlinkAccount } = useUserApi()
- const { dispatch } = useModalManager()
- const [isConfirm, setIsConfirm] = useState(false)
- const [unlinkProps, setUnlinkProps] = useState(undefined)
- const unlinkAccountQuery = useUnlinkAccount()
- const { primaryIdentity, secondaryIdentities, suggestedIdentities, email } = profile
-
- return (
- <>
-
-
Identities
-
- {/* The user's primary account identity */}
- 0,
- }}
- />
-
- {/* Any identities the user has already linked */}
- {secondaryIdentities.map((si) => (
- {
- setUnlinkProps(data)
- setIsConfirm(true)
- }}
- />
- ))}
-
- {/* Accounts that can be linked */}
- {suggestedIdentities.map((si) => (
-
- dispatch({
- type: 'open',
- key: 'linkAuth0Accounts',
- props: { secondaryProvider: si.provider },
- })
- }
- />
- ))}
-
- {/* If the primary is an email/password account and has no linked or suggested identities,
- we can suggest they link an Apple account */}
- {!primaryIdentity.isSocial &&
- !secondaryIdentities.length &&
- !suggestedIdentities.length && (
-
- dispatch({
- type: 'open',
- key: 'linkAuth0Accounts',
- props: { secondaryProvider: 'apple' },
- })
- }
- />
- )}
-
-
- setIsConfirm(false)}
- onConfirm={async () => {
- setIsConfirm(false)
- await unlinkAccountQuery.mutateAsync(unlinkProps!)
- }}
- title="Unlink account?"
- >
-
-
- Unlinking this account will remove the connection permanently.{' '}
- No data will be lost.
-
-
- After unlinking, each login will become a{' '}
- separate Maybe account.
-
-
-
- >
- )
-}
diff --git a/libs/client/features/src/user-details/index.ts b/libs/client/features/src/user-details/index.ts
index 840d8672..f47e2ff5 100644
--- a/libs/client/features/src/user-details/index.ts
+++ b/libs/client/features/src/user-details/index.ts
@@ -1,2 +1 @@
export * from './UserDetails'
-export * from './LinkAccountFlow'
diff --git a/libs/client/features/src/user-notifications/NotificationPreferences.tsx b/libs/client/features/src/user-notifications/NotificationPreferences.tsx
deleted file mode 100644
index 6efb0b60..00000000
--- a/libs/client/features/src/user-notifications/NotificationPreferences.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { useUserApi } from '@maybe-finance/client/shared'
-
-export function GeneralPreferences() {
- const { useProfile } = useUserApi()
-
- const userProfile = useProfile()
-
- if (!userProfile.data) {
- return null
- }
-
- return (
- <>
- Ask the advisor
-
- {/* TODO: Update notifications or remove */}
-
- >
- )
-}
diff --git a/libs/client/features/src/user-notifications/index.ts b/libs/client/features/src/user-notifications/index.ts
deleted file mode 100644
index 3ac0b127..00000000
--- a/libs/client/features/src/user-notifications/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './NotificationPreferences'
diff --git a/libs/client/features/src/user-security/MultiFactorAuthentication.tsx b/libs/client/features/src/user-security/MultiFactorAuthentication.tsx
deleted file mode 100644
index 0841e161..00000000
--- a/libs/client/features/src/user-security/MultiFactorAuthentication.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import type { SharedType } from '@maybe-finance/shared'
-import { useAuth0 } from '@auth0/auth0-react'
-import { useUserApi } from '@maybe-finance/client/shared'
-import { Button } from '@maybe-finance/design-system'
-import { toast } from 'react-hot-toast'
-
-export function MultiFactorAuthentication({ enabled }: { enabled: boolean }) {
- const { useUpdateAuth0Profile } = useUserApi()
-
- const { loginWithPopup } = useAuth0()
-
- const updateProfile = useUpdateAuth0Profile({
- onSuccess(data) {
- toast.success('MFA setting updated successfully')
- if (data?.user_metadata?.enrolled_mfa === true) {
- loginWithPopup(
- {
- authorizationParams: {
- connection: 'Username-Password-Authentication',
- screen_hint: 'show-form-only',
- display: 'page',
- },
- },
- { timeoutInSeconds: 360 }
- )
- }
- },
- onError() {
- toast.error('Something went wrong enabling MFA on this account.')
- },
- })
-
- return (
-
- )
-}
diff --git a/libs/client/features/src/user-security/SecurityPreferences.tsx b/libs/client/features/src/user-security/SecurityPreferences.tsx
index 34852883..8a68f4cb 100644
--- a/libs/client/features/src/user-security/SecurityPreferences.tsx
+++ b/libs/client/features/src/user-security/SecurityPreferences.tsx
@@ -1,47 +1,12 @@
import { useUserApi } from '@maybe-finance/client/shared'
import { Button, LoadingSpinner } from '@maybe-finance/design-system'
-import { MultiFactorAuthentication } from './MultiFactorAuthentication'
import { PasswordReset } from './PasswordReset'
export function SecurityPreferences() {
- const { useAuth0Profile } = useUserApi()
- const profileQuery = useAuth0Profile()
-
- if (profileQuery.isLoading) {
- return
- }
-
- if (profileQuery.isError) {
- return (
-
- Something went wrong loading your security preferences...
-
- )
- }
-
- const { socialOnlyUser, mfaEnabled } = profileQuery.data
-
- return socialOnlyUser ? (
- <>
-
- Your account credentials are managed by Apple. To reset your password, click the
- button below to go to your Apple settings.
-
-
- Manage Apple Account
-
- >
- ) : (
+ return (
<>
Password
-
- Multi-Factor Authentication
-
- Add an extra layer of security by setting up multi-factor authentication. This will
- need an app like Google Authenticator or Authy.
-
-
>
)
}
diff --git a/libs/client/features/src/user/AuthLoader.tsx b/libs/client/features/src/user/AuthLoader.tsx
deleted file mode 100644
index 9ccc2c54..00000000
--- a/libs/client/features/src/user/AuthLoader.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { useAuth0 } from '@auth0/auth0-react'
-import { LoadingSpinner } from '@maybe-finance/design-system'
-import { SharedType } from '@maybe-finance/shared'
-import { useMemo } from 'react'
-import { RiInformationLine } from 'react-icons/ri'
-
-export function AuthLoader({ message }: { message?: string }): JSX.Element {
- const { user } = useAuth0()
-
- const currentLoginType = useMemo(() => {
- const primaryIdentity =
- user && user[SharedType.Auth0CustomNamespace.PrimaryIdentity]?.provider
-
- return primaryIdentity ? primaryIdentity : undefined
- }, [user])
-
- return (
- <>
- {message && (
-
-
-
-
- You are currently logged in to your{' '}
-
- {currentLoginType === 'apple' ? 'Apple ' : 'Email/Password '}
- {' '}
- account. Please login with your{' '}
-
- {currentLoginType === 'apple' ? 'Email/Password ' : 'Apple '}
- account
-
- , and we'll merge the data between the two (no data will be lost).
-
-
-
- )}
-
-
- >
- )
-}
diff --git a/libs/client/features/src/user/index.ts b/libs/client/features/src/user/index.ts
deleted file mode 100644
index ba4b1d08..00000000
--- a/libs/client/features/src/user/index.ts
+++ /dev/null
@@ -1 +0,0 @@
-export * from './AuthLoader'
diff --git a/libs/client/shared/src/api/index.ts b/libs/client/shared/src/api/index.ts
index bb7a811f..6fe77f31 100644
--- a/libs/client/shared/src/api/index.ts
+++ b/libs/client/shared/src/api/index.ts
@@ -1,5 +1,6 @@
export * from './useAccountApi'
export * from './useAccountConnectionApi'
+export * from './useAuthUserApi'
export * from './useFinicityApi'
export * from './useInstitutionApi'
export * from './useUserApi'
diff --git a/libs/client/shared/src/api/useAuthUserApi.ts b/libs/client/shared/src/api/useAuthUserApi.ts
new file mode 100644
index 00000000..8a17c392
--- /dev/null
+++ b/libs/client/shared/src/api/useAuthUserApi.ts
@@ -0,0 +1,28 @@
+import type { SharedType } from '@maybe-finance/shared'
+import type { AxiosInstance } from 'axios'
+import { useMemo } from 'react'
+import { useQuery } from '@tanstack/react-query'
+import { useAxiosWithAuth } from '../hooks/useAxiosWithAuth'
+
+const AuthUserApi = (axios: AxiosInstance) => ({
+ async getByEmail(email: string) {
+ const { data } = await axios.get(`/auth-users/${email}`)
+ return data
+ },
+})
+
+const staleTimes = {
+ user: 30_000,
+}
+
+export function useAuthUserApi() {
+ const { axios } = useAxiosWithAuth()
+ const api = useMemo(() => AuthUserApi(axios), [axios])
+
+ const useGetByEmail = (email: string) =>
+ useQuery(['auth-users', email], () => api.getByEmail(email), { staleTime: staleTimes.user })
+
+ return {
+ useGetByEmail,
+ }
+}
diff --git a/libs/client/shared/src/api/useUserApi.ts b/libs/client/shared/src/api/useUserApi.ts
index 83148bcd..65a2d9a6 100644
--- a/libs/client/shared/src/api/useUserApi.ts
+++ b/libs/client/shared/src/api/useUserApi.ts
@@ -1,20 +1,14 @@
import type { UseMutationOptions, UseQueryOptions } from '@tanstack/react-query'
import type { SharedType } from '@maybe-finance/shared'
-import type { Auth0ContextInterface } from '@auth0/auth0-react'
import type { AxiosInstance } from 'axios'
-import Axios from 'axios'
import * as Sentry from '@sentry/react'
import { useMemo } from 'react'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { toast } from 'react-hot-toast'
import { DateTime } from 'luxon'
import { useAxiosWithAuth } from '../hooks/useAxiosWithAuth'
-import { useAuth0 } from '@auth0/auth0-react'
-const UserApi = (
- axios: AxiosInstance,
- auth0: Auth0ContextInterface
-) => ({
+const UserApi = (axios: AxiosInstance) => ({
async getNetWorthSeries(start: string, end: string) {
const { data } = await axios.get(
`/users/net-worth`,
@@ -65,16 +59,8 @@ const UserApi = (
return data
},
- async getAuth0Profile() {
- const { data } = await axios.get('/users/auth0-profile')
- return data
- },
-
- async updateAuth0Profile(newProfile: SharedType.UpdateAuth0User) {
- const { data } = await axios.put<
- SharedType.Auth0User,
- SharedType.ApiResponse
- >('/users/auth0-profile', newProfile)
+ async getAuthProfile() {
+ const { data } = await axios.get('/users/auth-profile')
return data
},
@@ -83,49 +69,6 @@ const UserApi = (
return data
},
- async toggleMFA(desiredMFAState: 'enabled' | 'disabled'): Promise<{
- actualMFAState: 'enabled' | 'disabled'
- desiredMFAState: 'enabled' | 'disabled'
- mfaRegistrationComplete: boolean
- }> {
- const audience = process.env.NEXT_PUBLIC_AUTH0_AUDIENCE || 'https://maybe-finance-api/v1'
-
- await axios.put>(
- '/users/auth0-profile',
- {
- user_metadata: { enrolled_mfa: desiredMFAState === 'enabled' ? true : false },
- }
- )
-
- // If the user is enabling MFA, prompt them to set it up immediately
- if (desiredMFAState === 'enabled') {
- await auth0.loginWithPopup(
- {
- authorizationParams: {
- connection: 'Username-Password-Authentication',
- screen_hint: 'show-form-only',
- display: 'page',
- audience,
- },
- },
- { timeoutInSeconds: 360 }
- )
- }
-
- const currentIdTokenMFAState = auth0.user?.['https://maybe.co/user-metadata']?.enrolled_mfa
- ? 'enabled'
- : 'disabled'
-
- // If the ID token is the same as the user's intended MFA state, that means they successfully
- // completed the flow. If not, they closed the popup early.
- return {
- actualMFAState: currentIdTokenMFAState,
- desiredMFAState,
- mfaRegistrationComplete:
- currentIdTokenMFAState === desiredMFAState || desiredMFAState === 'disabled',
- }
- },
-
async changePassword(newPassword: SharedType.PasswordReset) {
const { data } = await axios.put<
SharedType.PasswordReset,
@@ -134,36 +77,10 @@ const UserApi = (
return data
},
- async linkAccounts({ secondaryJWT, secondaryProvider }: SharedType.LinkAccounts) {
- try {
- const { data } = await axios.post<
- SharedType.LinkAccounts,
- SharedType.ApiResponse
- >('/users/link-accounts', { secondaryJWT, secondaryProvider })
- return data
- } catch (err) {
- if (Axios.isAxiosError(err)) {
- const message = err.response?.data?.errors?.[0]?.title
- throw new Error(message ?? 'Something went wrong')
- }
-
- throw err
- }
- },
-
- async unlinkAccount(unlinkData: SharedType.UnlinkAccount) {
- const { data } = await axios.post<
- SharedType.UnlinkAccount,
- SharedType.ApiResponse
- >('/users/unlink-account', unlinkData)
-
- return data
- },
-
- async resendEmailVerification(auth0Id?: string) {
+ async resendEmailVerification(authId?: string) {
const { data } = await axios.post<{ success: boolean }>(
'/users/resend-verification-email',
- { auth0Id }
+ { authId }
)
return data
@@ -201,8 +118,7 @@ const staleTimes = {
export function useUserApi() {
const queryClient = useQueryClient()
const { axios } = useAxiosWithAuth()
- const auth0 = useAuth0()
- const api = useMemo(() => UserApi(axios, auth0), [axios, auth0])
+ const api = useMemo(() => UserApi(axios), [axios])
const useNetWorthSeries = (
{ start, end }: { start: string; end: string },
@@ -288,21 +204,14 @@ export function useUserApi() {
...options,
})
- const useAuth0Profile = (
- options?: Omit, 'queryKey' | 'queryFn'>
- ) => useQuery(['users', 'auth0-profile'], api.getAuth0Profile, options)
-
- const useUpdateAuth0Profile = (
- options?: UseMutationOptions<
- SharedType.Auth0User | undefined,
- unknown,
- SharedType.UpdateAuth0User
+ const useAuthProfile = (
+ options?: Omit<
+ UseQueryOptions,
+ 'queryKey' | 'queryFn'
>
) =>
- useMutation(api.updateAuth0Profile, {
- onSettled() {
- queryClient.invalidateQueries(['users', 'auth0-profile'])
- },
+ useQuery(['auth-profile'], api.getAuthProfile, {
+ staleTime: staleTimes.user,
...options,
})
@@ -323,33 +232,6 @@ export function useUserApi() {
},
})
- const useLinkAccounts = (
- options?: UseMutationOptions<
- SharedType.Auth0User | undefined,
- unknown,
- SharedType.LinkAccounts
- >
- ) => useMutation(api.linkAccounts, options)
-
- const useUnlinkAccount = (
- options?: UseMutationOptions<
- SharedType.Auth0User | undefined,
- unknown,
- SharedType.UnlinkAccount
- >
- ) =>
- useMutation(api.unlinkAccount, {
- onSuccess: () => {
- toast.success('Account unlinked!')
- queryClient.invalidateQueries(['users'])
- },
- onError: (err) => {
- Sentry.captureException(err)
- toast.error('Error unlinking user account')
- },
- ...options,
- })
-
const useResendEmailVerification = (
options?: UseMutationOptions<{ success: boolean } | undefined, unknown, string | undefined>
) =>
@@ -403,12 +285,9 @@ export function useUserApi() {
useCurrentNetWorth,
useProfile,
useUpdateProfile,
- useAuth0Profile,
- useUpdateAuth0Profile,
+ useAuthProfile,
useSubscription,
useChangePassword,
- useLinkAccounts,
- useUnlinkAccount,
useResendEmailVerification,
useCreateCheckoutSession,
useCreateCustomerPortalSession,
diff --git a/libs/client/shared/src/components/dialogs/ConfirmDialog.tsx b/libs/client/shared/src/components/dialogs/ConfirmDialog.tsx
deleted file mode 100644
index 98434336..00000000
--- a/libs/client/shared/src/components/dialogs/ConfirmDialog.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import type { PropsWithChildren } from 'react'
-import { Button, Dialog } from '@maybe-finance/design-system'
-
-export type UnlinkAccountConfirmProps = PropsWithChildren<{
- isOpen: boolean
- onCancel: () => void
- onConfirm: () => void
- title: string
- showClose?: boolean
-}>
-
-export function ConfirmDialog({
- isOpen,
- onCancel,
- onConfirm,
- title,
- showClose = false,
- children,
-}: UnlinkAccountConfirmProps) {
- return (
-
- {title}
- {children}
-
-
- Cancel
-
-
- Unlink Account
-
-
-
- )
-}
diff --git a/libs/client/shared/src/components/dialogs/FeedbackDialog.tsx b/libs/client/shared/src/components/dialogs/FeedbackDialog.tsx
index 4089461d..d6f86e9f 100644
--- a/libs/client/shared/src/components/dialogs/FeedbackDialog.tsx
+++ b/libs/client/shared/src/components/dialogs/FeedbackDialog.tsx
@@ -1,4 +1,4 @@
-import { useAuth0 } from '@auth0/auth0-react'
+import { useSession } from 'next-auth/react'
import { Button, Dialog } from '@maybe-finance/design-system'
import { useState } from 'react'
import axios from 'axios'
@@ -12,7 +12,7 @@ export interface FeedbackDialogProps {
export function FeedbackDialog({ isOpen, onClose, notImplementedNotice }: FeedbackDialogProps) {
const [feedback, setFeedback] = useState('')
- const { user } = useAuth0()
+ const { data: session } = useSession()
return (
@@ -41,10 +41,14 @@ export function FeedbackDialog({ isOpen, onClose, notImplementedNotice }: Feedba
try {
await axios
.create({ transformRequest: [(data) => JSON.stringify(data)] })
- .post('https://hooks.zapier.com/hooks/catch/10143005/buyo6na/', {
- comment: `**From user:** ${user?.sub}\n\n${feedback}`,
- page: `**Main app feedback**: ${window.location.href}`,
- })
+ .post(
+ process.env.NEXT_PUBLIC_ZAPIER_FEEDBACK_HOOK_URL ||
+ 'REPLACE_THIS',
+ {
+ comment: `**From user:** ${session?.user?.email}\n\n${feedback}`,
+ page: `**Main app feedback**: ${window.location.href}`,
+ }
+ )
toast.success('Your feedback was submitted!')
} catch (e) {
diff --git a/libs/client/shared/src/components/dialogs/index.ts b/libs/client/shared/src/components/dialogs/index.ts
index ea60cae2..92655e0b 100644
--- a/libs/client/shared/src/components/dialogs/index.ts
+++ b/libs/client/shared/src/components/dialogs/index.ts
@@ -1,3 +1,2 @@
export * from './FeedbackDialog'
-export * from './ConfirmDialog'
export * from './NonUSDDialog'
diff --git a/libs/client/shared/src/providers/AuthProvider.tsx b/libs/client/shared/src/providers/AuthProvider.tsx
deleted file mode 100644
index e0254d67..00000000
--- a/libs/client/shared/src/providers/AuthProvider.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import type { Auth0ContextInterface } from '@auth0/auth0-react'
-import { createContext } from 'react'
-import type { PropsWithChildren } from 'react'
-import { Auth0Provider, type Auth0ProviderOptions } from '@auth0/auth0-react'
-import { useRouter } from 'next/router'
-
-export const linkAuth0AccountCtx = createContext(
- null
-) as Auth0ProviderOptions['context']
-
-/**
- * Auth0 Context Provider
- *
- * Why 2 configs?
- *
- * For user account linking, we need two contexts so that when the secondary
- * user is authenticated prior to linking, it doesn't log the primary user out.
- *
- * @see https://github.com/auth0/auth0-react/issues/425#issuecomment-1303619555
- */
-export function AuthProvider({ children }: PropsWithChildren) {
- const router = useRouter()
-
- const sharedConfig: Auth0ProviderOptions = {
- domain: process.env.NEXT_PUBLIC_AUTH0_DOMAIN || 'REPLACE_THIS',
- clientId: process.env.NEXT_PUBLIC_AUTH0_CLIENT_ID || 'REPLACE_THIS',
- onRedirectCallback: (appState) => router.replace(appState?.returnTo || '/'),
- authorizationParams: {
- audience: process.env.NEXT_PUBLIC_AUTH0_AUDIENCE || 'https://maybe-finance-api/v1',
- screen_hint: router.pathname === '/register' ? 'signup' : 'login',
- },
- }
-
- const isBrowser = typeof window !== 'undefined'
-
- return (
-
-
- {children}
-
-
- )
-}
diff --git a/libs/client/shared/src/providers/AxiosProvider.tsx b/libs/client/shared/src/providers/AxiosProvider.tsx
index 0daae1c3..38aa8d50 100644
--- a/libs/client/shared/src/providers/AxiosProvider.tsx
+++ b/libs/client/shared/src/providers/AxiosProvider.tsx
@@ -1,11 +1,8 @@
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import type { SharedType } from '@maybe-finance/shared'
import { superjson } from '@maybe-finance/shared'
-import { createContext, type PropsWithChildren, useCallback, useMemo } from 'react'
-import { useAuth0 } from '@auth0/auth0-react'
+import { createContext, type PropsWithChildren, useMemo } from 'react'
import Axios from 'axios'
-import * as Sentry from '@sentry/react'
-import { useRouter } from 'next/router'
type CreateInstanceOptions = {
getToken?: () => Promise
@@ -15,7 +12,6 @@ type CreateInstanceOptions = {
}
export type AxiosContextValue = {
- getToken: () => Promise
defaultBaseUrl: string
axios: AxiosInstance
createInstance: (options?: CreateInstanceOptions) => AxiosInstance
@@ -71,63 +67,29 @@ function createInstance(options?: CreateInstanceOptions) {
return instance
}
-/**
- * Injects the Auth0 access token into every axios request
- *
- * @see https://github.com/auth0/auth0-react/issues/266#issuecomment-919222402
- */
export function AxiosProvider({ children }: PropsWithChildren) {
- // Rather than storing access token in localStorage (insecure), we use this method to retrieve it prior to making API calls
- const { getAccessTokenSilently, isAuthenticated, loginWithRedirect } = useAuth0()
- const router = useRouter()
-
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3333'
- const getToken = useCallback(async () => {
- if (!isAuthenticated) return null
-
- try {
- const token = await getAccessTokenSilently()
- return token
- } catch (err) {
- const authErr =
- err && typeof err === 'object' && 'error' in err && typeof err['error'] === 'string'
- ? err['error']
- : null
- const isRecoverable = authErr
- ? [
- 'mfa_required',
- 'consent_required',
- 'interaction_required',
- 'login_required',
- ].includes(authErr)
- : false
-
- if (isRecoverable) {
- await loginWithRedirect({ appState: { returnTo: router.asPath } })
- } else {
- Sentry.captureException(err)
- }
-
- return null
- }
- }, [isAuthenticated, getAccessTokenSilently, loginWithRedirect, router])
-
// Expose a default instance with auth, superjson, headers
const defaultInstance = useMemo(() => {
- const defaultHeaders = { 'Content-Type': 'application/json' }
+ const defaultHeaders = {
+ 'Content-Type': 'application/json',
+ 'Access-Control-Allow-Credentials': true,
+ }
return createInstance({
- getToken,
- axiosOptions: { baseURL: `${API_URL}/v1`, headers: defaultHeaders },
+ axiosOptions: {
+ baseURL: `${API_URL}/v1`,
+ headers: defaultHeaders,
+ withCredentials: true,
+ },
serialize: true,
deserialize: true,
})
- }, [getToken, API_URL])
+ }, [API_URL])
return (
(null)
const [isOpen, setIsOpen] = useState(false)
+ const isOpenRef = useRef(false)
+
const { styles, attributes, update } = usePopper(referenceElement, popperElement, {
placement,
modifiers: [
@@ -180,6 +182,7 @@ function Options({
useEffect(() => {
if (isOpen && update) update()
+ if (isOpenRef.current !== isOpen) setIsOpen(isOpenRef.current)
}, [isOpen, update])
return (
@@ -198,7 +201,7 @@ function Options({
{...rest}
>
{({ open }) => {
- setIsOpen(open)
+ isOpenRef.current = open
return children
}}
diff --git a/libs/design-system/src/lib/Menu/Menu.tsx b/libs/design-system/src/lib/Menu/Menu.tsx
index eac82024..7d625883 100644
--- a/libs/design-system/src/lib/Menu/Menu.tsx
+++ b/libs/design-system/src/lib/Menu/Menu.tsx
@@ -46,6 +46,8 @@ function Items({
const [popperElement, setPopperElement] = useState(null)
const [isOpen, setIsOpen] = useState(false)
+ const isOpenRef = useRef(false)
+
const { styles, attributes, update } = usePopper(referenceElement?.current, popperElement, {
placement,
modifiers: [
@@ -60,6 +62,7 @@ function Items({
useEffect(() => {
if (isOpen && update) update()
+ if (isOpenRef.current !== isOpen) setIsOpen(isOpenRef.current)
}, [isOpen, update])
return (
@@ -75,7 +78,7 @@ function Items({
{...rest}
>
{(renderProps) => {
- setIsOpen(renderProps.open)
+ isOpenRef.current = renderProps.open
return typeof children === 'function' ? children(renderProps) : children
}}
diff --git a/libs/server/features/src/auth-user/auth-user.service.ts b/libs/server/features/src/auth-user/auth-user.service.ts
new file mode 100644
index 00000000..81fddd02
--- /dev/null
+++ b/libs/server/features/src/auth-user/auth-user.service.ts
@@ -0,0 +1,52 @@
+import type { AuthUser, PrismaClient, Prisma } from '@prisma/client'
+import type { Logger } from 'winston'
+import bcrypt from 'bcrypt'
+
+export interface IAuthUserService {
+ get(id: AuthUser['id']): Promise
+ delete(id: AuthUser['id']): Promise
+}
+
+export class AuthUserService implements IAuthUserService {
+ constructor(private readonly logger: Logger, private readonly prisma: PrismaClient) {}
+
+ async get(id: AuthUser['id']) {
+ return await this.prisma.authUser.findUniqueOrThrow({
+ where: { id },
+ })
+ }
+
+ async getByEmail(email: AuthUser['email']) {
+ if (!email) throw new Error('No email provided')
+ return await this.prisma.authUser.findUnique({
+ where: { email },
+ })
+ }
+
+ async updatePassword(id: AuthUser['id'], oldPassword: string, newPassword: string) {
+ const authUser = await this.get(id)
+ const isMatch = await bcrypt.compare(oldPassword, authUser.password!)
+ if (!isMatch) {
+ throw new Error('Could not reset password')
+ } else {
+ const hashedPassword = await bcrypt.hash(newPassword, 10)
+ return await this.prisma.authUser.update({
+ where: { id },
+ data: { password: hashedPassword },
+ })
+ }
+ }
+
+ async create(data: Prisma.AuthUserCreateInput & { firstName: string; lastName: string }) {
+ const authUser = await this.prisma.authUser.create({ data: { ...data } })
+ return authUser
+ }
+
+ async delete(id: AuthUser['id']) {
+ const authUser = await this.get(id)
+
+ this.logger.info(`Removing user ${authUser.id} from Prisma`)
+ const user = await this.prisma.authUser.delete({ where: { id } })
+ return user
+ }
+}
diff --git a/libs/server/features/src/auth-user/index.ts b/libs/server/features/src/auth-user/index.ts
new file mode 100644
index 00000000..b1075bc3
--- /dev/null
+++ b/libs/server/features/src/auth-user/index.ts
@@ -0,0 +1 @@
+export * from './auth-user.service'
diff --git a/libs/server/features/src/email/email.processor.ts b/libs/server/features/src/email/email.processor.ts
index 054583e7..ab7913fe 100644
--- a/libs/server/features/src/email/email.processor.ts
+++ b/libs/server/features/src/email/email.processor.ts
@@ -2,7 +2,6 @@ import type { Logger } from 'winston'
import type { PrismaClient } from '@prisma/client'
import type { SendEmailQueueJobData } from '@maybe-finance/server/shared'
import type { IEmailService } from './email.service'
-import type { ManagementClient } from 'auth0'
import { DateTime } from 'luxon'
export interface IEmailProcessor {
@@ -14,7 +13,6 @@ export class EmailProcessor implements IEmailProcessor {
constructor(
private readonly logger: Logger,
private readonly prisma: PrismaClient,
- private readonly auth0: ManagementClient,
private readonly emailService: IEmailService
) {}
diff --git a/libs/server/features/src/index.ts b/libs/server/features/src/index.ts
index ab7e0196..4fa66d75 100644
--- a/libs/server/features/src/index.ts
+++ b/libs/server/features/src/index.ts
@@ -4,6 +4,7 @@ export * from './account-balance'
export * from './email'
export * from './institution'
export * from './security-pricing'
+export * from './auth-user'
export * from './user'
export * from './valuation'
export * from './providers'
diff --git a/libs/server/features/src/stripe/stripe.webhook.ts b/libs/server/features/src/stripe/stripe.webhook.ts
index d139a06b..caaffa12 100644
--- a/libs/server/features/src/stripe/stripe.webhook.ts
+++ b/libs/server/features/src/stripe/stripe.webhook.ts
@@ -27,7 +27,7 @@ export class StripeWebhookHandler implements IStripeWebhookHandler {
await this.prisma.user.updateMany({
where: {
- auth0Id: session.client_reference_id,
+ authId: session.client_reference_id,
},
data: {
trialEnd: null,
diff --git a/libs/server/features/src/user/user.service.ts b/libs/server/features/src/user/user.service.ts
index 710d9a73..785ae3cc 100644
--- a/libs/server/features/src/user/user.service.ts
+++ b/libs/server/features/src/user/user.service.ts
@@ -1,16 +1,6 @@
-import type {
- AccountCategory,
- AccountType,
- PrismaClient,
- User,
-} from '@prisma/client'
+import type { AccountCategory, AccountType, PrismaClient, User } from '@prisma/client'
import type { Logger } from 'winston'
-import {
- AuthUtil,
- type PurgeUserQueue,
- type SyncUserQueue,
-} from '@maybe-finance/server/shared'
-import type { ManagementClient, UnlinkAccountsParamsProvider } from 'auth0'
+import type { PurgeUserQueue, SyncUserQueue } from '@maybe-finance/server/shared'
import type Stripe from 'stripe'
import type { IBalanceSyncStrategyFactory } from '../account-balance'
import type { IAccountQueryService } from '../account'
@@ -64,7 +54,6 @@ export class UserService implements IUserService {
private readonly balanceSyncStrategyFactory: IBalanceSyncStrategyFactory,
private readonly syncQueue: SyncUserQueue,
private readonly purgeQueue: PurgeUserQueue,
- private readonly auth0: ManagementClient,
private readonly stripe: Stripe
) {}
@@ -74,46 +63,11 @@ export class UserService implements IUserService {
})
}
- async getAuth0Profile(user: User): Promise {
- if (!user.email) throw new Error('No email found for user')
-
- const usersWithMatchingEmail = await this.auth0.getUsersByEmail(user.email)
- const autoPromptEnabled = user.linkAccountDismissedAt == null
- const currentUser = usersWithMatchingEmail.find((u) => u.user_id === user.auth0Id)
- const primaryIdentity = currentUser?.identities?.find(
- (identity) => !('profileData' in identity)
- )
- const secondaryIdentities =
- currentUser?.identities?.filter((identity) => 'profileData' in identity) ?? []
- if (!currentUser || !primaryIdentity) throw new Error('Failed to get Auth0 user')
-
- const socialOnlyUser =
- primaryIdentity.isSocial && secondaryIdentities.every((i) => i.isSocial)
-
- const suggestedIdentities = usersWithMatchingEmail
- .filter(
- (match) =>
- match.email_verified &&
- match.user_id !== user.auth0Id &&
- match.identities?.at(0) != null
- )
- .map((user) => user.identities!.at(0)!)
-
- // Auth0 returns 'true' (mis-typing) or true, so normalize the type here
- const email_verified =
- (currentUser.email_verified as unknown as string) === 'true' ||
- currentUser.email_verified === true
-
- return {
- ...currentUser,
- email_verified,
- primaryIdentity,
- secondaryIdentities,
- suggestedIdentities,
- socialOnlyUser,
- autoPromptEnabled,
- mfaEnabled: currentUser.user_metadata?.enrolled_mfa === true,
- }
+ async getAuthProfile(id: User['id']): Promise {
+ const user = await this.get(id)
+ return this.prisma.authUser.findUniqueOrThrow({
+ where: { id: user.authId },
+ })
}
async sync(id: User['id']) {
@@ -166,9 +120,10 @@ export class UserService implements IUserService {
// Delete Stripe customer, ending any active subscriptions
if (user.stripeCustomerId) await this.stripe.customers.del(user.stripeCustomerId)
- // Delete user from Auth0 so that it cannot be accessed in a partially-purged state
- this.logger.info(`Removing user ${user.id} from Auth0 (${user.auth0Id})`)
- await this.auth0.deleteUser({ id: user.auth0Id })
+ // Delete user from Auth so that it cannot be accessed in a partially-purged state
+ // TODO: Update this to use new Auth
+ this.logger.info(`Removing user ${user.id} from Auth (${user.authId})`)
+ await this.prisma.authUser.delete({ where: { id: user.authId } })
await this.purgeQueue.add('purge-user', { userId: user.id })
@@ -321,7 +276,7 @@ export class UserService implements IUserService {
const user = await this.prisma.user.findUniqueOrThrow({
where: { id: userId },
select: {
- auth0Id: true,
+ authId: true,
onboarding: true,
dob: true,
household: true,
@@ -336,10 +291,12 @@ export class UserService implements IUserService {
},
})
- const auth0User = await this.auth0.getUser({ id: user.auth0Id })
+ const authUser = await this.prisma.authUser.findUniqueOrThrow({
+ where: { id: user.authId },
+ })
- // Auth0 has this mis-typed and it comes in as a 'true' string
- const email_verified = auth0User.email_verified as unknown as string | boolean
+ // NextAuth used DateTime for this field
+ const email_verified = authUser.emailVerified === null ? false : true
const typedOnboarding = user.onboarding as OnboardingState | null
const onboardingState = typedOnboarding
@@ -350,8 +307,8 @@ export class UserService implements IUserService {
{
...user,
onboarding: onboardingState,
- emailVerified: email_verified === true || email_verified === 'true',
- isAppleIdentity: auth0User.identities?.[0].provider === 'apple',
+ emailVerified: email_verified,
+ isAppleIdentity: false,
},
onboardingState.markedComplete
)
@@ -375,7 +332,7 @@ export class UserService implements IUserService {
.setTitle((_) => "Before we start, let's verify your email")
.addToGroup('setup')
.completeIf((user) => user.emailVerified)
- .excludeIf((user) => user.isAppleIdentity) // Auth0 auto-verifies Apple identities.
+ .excludeIf((user) => user.isAppleIdentity || true) // TODO: Needs email service to send, skip for now
onboarding
.addStep('firstAccount')
@@ -551,45 +508,4 @@ export class UserService implements IUserService {
return onboarding
}
-
- async linkAccounts(
- primaryAuth0Id: User['auth0Id'],
- provider: string,
- secondaryJWT: { token: string; domain: string; audience: string }
- ) {
- const validatedJWT = await AuthUtil.validateRS256JWT(
- `Bearer ${secondaryJWT.token}`,
- secondaryJWT.domain,
- secondaryJWT.audience
- )
-
- const user = await this.prisma.user.findFirst({ where: { auth0Id: validatedJWT.auth0Id } })
-
- if (user?.stripePriceId) {
- throw new Error(
- 'The account you are trying to link has an active Stripe trial or subscription. We cannot link this identity at this time.'
- )
- }
-
- return this.auth0.linkUsers(primaryAuth0Id, {
- user_id: validatedJWT.auth0Id,
- provider,
- })
- }
-
- async unlinkAccounts(
- primaryAuth0Id: User['auth0Id'],
- secondaryAuth0Id: User['auth0Id'],
- secondaryProvider: UnlinkAccountsParamsProvider
- ) {
- const response = await this.auth0.unlinkUsers({
- id: primaryAuth0Id,
- provider: secondaryProvider,
- user_id: secondaryAuth0Id,
- })
-
- this.logger.info(`Unlinked ${secondaryAuth0Id} from ${primaryAuth0Id}`)
-
- return response
- }
}
diff --git a/libs/server/shared/src/utils/auth-utils.ts b/libs/server/shared/src/utils/auth-utils.ts
index 08d7492b..42c509c5 100644
--- a/libs/server/shared/src/utils/auth-utils.ts
+++ b/libs/server/shared/src/utils/auth-utils.ts
@@ -13,7 +13,7 @@ export async function validateRS256JWT(
domain: string,
audience: string
): Promise<{
- auth0Id: string
+ authId: string
userMetadata: SharedType.MaybeUserMetadata
appMetadata: SharedType.MaybeAppMetadata
}> {
@@ -50,7 +50,7 @@ export async function validateRS256JWT(
if (typeof payload !== 'object') return reject('payload not an object')
resolve({
- auth0Id: payload.sub!,
+ authId: payload.sub!,
appMetadata: payload[SharedType.Auth0CustomNamespace.AppMetadata] ?? {},
userMetadata: payload[SharedType.Auth0CustomNamespace.UserMetadata] ?? {},
})
diff --git a/libs/shared/src/types/user-types.ts b/libs/shared/src/types/user-types.ts
index c833224c..ddea52a8 100644
--- a/libs/shared/src/types/user-types.ts
+++ b/libs/shared/src/types/user-types.ts
@@ -1,5 +1,3 @@
-import type { User as Auth0UserClient } from '@auth0/auth0-react'
-import type { Identity, User as Auth0UserServer } from 'auth0'
import type {
AccountCategory,
AccountClassification,
@@ -7,6 +5,7 @@ import type {
Prisma,
Security,
User as PrismaUser,
+ AuthUser,
} from '@prisma/client'
import type { Institution } from 'plaid'
import type { TimeSeries, TimeSeriesResponseWithDetail, Trend } from './general-types'
@@ -27,6 +26,14 @@ export type UpdateUser = Partial<
}
>
+/**
+ * ================================================================
+ * ====== Auth User ======
+ * ================================================================
+ */
+
+export type { AuthUser }
+
/**
* ================================================================
* ====== Net Worth ======
@@ -182,39 +189,11 @@ export type MaybeCustomClaims = {
[Auth0CustomNamespace.PrimaryIdentity]?: PrimaryAuth0Identity
}
-export type Auth0ReactUser = Auth0UserClient & MaybeCustomClaims
-export type Auth0User = Auth0UserServer
-export type Auth0Profile = Auth0User & {
- primaryIdentity: Identity // actual
- secondaryIdentities: Identity[] // linked
- suggestedIdentities: Identity[] // potential links
- autoPromptEnabled: boolean
- socialOnlyUser: boolean
- mfaEnabled: boolean
-}
-
-export type UpdateAuth0User = { enrolled_mfa: boolean }
-
export interface PasswordReset {
currentPassword: string
newPassword: string
}
-export type LinkAccountStatus = {
- autoPromptEnabled: boolean
- suggestedUsers: Auth0User[]
-}
-
-export interface LinkAccounts {
- secondaryJWT: string
- secondaryProvider: string
-}
-
-export interface UnlinkAccount {
- secondaryAuth0Id: string
- secondaryProvider: string
-}
-
export type UserSubscription = {
subscribed: boolean
trialing: boolean
diff --git a/package.json b/package.json
index 1dd9aeb7..518f953f 100644
--- a/package.json
+++ b/package.json
@@ -26,10 +26,6 @@
"dev:docker:reset": "docker-compose down -v --rmi all --remove-orphans && docker system prune --all --volumes && docker-compose build",
"dev:circular": "npx madge --circular --extensions ts libs",
"analyze:client": "ANALYZE=true nx build client --skip-nx-cache",
- "auth0:sync": "node auth0/sync",
- "auth0:deploy": "node auth0/deploy",
- "auth0:test": "auth0 test login 7MtD6RWsXKInGPrFyeEseo7Y8PXSBEiV --tenant maybe-finance-development.us.auth0.com --force",
- "auth0:edit": "live-server auth0",
"tools:pages": "live-server tools/pages",
"prepare": "husky install"
},
@@ -38,8 +34,7 @@
},
"private": true,
"dependencies": {
- "@auth0/auth0-react": "^2.0.0",
- "@auth0/nextjs-auth0": "^2.0.0",
+ "@auth/prisma-adapter": "^1.0.14",
"@bull-board/express": "^4.6.4",
"@casl/ability": "^6.3.2",
"@casl/prisma": "^1.4.1",
@@ -93,8 +88,10 @@
"auth0-deploy-cli": "^7.15.1",
"autoprefixer": "10.4.13",
"axios": "^0.26.1",
+ "bcrypt": "^5.1.1",
"bull": "^4.10.2",
"classnames": "^2.3.1",
+ "cookie-parser": "^1.4.6",
"core-js": "^3.6.5",
"cors": "^2.8.5",
"crypto-js": "^4.1.1",
@@ -122,6 +119,7 @@
"morgan": "^1.10.0",
"multer": "^1.4.5-lts.1",
"next": "13.1.1",
+ "next-auth": "^4.24.5",
"pg": "^8.8.0",
"plaid": "^12.1.0",
"postcss": "8.4.19",
@@ -195,6 +193,7 @@
"@testing-library/react": "13.4.0",
"@testing-library/user-event": "^13.2.1",
"@types/auth0": "^2.35.7",
+ "@types/bcrypt": "^5.0.2",
"@types/cors": "^2.8.12",
"@types/crypto-js": "^4.1.1",
"@types/d3-array": "^3.0.3",
diff --git a/prisma/migrations/20240111213125_next_auth_models/migration.sql b/prisma/migrations/20240111213125_next_auth_models/migration.sql
new file mode 100644
index 00000000..26719743
--- /dev/null
+++ b/prisma/migrations/20240111213125_next_auth_models/migration.sql
@@ -0,0 +1,79 @@
+/*
+ Warnings:
+
+ - A unique constraint covering the columns `[auth_id]` on the table `user` will be added. If there are existing duplicate values, this will fail.
+ - Added the required column `auth_id` to the `user` table without a default value. This is not possible if the table is not empty.
+
+*/
+-- AlterTable
+ALTER TABLE "user" ADD COLUMN "auth_id" TEXT NOT NULL;
+
+-- CreateTable
+CREATE TABLE "auth_account" (
+ "id" TEXT NOT NULL,
+ "user_id" TEXT NOT NULL,
+ "type" TEXT NOT NULL,
+ "provider" TEXT NOT NULL,
+ "provider_account_id" TEXT NOT NULL,
+ "refresh_token" TEXT,
+ "access_token" TEXT,
+ "expires_at" INTEGER,
+ "token_type" TEXT,
+ "scope" TEXT,
+ "id_token" TEXT,
+ "session_state" TEXT,
+
+ CONSTRAINT "auth_account_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "auth_user" (
+ "id" TEXT NOT NULL,
+ "name" TEXT,
+ "email" TEXT,
+ "email_verified" TIMESTAMP(3),
+ "image" TEXT,
+
+ CONSTRAINT "auth_user_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "auth_session" (
+ "id" TEXT NOT NULL,
+ "session_token" TEXT NOT NULL,
+ "user_id" TEXT NOT NULL,
+ "expires" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "auth_session_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "auth_verification_token" (
+ "identifier" TEXT NOT NULL,
+ "token" TEXT NOT NULL,
+ "expires" TIMESTAMP(3) NOT NULL
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "auth_account_provider_provider_account_id_key" ON "auth_account"("provider", "provider_account_id");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "auth_user_email_key" ON "auth_user"("email");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "auth_session_session_token_key" ON "auth_session"("session_token");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "auth_verification_token_token_key" ON "auth_verification_token"("token");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "auth_verification_token_identifier_token_key" ON "auth_verification_token"("identifier", "token");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "user_auth_id_key" ON "user"("auth_id");
+
+-- AddForeignKey
+ALTER TABLE "auth_account" ADD CONSTRAINT "auth_account_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth_user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "auth_session" ADD CONSTRAINT "auth_session_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "auth_user"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/migrations/20240111213725_add_password_to_auth_user/migration.sql b/prisma/migrations/20240111213725_add_password_to_auth_user/migration.sql
new file mode 100644
index 00000000..846bae97
--- /dev/null
+++ b/prisma/migrations/20240111213725_add_password_to_auth_user/migration.sql
@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "auth_user" ADD COLUMN "password" TEXT;
diff --git a/prisma/migrations/20240112201750_remove_auth0id_from_user/migration.sql b/prisma/migrations/20240112201750_remove_auth0id_from_user/migration.sql
new file mode 100644
index 00000000..ae4de81f
--- /dev/null
+++ b/prisma/migrations/20240112201750_remove_auth0id_from_user/migration.sql
@@ -0,0 +1,11 @@
+/*
+ Warnings:
+
+ - You are about to drop the column `auth0_id` on the `user` table. All the data in the column will be lost.
+
+*/
+-- DropIndex
+DROP INDEX "user_auth0_id_key";
+
+-- AlterTable
+ALTER TABLE "user" DROP COLUMN "auth0_id";
diff --git a/prisma/migrations/20240112204004_add_first_last_to_authuser/migration.sql b/prisma/migrations/20240112204004_add_first_last_to_authuser/migration.sql
new file mode 100644
index 00000000..ddb975b2
--- /dev/null
+++ b/prisma/migrations/20240112204004_add_first_last_to_authuser/migration.sql
@@ -0,0 +1,3 @@
+-- AlterTable
+ALTER TABLE "auth_user" ADD COLUMN "first_name" TEXT,
+ADD COLUMN "last_name" TEXT;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index e7402b75..587a274c 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -395,7 +395,7 @@ model User {
id Int @id @default(autoincrement())
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
- auth0Id String @unique @map("auth0_id")
+ authId String @unique @map("auth_id") // NextAuth user id
// profile
email String @db.Citext
@@ -560,6 +560,61 @@ model PlanMilestone {
@@map("plan_milestone")
}
+// NextAuth Models
+model AuthAccount {
+ id String @id @default(cuid())
+ userId String @map("user_id")
+ type String
+ provider String
+ providerAccountId String @map("provider_account_id")
+ refresh_token String? @db.Text
+ access_token String? @db.Text
+ expires_at Int?
+ token_type String?
+ scope String?
+ id_token String? @db.Text
+ session_state String?
+
+ user AuthUser @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@unique([provider, providerAccountId])
+ @@map("auth_account")
+}
+
+model AuthUser {
+ id String @id @default(cuid())
+ name String?
+ firstName String? @map("first_name")
+ lastName String? @map("last_name")
+ email String? @unique
+ emailVerified DateTime? @map("email_verified")
+ password String?
+ image String?
+ accounts AuthAccount[]
+ sessions AuthSession[]
+
+ @@map("auth_user")
+}
+
+model AuthSession {
+ id String @id @default(cuid())
+ sessionToken String @unique @map("session_token")
+ userId String @map("user_id")
+ expires DateTime
+ user AuthUser @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@map("auth_session")
+}
+
+model AuthVerificationToken {
+ identifier String
+ token String @unique
+ expires DateTime
+
+ @@unique([identifier, token])
+ @@map("auth_verification_token")
+}
+
enum ApprovalStatus {
pending
approved
diff --git a/yarn.lock b/yarn.lock
index 3221ad77..70375fc9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9,32 +9,25 @@
dependencies:
"@jridgewell/trace-mapping" "^0.3.0"
-"@auth0/auth0-react@^2.0.0":
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/@auth0/auth0-react/-/auth0-react-2.0.0.tgz#74e4d3662896e71dd95cca70b395715825da3b4e"
- integrity sha512-3pf41wU6ksm/6uPYAwjX5bZ7ma/K4LethibagTrKkMPuS8UatBvxLDtl3Aq52ZlJi1I+I42ckEfzWqloNxssIg==
+"@auth/core@0.20.0":
+ version "0.20.0"
+ resolved "https://registry.yarnpkg.com/@auth/core/-/core-0.20.0.tgz#18b706b9708973b1fd4cb6aac5ef47d89201f925"
+ integrity sha512-04lQH58H5d/9xQ63MOTDTOC7sXWYlr/RhJ97wfFLXzll7nYyCKbkrT3ZMdzdLC5O+qt90sQDK85TAtLlcZ2WBg==
dependencies:
- "@auth0/auth0-spa-js" "^2.0.2"
+ "@panva/hkdf" "^1.1.1"
+ "@types/cookie" "0.6.0"
+ cookie "0.6.0"
+ jose "^5.1.3"
+ oauth4webapi "^2.4.0"
+ preact "10.11.3"
+ preact-render-to-string "5.2.3"
-"@auth0/auth0-spa-js@^2.0.2":
- version "2.0.2"
- resolved "https://registry.yarnpkg.com/@auth0/auth0-spa-js/-/auth0-spa-js-2.0.2.tgz#fe0d5eeb6f0da48c24913a07b3565d48792de6d5"
- integrity sha512-sxK9Lb6gXGImqjmWBfndA/OSNY4YLPTPwJEVuitXIOZ1p3EoqHM4zjIHvcdiYIaVo+cUfEf3l0bf8UA7Xi4tjg==
-
-"@auth0/nextjs-auth0@^2.0.0":
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/@auth0/nextjs-auth0/-/nextjs-auth0-2.0.0.tgz#6695c6eb0d657f4ee6e4234891d3cb50ba0893d1"
- integrity sha512-LwV3AqJh0CXzzM1vUgSVKvYcZQsp3NzV4xboBCwvfzz7DIWVu1Ge1v0uQBGnzl3XDtFFQNYqVGjcPw5Wu1/L6A==
+"@auth/prisma-adapter@^1.0.14":
+ version "1.0.14"
+ resolved "https://registry.yarnpkg.com/@auth/prisma-adapter/-/prisma-adapter-1.0.14.tgz#3b46f86beec618ab5d6756fdd1b520535cf010ac"
+ integrity sha512-7urwnDT+K81SocU0SbfY/vtY/NbXgj8/AU2k6Ek8waHT/7YPLsOQnXQsTWROmolFshNVkt2kq9Z/HOVnRdHrkQ==
dependencies:
- "@panva/hkdf" "^1.0.2"
- cookie "^0.5.0"
- debug "^4.3.4"
- http-errors "^1.8.1"
- joi "^17.6.0"
- jose "^4.9.2"
- openid-client "^5.2.1"
- tslib "^2.4.0"
- url-join "^4.0.1"
+ "@auth/core" "0.20.0"
"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.10.4", "@babel/code-frame@^7.16.7", "@babel/code-frame@^7.18.6", "@babel/code-frame@^7.5.5", "@babel/code-frame@^7.8.3":
version "7.18.6"
@@ -1464,6 +1457,13 @@
dependencies:
regenerator-runtime "^0.13.10"
+"@babel/runtime@^7.20.13":
+ version "7.23.8"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.8.tgz#8ee6fe1ac47add7122902f257b8ddf55c898f650"
+ integrity sha512-Y7KbAP984rn1VGMbGqKmBLio9V7y5Je9GvU4rQPCPinCyNfUcToxIXl06d59URp/F3LwinvODxab5N/G6qggkw==
+ dependencies:
+ regenerator-runtime "^0.14.0"
+
"@babel/template@^7.12.7", "@babel/template@^7.16.7", "@babel/template@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.6.tgz#1283f4993e00b929d6e2d3c72fdc9168a2977a31"
@@ -2162,6 +2162,21 @@
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==
+"@mapbox/node-pre-gyp@^1.0.11":
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz#417db42b7f5323d79e93b34a6d7a2a12c0df43fa"
+ integrity sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==
+ dependencies:
+ detect-libc "^2.0.0"
+ https-proxy-agent "^5.0.0"
+ make-dir "^3.1.0"
+ node-fetch "^2.6.7"
+ nopt "^5.0.0"
+ npmlog "^5.0.1"
+ rimraf "^3.0.2"
+ semver "^7.3.5"
+ tar "^6.1.11"
+
"@mdx-js/mdx@^1.6.22":
version "1.6.22"
resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-1.6.22.tgz#8a723157bf90e78f17dc0f27995398e6c731f1ba"
@@ -2696,6 +2711,11 @@
resolved "https://registry.yarnpkg.com/@panva/hkdf/-/hkdf-1.0.2.tgz#bab0f09d09de9fd83628220d496627681bc440d6"
integrity sha512-MSAs9t3Go7GUkMhpKC44T58DJ5KGk2vBo+h1cqQeqlMfdGkxaVB78ZWpv9gYi/g2fa4sopag9gJsNvS8XGgWJA==
+"@panva/hkdf@^1.1.1":
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/@panva/hkdf/-/hkdf-1.1.1.tgz#ab9cd8755d1976e72fc77a00f7655a64efe6cd5d"
+ integrity sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA==
+
"@parcel/watcher@2.0.4":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@parcel/watcher/-/watcher-2.0.4.tgz#f300fef4cc38008ff4b8c29d92588eced3ce014b"
@@ -4407,6 +4427,13 @@
dependencies:
"@babel/types" "^7.3.0"
+"@types/bcrypt@^5.0.2":
+ version "5.0.2"
+ resolved "https://registry.yarnpkg.com/@types/bcrypt/-/bcrypt-5.0.2.tgz#22fddc11945ea4fbc3655b3e8b8847cc9f811477"
+ integrity sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ==
+ dependencies:
+ "@types/node" "*"
+
"@types/body-parser@*":
version "1.19.2"
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0"
@@ -4447,6 +4474,11 @@
dependencies:
"@types/node" "*"
+"@types/cookie@0.6.0":
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.6.0.tgz#eac397f28bf1d6ae0ae081363eca2f425bedf0d5"
+ integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==
+
"@types/cors@^2.8.12":
version "2.8.12"
resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.12.tgz#6b2c510a7ad7039e98e7b8d3d6598f4359e5c080"
@@ -5857,6 +5889,11 @@ abab@^2.0.6:
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==
+abbrev@1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
+ integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
+
accepts@~1.3.4:
version "1.3.7"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd"
@@ -6841,6 +6878,14 @@ bcrypt-pbkdf@^1.0.0:
dependencies:
tweetnacl "^0.14.3"
+bcrypt@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-5.1.1.tgz#0f732c6dcb4e12e5b70a25e326a72965879ba6e2"
+ integrity sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww==
+ dependencies:
+ "@mapbox/node-pre-gyp" "^1.0.11"
+ node-addon-api "^5.0.0"
+
bcryptjs@^2.3.0:
version "2.4.3"
resolved "https://registry.yarnpkg.com/bcryptjs/-/bcryptjs-2.4.3.tgz#9ab5627b93e60621ff7cdac5da9733027df1d0cb"
@@ -8045,11 +8090,24 @@ convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
dependencies:
safe-buffer "~5.1.1"
+cookie-parser@^1.4.6:
+ version "1.4.6"
+ resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.6.tgz#3ac3a7d35a7a03bbc7e365073a26074824214594"
+ integrity sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA==
+ dependencies:
+ cookie "0.4.1"
+ cookie-signature "1.0.6"
+
cookie-signature@1.0.6:
version "1.0.6"
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
+cookie@0.4.1, cookie@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
+ integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==
+
cookie@0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432"
@@ -8060,10 +8118,10 @@ cookie@0.5.0, cookie@^0.5.0:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
-cookie@^0.4.1:
- version "0.4.1"
- resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
- integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==
+cookie@0.6.0:
+ version "0.6.0"
+ resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
+ integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
cookiejar@^2.1.2:
version "2.1.3"
@@ -8894,6 +8952,11 @@ detab@2.0.4:
dependencies:
repeat-string "^1.5.4"
+detect-libc@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.2.tgz#8ccf2ba9315350e1241b88d0ac3b0e1fbd99605d"
+ integrity sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==
+
detect-newline@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
@@ -13074,16 +13137,21 @@ jose@^2.0.6:
dependencies:
"@panva/asn1.js" "^1.0.0"
-jose@^4.10.0, jose@^4.9.2:
- version "4.11.1"
- resolved "https://registry.yarnpkg.com/jose/-/jose-4.11.1.tgz#8f7443549befe5bddcf4bae664a9cbc1a62da4fa"
- integrity sha512-YRv4Tk/Wlug8qicwqFNFVEZSdbROCHRAC6qu/i0dyNKr5JQdoa2pIGoS04lLO/jXQX7Z9omoNewYIVIxqZBd9Q==
-
jose@^4.10.3:
version "4.11.0"
resolved "https://registry.yarnpkg.com/jose/-/jose-4.11.0.tgz#1c7f5c7806383d3e836434e8f49da531cb046a9d"
integrity sha512-wLe+lJHeG8Xt6uEubS4x0LVjS/3kXXu9dGoj9BNnlhYq7Kts0Pbb2pvv5KiI0yaKH/eaiR0LUOBhOVo9ktd05A==
+jose@^4.11.4, jose@^4.15.4:
+ version "4.15.4"
+ resolved "https://registry.yarnpkg.com/jose/-/jose-4.15.4.tgz#02a9a763803e3872cf55f29ecef0dfdcc218cc03"
+ integrity sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==
+
+jose@^5.1.3:
+ version "5.2.0"
+ resolved "https://registry.yarnpkg.com/jose/-/jose-5.2.0.tgz#d0ffd7f7e31253f633eefb190a930cd14a916995"
+ integrity sha512-oW3PCnvyrcm1HMvGTzqjxxfnEs9EoFOFWi2HsEGhlFVOXxTE3K9GKWVMFoFw06yPUqwpvEWic1BmtUZBI/tIjw==
+
js-string-escape@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef"
@@ -14343,6 +14411,11 @@ minipass@^3.0.0, minipass@^3.1.1:
dependencies:
yallist "^4.0.0"
+minipass@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d"
+ integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==
+
minizlib@^2.1.1:
version "2.1.2"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
@@ -14570,6 +14643,21 @@ nested-error-stacks@^2.0.0, nested-error-stacks@^2.1.0:
resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz#26c8a3cee6cc05fbcf1e333cd2fc3e003326c0b5"
integrity sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw==
+next-auth@^4.24.5:
+ version "4.24.5"
+ resolved "https://registry.yarnpkg.com/next-auth/-/next-auth-4.24.5.tgz#1fd1bfc0603c61fd2ba6fd81b976af690edbf07e"
+ integrity sha512-3RafV3XbfIKk6rF6GlLE4/KxjTcuMCifqrmD+98ejFq73SRoj2rmzoca8u764977lH/Q7jo6Xu6yM+Re1Mz/Og==
+ dependencies:
+ "@babel/runtime" "^7.20.13"
+ "@panva/hkdf" "^1.0.2"
+ cookie "^0.5.0"
+ jose "^4.11.4"
+ oauth "^0.9.15"
+ openid-client "^5.4.0"
+ preact "^10.6.3"
+ preact-render-to-string "^5.1.19"
+ uuid "^8.3.2"
+
next-tick@^1.0.0, next-tick@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb"
@@ -14633,6 +14721,11 @@ node-addon-api@^3.2.1:
resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161"
integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==
+node-addon-api@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-5.1.0.tgz#49da1ca055e109a23d537e9de43c09cca21eb762"
+ integrity sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==
+
node-dir@^0.1.10:
version "0.1.17"
resolved "https://registry.yarnpkg.com/node-dir/-/node-dir-0.1.17.tgz#5f5665d93351335caabef8f1c554516cf5f1e4e5"
@@ -14701,6 +14794,13 @@ node-releases@^2.0.2:
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.2.tgz#7139fe71e2f4f11b47d4d2986aaf8c48699e0c01"
integrity sha512-XxYDdcQ6eKqp/YjI+tb2C5WM2LgjnZrfYg4vgQt49EK268b6gYCHsBLrK2qvJo4FmCtqmKezb0WZFK4fkrZNsg==
+nopt@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88"
+ integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==
+ dependencies:
+ abbrev "1"
+
normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8"
@@ -14815,6 +14915,16 @@ nx@15.5.2:
yargs "^17.6.2"
yargs-parser "21.1.1"
+oauth4webapi@^2.4.0:
+ version "2.6.0"
+ resolved "https://registry.yarnpkg.com/oauth4webapi/-/oauth4webapi-2.6.0.tgz#776a2eb5ca6ad5e5249c4bb6194516318406d254"
+ integrity sha512-4P43og0d8fQ61RMQEl9L7zwGVduuYbLED7uP99MkFSGuOUvJL1Fs52/D3tRtKoFtiSwKblScTYJI+utQn3SUDg==
+
+oauth@^0.9.15:
+ version "0.9.15"
+ resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
+ integrity sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==
+
object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.1, object-assign@latest:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@@ -14829,7 +14939,7 @@ object-copy@^0.1.0:
define-property "^0.2.5"
kind-of "^3.0.3"
-object-hash@^2.0.1:
+object-hash@^2.0.1, object-hash@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5"
integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==
@@ -14988,6 +15098,11 @@ oidc-token-hash@^5.0.1:
resolved "https://registry.yarnpkg.com/oidc-token-hash/-/oidc-token-hash-5.0.1.tgz#ae6beec3ec20f0fd885e5400d175191d6e2f10c6"
integrity sha512-EvoOtz6FIEBzE+9q253HsLCVRiK/0doEJ2HCvvqMQb3dHZrP3WlJKYtJ55CRTw4jmYomzH4wkPuCj/I3ZvpKxQ==
+oidc-token-hash@^5.0.3:
+ version "5.0.3"
+ resolved "https://registry.yarnpkg.com/oidc-token-hash/-/oidc-token-hash-5.0.3.tgz#9a229f0a1ce9d4fc89bcaee5478c97a889e7b7b6"
+ integrity sha512-IF4PcGgzAr6XXSff26Sk/+P4KZFJVuHAJZj3wgO3vX2bMdNVp/QXTP3P7CEm9V1IdG8lDLY3HhiqpsE/nOwpPw==
+
on-finished@2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
@@ -15063,15 +15178,15 @@ openid-client@^4.9.1:
object-hash "^2.0.1"
oidc-token-hash "^5.0.1"
-openid-client@^5.2.1:
- version "5.3.1"
- resolved "https://registry.yarnpkg.com/openid-client/-/openid-client-5.3.1.tgz#69a5fa7d2b5ad479032f576852d40b4d4435488a"
- integrity sha512-RLfehQiHch9N6tRWNx68cicf3b1WR0x74bJWHRc25uYIbSRwjxYcTFaRnzbbpls5jroLAaB/bFIodTgA5LJMvw==
+openid-client@^5.4.0:
+ version "5.6.4"
+ resolved "https://registry.yarnpkg.com/openid-client/-/openid-client-5.6.4.tgz#b2c25e6d5338ba3ce00e04341bb286798a196177"
+ integrity sha512-T1h3B10BRPKfcObdBklX639tVz+xh34O7GjofqrqiAQdm7eHsQ00ih18x6wuJ/E6FxdtS2u3FmUGPDeEcMwzNA==
dependencies:
- jose "^4.10.0"
+ jose "^4.15.4"
lru-cache "^6.0.0"
- object-hash "^2.0.1"
- oidc-token-hash "^5.0.1"
+ object-hash "^2.2.0"
+ oidc-token-hash "^5.0.3"
opn@latest:
version "6.0.0"
@@ -16143,11 +16258,30 @@ postmark@^3.0.14:
dependencies:
axios "^0.25.0"
-preact@^10.5.13:
+preact-render-to-string@5.2.3:
+ version "5.2.3"
+ resolved "https://registry.yarnpkg.com/preact-render-to-string/-/preact-render-to-string-5.2.3.tgz#23d17376182af720b1060d5a4099843c7fe92fe4"
+ integrity sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==
+ dependencies:
+ pretty-format "^3.8.0"
+
+preact-render-to-string@^5.1.19:
+ version "5.2.6"
+ resolved "https://registry.yarnpkg.com/preact-render-to-string/-/preact-render-to-string-5.2.6.tgz#0ff0c86cd118d30affb825193f18e92bd59d0604"
+ integrity sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==
+ dependencies:
+ pretty-format "^3.8.0"
+
+preact@10.11.3, preact@^10.5.13:
version "10.11.3"
resolved "https://registry.yarnpkg.com/preact/-/preact-10.11.3.tgz#8a7e4ba19d3992c488b0785afcc0f8aa13c78d19"
integrity sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==
+preact@^10.6.3:
+ version "10.19.3"
+ resolved "https://registry.yarnpkg.com/preact/-/preact-10.19.3.tgz#7a7107ed2598a60676c943709ea3efb8aaafa899"
+ integrity sha512-nHHTeFVBTHRGxJXKkKu5hT8C/YWBkPso4/Gad6xuj5dbptt9iF9NZr9pHbPhBrnT2klheu7mHTxTZ/LjwJiEiQ==
+
prelude-ls@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
@@ -16217,6 +16351,11 @@ pretty-format@^28.1.1, pretty-format@^28.1.3:
ansi-styles "^5.0.0"
react-is "^18.0.0"
+pretty-format@^3.8.0:
+ version "3.8.0"
+ resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-3.8.0.tgz#bfbed56d5e9a776645f4b1ff7aa1a3ac4fa3c385"
+ integrity sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==
+
pretty-hrtime@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
@@ -16972,6 +17111,11 @@ regenerator-runtime@^0.13.4, regenerator-runtime@^0.13.7:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52"
integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==
+regenerator-runtime@^0.14.0:
+ version "0.14.1"
+ resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
+ integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==
+
regenerator-transform@^0.15.0:
version "0.15.0"
resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.0.tgz#cbd9ead5d77fae1a48d957cf889ad0586adb6537"
@@ -18773,6 +18917,18 @@ tar@^6.0.2:
mkdirp "^1.0.3"
yallist "^4.0.0"
+tar@^6.1.11:
+ version "6.2.0"
+ resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.0.tgz#b14ce49a79cb1cd23bc9b016302dea5474493f73"
+ integrity sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==
+ dependencies:
+ chownr "^2.0.0"
+ fs-minipass "^2.0.0"
+ minipass "^5.0.0"
+ minizlib "^2.1.1"
+ mkdirp "^1.0.3"
+ yallist "^4.0.0"
+
telejson@^6.0.8:
version "6.0.8"
resolved "https://registry.yarnpkg.com/telejson/-/telejson-6.0.8.tgz#1c432db7e7a9212c1fbd941c3e5174ec385148f7"