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? -

- -
- - {secondaryProvider === 'apple' ? ( - - ) : ( - - )} -
- - ) -} - -function ConfirmStep({ - isLoading, - isReady, - onCancel, - onNext, -}: StepProps & { isLoading: boolean; isReady: boolean }) { - if (!isReady) { - return ( - <> - - -

Authentication in progress...

- - - - ) - } - - return ( - <> - - -

- {isLoading ? 'Linking accounts ...' : 'Continue linking accounts?'} -

- -
- {isLoading ? ( -

- Your accounts are being linked and data is being merged. This may take a few - seconds. -

- ) : ( - <> -

- After linking, both logins will use the data in{' '} - the current account. Any data you - have in your secondary account will be archived and no longer available - to you. If you ever wish to recover that data, you can reverse this - process by unlinking the account in your settings.{' '} -

- -

No data will be deleted.

- - )} -
- -
- - - -
- - ) -} - -function LinkComplete({ onClose }: { onClose(): void }) { - return ( - <> - - -

Accounts linked successfully!

- -

- Your accounts have been linked and the data has been merged successfully. -

- -
- -
- - ) -} - -function LinkError({ onClose, error }: { onClose(): void; error: string }) { - return ( - <> - - -

Account linking failed

- -

{error}

- - - Please contact us. - - -
- -
- - ) -} diff --git a/libs/client/features/src/user-details/UserIdentityCard.tsx b/libs/client/features/src/user-details/UserIdentityCard.tsx deleted file mode 100644 index c092bfa7..00000000 --- a/libs/client/features/src/user-details/UserIdentityCard.tsx +++ /dev/null @@ -1,93 +0,0 @@ -import type { SharedType } from '@maybe-finance/shared' -import { Button } from '@maybe-finance/design-system' -import { RiAppleFill, RiMailLine, RiCheckboxCircleFill } from 'react-icons/ri' - -export type UserIdentity = - | { - variant: 'primary' - provider: string - email: string - auth0Id?: string - isLinked?: boolean - } - | { - variant: 'linked' - provider: string - email: string - auth0Id: string - isLinked?: never - } - | { - variant: 'unlinked' - provider: string - email?: string // We might have an email here if we've detected a duplicate account - auth0Id?: never - isLinked?: never - } - -export function UserIdentityCard({ - identity, - onUnlink, - onLink, -}: { - identity: UserIdentity - onUnlink?(data: SharedType.UnlinkAccount): void - onLink?(): void -}) { - return ( -
-
- {identity.provider === 'apple' ? ( - - ) : ( - - )} -
- -
- {identity.email ?? ''} -
- {!identity.email && ( - - {identity.provider === 'apple' ? 'Apple account' : 'Email account'} - - )} - {identity.variant === 'primary' && ( - - - Main - - )} -
-
- - {identity.isLinked && ( - - - Linked - - )} - - {identity.variant === 'linked' && ( - - )} - - {identity.variant === 'unlinked' && ( - - )} -
- ) -} diff --git a/libs/client/features/src/user-details/UserIdentityList.tsx b/libs/client/features/src/user-details/UserIdentityList.tsx deleted file mode 100644 index 13324bfe..00000000 --- a/libs/client/features/src/user-details/UserIdentityList.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import type { SharedType } from '@maybe-finance/shared' -import { ConfirmDialog, useModalManager, useUserApi } from '@maybe-finance/client/shared' -import { useState } from 'react' -import { UserIdentityCard } from './UserIdentityCard' - -export function UserIdentityList({ profile }: { profile: SharedType.Auth0Profile }) { - const { useUnlinkAccount } = useUserApi() - const { dispatch } = useModalManager() - const [isConfirm, setIsConfirm] = useState(false) - const [unlinkProps, setUnlinkProps] = useState(undefined) - const unlinkAccountQuery = useUnlinkAccount() - const { primaryIdentity, secondaryIdentities, suggestedIdentities, email } = profile - - return ( - <> -
-

Identities

-
- {/* The user's primary account identity */} - 0, - }} - /> - - {/* Any identities the user has already linked */} - {secondaryIdentities.map((si) => ( - { - setUnlinkProps(data) - setIsConfirm(true) - }} - /> - ))} - - {/* Accounts that can be linked */} - {suggestedIdentities.map((si) => ( - - dispatch({ - type: 'open', - key: 'linkAuth0Accounts', - props: { secondaryProvider: si.provider }, - }) - } - /> - ))} - - {/* If the primary is an email/password account and has no linked or suggested identities, - we can suggest they link an Apple account */} - {!primaryIdentity.isSocial && - !secondaryIdentities.length && - !suggestedIdentities.length && ( - - dispatch({ - type: 'open', - key: 'linkAuth0Accounts', - props: { secondaryProvider: 'apple' }, - }) - } - /> - )} -
-
- setIsConfirm(false)} - onConfirm={async () => { - setIsConfirm(false) - await unlinkAccountQuery.mutateAsync(unlinkProps!) - }} - title="Unlink account?" - > -
-

- Unlinking this account will remove the connection permanently.{' '} - No data will be lost. -

-

- After unlinking, each login will become a{' '} - separate Maybe account. -

-
-
- - ) -} diff --git a/libs/client/features/src/user-details/index.ts b/libs/client/features/src/user-details/index.ts index 840d8672..f47e2ff5 100644 --- a/libs/client/features/src/user-details/index.ts +++ b/libs/client/features/src/user-details/index.ts @@ -1,2 +1 @@ export * from './UserDetails' -export * from './LinkAccountFlow' diff --git a/libs/client/features/src/user-notifications/NotificationPreferences.tsx b/libs/client/features/src/user-notifications/NotificationPreferences.tsx deleted file mode 100644 index 6efb0b60..00000000 --- a/libs/client/features/src/user-notifications/NotificationPreferences.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { useUserApi } from '@maybe-finance/client/shared' - -export function GeneralPreferences() { - const { useProfile } = useUserApi() - - const userProfile = useProfile() - - if (!userProfile.data) { - return null - } - - return ( - <> -

Ask the advisor

- - {/* TODO: Update notifications or remove */} -
- - ) -} diff --git a/libs/client/features/src/user-notifications/index.ts b/libs/client/features/src/user-notifications/index.ts deleted file mode 100644 index 3ac0b127..00000000 --- a/libs/client/features/src/user-notifications/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './NotificationPreferences' diff --git a/libs/client/features/src/user-security/MultiFactorAuthentication.tsx b/libs/client/features/src/user-security/MultiFactorAuthentication.tsx deleted file mode 100644 index 0841e161..00000000 --- a/libs/client/features/src/user-security/MultiFactorAuthentication.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import type { SharedType } from '@maybe-finance/shared' -import { useAuth0 } from '@auth0/auth0-react' -import { useUserApi } from '@maybe-finance/client/shared' -import { Button } from '@maybe-finance/design-system' -import { toast } from 'react-hot-toast' - -export function MultiFactorAuthentication({ enabled }: { enabled: boolean }) { - const { useUpdateAuth0Profile } = useUserApi() - - const { loginWithPopup } = useAuth0() - - const updateProfile = useUpdateAuth0Profile({ - onSuccess(data) { - toast.success('MFA setting updated successfully') - if (data?.user_metadata?.enrolled_mfa === true) { - loginWithPopup( - { - authorizationParams: { - connection: 'Username-Password-Authentication', - screen_hint: 'show-form-only', - display: 'page', - }, - }, - { timeoutInSeconds: 360 } - ) - } - }, - onError() { - toast.error('Something went wrong enabling MFA on this account.') - }, - }) - - return ( -
-
-
- {enabled ? ( - - ) : ( - - )} -
- -
-
- Multi-factor authentication - {enabled ? ( - - Enabled - - ) : ( - - Not enabled - - )} -
- {!enabled && ( -
Requires authenticator app
- )} -
- - -
-
- ) -} diff --git a/libs/client/features/src/user-security/SecurityPreferences.tsx b/libs/client/features/src/user-security/SecurityPreferences.tsx index 34852883..8a68f4cb 100644 --- a/libs/client/features/src/user-security/SecurityPreferences.tsx +++ b/libs/client/features/src/user-security/SecurityPreferences.tsx @@ -1,47 +1,12 @@ import { useUserApi } from '@maybe-finance/client/shared' import { Button, LoadingSpinner } from '@maybe-finance/design-system' -import { MultiFactorAuthentication } from './MultiFactorAuthentication' import { PasswordReset } from './PasswordReset' export function SecurityPreferences() { - const { useAuth0Profile } = useUserApi() - const profileQuery = useAuth0Profile() - - if (profileQuery.isLoading) { - return - } - - if (profileQuery.isError) { - return ( -

- Something went wrong loading your security preferences... -

- ) - } - - const { socialOnlyUser, mfaEnabled } = profileQuery.data - - return socialOnlyUser ? ( - <> -

- Your account credentials are managed by Apple. To reset your password, click the - button below to go to your Apple settings. -

- - - ) : ( + return ( <>

Password

- -

Multi-Factor Authentication

-

- Add an extra layer of security by setting up multi-factor authentication. This will - need an app like Google Authenticator or Authy. -

- ) } diff --git a/libs/client/features/src/user/AuthLoader.tsx b/libs/client/features/src/user/AuthLoader.tsx deleted file mode 100644 index 9ccc2c54..00000000 --- a/libs/client/features/src/user/AuthLoader.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { useAuth0 } from '@auth0/auth0-react' -import { LoadingSpinner } from '@maybe-finance/design-system' -import { SharedType } from '@maybe-finance/shared' -import { useMemo } from 'react' -import { RiInformationLine } from 'react-icons/ri' - -export function AuthLoader({ message }: { message?: string }): JSX.Element { - const { user } = useAuth0() - - const currentLoginType = useMemo(() => { - const primaryIdentity = - user && user[SharedType.Auth0CustomNamespace.PrimaryIdentity]?.provider - - return primaryIdentity ? primaryIdentity : undefined - }, [user]) - - return ( - <> - {message && ( -
-
- -

- You are currently logged in to your{' '} - - {currentLoginType === 'apple' ? 'Apple ' : 'Email/Password '} - {' '} - account. Please login with your{' '} - - {currentLoginType === 'apple' ? 'Email/Password ' : 'Apple '} - account - - , and we'll merge the data between the two (no data will be lost). -

-
-
- )} - -
- -

{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 ( - - {title} - {children} - - - - - - ) -} 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