mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-08 23:15:24 +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=
|
AUTH0_DEPLOY_CLIENT_SECRET=
|
||||||
POSTMARK_SMTP_PASS=
|
POSTMARK_SMTP_PASS=
|
||||||
NX_SESSION_SECRET=
|
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)
|
# 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=
|
AWS_ACCESS_KEY_ID=
|
||||||
|
|
|
@ -2,11 +2,8 @@ import {
|
||||||
type ModalKey,
|
type ModalKey,
|
||||||
type ModalManagerAction,
|
type ModalManagerAction,
|
||||||
ModalManagerContext,
|
ModalManagerContext,
|
||||||
useUserApi,
|
|
||||||
useLocalStorage,
|
|
||||||
} from '@maybe-finance/client/shared'
|
} from '@maybe-finance/client/shared'
|
||||||
import { type PropsWithChildren, useReducer, useEffect } from 'react'
|
import { type PropsWithChildren, useReducer } from 'react'
|
||||||
import { LinkAccountFlow } from '@maybe-finance/client/features'
|
|
||||||
|
|
||||||
function reducer(
|
function reducer(
|
||||||
state: Record<ModalKey, { isOpen: boolean; props: any }>,
|
state: Record<ModalKey, { isOpen: boolean; props: any }>,
|
||||||
|
@ -24,42 +21,11 @@ function reducer(
|
||||||
* Manages auto-prompt modals and regular modals to avoid stacking collisions
|
* Manages auto-prompt modals and regular modals to avoid stacking collisions
|
||||||
*/
|
*/
|
||||||
export default function ModalManager({ children }: PropsWithChildren) {
|
export default function ModalManager({ children }: PropsWithChildren) {
|
||||||
const [accountLinkHidden, setAccountLinkHidden] = useLocalStorage('account-link-hidden', false)
|
const [, dispatch] = useReducer(reducer, {
|
||||||
|
|
||||||
const [state, dispatch] = useReducer(reducer, {
|
|
||||||
linkAuth0Accounts: { isOpen: false, props: null },
|
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 (
|
return (
|
||||||
<ModalManagerContext.Provider value={{ dispatch }}>
|
<ModalManagerContext.Provider value={{ dispatch }}>{children}</ModalManagerContext.Provider>
|
||||||
{children}
|
|
||||||
|
|
||||||
<LinkAccountFlow
|
|
||||||
isOpen={state.linkAuth0Accounts.isOpen}
|
|
||||||
onClose={() => {
|
|
||||||
dispatch({ type: 'close', key: 'linkAuth0Accounts' })
|
|
||||||
setAccountLinkHidden(true)
|
|
||||||
}}
|
|
||||||
{...state.linkAuth0Accounts.props}
|
|
||||||
/>
|
|
||||||
</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 CredentialsProvider from 'next-auth/providers/credentials'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { PrismaClient } from '@prisma/client'
|
import { PrismaClient, type Prisma } from '@prisma/client'
|
||||||
import { PrismaAdapter } from '@auth/prisma-adapter'
|
import { PrismaAdapter } from '@auth/prisma-adapter'
|
||||||
import axios from 'axios'
|
|
||||||
import bcrypt from 'bcrypt'
|
import bcrypt from 'bcrypt'
|
||||||
|
|
||||||
const prisma = new PrismaClient()
|
let prismaInstance: PrismaClient | null = null
|
||||||
axios.defaults.baseURL = `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3333'}/v1`
|
|
||||||
|
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 = {
|
const authPrisma = {
|
||||||
account: prisma.authAccount,
|
account: prisma.authAccount,
|
||||||
|
@ -18,62 +40,82 @@ const authPrisma = {
|
||||||
|
|
||||||
export const authOptions = {
|
export const authOptions = {
|
||||||
adapter: PrismaAdapter(authPrisma),
|
adapter: PrismaAdapter(authPrisma),
|
||||||
secret: process.env.AUTH_SECRET || 'CHANGE_ME',
|
secret: process.env.NEXTAUTH_SECRET || 'CHANGE_ME',
|
||||||
pages: {
|
pages: {
|
||||||
signIn: '/login',
|
signIn: '/login',
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
strategy: 'jwt' as SessionStrategy,
|
strategy: 'jwt' as SessionStrategy,
|
||||||
maxAge: 7 * 24 * 60 * 60, // 7 Days
|
maxAge: 1 * 24 * 60 * 60, // 1 Day
|
||||||
},
|
},
|
||||||
|
|
||||||
providers: [
|
providers: [
|
||||||
CredentialsProvider({
|
CredentialsProvider({
|
||||||
name: 'Credentials',
|
name: 'Credentials',
|
||||||
type: 'credentials',
|
type: 'credentials',
|
||||||
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' },
|
email: { label: 'Email', type: 'email', placeholder: 'hello@maybe.co' },
|
||||||
password: { label: 'Password', type: 'password' },
|
password: { label: 'Password', type: 'password' },
|
||||||
},
|
},
|
||||||
async authorize(credentials) {
|
async authorize(credentials) {
|
||||||
const parsedCredentials = z
|
const parsedCredentials = z
|
||||||
.object({
|
.object({
|
||||||
name: z.string().optional(),
|
firstName: z.string().optional(),
|
||||||
|
lastName: z.string().optional(),
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
password: z.string().min(6),
|
password: z.string().min(6),
|
||||||
})
|
})
|
||||||
.safeParse(credentials)
|
.safeParse(credentials)
|
||||||
|
|
||||||
if (parsedCredentials.success) {
|
if (parsedCredentials.success) {
|
||||||
const { name, email, password } = parsedCredentials.data
|
const { firstName, lastName, email, password } = parsedCredentials.data
|
||||||
|
|
||||||
const { data } = await axios.get(`/auth-users`, {
|
const authUser = await getAuthUserByEmail(email)
|
||||||
params: { email: email },
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO: use superjson to parse this more cleanly
|
if (!authUser) {
|
||||||
const user = data.data['json']
|
if (!firstName || !lastName) throw new Error('First and last name required')
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
const hashedPassword = await bcrypt.hash(password, 10)
|
const hashedPassword = await bcrypt.hash(password, 10)
|
||||||
const { data } = await axios.post('/auth-users', {
|
const newAuthUser = await createAuthUser({
|
||||||
name,
|
firstName,
|
||||||
|
lastName,
|
||||||
|
name: `${firstName} ${lastName}`,
|
||||||
email,
|
email,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
})
|
})
|
||||||
const newUser = data.data['json']
|
|
||||||
if (newUser) return newUser
|
if (newAuthUser) return newAuthUser
|
||||||
throw new Error('Could not create user')
|
throw new Error('Could not create user')
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwordsMatch = await bcrypt.compare(password, user.password)
|
const passwordsMatch = await bcrypt.compare(password, authUser.password!)
|
||||||
if (passwordsMatch) return user
|
if (passwordsMatch) return authUser
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
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)
|
export default NextAuth(authOptions)
|
||||||
|
|
|
@ -38,7 +38,7 @@ export default function LoginPage() {
|
||||||
strategy="lazyOnload"
|
strategy="lazyOnload"
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
<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">
|
<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
|
<img
|
||||||
className="mb-8"
|
className="mb-8"
|
||||||
|
|
|
@ -8,7 +8,8 @@ import Script from 'next/script'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
export default function RegisterPage() {
|
export default function RegisterPage() {
|
||||||
const [name, setName] = useState('')
|
const [firstName, setFirstName] = useState('')
|
||||||
|
const [lastName, setLastName] = useState('')
|
||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
const [isValid, setIsValid] = useState(false)
|
const [isValid, setIsValid] = useState(false)
|
||||||
|
@ -23,14 +24,16 @@ export default function RegisterPage() {
|
||||||
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|
||||||
setName('')
|
setFirstName('')
|
||||||
|
setLastName('')
|
||||||
setEmail('')
|
setEmail('')
|
||||||
setPassword('')
|
setPassword('')
|
||||||
|
|
||||||
await signIn('credentials', {
|
await signIn('credentials', {
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
name,
|
firstName,
|
||||||
|
lastName,
|
||||||
redirect: false,
|
redirect: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -55,9 +58,15 @@ export default function RegisterPage() {
|
||||||
<form className="space-y-4 w-full px-4" onSubmit={onSubmit}>
|
<form className="space-y-4 w-full px-4" onSubmit={onSubmit}>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
label="Name"
|
label="First name"
|
||||||
value={name}
|
value={firstName}
|
||||||
onChange={(e) => setName(e.currentTarget.value)}
|
onChange={(e) => setFirstName(e.currentTarget.value)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
label="Last name"
|
||||||
|
value={lastName}
|
||||||
|
onChange={(e) => setLastName(e.currentTarget.value)}
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
|
|
|
@ -19,7 +19,7 @@ import logger from './lib/logger'
|
||||||
import prisma from './lib/prisma'
|
import prisma from './lib/prisma'
|
||||||
import {
|
import {
|
||||||
defaultErrorHandler,
|
defaultErrorHandler,
|
||||||
validateAuth0Jwt,
|
validateAuthJwt,
|
||||||
superjson,
|
superjson,
|
||||||
authErrorHandler,
|
authErrorHandler,
|
||||||
maintenance,
|
maintenance,
|
||||||
|
@ -28,7 +28,6 @@ import {
|
||||||
} from './middleware'
|
} from './middleware'
|
||||||
import {
|
import {
|
||||||
usersRouter,
|
usersRouter,
|
||||||
authUserRouter,
|
|
||||||
accountsRouter,
|
accountsRouter,
|
||||||
connectionsRouter,
|
connectionsRouter,
|
||||||
adminRouter,
|
adminRouter,
|
||||||
|
@ -89,7 +88,7 @@ app.use(express.static(__dirname + '/assets'))
|
||||||
|
|
||||||
const origin = [env.NX_CLIENT_URL, ...env.NX_CORS_ORIGINS]
|
const origin = [env.NX_CLIENT_URL, ...env.NX_CORS_ORIGINS]
|
||||||
logger.info(`CORS origins: ${origin}`)
|
logger.info(`CORS origins: ${origin}`)
|
||||||
app.use(cors({ origin }))
|
app.use(cors({ origin, credentials: true }))
|
||||||
app.options('*', cors() as RequestHandler)
|
app.options('*', cors() as RequestHandler)
|
||||||
|
|
||||||
app.set('view engine', 'ejs').set('views', __dirname + '/app/admin/views')
|
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(
|
app.use(
|
||||||
'/trpc',
|
'/trpc',
|
||||||
validateAuth0Jwt,
|
validateAuthJwt,
|
||||||
trpcExpress.createExpressMiddleware({
|
trpcExpress.createExpressMiddleware({
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
createContext: createTRPCContext,
|
createContext: createTRPCContext,
|
||||||
|
@ -149,10 +148,9 @@ app.use('/tools', devOnly, toolsRouter)
|
||||||
app.use('/v1', webhooksRouter)
|
app.use('/v1', webhooksRouter)
|
||||||
|
|
||||||
app.use('/v1', publicRouter)
|
app.use('/v1', publicRouter)
|
||||||
app.use('/v1/auth-users', authUserRouter)
|
|
||||||
|
|
||||||
// All routes AFTER this line are protected via OAuth
|
// All routes AFTER this line are protected via OAuth
|
||||||
app.use('/v1', validateAuth0Jwt)
|
app.use('/v1', validateAuthJwt)
|
||||||
|
|
||||||
// Private routes
|
// Private routes
|
||||||
app.use('/v1/users', usersRouter)
|
app.use('/v1/users', usersRouter)
|
||||||
|
|
|
@ -283,23 +283,23 @@ const stripeWebhooks = new StripeWebhookHandler(
|
||||||
)
|
)
|
||||||
|
|
||||||
// helper function for parsing JWT and loading User record
|
// helper function for parsing JWT and loading User record
|
||||||
|
// TODO: update this with roles, identity, and metadata
|
||||||
async function getCurrentUser(jwt: NonNullable<Request['user']>) {
|
async function getCurrentUser(jwt: NonNullable<Request['user']>) {
|
||||||
if (!jwt.sub) throw new Error(`jwt missing sub`)
|
if (!jwt.sub) throw new Error(`jwt missing sub`)
|
||||||
if (!jwt['https://maybe.co/email']) throw new Error(`jwt missing email`)
|
if (!jwt['https://maybe.co/email']) throw new Error(`jwt missing email`)
|
||||||
|
|
||||||
const user =
|
const user =
|
||||||
(await prisma.user.findUnique({
|
(await prisma.user.findUnique({
|
||||||
where: { auth0Id: jwt.sub },
|
where: { authId: jwt.sub },
|
||||||
})) ??
|
})) ??
|
||||||
(await prisma.user.upsert({
|
(await prisma.user.upsert({
|
||||||
where: { auth0Id: jwt.sub },
|
where: { authId: jwt.sub },
|
||||||
create: {
|
create: {
|
||||||
auth0Id: jwt.sub,
|
|
||||||
authId: jwt.sub,
|
authId: jwt.sub,
|
||||||
email: jwt['https://maybe.co/email'],
|
email: jwt['https://maybe.co/email'],
|
||||||
picture: jwt[SharedType.Auth0CustomNamespace.Picture],
|
picture: jwt['picture'],
|
||||||
firstName: jwt[SharedType.Auth0CustomNamespace.UserMetadata]?.['firstName'],
|
firstName: jwt['firstName'],
|
||||||
lastName: jwt[SharedType.Auth0CustomNamespace.UserMetadata]?.['lastName'],
|
lastName: jwt['lastName'],
|
||||||
},
|
},
|
||||||
update: {},
|
update: {},
|
||||||
}))
|
}))
|
||||||
|
|
|
@ -3,6 +3,7 @@ export * from './error-handler'
|
||||||
export * from './auth-error-handler'
|
export * from './auth-error-handler'
|
||||||
export * from './superjson'
|
export * from './superjson'
|
||||||
export * from './validate-auth0-jwt'
|
export * from './validate-auth0-jwt'
|
||||||
|
export * from './validate-auth-jwt'
|
||||||
export * from './validate-plaid-jwt'
|
export * from './validate-plaid-jwt'
|
||||||
export * from './validate-finicity-signature'
|
export * from './validate-finicity-signature'
|
||||||
export { default as maintenance } from './maintenance'
|
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 { Router } from 'express'
|
||||||
import { z } from 'zod'
|
|
||||||
import endpoint from '../lib/endpoint'
|
|
||||||
|
|
||||||
|
// Placeholder router if needed
|
||||||
const router = Router()
|
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
|
export default router
|
||||||
|
|
|
@ -47,13 +47,12 @@ router.post(
|
||||||
trialLapsed: z.boolean().default(false),
|
trialLapsed: z.boolean().default(false),
|
||||||
}),
|
}),
|
||||||
resolve: async ({ ctx, input }) => {
|
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([
|
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({
|
ctx.prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
auth0Id: ctx.user!.auth0Id,
|
|
||||||
authId: ctx.user!.authId,
|
authId: ctx.user!.authId,
|
||||||
email: 'REPLACE_THIS',
|
email: 'REPLACE_THIS',
|
||||||
dob: new Date('1990-01-01'),
|
dob: new Date('1990-01-01'),
|
||||||
|
|
|
@ -105,6 +105,7 @@ router.put(
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: Remove this endpoint
|
||||||
router.get(
|
router.get(
|
||||||
'/auth0-profile',
|
'/auth0-profile',
|
||||||
endpoint.create({
|
endpoint.create({
|
||||||
|
@ -122,7 +123,7 @@ router.put(
|
||||||
}),
|
}),
|
||||||
resolve: ({ input, ctx }) => {
|
resolve: ({ input, ctx }) => {
|
||||||
return ctx.managementClient.updateUser(
|
return ctx.managementClient.updateUser(
|
||||||
{ id: ctx.user!.auth0Id },
|
{ id: ctx.user!.authId }, // TODO: Remove this endpoint
|
||||||
{ user_metadata: { enrolled_mfa: input.enrolled_mfa } }
|
{ 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(
|
router.post(
|
||||||
'/link-accounts',
|
'/link-accounts',
|
||||||
endpoint.create({
|
endpoint.create({
|
||||||
|
@ -284,7 +286,7 @@ router.post(
|
||||||
secondaryProvider: z.string(),
|
secondaryProvider: z.string(),
|
||||||
}),
|
}),
|
||||||
resolve: async ({ input, ctx }) => {
|
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,
|
token: input.secondaryJWT,
|
||||||
domain: env.NX_AUTH0_CUSTOM_DOMAIN,
|
domain: env.NX_AUTH0_CUSTOM_DOMAIN,
|
||||||
audience: env.NX_AUTH0_AUDIENCE,
|
audience: env.NX_AUTH0_AUDIENCE,
|
||||||
|
@ -293,6 +295,7 @@ router.post(
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: Remove this endpoint or refactor to work with new Auth
|
||||||
router.post(
|
router.post(
|
||||||
'/unlink-account',
|
'/unlink-account',
|
||||||
endpoint.create({
|
endpoint.create({
|
||||||
|
@ -302,7 +305,7 @@ router.post(
|
||||||
}),
|
}),
|
||||||
resolve: async ({ input, ctx }) => {
|
resolve: async ({ input, ctx }) => {
|
||||||
return ctx.userService.unlinkAccounts(
|
return ctx.userService.unlinkAccounts(
|
||||||
ctx.user!.auth0Id,
|
ctx.user!.authId,
|
||||||
input.secondaryAuth0Id,
|
input.secondaryAuth0Id,
|
||||||
input.secondaryProvider as UnlinkAccountsParamsProvider
|
input.secondaryProvider as UnlinkAccountsParamsProvider
|
||||||
)
|
)
|
||||||
|
@ -310,19 +313,20 @@ router.post(
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: Refactor this to use the Auth Id instead of Auth0
|
||||||
router.post(
|
router.post(
|
||||||
'/resend-verification-email',
|
'/resend-verification-email',
|
||||||
endpoint.create({
|
endpoint.create({
|
||||||
input: z.object({
|
input: z.object({
|
||||||
auth0Id: z.string().optional(),
|
authId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
resolve: async ({ input, ctx }) => {
|
resolve: async ({ input, ctx }) => {
|
||||||
const auth0Id = input.auth0Id ?? ctx.user?.auth0Id
|
const authId = input.authId ?? ctx.user?.authId
|
||||||
if (!auth0Id) throw new Error('User not found')
|
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 }
|
return { success: true }
|
||||||
},
|
},
|
||||||
|
|
|
@ -82,7 +82,7 @@ export class BullQueueEventHandler implements IBullQueueEventHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getUserFromJob(job: Job) {
|
private async getUserFromJob(job: Job) {
|
||||||
let user: Pick<User, 'id' | 'auth0Id'> | undefined
|
let user: Pick<User, 'id' | 'authId'> | undefined
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (job.queue.name === 'sync-account' && 'accountId' in job.data) {
|
if (job.queue.name === 'sync-account' && 'accountId' in job.data) {
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||||
import type { SharedType } from '@maybe-finance/shared'
|
import type { SharedType } from '@maybe-finance/shared'
|
||||||
import { superjson } from '@maybe-finance/shared'
|
import { superjson } from '@maybe-finance/shared'
|
||||||
import { createContext, type PropsWithChildren, useCallback, useMemo } from 'react'
|
import { createContext, type PropsWithChildren, useMemo } from 'react'
|
||||||
import { useAuth0 } from '@auth0/auth0-react'
|
|
||||||
import Axios from 'axios'
|
import Axios from 'axios'
|
||||||
import * as Sentry from '@sentry/react'
|
|
||||||
import { useRouter } from 'next/router'
|
|
||||||
|
|
||||||
type CreateInstanceOptions = {
|
type CreateInstanceOptions = {
|
||||||
getToken?: () => Promise<string | null>
|
getToken?: () => Promise<string | null>
|
||||||
|
@ -15,7 +12,6 @@ type CreateInstanceOptions = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AxiosContextValue = {
|
export type AxiosContextValue = {
|
||||||
getToken: () => Promise<string | null>
|
|
||||||
defaultBaseUrl: string
|
defaultBaseUrl: string
|
||||||
axios: AxiosInstance
|
axios: AxiosInstance
|
||||||
createInstance: (options?: CreateInstanceOptions) => AxiosInstance
|
createInstance: (options?: CreateInstanceOptions) => AxiosInstance
|
||||||
|
@ -78,56 +74,28 @@ function createInstance(options?: CreateInstanceOptions) {
|
||||||
*/
|
*/
|
||||||
export function AxiosProvider({ children }: PropsWithChildren) {
|
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
|
// 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 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
|
// Expose a default instance with auth, superjson, headers
|
||||||
const defaultInstance = useMemo(() => {
|
const defaultInstance = useMemo(() => {
|
||||||
const defaultHeaders = { 'Content-Type': 'application/json' }
|
const defaultHeaders = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Access-Control-Allow-Credentials': true,
|
||||||
|
}
|
||||||
return createInstance({
|
return createInstance({
|
||||||
getToken,
|
axiosOptions: {
|
||||||
axiosOptions: { baseURL: `${API_URL}/v1`, headers: defaultHeaders },
|
baseURL: `${API_URL}/v1`,
|
||||||
|
headers: defaultHeaders,
|
||||||
|
withCredentials: true,
|
||||||
|
},
|
||||||
serialize: true,
|
serialize: true,
|
||||||
deserialize: true,
|
deserialize: true,
|
||||||
})
|
})
|
||||||
}, [getToken, API_URL])
|
}, [API_URL])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AxiosContext.Provider
|
<AxiosContext.Provider
|
||||||
value={{
|
value={{
|
||||||
getToken,
|
|
||||||
defaultBaseUrl: `${API_URL}/v1`,
|
defaultBaseUrl: `${API_URL}/v1`,
|
||||||
axios: defaultInstance,
|
axios: defaultInstance,
|
||||||
createInstance,
|
createInstance,
|
||||||
|
|
|
@ -22,9 +22,9 @@ export class AuthUserService implements IAuthUserService {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(data: Prisma.AuthUserCreateInput) {
|
async create(data: Prisma.AuthUserCreateInput & { firstName: string; lastName: string }) {
|
||||||
const user = await this.prisma.authUser.create({ data: { ...data } })
|
const authUser = await this.prisma.authUser.create({ data: { ...data } })
|
||||||
return user
|
return authUser
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: AuthUser['id']) {
|
async delete(id: AuthUser['id']) {
|
||||||
|
|
|
@ -27,7 +27,7 @@ export class StripeWebhookHandler implements IStripeWebhookHandler {
|
||||||
|
|
||||||
await this.prisma.user.updateMany({
|
await this.prisma.user.updateMany({
|
||||||
where: {
|
where: {
|
||||||
auth0Id: session.client_reference_id,
|
authId: session.client_reference_id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
trialEnd: null,
|
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> {
|
async getAuth0Profile(user: User): Promise<SharedType.Auth0Profile> {
|
||||||
if (!user.email) throw new Error('No email found for user')
|
if (!user.email) throw new Error('No email found for user')
|
||||||
|
|
||||||
const usersWithMatchingEmail = await this.auth0.getUsersByEmail(user.email)
|
const usersWithMatchingEmail = await this.auth0.getUsersByEmail(user.email)
|
||||||
const autoPromptEnabled = user.linkAccountDismissedAt == null
|
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(
|
const primaryIdentity = currentUser?.identities?.find(
|
||||||
(identity) => !('profileData' in identity)
|
(identity) => !('profileData' in identity)
|
||||||
)
|
)
|
||||||
|
@ -85,7 +87,7 @@ export class UserService implements IUserService {
|
||||||
.filter(
|
.filter(
|
||||||
(match) =>
|
(match) =>
|
||||||
match.email_verified &&
|
match.email_verified &&
|
||||||
match.user_id !== user.auth0Id &&
|
match.user_id !== user.authId &&
|
||||||
match.identities?.at(0) != null
|
match.identities?.at(0) != null
|
||||||
)
|
)
|
||||||
.map((user) => user.identities!.at(0)!)
|
.map((user) => user.identities!.at(0)!)
|
||||||
|
@ -157,9 +159,10 @@ export class UserService implements IUserService {
|
||||||
// Delete Stripe customer, ending any active subscriptions
|
// Delete Stripe customer, ending any active subscriptions
|
||||||
if (user.stripeCustomerId) await this.stripe.customers.del(user.stripeCustomerId)
|
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
|
// Delete user from Auth so that it cannot be accessed in a partially-purged state
|
||||||
this.logger.info(`Removing user ${user.id} from Auth0 (${user.auth0Id})`)
|
// TODO: Update this to use new Auth
|
||||||
await this.auth0.deleteUser({ id: user.auth0Id })
|
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 })
|
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({
|
const user = await this.prisma.user.findUniqueOrThrow({
|
||||||
where: { id: userId },
|
where: { id: userId },
|
||||||
select: {
|
select: {
|
||||||
auth0Id: true,
|
authId: true,
|
||||||
onboarding: true,
|
onboarding: true,
|
||||||
dob: true,
|
dob: true,
|
||||||
household: 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
|
// NextAuth used DateTime for this field
|
||||||
const email_verified = auth0User.email_verified as unknown as string | boolean
|
const email_verified = authUser.emailVerified === null ? false : true
|
||||||
|
|
||||||
const typedOnboarding = user.onboarding as OnboardingState | null
|
const typedOnboarding = user.onboarding as OnboardingState | null
|
||||||
const onboardingState = typedOnboarding
|
const onboardingState = typedOnboarding
|
||||||
|
@ -341,8 +346,8 @@ export class UserService implements IUserService {
|
||||||
{
|
{
|
||||||
...user,
|
...user,
|
||||||
onboarding: onboardingState,
|
onboarding: onboardingState,
|
||||||
emailVerified: email_verified === true || email_verified === 'true',
|
emailVerified: email_verified,
|
||||||
isAppleIdentity: auth0User.identities?.[0].provider === 'apple',
|
isAppleIdentity: false,
|
||||||
},
|
},
|
||||||
onboardingState.markedComplete
|
onboardingState.markedComplete
|
||||||
)
|
)
|
||||||
|
@ -543,8 +548,9 @@ export class UserService implements IUserService {
|
||||||
return onboarding
|
return onboarding
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Update to work with new Auth
|
||||||
async linkAccounts(
|
async linkAccounts(
|
||||||
primaryAuth0Id: User['auth0Id'],
|
primaryAuth0Id: User['authId'],
|
||||||
provider: string,
|
provider: string,
|
||||||
secondaryJWT: { token: string; domain: string; audience: string }
|
secondaryJWT: { token: string; domain: string; audience: string }
|
||||||
) {
|
) {
|
||||||
|
@ -554,7 +560,7 @@ export class UserService implements IUserService {
|
||||||
secondaryJWT.audience
|
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) {
|
if (user?.stripePriceId) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
|
@ -563,14 +569,14 @@ export class UserService implements IUserService {
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.auth0.linkUsers(primaryAuth0Id, {
|
return this.auth0.linkUsers(primaryAuth0Id, {
|
||||||
user_id: validatedJWT.auth0Id,
|
user_id: validatedJWT.authId,
|
||||||
provider,
|
provider,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async unlinkAccounts(
|
async unlinkAccounts(
|
||||||
primaryAuth0Id: User['auth0Id'],
|
primaryAuth0Id: User['authId'],
|
||||||
secondaryAuth0Id: User['auth0Id'],
|
secondaryAuth0Id: User['authId'],
|
||||||
secondaryProvider: UnlinkAccountsParamsProvider
|
secondaryProvider: UnlinkAccountsParamsProvider
|
||||||
) {
|
) {
|
||||||
const response = await this.auth0.unlinkUsers({
|
const response = await this.auth0.unlinkUsers({
|
||||||
|
|
|
@ -13,7 +13,7 @@ export async function validateRS256JWT(
|
||||||
domain: string,
|
domain: string,
|
||||||
audience: string
|
audience: string
|
||||||
): Promise<{
|
): Promise<{
|
||||||
auth0Id: string
|
authId: string
|
||||||
userMetadata: SharedType.MaybeUserMetadata
|
userMetadata: SharedType.MaybeUserMetadata
|
||||||
appMetadata: SharedType.MaybeAppMetadata
|
appMetadata: SharedType.MaybeAppMetadata
|
||||||
}> {
|
}> {
|
||||||
|
@ -50,7 +50,7 @@ export async function validateRS256JWT(
|
||||||
if (typeof payload !== 'object') return reject('payload not an object')
|
if (typeof payload !== 'object') return reject('payload not an object')
|
||||||
|
|
||||||
resolve({
|
resolve({
|
||||||
auth0Id: payload.sub!,
|
authId: payload.sub!,
|
||||||
appMetadata: payload[SharedType.Auth0CustomNamespace.AppMetadata] ?? {},
|
appMetadata: payload[SharedType.Auth0CustomNamespace.AppMetadata] ?? {},
|
||||||
userMetadata: payload[SharedType.Auth0CustomNamespace.UserMetadata] ?? {},
|
userMetadata: payload[SharedType.Auth0CustomNamespace.UserMetadata] ?? {},
|
||||||
})
|
})
|
||||||
|
|
|
@ -102,6 +102,7 @@
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"bull": "^4.10.2",
|
"bull": "^4.10.2",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
|
@ -203,6 +204,7 @@
|
||||||
"@testing-library/react": "13.4.0",
|
"@testing-library/react": "13.4.0",
|
||||||
"@testing-library/user-event": "^13.2.1",
|
"@testing-library/user-event": "^13.2.1",
|
||||||
"@types/auth0": "^2.35.7",
|
"@types/auth0": "^2.35.7",
|
||||||
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/cors": "^2.8.12",
|
"@types/cors": "^2.8.12",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
"@types/d3-array": "^3.0.3",
|
"@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())
|
id Int @id @default(autoincrement())
|
||||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_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
|
authId String @unique @map("auth_id") // NextAuth user id
|
||||||
|
|
||||||
// profile
|
// profile
|
||||||
|
@ -585,6 +584,8 @@ model AuthAccount {
|
||||||
model AuthUser {
|
model AuthUser {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String?
|
name String?
|
||||||
|
firstName String? @map("first_name")
|
||||||
|
lastName String? @map("last_name")
|
||||||
email String? @unique
|
email String? @unique
|
||||||
emailVerified DateTime? @map("email_verified")
|
emailVerified DateTime? @map("email_verified")
|
||||||
password String?
|
password String?
|
||||||
|
|
25
yarn.lock
25
yarn.lock
|
@ -5724,6 +5724,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "^7.3.0"
|
"@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@*":
|
"@types/body-parser@*":
|
||||||
version "1.19.2"
|
version "1.19.2"
|
||||||
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0"
|
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:
|
dependencies:
|
||||||
safe-buffer "~5.1.1"
|
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:
|
cookie-signature@1.0.6:
|
||||||
version "1.0.6"
|
version "1.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
|
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
|
||||||
integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
|
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:
|
cookie@0.4.2:
|
||||||
version "0.4.2"
|
version "0.4.2"
|
||||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432"
|
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"
|
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
|
||||||
integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
|
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:
|
cookiejar@^2.1.2:
|
||||||
version "2.1.3"
|
version "2.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.3.tgz#fc7a6216e408e74414b90230050842dacda75acc"
|
resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.3.tgz#fc7a6216e408e74414b90230050842dacda75acc"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue