mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-09 07:25:19 +02:00
add middleware check, refactor axios provider
This commit is contained in:
parent
e740fcb497
commit
633765d4b0
23 changed files with 220 additions and 196 deletions
|
@ -16,7 +16,7 @@ NX_AUTH0_CLIENT_SECRET=
|
|||
AUTH0_DEPLOY_CLIENT_SECRET=
|
||||
POSTMARK_SMTP_PASS=
|
||||
NX_SESSION_SECRET=
|
||||
AUTH_SECRET=
|
||||
NEXTAUTH_SECRET=
|
||||
|
||||
# If you want to test any code that utilizes the AWS SDK locally, add temporary STAGING credentials (can be retrieved from SSO dashboard)
|
||||
AWS_ACCESS_KEY_ID=
|
||||
|
|
|
@ -2,11 +2,8 @@ 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'
|
||||
import { type PropsWithChildren, useReducer } from 'react'
|
||||
|
||||
function reducer(
|
||||
state: Record<ModalKey, { isOpen: boolean; props: any }>,
|
||||
|
@ -24,42 +21,11 @@ function reducer(
|
|||
* 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, {
|
||||
const [, 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 (
|
||||
<ModalManagerContext.Provider value={{ dispatch }}>
|
||||
{children}
|
||||
|
||||
<LinkAccountFlow
|
||||
isOpen={state.linkAuth0Accounts.isOpen}
|
||||
onClose={() => {
|
||||
dispatch({ type: 'close', key: 'linkAuth0Accounts' })
|
||||
setAccountLinkHidden(true)
|
||||
}}
|
||||
{...state.linkAuth0Accounts.props}
|
||||
/>
|
||||
</ModalManagerContext.Provider>
|
||||
<ModalManagerContext.Provider value={{ dispatch }}>{children}</ModalManagerContext.Provider>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,13 +1,35 @@
|
|||
import NextAuth, { type SessionStrategy } from 'next-auth'
|
||||
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 } from '@prisma/client'
|
||||
import { PrismaClient, type Prisma } from '@prisma/client'
|
||||
import { PrismaAdapter } from '@auth/prisma-adapter'
|
||||
import axios from 'axios'
|
||||
import bcrypt from 'bcrypt'
|
||||
|
||||
const prisma = new PrismaClient()
|
||||
axios.defaults.baseURL = `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3333'}/v1`
|
||||
let prismaInstance: PrismaClient | null = null
|
||||
|
||||
function getPrismaInstance() {
|
||||
if (!prismaInstance) {
|
||||
prismaInstance = new PrismaClient()
|
||||
}
|
||||
return prismaInstance
|
||||
}
|
||||
|
||||
const prisma = getPrismaInstance()
|
||||
|
||||
async function createAuthUser(
|
||||
data: Prisma.AuthUserCreateInput & { firstName: string; lastName: string }
|
||||
) {
|
||||
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 },
|
||||
})
|
||||
}
|
||||
|
||||
const authPrisma = {
|
||||
account: prisma.authAccount,
|
||||
|
@ -18,62 +40,82 @@ const authPrisma = {
|
|||
|
||||
export const authOptions = {
|
||||
adapter: PrismaAdapter(authPrisma),
|
||||
secret: process.env.AUTH_SECRET || 'CHANGE_ME',
|
||||
secret: process.env.NEXTAUTH_SECRET || 'CHANGE_ME',
|
||||
pages: {
|
||||
signIn: '/login',
|
||||
},
|
||||
session: {
|
||||
strategy: 'jwt' as SessionStrategy,
|
||||
maxAge: 7 * 24 * 60 * 60, // 7 Days
|
||||
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 parsedCredentials = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
firstName: z.string().optional(),
|
||||
lastName: z.string().optional(),
|
||||
email: z.string().email(),
|
||||
password: z.string().min(6),
|
||||
})
|
||||
.safeParse(credentials)
|
||||
|
||||
if (parsedCredentials.success) {
|
||||
const { name, email, password } = parsedCredentials.data
|
||||
const { firstName, lastName, email, password } = parsedCredentials.data
|
||||
|
||||
const { data } = await axios.get(`/auth-users`, {
|
||||
params: { email: email },
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
const authUser = await getAuthUserByEmail(email)
|
||||
|
||||
// TODO: use superjson to parse this more cleanly
|
||||
const user = data.data['json']
|
||||
|
||||
if (!user) {
|
||||
if (!authUser) {
|
||||
if (!firstName || !lastName) throw new Error('First and last name required')
|
||||
const hashedPassword = await bcrypt.hash(password, 10)
|
||||
const { data } = await axios.post('/auth-users', {
|
||||
name,
|
||||
const newAuthUser = await createAuthUser({
|
||||
firstName,
|
||||
lastName,
|
||||
name: `${firstName} ${lastName}`,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
})
|
||||
const newUser = data.data['json']
|
||||
if (newUser) return newUser
|
||||
|
||||
if (newAuthUser) return newAuthUser
|
||||
throw new Error('Could not create user')
|
||||
}
|
||||
|
||||
const passwordsMatch = await bcrypt.compare(password, user.password)
|
||||
if (passwordsMatch) return user
|
||||
const passwordsMatch = await bcrypt.compare(password, authUser.password!)
|
||||
if (passwordsMatch) return authUser
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
}),
|
||||
],
|
||||
}
|
||||
callbacks: {
|
||||
async jwt({ token, user: authUser, account }: { token: any; user: any; account: any }) {
|
||||
if (authUser && account) {
|
||||
token.sub = authUser.id
|
||||
token['https://maybe.co/email'] = authUser.email
|
||||
token.firstName = authUser.firstName
|
||||
token.lastName = authUser.lastName
|
||||
}
|
||||
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
|
||||
return session
|
||||
},
|
||||
},
|
||||
} as NextAuthOptions
|
||||
|
||||
export default NextAuth(authOptions)
|
||||
|
|
|
@ -38,7 +38,7 @@ export default function LoginPage() {
|
|||
strategy="lazyOnload"
|
||||
/>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
||||
<div className="p-px w-96 bg-white bg-opacity-10 card-light rounded-3xl radial-gradient-background">
|
||||
<div className="p-px w-80 md:w-96 bg-white bg-opacity-10 card-light rounded-3xl radial-gradient-background">
|
||||
<div className="bg-black bg-opacity-75 p-8 rounded-3xl w-full h-full items-center flex flex-col radial-gradient-background-dark">
|
||||
<img
|
||||
className="mb-8"
|
||||
|
|
|
@ -8,7 +8,8 @@ import Script from 'next/script'
|
|||
import Link from 'next/link'
|
||||
|
||||
export default function RegisterPage() {
|
||||
const [name, setName] = useState('')
|
||||
const [firstName, setFirstName] = useState('')
|
||||
const [lastName, setLastName] = useState('')
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [isValid, setIsValid] = useState(false)
|
||||
|
@ -23,14 +24,16 @@ export default function RegisterPage() {
|
|||
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
|
||||
setName('')
|
||||
setFirstName('')
|
||||
setLastName('')
|
||||
setEmail('')
|
||||
setPassword('')
|
||||
|
||||
await signIn('credentials', {
|
||||
email,
|
||||
password,
|
||||
name,
|
||||
firstName,
|
||||
lastName,
|
||||
redirect: false,
|
||||
})
|
||||
}
|
||||
|
@ -55,9 +58,15 @@ export default function RegisterPage() {
|
|||
<form className="space-y-4 w-full px-4" onSubmit={onSubmit}>
|
||||
<Input
|
||||
type="text"
|
||||
label="Name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.currentTarget.value)}
|
||||
label="First name"
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.currentTarget.value)}
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
label="Last name"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.currentTarget.value)}
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
|
|
|
@ -19,7 +19,7 @@ import logger from './lib/logger'
|
|||
import prisma from './lib/prisma'
|
||||
import {
|
||||
defaultErrorHandler,
|
||||
validateAuth0Jwt,
|
||||
validateAuthJwt,
|
||||
superjson,
|
||||
authErrorHandler,
|
||||
maintenance,
|
||||
|
@ -28,7 +28,6 @@ import {
|
|||
} from './middleware'
|
||||
import {
|
||||
usersRouter,
|
||||
authUserRouter,
|
||||
accountsRouter,
|
||||
connectionsRouter,
|
||||
adminRouter,
|
||||
|
@ -89,7 +88,7 @@ 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')
|
||||
|
@ -117,7 +116,7 @@ app.use(express.json({ limit: '50mb' })) // Finicity sends large response bodies
|
|||
|
||||
app.use(
|
||||
'/trpc',
|
||||
validateAuth0Jwt,
|
||||
validateAuthJwt,
|
||||
trpcExpress.createExpressMiddleware({
|
||||
router: appRouter,
|
||||
createContext: createTRPCContext,
|
||||
|
@ -149,10 +148,9 @@ app.use('/tools', devOnly, toolsRouter)
|
|||
app.use('/v1', webhooksRouter)
|
||||
|
||||
app.use('/v1', publicRouter)
|
||||
app.use('/v1/auth-users', authUserRouter)
|
||||
|
||||
// All routes AFTER this line are protected via OAuth
|
||||
app.use('/v1', validateAuth0Jwt)
|
||||
app.use('/v1', validateAuthJwt)
|
||||
|
||||
// Private routes
|
||||
app.use('/v1/users', usersRouter)
|
||||
|
|
|
@ -283,23 +283,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<Request['user']>) {
|
||||
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: {},
|
||||
}))
|
||||
|
|
|
@ -3,6 +3,7 @@ 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'
|
||||
|
|
30
apps/server/src/app/middleware/validate-auth-jwt.ts
Normal file
30
apps/server/src/app/middleware/validate-auth-jwt.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import cookieParser from 'cookie-parser'
|
||||
import { getToken } from 'next-auth/jwt'
|
||||
|
||||
const SECRET = process.env.NEXTAUTH_SECRET
|
||||
|
||||
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 getToken({ req, 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' })
|
||||
}
|
||||
})
|
||||
}
|
|
@ -1,37 +1,6 @@
|
|||
import { Router } from 'express'
|
||||
import { z } from 'zod'
|
||||
import endpoint from '../lib/endpoint'
|
||||
|
||||
// Placeholder router if needed
|
||||
const router = Router()
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
endpoint.create({
|
||||
resolve: async ({ ctx, req }) => {
|
||||
const email = req.query.email
|
||||
const user = await ctx.authUserService.getByEmail(email as string)
|
||||
if (user) return user
|
||||
return null
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
endpoint.create({
|
||||
input: z.object({
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
password: z.string().min(6),
|
||||
}),
|
||||
resolve: async ({ input, ctx }) => {
|
||||
return await ctx.authUserService.create({
|
||||
name: input.name,
|
||||
email: input.email,
|
||||
password: input.password,
|
||||
})
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
export default router
|
||||
|
|
|
@ -47,13 +47,12 @@ 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'),
|
||||
|
|
|
@ -105,6 +105,7 @@ router.put(
|
|||
})
|
||||
)
|
||||
|
||||
// TODO: Remove this endpoint
|
||||
router.get(
|
||||
'/auth0-profile',
|
||||
endpoint.create({
|
||||
|
@ -122,7 +123,7 @@ router.put(
|
|||
}),
|
||||
resolve: ({ input, ctx }) => {
|
||||
return ctx.managementClient.updateUser(
|
||||
{ id: ctx.user!.auth0Id },
|
||||
{ id: ctx.user!.authId }, // TODO: Remove this endpoint
|
||||
{ user_metadata: { enrolled_mfa: input.enrolled_mfa } }
|
||||
)
|
||||
},
|
||||
|
@ -276,6 +277,7 @@ router.get(
|
|||
})
|
||||
)
|
||||
|
||||
// TODO: Remove this endpoint or refactor to work with new Auth
|
||||
router.post(
|
||||
'/link-accounts',
|
||||
endpoint.create({
|
||||
|
@ -284,7 +286,7 @@ router.post(
|
|||
secondaryProvider: z.string(),
|
||||
}),
|
||||
resolve: async ({ input, ctx }) => {
|
||||
return ctx.userService.linkAccounts(ctx.user!.auth0Id, input.secondaryProvider, {
|
||||
return ctx.userService.linkAccounts(ctx.user!.authId, input.secondaryProvider, {
|
||||
token: input.secondaryJWT,
|
||||
domain: env.NX_AUTH0_CUSTOM_DOMAIN,
|
||||
audience: env.NX_AUTH0_AUDIENCE,
|
||||
|
@ -293,6 +295,7 @@ router.post(
|
|||
})
|
||||
)
|
||||
|
||||
// TODO: Remove this endpoint or refactor to work with new Auth
|
||||
router.post(
|
||||
'/unlink-account',
|
||||
endpoint.create({
|
||||
|
@ -302,7 +305,7 @@ router.post(
|
|||
}),
|
||||
resolve: async ({ input, ctx }) => {
|
||||
return ctx.userService.unlinkAccounts(
|
||||
ctx.user!.auth0Id,
|
||||
ctx.user!.authId,
|
||||
input.secondaryAuth0Id,
|
||||
input.secondaryProvider as UnlinkAccountsParamsProvider
|
||||
)
|
||||
|
@ -310,19 +313,20 @@ router.post(
|
|||
})
|
||||
)
|
||||
|
||||
// TODO: Refactor this to use the Auth Id 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 }
|
||||
},
|
||||
|
|
|
@ -82,7 +82,7 @@ export class BullQueueEventHandler implements IBullQueueEventHandler {
|
|||
}
|
||||
|
||||
private async getUserFromJob(job: Job) {
|
||||
let user: Pick<User, 'id' | 'auth0Id'> | undefined
|
||||
let user: Pick<User, 'id' | 'authId'> | undefined
|
||||
|
||||
try {
|
||||
if (job.queue.name === 'sync-account' && 'accountId' in job.data) {
|
||||
|
|
|
@ -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<string | null>
|
||||
|
@ -15,7 +12,6 @@ type CreateInstanceOptions = {
|
|||
}
|
||||
|
||||
export type AxiosContextValue = {
|
||||
getToken: () => Promise<string | null>
|
||||
defaultBaseUrl: string
|
||||
axios: AxiosInstance
|
||||
createInstance: (options?: CreateInstanceOptions) => AxiosInstance
|
||||
|
@ -78,56 +74,28 @@ function createInstance(options?: CreateInstanceOptions) {
|
|||
*/
|
||||
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 (
|
||||
<AxiosContext.Provider
|
||||
value={{
|
||||
getToken,
|
||||
defaultBaseUrl: `${API_URL}/v1`,
|
||||
axios: defaultInstance,
|
||||
createInstance,
|
||||
|
|
|
@ -22,9 +22,9 @@ export class AuthUserService implements IAuthUserService {
|
|||
})
|
||||
}
|
||||
|
||||
async create(data: Prisma.AuthUserCreateInput) {
|
||||
const user = await this.prisma.authUser.create({ data: { ...data } })
|
||||
return user
|
||||
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']) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -65,12 +65,14 @@ export class UserService implements IUserService {
|
|||
})
|
||||
}
|
||||
|
||||
// TODO: Update this to use new Auth
|
||||
async getAuth0Profile(user: User): Promise<SharedType.Auth0Profile> {
|
||||
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)
|
||||
// TODO: Update this to use new Auth
|
||||
const currentUser = usersWithMatchingEmail.find((u) => u.user_id === user.authId)
|
||||
const primaryIdentity = currentUser?.identities?.find(
|
||||
(identity) => !('profileData' in identity)
|
||||
)
|
||||
|
@ -85,7 +87,7 @@ export class UserService implements IUserService {
|
|||
.filter(
|
||||
(match) =>
|
||||
match.email_verified &&
|
||||
match.user_id !== user.auth0Id &&
|
||||
match.user_id !== user.authId &&
|
||||
match.identities?.at(0) != null
|
||||
)
|
||||
.map((user) => user.identities!.at(0)!)
|
||||
|
@ -157,9 +159,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 })
|
||||
|
||||
|
@ -312,7 +315,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,
|
||||
|
@ -327,10 +330,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
|
||||
|
@ -341,8 +346,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
|
||||
)
|
||||
|
@ -543,8 +548,9 @@ export class UserService implements IUserService {
|
|||
return onboarding
|
||||
}
|
||||
|
||||
// TODO: Update to work with new Auth
|
||||
async linkAccounts(
|
||||
primaryAuth0Id: User['auth0Id'],
|
||||
primaryAuth0Id: User['authId'],
|
||||
provider: string,
|
||||
secondaryJWT: { token: string; domain: string; audience: string }
|
||||
) {
|
||||
|
@ -554,7 +560,7 @@ export class UserService implements IUserService {
|
|||
secondaryJWT.audience
|
||||
)
|
||||
|
||||
const user = await this.prisma.user.findFirst({ where: { auth0Id: validatedJWT.auth0Id } })
|
||||
const user = await this.prisma.user.findFirst({ where: { authId: validatedJWT.authId } })
|
||||
|
||||
if (user?.stripePriceId) {
|
||||
throw new Error(
|
||||
|
@ -563,14 +569,14 @@ export class UserService implements IUserService {
|
|||
}
|
||||
|
||||
return this.auth0.linkUsers(primaryAuth0Id, {
|
||||
user_id: validatedJWT.auth0Id,
|
||||
user_id: validatedJWT.authId,
|
||||
provider,
|
||||
})
|
||||
}
|
||||
|
||||
async unlinkAccounts(
|
||||
primaryAuth0Id: User['auth0Id'],
|
||||
secondaryAuth0Id: User['auth0Id'],
|
||||
primaryAuth0Id: User['authId'],
|
||||
secondaryAuth0Id: User['authId'],
|
||||
secondaryProvider: UnlinkAccountsParamsProvider
|
||||
) {
|
||||
const response = await this.auth0.unlinkUsers({
|
||||
|
|
|
@ -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] ?? {},
|
||||
})
|
||||
|
|
|
@ -102,6 +102,7 @@
|
|||
"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",
|
||||
|
@ -203,6 +204,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",
|
||||
|
|
|
@ -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";
|
|
@ -0,0 +1,3 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "auth_user" ADD COLUMN "first_name" TEXT,
|
||||
ADD COLUMN "last_name" TEXT;
|
|
@ -395,7 +395,6 @@ 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
|
||||
|
@ -585,6 +584,8 @@ model AuthAccount {
|
|||
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?
|
||||
|
|
25
yarn.lock
25
yarn.lock
|
@ -5724,6 +5724,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"
|
||||
|
@ -9412,11 +9419,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"
|
||||
|
@ -9432,11 +9452,6 @@ cookie@0.6.0:
|
|||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
|
||||
integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
|
||||
|
||||
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==
|
||||
|
||||
cookiejar@^2.1.2:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.3.tgz#fc7a6216e408e74414b90230050842dacda75acc"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue