setName(e.currentTarget.value)}
+ label="First name"
+ value={firstName}
+ onChange={(e) => setFirstName(e.currentTarget.value)}
+ />
+ setLastName(e.currentTarget.value)}
/>
) {
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: {},
}))
diff --git a/apps/server/src/app/middleware/index.ts b/apps/server/src/app/middleware/index.ts
index 8e60cf38..28b008a7 100644
--- a/apps/server/src/app/middleware/index.ts
+++ b/apps/server/src/app/middleware/index.ts
@@ -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'
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..8eebe1c1
--- /dev/null
+++ b/apps/server/src/app/middleware/validate-auth-jwt.ts
@@ -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' })
+ }
+ })
+}
diff --git a/apps/server/src/app/routes/auth-user.router.ts b/apps/server/src/app/routes/auth-user.router.ts
index a1d8a071..030e92dc 100644
--- a/apps/server/src/app/routes/auth-user.router.ts
+++ b/apps/server/src/app/routes/auth-user.router.ts
@@ -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
diff --git a/apps/server/src/app/routes/e2e.router.ts b/apps/server/src/app/routes/e2e.router.ts
index 5a98db21..0b4bc830 100644
--- a/apps/server/src/app/routes/e2e.router.ts
+++ b/apps/server/src/app/routes/e2e.router.ts
@@ -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'),
diff --git a/apps/server/src/app/routes/users.router.ts b/apps/server/src/app/routes/users.router.ts
index 15b80f16..f009fea2 100644
--- a/apps/server/src/app/routes/users.router.ts
+++ b/apps/server/src/app/routes/users.router.ts
@@ -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 }
},
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/libs/client/shared/src/providers/AxiosProvider.tsx b/libs/client/shared/src/providers/AxiosProvider.tsx
index 0daae1c3..46a709f1 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
@@ -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 (
{
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({
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/package.json b/package.json
index 780779ef..cd72e456 100644
--- a/package.json
+++ b/package.json
@@ -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",
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 91f04859..587a274c 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -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?
diff --git a/yarn.lock b/yarn.lock
index 5450e8d4..f6388665 100644
--- a/yarn.lock
+++ b/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"
From 6323495738a9cfdc06b5a590c5db8c6cac02ea51 Mon Sep 17 00:00:00 2001
From: Tyler Myracle
Date: Fri, 12 Jan 2024 21:43:45 -0600
Subject: [PATCH 06/20] add basic error messages for login and register
---
apps/client/pages/api/auth/[...nextauth].ts | 15 +++++++++++----
apps/client/pages/login.tsx | 16 ++++++++++++++--
apps/client/pages/register.tsx | 14 +++++++++++++-
3 files changed, 38 insertions(+), 7 deletions(-)
diff --git a/apps/client/pages/api/auth/[...nextauth].ts b/apps/client/pages/api/auth/[...nextauth].ts
index 1b7dd354..360d56fd 100644
--- a/apps/client/pages/api/auth/[...nextauth].ts
+++ b/apps/client/pages/api/auth/[...nextauth].ts
@@ -64,7 +64,7 @@ export const authOptions = {
.object({
firstName: z.string().optional(),
lastName: z.string().optional(),
- email: z.string().email(),
+ email: z.string().email({ message: 'Invalid email address' }),
password: z.string().min(6),
})
.safeParse(credentials)
@@ -75,7 +75,8 @@ export const authOptions = {
const authUser = await getAuthUserByEmail(email)
if (!authUser) {
- if (!firstName || !lastName) throw new Error('First and last name required')
+ if (!firstName || !lastName)
+ throw new Error(`Could not find an account with that email`)
const hashedPassword = await bcrypt.hash(password, 10)
const newAuthUser = await createAuthUser({
firstName,
@@ -91,9 +92,13 @@ export const authOptions = {
const passwordsMatch = await bcrypt.compare(password, authUser.password!)
if (passwordsMatch) return authUser
+ throw new Error('Email or password is invalid')
+ } else {
+ const errorMessages = parsedCredentials.error.issues.map(
+ (issue) => issue.message
+ )
+ throw new Error(errorMessages.join(', '))
}
-
- return null
},
}),
],
@@ -104,6 +109,7 @@ export const authOptions = {
token['https://maybe.co/email'] = authUser.email
token.firstName = authUser.firstName
token.lastName = authUser.lastName
+ token.name = authUser.name
}
return token
},
@@ -113,6 +119,7 @@ export const authOptions = {
session['https://maybe.co/email'] = token['https://maybe.co/email']
session.firstName = token.firstName
session.lastName = token.lastName
+ session.name = token.name
return session
},
},
diff --git a/apps/client/pages/login.tsx b/apps/client/pages/login.tsx
index b45d1fc3..f29ab664 100644
--- a/apps/client/pages/login.tsx
+++ b/apps/client/pages/login.tsx
@@ -11,6 +11,7 @@ export default function LoginPage() {
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()
@@ -21,13 +22,18 @@ export default function LoginPage() {
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault()
- setEmail('')
+ setErrorMessage(null)
setPassword('')
- await signIn('credentials', {
+
+ const response = await signIn('credentials', {
email,
password,
redirect: false,
})
+
+ if (response && response.error) {
+ setErrorMessage(response.error)
+ }
}
// _app.tsx will automatically redirect if not authenticated
@@ -70,6 +76,12 @@ export default function LoginPage() {
}
/>
+ {errorMessage && password.length === 0 ? (
+
+ {errorMessage}
+
+ ) : null}
+
))}
diff --git a/libs/client/shared/src/utils/image-loaders.ts b/libs/client/shared/src/utils/image-loaders.ts
index 6ccd7cf3..2fa529d4 100644
--- a/libs/client/shared/src/utils/image-loaders.ts
+++ b/libs/client/shared/src/utils/image-loaders.ts
@@ -1,7 +1,23 @@
import type { ImageLoaderProps } from 'next/legacy/image'
+function isJSON(str: string): boolean {
+ try {
+ JSON.parse(str)
+ return true
+ } catch (e) {
+ return false
+ }
+}
+
export function enhancerizerLoader({ src, width }: ImageLoaderProps): string {
- const parsed = JSON.parse(src) as { [key: string]: string | number }
+ let parsed: { [key: string]: string | number }
+
+ if (isJSON(src)) {
+ parsed = JSON.parse(src)
+ } else {
+ parsed = { src }
+ }
+
parsed.width ??= width
parsed.height ??= width
From 6100ab14c63b880432d13a01e6db4f84f9649b3f Mon Sep 17 00:00:00 2001
From: Tyler Myracle
Date: Sun, 14 Jan 2024 10:55:59 -0600
Subject: [PATCH 14/20] remove most of the Auth0 useage
---
.env.example | 9 +-
apps/client/components/ModalManager.tsx | 31 --
apps/client/pages/_app.tsx | 15 +-
.../__tests__/net-worth.integration.spec.ts | 2 -
.../app/__tests__/stripe.integration.spec.ts | 2 -
apps/server/src/app/__tests__/utils/axios.ts | 5 +-
apps/server/src/app/app.ts | 2 -
apps/server/src/app/lib/auth0.ts | 18 --
apps/server/src/app/lib/endpoint.ts | 3 -
.../src/app/middleware/identify-user.ts | 2 +-
apps/server/src/app/middleware/index.ts | 1 -
.../src/app/middleware/validate-auth0-jwt.ts | 22 --
apps/server/src/app/routes/admin.router.ts | 82 ------
apps/server/src/app/routes/index.ts | 1 -
apps/server/src/app/routes/users.router.ts | 110 +-------
apps/server/src/env.ts | 9 -
apps/workers/src/app/lib/auth0.ts | 18 --
apps/workers/src/app/lib/di.ts | 9 -
apps/workers/src/env.ts | 18 +-
libs/client/features/src/index.ts | 2 -
.../src/user-details/LinkAccountFlow.tsx | 267 ------------------
.../src/user-details/UserIdentityCard.tsx | 93 ------
.../src/user-details/UserIdentityList.tsx | 110 --------
.../client/features/src/user-details/index.ts | 1 -
.../NotificationPreferences.tsx | 20 --
.../features/src/user-notifications/index.ts | 1 -
.../MultiFactorAuthentication.tsx | 73 -----
.../src/user-security/SecurityPreferences.tsx | 37 +--
libs/client/features/src/user/AuthLoader.tsx | 45 ---
libs/client/features/src/user/index.ts | 1 -
libs/client/shared/src/api/useUserApi.ts | 142 +---------
.../src/components/dialogs/ConfirmDialog.tsx | 34 ---
.../shared/src/components/dialogs/index.ts | 1 -
.../src/auth-user/auth-user.service.ts | 15 +
.../features/src/email/email.processor.ts | 2 -
libs/server/features/src/user/user.service.ts | 90 +-----
libs/shared/src/types/user-types.ts | 30 --
37 files changed, 40 insertions(+), 1283 deletions(-)
delete mode 100644 apps/client/components/ModalManager.tsx
delete mode 100644 apps/server/src/app/lib/auth0.ts
delete mode 100644 apps/server/src/app/middleware/validate-auth0-jwt.ts
delete mode 100644 apps/server/src/app/routes/admin.router.ts
delete mode 100644 apps/workers/src/app/lib/auth0.ts
delete mode 100644 libs/client/features/src/user-details/LinkAccountFlow.tsx
delete mode 100644 libs/client/features/src/user-details/UserIdentityCard.tsx
delete mode 100644 libs/client/features/src/user-details/UserIdentityList.tsx
delete mode 100644 libs/client/features/src/user-notifications/NotificationPreferences.tsx
delete mode 100644 libs/client/features/src/user-notifications/index.ts
delete mode 100644 libs/client/features/src/user-security/MultiFactorAuthentication.tsx
delete mode 100644 libs/client/features/src/user/AuthLoader.tsx
delete mode 100644 libs/client/features/src/user/index.ts
delete mode 100644 libs/client/shared/src/components/dialogs/ConfirmDialog.tsx
diff --git a/.env.example b/.env.example
index af74028c..dcf61999 100644
--- a/.env.example
+++ b/.env.example
@@ -8,14 +8,13 @@ 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=
diff --git a/apps/client/components/ModalManager.tsx b/apps/client/components/ModalManager.tsx
deleted file mode 100644
index f1e27d6b..00000000
--- a/apps/client/components/ModalManager.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import {
- type ModalKey,
- type ModalManagerAction,
- ModalManagerContext,
-} from '@maybe-finance/client/shared'
-import { type PropsWithChildren, useReducer } from 'react'
-
-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 [, dispatch] = useReducer(reducer, {
- linkAuth0Accounts: { isOpen: false, props: null },
- })
-
- return (
- {children}
- )
-}
diff --git a/apps/client/pages/_app.tsx b/apps/client/pages/_app.tsx
index dc9ecf1f..d1fb65c9 100644
--- a/apps/client/pages/_app.tsx
+++ b/apps/client/pages/_app.tsx
@@ -16,7 +16,6 @@ import { BrowserTracing } from '@sentry/tracing'
import env from '../env'
import '../styles.css'
import { SessionProvider, useSession } from 'next-auth/react'
-import ModalManager from '../components/ModalManager'
import Meta from '../components/Meta'
import APM from '../components/APM'
import { useRouter } from 'next/router'
@@ -46,14 +45,12 @@ const WithAuth = function ({ children }: PropsWithChildren) {
if (session) {
return (
-
-
-
- {children}
-
-
-
-
+
+
+ {children}
+
+
+
)
}
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 9ac55758..a2ae1001 100644
--- a/apps/server/src/app/app.ts
+++ b/apps/server/src/app/app.ts
@@ -30,7 +30,6 @@ import {
usersRouter,
accountsRouter,
connectionsRouter,
- adminRouter,
webhooksRouter,
plaidRouter,
accountRollupRouter,
@@ -92,7 +91,6 @@ 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, {
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 ac75e664..535dd45f 100644
--- a/apps/server/src/app/lib/endpoint.ts
+++ b/apps/server/src/app/lib/endpoint.ts
@@ -57,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'
@@ -219,7 +218,6 @@ const userService = new UserService(
balanceSyncStrategyFactory,
queueService.getQueue('sync-user'),
queueService.getQueue('purge-user'),
- managementClient,
stripe
)
@@ -318,7 +316,6 @@ export async function createContext(req: Request) {
prisma,
plaid,
stripe,
- managementClient,
logger,
user,
ability: defineAbilityFor(user),
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 28b008a7..4a8ebce8 100644
--- a/apps/server/src/app/middleware/index.ts
+++ b/apps/server/src/app/middleware/index.ts
@@ -2,7 +2,6 @@ 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'
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/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 ef9bf4bc..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'
@@ -114,31 +112,6 @@ router.get(
})
)
-// TODO: Remove this endpoint
-router.get(
- '/auth0-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!.authId }, // TODO: Remove this endpoint
- { user_metadata: { enrolled_mfa: input.enrolled_mfa } }
- )
- },
- })
-)
-
router.get(
'/subscription',
endpoint.create({
@@ -286,43 +259,7 @@ router.get(
})
)
-// TODO: Remove this endpoint or refactor to work with new Auth
-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!.authId, input.secondaryProvider, {
- token: input.secondaryJWT,
- domain: env.NX_AUTH0_CUSTOM_DOMAIN,
- audience: env.NX_AUTH0_AUDIENCE,
- })
- },
- })
-)
-
-// TODO: Remove this endpoint or refactor to work with new Auth
-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!.authId,
- input.secondaryAuth0Id,
- input.secondaryProvider as UnlinkAccountsParamsProvider
- )
- },
- })
-)
-
-// TODO: Refactor this to use the Auth Id instead of Auth0
+// TODO: Implement verification email using Postmark instead of Auth0
router.post(
'/resend-verification-email',
endpoint.create({
@@ -333,7 +270,7 @@ router.post(
const authId = input.authId ?? ctx.user?.authId
if (!authId) throw new Error('User not found')
- await ctx.managementClient.sendEmailVerification({ user_id: authId })
+ //await ctx.managementClient.sendEmailVerification({ user_id: authId })
ctx.logger.info(`Sent verification email to ${authId}`)
@@ -354,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 }
},
})
@@ -439,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/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/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/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
-
- )}
-
- 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.{' '}
-
- 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).
-
-
-
- )}
-
-
-
-
{message || ''}
-
- >
- )
-}
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/useUserApi.ts b/libs/client/shared/src/api/useUserApi.ts
index 356f789a..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`,
@@ -70,67 +64,11 @@ 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)
- return data
- },
-
async getSubscription() {
const { data } = await axios.get('/users/subscription')
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,
@@ -139,32 +77,6 @@ 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(authId?: string) {
const { data } = await axios.post<{ success: boolean }>(
'/users/resend-verification-email',
@@ -206,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 },
@@ -304,24 +215,6 @@ 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
- >
- ) =>
- useMutation(api.updateAuth0Profile, {
- onSettled() {
- queryClient.invalidateQueries(['users', 'auth0-profile'])
- },
- ...options,
- })
-
const useSubscription = (
options?: Omit, 'queryKey' | 'queryFn'>
) => useQuery(['users', 'subscription'], api.getSubscription, options)
@@ -339,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>
) =>
@@ -420,12 +286,8 @@ export function useUserApi() {
useProfile,
useUpdateProfile,
useAuthProfile,
- useAuth0Profile,
- useUpdateAuth0Profile,
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 (
-
- )
-}
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/server/features/src/auth-user/auth-user.service.ts b/libs/server/features/src/auth-user/auth-user.service.ts
index 96854b10..cff2e791 100644
--- a/libs/server/features/src/auth-user/auth-user.service.ts
+++ b/libs/server/features/src/auth-user/auth-user.service.ts
@@ -1,5 +1,6 @@
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
@@ -22,6 +23,20 @@ export class AuthUserService implements IAuthUserService {
})
}
+ 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
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/user/user.service.ts b/libs/server/features/src/user/user.service.ts
index 5f21ac1f..785ae3cc 100644
--- a/libs/server/features/src/user/user.service.ts
+++ b/libs/server/features/src/user/user.service.ts
@@ -1,7 +1,6 @@
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'
@@ -55,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
) {}
@@ -72,50 +70,6 @@ export class UserService implements IUserService {
})
}
- // TODO: Update this to use new Auth
- 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
- // 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)
- )
- 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.authId &&
- 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 sync(id: User['id']) {
const user = await this.get(id)
await this.syncQueue.add('sync-user', { userId: user.id })
@@ -554,46 +508,4 @@ export class UserService implements IUserService {
return onboarding
}
-
- // TODO: Update to work with new Auth
- async linkAccounts(
- primaryAuth0Id: User['authId'],
- 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: { authId: validatedJWT.authId } })
-
- 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.authId,
- provider,
- })
- }
-
- async unlinkAccounts(
- primaryAuth0Id: User['authId'],
- secondaryAuth0Id: User['authId'],
- 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/shared/src/types/user-types.ts b/libs/shared/src/types/user-types.ts
index 33506d88..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,
@@ -191,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
From 56c78856f883cac549c54a002cf2b47ae88a166a Mon Sep 17 00:00:00 2001
From: Tyler Myracle
Date: Sun, 14 Jan 2024 11:19:10 -0600
Subject: [PATCH 15/20] more clean up
---
apps/client/pages/register.tsx | 1 -
.../features/src/account/AccountMenu.tsx | 23 ++-----------------
libs/client/shared/src/api/useAuthUserApi.ts | 2 --
libs/client/shared/src/utils/auth-utils.ts | 7 ------
libs/client/shared/src/utils/index.ts | 1 -
.../src/auth-user/auth-user.service.ts | 1 -
6 files changed, 2 insertions(+), 33 deletions(-)
delete mode 100644 libs/client/shared/src/utils/auth-utils.ts
diff --git a/apps/client/pages/register.tsx b/apps/client/pages/register.tsx
index 5b154963..9192e405 100644
--- a/apps/client/pages/register.tsx
+++ b/apps/client/pages/register.tsx
@@ -44,7 +44,6 @@ export default function RegisterPage() {
}
}
- // _app.tsx will automatically redirect if not authenticated
return (
<>
} onClick={() => editAccount(account)}>
Edit
- {BrowserUtil.hasRole(user, 'Admin') && (
- }
- destructive
- onClick={() => syncAccount.mutate(account.id)}
- >
- Sync
-
- )}
{!account.accountConnectionId && (
}
diff --git a/libs/client/shared/src/api/useAuthUserApi.ts b/libs/client/shared/src/api/useAuthUserApi.ts
index f6bae852..8a17c392 100644
--- a/libs/client/shared/src/api/useAuthUserApi.ts
+++ b/libs/client/shared/src/api/useAuthUserApi.ts
@@ -13,8 +13,6 @@ const AuthUserApi = (axios: AxiosInstance) => ({
const staleTimes = {
user: 30_000,
- netWorth: 30_000,
- insights: 30_000,
}
export function useAuthUserApi() {
diff --git a/libs/client/shared/src/utils/auth-utils.ts b/libs/client/shared/src/utils/auth-utils.ts
deleted file mode 100644
index b525fa66..00000000
--- a/libs/client/shared/src/utils/auth-utils.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import type { User } from '@auth0/auth0-react'
-
-export function hasRole(user: User | null | undefined, role: 'Admin'): boolean {
- if (!user) return false
- const roles = user['https://maybe.co/roles']
- return roles && Array.isArray(roles) && roles.includes(role)
-}
diff --git a/libs/client/shared/src/utils/index.ts b/libs/client/shared/src/utils/index.ts
index ea672173..a1ac6e57 100644
--- a/libs/client/shared/src/utils/index.ts
+++ b/libs/client/shared/src/utils/index.ts
@@ -2,4 +2,3 @@ export * from './image-loaders'
export * from './browser-utils'
export * from './account-utils'
export * from './form-utils'
-export * from './auth-utils'
diff --git a/libs/server/features/src/auth-user/auth-user.service.ts b/libs/server/features/src/auth-user/auth-user.service.ts
index cff2e791..81fddd02 100644
--- a/libs/server/features/src/auth-user/auth-user.service.ts
+++ b/libs/server/features/src/auth-user/auth-user.service.ts
@@ -45,7 +45,6 @@ export class AuthUserService implements IAuthUserService {
async delete(id: AuthUser['id']) {
const authUser = await this.get(id)
- // Delete user from prisma
this.logger.info(`Removing user ${authUser.id} from Prisma`)
const user = await this.prisma.authUser.delete({ where: { id } })
return user
From 30fa84b7732f6e82226664e3a3f7be19908bf108 Mon Sep 17 00:00:00 2001
From: Tyler Myracle
Date: Sun, 14 Jan 2024 11:23:43 -0600
Subject: [PATCH 16/20] remove @auth0 deps
---
package.json | 6 ------
yarn.lock | 42 ------------------------------------------
2 files changed, 48 deletions(-)
diff --git a/package.json b/package.json
index 9a5cb50d..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"
},
@@ -39,8 +35,6 @@
"private": true,
"dependencies": {
"@auth/prisma-adapter": "^1.0.14",
- "@auth0/auth0-react": "^2.0.0",
- "@auth0/nextjs-auth0": "^2.0.0",
"@bull-board/express": "^4.6.4",
"@casl/ability": "^6.3.2",
"@casl/prisma": "^1.4.1",
diff --git a/yarn.lock b/yarn.lock
index c20f6bec..70375fc9 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -29,33 +29,6 @@
dependencies:
"@auth/core" "0.20.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==
- dependencies:
- "@auth0/auth0-spa-js" "^2.0.2"
-
-"@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==
- 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"
-
"@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"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a"
@@ -13164,11 +13137,6 @@ 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"
@@ -15210,16 +15178,6 @@ 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==
- dependencies:
- jose "^4.10.0"
- lru-cache "^6.0.0"
- object-hash "^2.0.1"
- oidc-token-hash "^5.0.1"
-
openid-client@^5.4.0:
version "5.6.4"
resolved "https://registry.yarnpkg.com/openid-client/-/openid-client-5.6.4.tgz#b2c25e6d5338ba3ce00e04341bb286798a196177"
From c56d528329a762f5416a2e79f2097afb35852f0a Mon Sep 17 00:00:00 2001
From: Tyler Myracle
Date: Sun, 14 Jan 2024 12:23:46 -0600
Subject: [PATCH 17/20] update readme
---
README.md | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/README.md b/README.md
index 03281b11..d8052d90 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,13 @@ You'll need Docker installed to run the app locally.
```
cp .env.example .env
+```
+
+Create a new secret for NEXTAUTH_SECRET in .env using
+`openssl rand -base64 32`
+Then run
+
+```
yarn install
yarn run dev:services
yarn prisma:migrate:dev
From e5edb1e79d451c9b561819c5ddef1c705d236f8d Mon Sep 17 00:00:00 2001
From: Tyler Myracle
Date: Sun, 14 Jan 2024 12:25:56 -0600
Subject: [PATCH 18/20] update readme to generate secret
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index d8052d90..f010ce28 100644
--- a/README.md
+++ b/README.md
@@ -47,9 +47,9 @@ You'll need Docker installed to run the app locally.
cp .env.example .env
```
-Create a new secret for NEXTAUTH_SECRET in .env using
+Create a new secret for using
`openssl rand -base64 32`
-Then run
+and populate `NEXTAUTH_SECRET` in your `.env` file. Then run:
```
yarn install
From cedbe1b751fceedf04cb58ab330be9686cf6ec19 Mon Sep 17 00:00:00 2001
From: Tyler Myracle
Date: Sun, 14 Jan 2024 12:26:43 -0600
Subject: [PATCH 19/20] typo
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index f010ce28..a485b69a 100644
--- a/README.md
+++ b/README.md
@@ -47,7 +47,7 @@ You'll need Docker installed to run the app locally.
cp .env.example .env
```
-Create a new secret for using
+Create a new secret using
`openssl rand -base64 32`
and populate `NEXTAUTH_SECRET` in your `.env` file. Then run:
From 85b56ca7c8f3b8fbc98b6cc87e223292b0e32622 Mon Sep 17 00:00:00 2001
From: Josh Pigford
Date: Sun, 14 Jan 2024 12:29:50 -0600
Subject: [PATCH 20/20] Minor readme update for building the app
---
README.md | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index a485b69a..d2e47717 100644
--- a/README.md
+++ b/README.md
@@ -43,13 +43,15 @@ 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
```
-Create a new secret using
-`openssl rand -base64 32`
-and populate `NEXTAUTH_SECRET` in your `.env` file. Then run:
+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