From b34563c60c4976a423eef6190d5fecd49cf24392 Mon Sep 17 00:00:00 2001 From: Tyler Myracle Date: Tue, 16 Jan 2024 12:21:19 -0600 Subject: [PATCH 01/32] add useTeller hook --- libs/client/shared/src/hooks/index.ts | 1 + libs/client/shared/src/hooks/useTeller.ts | 71 +++++++++++++++++++++++ package.json | 1 + yarn.lock | 12 ++++ 4 files changed, 85 insertions(+) create mode 100644 libs/client/shared/src/hooks/useTeller.ts diff --git a/libs/client/shared/src/hooks/index.ts b/libs/client/shared/src/hooks/index.ts index 6481beb2..9410b7f4 100644 --- a/libs/client/shared/src/hooks/index.ts +++ b/libs/client/shared/src/hooks/index.ts @@ -9,5 +9,6 @@ export * from './useQueryParam' export * from './useScreenSize' export * from './useAccountNotifications' export * from './usePlaid' +export * from './useTeller' export * from './useProviderStatus' export * from './useModalManager' diff --git a/libs/client/shared/src/hooks/useTeller.ts b/libs/client/shared/src/hooks/useTeller.ts new file mode 100644 index 00000000..e44838f2 --- /dev/null +++ b/libs/client/shared/src/hooks/useTeller.ts @@ -0,0 +1,71 @@ +import { useState } from 'react' +import toast from 'react-hot-toast' +import * as Sentry from '@sentry/react' +import { useTellerConnect } from 'teller-connect-react' +import { useAccountContext, useUserAccountContext } from '../providers' +import { useLogger } from './useLogger' + +type TellerFailure = { + type: 'payee' | 'payment' + code: 'timeout' | 'error' + message: string +} + +export function useTeller() { + const logger = useLogger() + + const [institutionId, setInstitutionId] = useState(null) + + const { setExpectingAccounts } = useUserAccountContext() + const { setAccountManager } = useAccountContext() + + const tellerConfig = { + applicationId: process.env.NEXT_PUBLIC_TELLER_APP_ID, + institution: institutionId, + environment: process.env.NEXT_PUBLIC_TELLER_ENV, + selectAccount: 'disabled', + onInit: () => { + toast.dismiss(toastId) + logger.debug(`Teller Connect has initialized`) + }, + onSuccess: (enrollment) => { + logger.debug(`User enrolled successfully`, enrollment) + console.log(enrollment) + setExpectingAccounts(true) + }, + onExit: () => { + logger.debug(`Teller Connect exited`) + }, + onFailure: (failure: TellerFailure) => { + logger.error(`Teller Connect exited with error`, failure) + Sentry.captureEvent({ + level: 'error', + message: 'TELLER_CONNECT_ERROR', + tags: { + 'teller.error.code': failure.code, + 'teller.error.message': failure.message, + }, + }) + }, + } + + const { open, ready } = useTellerConnect(tellerConfig) + + useEffect(() => { + if (ready) { + open() + + if (selectAccount === 'disabled') { + setAccountManager({ view: 'idle' }) + } + } + }, [ready, open, setAccountManager]) + + return { + openTeller: async (institutionId: string) => { + toast('Initializing Teller...', { duration: 2_000 }) + setInstitutionId(institutionId) + }, + ready, + } +} diff --git a/package.json b/package.json index 96c52336..6b62db4a 100644 --- a/package.json +++ b/package.json @@ -153,6 +153,7 @@ "stripe": "^10.17.0", "superjson": "^1.11.0", "tailwindcss": "3.2.4", + "teller-connect-react": "^0.1.0", "tslib": "^2.3.0", "uuid": "^9.0.0", "winston": "^3.8.2", diff --git a/yarn.lock b/yarn.lock index e301ab27..1fb86738 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16600,6 +16600,11 @@ react-script-hook@^1.6.0: resolved "https://registry.yarnpkg.com/react-script-hook/-/react-script-hook-1.6.0.tgz#6a44ff5e65113cb29252eadad1b8306f5fe0c626" integrity sha512-aJm72XGWV+wJTKiqHmAaTNC/JQZV/Drv6A1kd1VQlzhzAXLqtBRBeTt3iTESImGe5TaBDHUOUeaGNw4v+7bqDw== +react-script-hook@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/react-script-hook/-/react-script-hook-1.7.2.tgz#ec130d67f9a25fcde57fbfd1faa87e5b97521948" + integrity sha512-fhyCEfXb94fag34UPRF0zry1XGwmVY+79iibWwTqAoOiCzYJQOYTiWJ7CnqglA9tMSV8g45cQpHCMcBwr7dwhA== + react-shallow-renderer@^16.15.0: version "16.15.0" resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz#48fb2cf9b23d23cde96708fe5273a7d3446f4457" @@ -18532,6 +18537,13 @@ telejson@^6.0.8: lodash "^4.17.21" memoizerific "^1.11.3" +teller-connect-react@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/teller-connect-react/-/teller-connect-react-0.1.0.tgz#b3bae24f4410d622eb8c88c7668adb003eb7bfd7" + integrity sha512-ZI+OULCsuo/v1qetpjepOgM7TyIzwnMVE/54IruOPguQtJ/Ui3C1ax3wUb65AKZDyVQ7ZyjA+8ypT/yMYD9bIQ== + dependencies: + react-script-hook "^1.7.2" + terminal-link@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/terminal-link/-/terminal-link-2.1.1.tgz#14a64a27ab3c0df933ea546fba55f2d078edc994" From 76ee026db79a94f62fe2b5f1f4763e5d77376a58 Mon Sep 17 00:00:00 2001 From: Enes Kaya Date: Tue, 16 Jan 2024 21:12:18 +0100 Subject: [PATCH 02/32] Set 'typescript.tsdk' version in VSCode settings --- .vscode/settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index b76293a8..c5d00b83 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,6 @@ }, "[html]": { "editor.defaultFormatter": "esbenp.prettier-vscode" - } + }, + "typescript.tsdk": "node_modules/typescript/lib" } From b61fececc7b698776bc65a115e241b913528c5a0 Mon Sep 17 00:00:00 2001 From: Tyler Myracle Date: Tue, 16 Jan 2024 15:18:21 -0600 Subject: [PATCH 03/32] teller front end progress --- apps/server/src/app/lib/endpoint.ts | 1 + apps/workers/src/app/lib/di.ts | 1 + apps/workers/src/main.ts | 13 ++++ .../accounts-manager/AccountTypeSelector.tsx | 38 +++++++++- libs/client/shared/src/hooks/index.ts | 1 - libs/client/shared/src/hooks/useTeller.ts | 71 ------------------- .../src/providers/AccountContextProvider.tsx | 1 + libs/client/shared/src/utils/index.ts | 1 + libs/client/shared/src/utils/teller-utils.ts | 39 ++++++++++ .../src/institution/institution.service.ts | 2 +- .../src/providers/teller/teller.service.ts | 19 ++--- .../shared/src/services/queue.service.ts | 2 +- libs/teller-api/src/teller-api.ts | 4 +- libs/teller-api/src/types/institutions.ts | 4 +- .../migration.sql | 2 + prisma/schema.prisma | 1 + 16 files changed, 109 insertions(+), 91 deletions(-) delete mode 100644 libs/client/shared/src/hooks/useTeller.ts create mode 100644 libs/client/shared/src/utils/teller-utils.ts create mode 100644 prisma/migrations/20240116185600_add_teller_provider/migration.sql diff --git a/apps/server/src/app/lib/endpoint.ts b/apps/server/src/app/lib/endpoint.ts index 8e01b00d..f6340672 100644 --- a/apps/server/src/app/lib/endpoint.ts +++ b/apps/server/src/app/lib/endpoint.ts @@ -240,6 +240,7 @@ const userService = new UserService( const institutionProviderFactory = new InstitutionProviderFactory({ PLAID: plaidService, FINICITY: finicityService, + TELLER: tellerService, }) const institutionService: IInstitutionService = new InstitutionService( diff --git a/apps/workers/src/app/lib/di.ts b/apps/workers/src/app/lib/di.ts index 987ebedd..9e6d68c5 100644 --- a/apps/workers/src/app/lib/di.ts +++ b/apps/workers/src/app/lib/di.ts @@ -259,6 +259,7 @@ export const securityPricingProcessor: ISecurityPricingProcessor = new SecurityP const institutionProviderFactory = new InstitutionProviderFactory({ PLAID: plaidService, FINICITY: finicityService, + TELLER: tellerService, }) export const institutionService: IInstitutionService = new InstitutionService( diff --git a/apps/workers/src/main.ts b/apps/workers/src/main.ts index 35c16cdd..7db31bb0 100644 --- a/apps/workers/src/main.ts +++ b/apps/workers/src/main.ts @@ -115,6 +115,11 @@ syncInstitutionQueue.process( async () => await institutionService.sync('FINICITY') ) +syncInstitutionQueue.process( + 'sync-teller-institutions', + async () => await institutionService.sync('TELLER') +) + syncInstitutionQueue.add( 'sync-plaid-institutions', {}, @@ -131,6 +136,14 @@ syncInstitutionQueue.add( } ) +syncInstitutionQueue.add( + 'sync-teller-institutions', + {}, + { + repeat: { cron: '* */24 * * *' }, // Run every 24 hours + } +) + /** * send-email queue */ diff --git a/libs/client/features/src/accounts-manager/AccountTypeSelector.tsx b/libs/client/features/src/accounts-manager/AccountTypeSelector.tsx index 2b7c81f8..d3637783 100644 --- a/libs/client/features/src/accounts-manager/AccountTypeSelector.tsx +++ b/libs/client/features/src/accounts-manager/AccountTypeSelector.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect } from 'react' +import { useState, useRef, useEffect, useMemo } from 'react' import { RiFolderLine, RiHandCoinLine, RiLockLine, RiSearchLine } from 'react-icons/ri' import maxBy from 'lodash/maxBy' import { @@ -8,11 +8,13 @@ import { usePlaid, useFinicity, } from '@maybe-finance/client/shared' - import { Input } from '@maybe-finance/design-system' import InstitutionGrid from './InstitutionGrid' import { AccountTypeGrid } from './AccountTypeGrid' import InstitutionList, { MIN_QUERY_LENGTH } from './InstitutionList' +import { useLogger } from '@maybe-finance/client/shared' +import { BrowserUtil } from '@maybe-finance/client/shared' +import { useTellerConnect, type TellerConnectOptions } from 'teller-connect-react' const SEARCH_DEBOUNCE_MS = 300 @@ -23,18 +25,27 @@ export default function AccountTypeSelector({ view: string onViewChange: (view: string) => void }) { + const logger = useLogger() const { setAccountManager } = useAccountContext() const [searchQuery, setSearchQuery] = useState('') const debouncedSearchQuery = useDebounce(searchQuery, SEARCH_DEBOUNCE_MS) + const [institutionId, setInstitutionId] = useState(undefined) + const showInstitutionList = searchQuery.length >= MIN_QUERY_LENGTH && debouncedSearchQuery.length >= MIN_QUERY_LENGTH && view !== 'manual' + const config = useMemo( + () => BrowserUtil.getTellerConfig(logger, institutionId), + [logger, institutionId] + ) as TellerConnectOptions + const { openPlaid } = usePlaid() const { openFinicity } = useFinicity() + const { open: openTeller } = useTellerConnect(config) const inputRef = useRef(null) @@ -44,6 +55,15 @@ export default function AccountTypeSelector({ } }, []) + const configRef = useRef(null) + + useEffect(() => { + if (institutionId) { + configRef.current = BrowserUtil.getTellerConfig(logger, institutionId) + openTeller() + } + }, [institutionId, logger, openTeller]) + return (
{/* Search */} @@ -68,6 +88,8 @@ export default function AccountTypeSelector({ if (!providerInstitution) { alert('No provider found for institution') return + } else { + setInstitutionId(providerInstitution.providerId) } switch (providerInstitution.provider) { @@ -77,6 +99,9 @@ export default function AccountTypeSelector({ case 'FINICITY': openFinicity(providerInstitution.providerId) break + case 'TELLER': + openTeller() + break default: break } @@ -142,7 +167,11 @@ export default function AccountTypeSelector({ return } - if (!data) return + if (!data) { + return + } else { + setInstitutionId(data.providerId) + } switch (data.provider) { case 'PLAID': @@ -151,6 +180,9 @@ export default function AccountTypeSelector({ case 'FINICITY': openFinicity(data.providerId) break + case 'TELLER': + openTeller() + break default: break } diff --git a/libs/client/shared/src/hooks/index.ts b/libs/client/shared/src/hooks/index.ts index 9410b7f4..6481beb2 100644 --- a/libs/client/shared/src/hooks/index.ts +++ b/libs/client/shared/src/hooks/index.ts @@ -9,6 +9,5 @@ export * from './useQueryParam' export * from './useScreenSize' export * from './useAccountNotifications' export * from './usePlaid' -export * from './useTeller' export * from './useProviderStatus' export * from './useModalManager' diff --git a/libs/client/shared/src/hooks/useTeller.ts b/libs/client/shared/src/hooks/useTeller.ts deleted file mode 100644 index e44838f2..00000000 --- a/libs/client/shared/src/hooks/useTeller.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { useState } from 'react' -import toast from 'react-hot-toast' -import * as Sentry from '@sentry/react' -import { useTellerConnect } from 'teller-connect-react' -import { useAccountContext, useUserAccountContext } from '../providers' -import { useLogger } from './useLogger' - -type TellerFailure = { - type: 'payee' | 'payment' - code: 'timeout' | 'error' - message: string -} - -export function useTeller() { - const logger = useLogger() - - const [institutionId, setInstitutionId] = useState(null) - - const { setExpectingAccounts } = useUserAccountContext() - const { setAccountManager } = useAccountContext() - - const tellerConfig = { - applicationId: process.env.NEXT_PUBLIC_TELLER_APP_ID, - institution: institutionId, - environment: process.env.NEXT_PUBLIC_TELLER_ENV, - selectAccount: 'disabled', - onInit: () => { - toast.dismiss(toastId) - logger.debug(`Teller Connect has initialized`) - }, - onSuccess: (enrollment) => { - logger.debug(`User enrolled successfully`, enrollment) - console.log(enrollment) - setExpectingAccounts(true) - }, - onExit: () => { - logger.debug(`Teller Connect exited`) - }, - onFailure: (failure: TellerFailure) => { - logger.error(`Teller Connect exited with error`, failure) - Sentry.captureEvent({ - level: 'error', - message: 'TELLER_CONNECT_ERROR', - tags: { - 'teller.error.code': failure.code, - 'teller.error.message': failure.message, - }, - }) - }, - } - - const { open, ready } = useTellerConnect(tellerConfig) - - useEffect(() => { - if (ready) { - open() - - if (selectAccount === 'disabled') { - setAccountManager({ view: 'idle' }) - } - } - }, [ready, open, setAccountManager]) - - return { - openTeller: async (institutionId: string) => { - toast('Initializing Teller...', { duration: 2_000 }) - setInstitutionId(institutionId) - }, - ready, - } -} diff --git a/libs/client/shared/src/providers/AccountContextProvider.tsx b/libs/client/shared/src/providers/AccountContextProvider.tsx index 510a4690..454533ca 100644 --- a/libs/client/shared/src/providers/AccountContextProvider.tsx +++ b/libs/client/shared/src/providers/AccountContextProvider.tsx @@ -48,6 +48,7 @@ type AccountManager = | { view: 'idle' } | { view: 'add-plaid'; linkToken: string } | { view: 'add-finicity' } + | { view: 'add-teller' } | { view: 'add-account' } | { view: 'add-property'; defaultValues: Partial } | { view: 'add-vehicle'; defaultValues: Partial } diff --git a/libs/client/shared/src/utils/index.ts b/libs/client/shared/src/utils/index.ts index a1ac6e57..90694d3a 100644 --- a/libs/client/shared/src/utils/index.ts +++ b/libs/client/shared/src/utils/index.ts @@ -2,3 +2,4 @@ export * from './image-loaders' export * from './browser-utils' export * from './account-utils' export * from './form-utils' +export * from './teller-utils' diff --git a/libs/client/shared/src/utils/teller-utils.ts b/libs/client/shared/src/utils/teller-utils.ts new file mode 100644 index 00000000..4265ae84 --- /dev/null +++ b/libs/client/shared/src/utils/teller-utils.ts @@ -0,0 +1,39 @@ +import * as Sentry from '@sentry/react' +import type { Logger } from '../providers/LogProvider' +import type { + TellerConnectEnrollment, + TellerConnectFailure, + TellerConnectOptions, +} from 'teller-connect-react' + +type TellerEnvironment = 'sandbox' | 'development' | 'production' | undefined +type TellerAccountSelection = 'disabled' | 'single' | 'multiple' | undefined + +export const getTellerConfig = (logger: Logger, institutionId: string | undefined) => { + return { + applicationId: process.env.NEXT_PUBLIC_TELLER_APP_ID ?? 'ADD_TELLER_APP_ID', + environment: (process.env.NEXT_PUBLIC_TELLER_ENV as TellerEnvironment) ?? 'sandbox', + selectAccount: 'disabled' as TellerAccountSelection, + ...(institutionId !== undefined ? { institution: institutionId } : {}), + onInit: () => { + logger.debug(`Teller Connect has initialized`) + }, + onSuccess: (enrollment: TellerConnectEnrollment) => { + logger.debug(`User enrolled successfully`, enrollment) + }, + onExit: () => { + logger.debug(`Teller Connect exited`) + }, + onFailure: (failure: TellerConnectFailure) => { + logger.error(`Teller Connect exited with error`, failure) + Sentry.captureEvent({ + level: 'error', + message: 'TELLER_CONNECT_ERROR', + tags: { + 'teller.error.code': failure.code, + 'teller.error.message': failure.message, + }, + }) + }, + } as TellerConnectOptions +} diff --git a/libs/server/features/src/institution/institution.service.ts b/libs/server/features/src/institution/institution.service.ts index 32cfeebd..ed76de28 100644 --- a/libs/server/features/src/institution/institution.service.ts +++ b/libs/server/features/src/institution/institution.service.ts @@ -271,7 +271,7 @@ export class InstitutionService implements IInstitutionService { provider_institution pi SET institution_id = i.id, - rank = (CASE WHEN pi.provider = 'PLAID' THEN 1 ELSE 0 END) + rank = (CASE WHEN pi.provider = 'TELLER' THEN 1 ELSE 0 END) FROM duplicates d INNER JOIN institutions i ON i.name = d.name AND i.url = d.url diff --git a/libs/server/features/src/providers/teller/teller.service.ts b/libs/server/features/src/providers/teller/teller.service.ts index e884639f..a5847041 100644 --- a/libs/server/features/src/providers/teller/teller.service.ts +++ b/libs/server/features/src/providers/teller/teller.service.ts @@ -79,7 +79,7 @@ export class TellerService implements IAccountConnectionProvider, IInstitutionPr async getInstitutions() { const tellerInstitutions = await SharedUtil.paginate({ - pageSize: 500, + pageSize: 10000, delay: process.env.NODE_ENV !== 'production' ? { @@ -87,20 +87,20 @@ export class TellerService implements IAccountConnectionProvider, IInstitutionPr milliseconds: 7_000, // Sandbox rate limited at 10 calls / minute } : undefined, - fetchData: (offset, count) => + fetchData: () => SharedUtil.withRetry( () => this.teller.getInstitutions().then((data) => { this.logger.debug( - `paginated teller fetch inst=${data.institutions.length} (total=${data.institutions.length} offset=${offset} count=${count})` + `teller fetch inst=${data.length} (total=${data.length})` ) - return data.institutions + return data }), { maxRetries: 3, onError: (error, attempt) => { this.logger.error( - `Teller fetch institutions request failed attempt=${attempt} offset=${offset} count=${count}`, + `Teller fetch institutions request failed attempt=${attempt}`, { error: ErrorUtil.parseError(error) } ) @@ -115,10 +115,11 @@ export class TellerService implements IAccountConnectionProvider, IInstitutionPr return { providerId: id, name, - url: undefined, - logo: `https://teller.io/images/banks/${id}.jpg}`, - primaryColor: undefined, - oauth: undefined, + url: null, + logo: null, + logoUrl: `https://teller.io/images/banks/${id}.jpg`, + primaryColor: null, + oauth: false, data: tellerInstitution, } }) diff --git a/libs/server/shared/src/services/queue.service.ts b/libs/server/shared/src/services/queue.service.ts index 319fef92..c0c1d8c7 100644 --- a/libs/server/shared/src/services/queue.service.ts +++ b/libs/server/shared/src/services/queue.service.ts @@ -70,7 +70,7 @@ export type SyncSecurityQueue = IQueue export type SyncInstitutionQueue = IQueue< {}, - 'sync-finicity-institutions' | 'sync-plaid-institutions' + 'sync-finicity-institutions' | 'sync-plaid-institutions' | 'sync-teller-institutions' > export type SendEmailQueue = IQueue diff --git a/libs/teller-api/src/teller-api.ts b/libs/teller-api/src/teller-api.ts index ca3a180a..16b982f7 100644 --- a/libs/teller-api/src/teller-api.ts +++ b/libs/teller-api/src/teller-api.ts @@ -137,8 +137,8 @@ export class TellerApi { } private async getApi(accessToken: string): Promise { - const cert = fs.readFileSync('../../../certs/teller-certificate.pem', 'utf8') - const key = fs.readFileSync('../../../certs/teller-private-key.pem', 'utf8') + const cert = fs.readFileSync('./certs/certificate.pem', 'utf8') + const key = fs.readFileSync('./certs/private_key.pem', 'utf8') const agent = new https.Agent({ cert, diff --git a/libs/teller-api/src/types/institutions.ts b/libs/teller-api/src/types/institutions.ts index 6f243375..3a593e14 100644 --- a/libs/teller-api/src/types/institutions.ts +++ b/libs/teller-api/src/types/institutions.ts @@ -9,6 +9,4 @@ export type Institution = { type Capability = 'detail' | 'balance' | 'transaction' | 'identity' -export type GetInstitutionsResponse = { - institutions: Institution[] -} +export type GetInstitutionsResponse = Institution[] diff --git a/prisma/migrations/20240116185600_add_teller_provider/migration.sql b/prisma/migrations/20240116185600_add_teller_provider/migration.sql new file mode 100644 index 00000000..20c526f2 --- /dev/null +++ b/prisma/migrations/20240116185600_add_teller_provider/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "Provider" ADD VALUE 'TELLER'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 007cad61..918f1349 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -493,6 +493,7 @@ model Institution { enum Provider { PLAID FINICITY + TELLER } model ProviderInstitution { From 97dc37fad191189ee15f92e44d2ed132817d4fb7 Mon Sep 17 00:00:00 2001 From: Tyler Myracle Date: Tue, 16 Jan 2024 18:29:00 -0600 Subject: [PATCH 04/32] more progress on Teller --- apps/server/src/app/app.ts | 2 + apps/server/src/app/routes/index.ts | 1 + apps/server/src/app/routes/teller.router.ts | 45 +++++ .../accounts-manager/AccountTypeSelector.tsx | 33 +--- libs/client/shared/src/api/index.ts | 1 + libs/client/shared/src/api/useTellerApi.ts | 63 ++++++ libs/client/shared/src/hooks/index.ts | 1 + libs/client/shared/src/hooks/useTeller.ts | 180 ++++++++++++++++++ libs/client/shared/src/utils/index.ts | 1 - libs/client/shared/src/utils/teller-utils.ts | 39 ---- .../src/providers/teller/teller.service.ts | 69 ++++++- libs/teller-api/src/teller-api.ts | 21 +- libs/teller-api/src/types/enrollment.ts | 13 ++ libs/teller-api/src/types/index.ts | 1 + package.json | 1 + .../migration.sql | 9 + prisma/schema.prisma | 2 +- 17 files changed, 396 insertions(+), 86 deletions(-) create mode 100644 apps/server/src/app/routes/teller.router.ts create mode 100644 libs/client/shared/src/api/useTellerApi.ts create mode 100644 libs/client/shared/src/hooks/useTeller.ts delete mode 100644 libs/client/shared/src/utils/teller-utils.ts create mode 100644 libs/teller-api/src/types/enrollment.ts create mode 100644 prisma/migrations/20240116224800_add_enrollment_id_for_teller/migration.sql diff --git a/apps/server/src/app/app.ts b/apps/server/src/app/app.ts index c495a30d..06c0870c 100644 --- a/apps/server/src/app/app.ts +++ b/apps/server/src/app/app.ts @@ -36,6 +36,7 @@ import { valuationsRouter, institutionsRouter, finicityRouter, + tellerRouter, transactionsRouter, holdingsRouter, securitiesRouter, @@ -156,6 +157,7 @@ app.use('/v1/users', usersRouter) app.use('/v1/e2e', e2eRouter) app.use('/v1/plaid', plaidRouter) app.use('/v1/finicity', finicityRouter) +app.use('/v1/teller', tellerRouter) app.use('/v1/accounts', accountsRouter) app.use('/v1/account-rollup', accountRollupRouter) app.use('/v1/connections', connectionsRouter) diff --git a/apps/server/src/app/routes/index.ts b/apps/server/src/app/routes/index.ts index 135a9a24..40eb5d16 100644 --- a/apps/server/src/app/routes/index.ts +++ b/apps/server/src/app/routes/index.ts @@ -5,6 +5,7 @@ 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 tellerRouter } from './teller.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/teller.router.ts b/apps/server/src/app/routes/teller.router.ts new file mode 100644 index 00000000..c802ff70 --- /dev/null +++ b/apps/server/src/app/routes/teller.router.ts @@ -0,0 +1,45 @@ +import { Router } from 'express' +import { z } from 'zod' +import endpoint from '../lib/endpoint' + +const router = Router() + +router.post( + '/handle-enrollment', + endpoint.create({ + input: z.object({ + institution: z.object({ + name: z.string(), + id: z.string(), + }), + enrollment: z.object({ + accessToken: z.string(), + user: z.object({ + id: z.string(), + }), + enrollment: z.object({ + id: z.string(), + institution: z.object({ + name: z.string(), + }), + }), + signatures: z.array(z.string()).optional(), + }), + }), + resolve: ({ input: { institution, enrollment }, ctx }) => { + return ctx.tellerService.handleEnrollment(ctx.user!.id, institution, enrollment) + }, + }) +) + +router.post( + '/institutions/sync', + endpoint.create({ + resolve: async ({ ctx }) => { + ctx.ability.throwUnlessCan('manage', 'Institution') + await ctx.queueService.getQueue('sync-institution').add('sync-teller-institutions', {}) + }, + }) +) + +export default router diff --git a/libs/client/features/src/accounts-manager/AccountTypeSelector.tsx b/libs/client/features/src/accounts-manager/AccountTypeSelector.tsx index d3637783..d3be9221 100644 --- a/libs/client/features/src/accounts-manager/AccountTypeSelector.tsx +++ b/libs/client/features/src/accounts-manager/AccountTypeSelector.tsx @@ -1,4 +1,4 @@ -import { useState, useRef, useEffect, useMemo } from 'react' +import { useState, useRef, useEffect } from 'react' import { RiFolderLine, RiHandCoinLine, RiLockLine, RiSearchLine } from 'react-icons/ri' import maxBy from 'lodash/maxBy' import { @@ -7,14 +7,14 @@ import { useDebounce, usePlaid, useFinicity, + useTellerConfig, + useTellerConnect, } from '@maybe-finance/client/shared' import { Input } from '@maybe-finance/design-system' import InstitutionGrid from './InstitutionGrid' import { AccountTypeGrid } from './AccountTypeGrid' import InstitutionList, { MIN_QUERY_LENGTH } from './InstitutionList' import { useLogger } from '@maybe-finance/client/shared' -import { BrowserUtil } from '@maybe-finance/client/shared' -import { useTellerConnect, type TellerConnectOptions } from 'teller-connect-react' const SEARCH_DEBOUNCE_MS = 300 @@ -31,21 +31,16 @@ export default function AccountTypeSelector({ const [searchQuery, setSearchQuery] = useState('') const debouncedSearchQuery = useDebounce(searchQuery, SEARCH_DEBOUNCE_MS) - const [institutionId, setInstitutionId] = useState(undefined) - const showInstitutionList = searchQuery.length >= MIN_QUERY_LENGTH && debouncedSearchQuery.length >= MIN_QUERY_LENGTH && view !== 'manual' - const config = useMemo( - () => BrowserUtil.getTellerConfig(logger, institutionId), - [logger, institutionId] - ) as TellerConnectOptions + const config = useTellerConfig(logger) const { openPlaid } = usePlaid() const { openFinicity } = useFinicity() - const { open: openTeller } = useTellerConnect(config) + const { open: openTeller } = useTellerConnect(config, logger) const inputRef = useRef(null) @@ -55,15 +50,6 @@ export default function AccountTypeSelector({ } }, []) - const configRef = useRef(null) - - useEffect(() => { - if (institutionId) { - configRef.current = BrowserUtil.getTellerConfig(logger, institutionId) - openTeller() - } - }, [institutionId, logger, openTeller]) - return (
{/* Search */} @@ -88,8 +74,6 @@ export default function AccountTypeSelector({ if (!providerInstitution) { alert('No provider found for institution') return - } else { - setInstitutionId(providerInstitution.providerId) } switch (providerInstitution.provider) { @@ -100,7 +84,7 @@ export default function AccountTypeSelector({ openFinicity(providerInstitution.providerId) break case 'TELLER': - openTeller() + openTeller(providerInstitution.providerId) break default: break @@ -163,14 +147,11 @@ export default function AccountTypeSelector({ categoryUser: 'crypto', }, }) - return } if (!data) { return - } else { - setInstitutionId(data.providerId) } switch (data.provider) { @@ -181,7 +162,7 @@ export default function AccountTypeSelector({ openFinicity(data.providerId) break case 'TELLER': - openTeller() + openTeller(data.providerId) break default: break diff --git a/libs/client/shared/src/api/index.ts b/libs/client/shared/src/api/index.ts index 6fe77f31..c03040f8 100644 --- a/libs/client/shared/src/api/index.ts +++ b/libs/client/shared/src/api/index.ts @@ -5,6 +5,7 @@ export * from './useFinicityApi' export * from './useInstitutionApi' export * from './useUserApi' export * from './usePlaidApi' +export * from './useTellerApi' export * from './useValuationApi' export * from './useTransactionApi' export * from './useHoldingApi' diff --git a/libs/client/shared/src/api/useTellerApi.ts b/libs/client/shared/src/api/useTellerApi.ts new file mode 100644 index 00000000..2a323da7 --- /dev/null +++ b/libs/client/shared/src/api/useTellerApi.ts @@ -0,0 +1,63 @@ +import { useMemo } from 'react' +import toast from 'react-hot-toast' +import { useAxiosWithAuth } from '../hooks/useAxiosWithAuth' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import type { SharedType } from '@maybe-finance/shared' +import { invalidateAccountQueries } from '../utils' +import type { AxiosInstance } from 'axios' +import type { TellerTypes } from '@maybe-finance/teller-api' + +type TellerInstitution = { + name: string + id: string +} + +const TellerApi = (axios: AxiosInstance) => ({ + async handleEnrollment(input: { + institution: TellerInstitution + enrollment: TellerTypes.Enrollment + }) { + const { data } = await axios.post( + '/teller/handle-enrollment', + input + ) + return data + }, +}) + +export function useTellerApi() { + const queryClient = useQueryClient() + const { axios } = useAxiosWithAuth() + const api = useMemo(() => TellerApi(axios), [axios]) + + const addConnectionToState = (connection: SharedType.AccountConnection) => { + const accountsData = queryClient.getQueryData(['accounts']) + if (!accountsData) + queryClient.setQueryData(['accounts'], { + connections: [{ ...connection, accounts: [] }], + accounts: [], + }) + else { + const { connections, ...rest } = accountsData + queryClient.setQueryData(['accounts'], { + connections: [...connections, { ...connection, accounts: [] }], + ...rest, + }) + } + } + + const useHandleEnrollment = () => + useMutation(api.handleEnrollment, { + onSuccess: (_connection) => { + addConnectionToState(_connection) + toast.success(`Account connection added!`) + }, + onSettled: () => { + invalidateAccountQueries(queryClient, false) + }, + }) + + return { + useHandleEnrollment, + } +} diff --git a/libs/client/shared/src/hooks/index.ts b/libs/client/shared/src/hooks/index.ts index 6481beb2..9410b7f4 100644 --- a/libs/client/shared/src/hooks/index.ts +++ b/libs/client/shared/src/hooks/index.ts @@ -9,5 +9,6 @@ export * from './useQueryParam' export * from './useScreenSize' export * from './useAccountNotifications' export * from './usePlaid' +export * from './useTeller' export * from './useProviderStatus' export * from './useModalManager' diff --git a/libs/client/shared/src/hooks/useTeller.ts b/libs/client/shared/src/hooks/useTeller.ts new file mode 100644 index 00000000..b20c056b --- /dev/null +++ b/libs/client/shared/src/hooks/useTeller.ts @@ -0,0 +1,180 @@ +import { useEffect, useState } from 'react' +import * as Sentry from '@sentry/react' +import type { Logger } from '../providers/LogProvider' +import toast from 'react-hot-toast' +import { useTellerApi } from '../api' +import type { + TellerConnectEnrollment, + TellerConnectFailure, + TellerConnectOptions, + TellerConnectInstance, +} from 'teller-connect-react' +import useScript from 'react-script-hook' +type TellerEnvironment = 'sandbox' | 'development' | 'production' | undefined +type TellerAccountSelection = 'disabled' | 'single' | 'multiple' | undefined +const TC_JS = 'https://cdn.teller.io/connect/connect.js' + +// Create the base configuration for Teller Connect +export const useTellerConfig = (logger: Logger) => { + return { + applicationId: process.env.NEXT_PUBLIC_TELLER_APP_ID ?? 'ADD_TELLER_APP_ID', + environment: (process.env.NEXT_PUBLIC_TELLER_ENV as TellerEnvironment) ?? 'sandbox', + selectAccount: 'disabled' as TellerAccountSelection, + onInit: () => { + logger.debug(`Teller Connect has initialized`) + }, + onSuccess: {}, + onExit: () => { + logger.debug(`Teller Connect exited`) + }, + onFailure: (failure: TellerConnectFailure) => { + logger.error(`Teller Connect exited with error`, failure) + Sentry.captureEvent({ + level: 'error', + message: 'TELLER_CONNECT_ERROR', + tags: { + 'teller.error.code': failure.code, + 'teller.error.message': failure.message, + }, + }) + }, + } as TellerConnectOptions +} + +// Custom implementation of useTellerHook to handle institution id being passed in +export const useTellerConnect = (options: TellerConnectOptions, logger: Logger) => { + const { useHandleEnrollment } = useTellerApi() + const handleEnrollment = useHandleEnrollment() + const [loading, error] = useScript({ + src: TC_JS, + checkForExisting: true, + }) + + const [teller, setTeller] = useState(null) + const [iframeLoaded, setIframeLoaded] = useState(false) + + const createTellerInstance = (institutionId: string) => { + return createTeller( + { + ...options, + onSuccess: async (enrollment: TellerConnectEnrollment) => { + logger.debug(`User enrolled successfully`, enrollment) + try { + await handleEnrollment.mutateAsync({ + institution: { + id: institutionId!, + name: enrollment.enrollment.institution.name, + }, + enrollment, + }) + } catch (error) { + toast.error(`Failed to add account`) + } + }, + institution: institutionId, + onInit: () => { + setIframeLoaded(true) + options.onInit && options.onInit() + }, + }, + window.TellerConnect.setup + ) + } + + useEffect(() => { + if (loading) { + return + } + + if (!options.applicationId) { + return + } + + if (error || !window.TellerConnect) { + console.error('Error loading TellerConnect:', error) + return + } + + if (teller != null) { + teller.destroy() + } + + return () => teller?.destroy() + }, [ + loading, + error, + options.applicationId, + options.enrollmentId, + options.connectToken, + options.products, + ]) + + const ready = teller != null && (!loading || iframeLoaded) + + const logIt = () => { + if (!options.applicationId) { + console.error('teller-connect-react: open() called without a valid applicationId.') + } + } + + return { + error, + ready, + open: (institutionId: string) => { + logIt() + const tellerInstance = createTellerInstance(institutionId) + tellerInstance.open() + setTeller(tellerInstance) + }, + } +} + +interface ManagerState { + teller: TellerConnectInstance | null + open: boolean +} + +export const createTeller = ( + config: TellerConnectOptions, + creator: (config: TellerConnectOptions) => TellerConnectInstance +) => { + const state: ManagerState = { + teller: null, + open: false, + } + + if (typeof window === 'undefined' || !window.TellerConnect) { + throw new Error('TellerConnect is not loaded') + } + + state.teller = creator({ + ...config, + onExit: () => { + state.open = false + config.onExit && config.onExit() + }, + }) + + const open = () => { + if (!state.teller) { + return + } + + state.open = true + state.teller.open() + } + + const destroy = () => { + if (!state.teller) { + return + } + + state.teller.destroy() + state.teller = null + } + + return { + open, + destroy, + } +} diff --git a/libs/client/shared/src/utils/index.ts b/libs/client/shared/src/utils/index.ts index 90694d3a..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 './teller-utils' diff --git a/libs/client/shared/src/utils/teller-utils.ts b/libs/client/shared/src/utils/teller-utils.ts deleted file mode 100644 index 4265ae84..00000000 --- a/libs/client/shared/src/utils/teller-utils.ts +++ /dev/null @@ -1,39 +0,0 @@ -import * as Sentry from '@sentry/react' -import type { Logger } from '../providers/LogProvider' -import type { - TellerConnectEnrollment, - TellerConnectFailure, - TellerConnectOptions, -} from 'teller-connect-react' - -type TellerEnvironment = 'sandbox' | 'development' | 'production' | undefined -type TellerAccountSelection = 'disabled' | 'single' | 'multiple' | undefined - -export const getTellerConfig = (logger: Logger, institutionId: string | undefined) => { - return { - applicationId: process.env.NEXT_PUBLIC_TELLER_APP_ID ?? 'ADD_TELLER_APP_ID', - environment: (process.env.NEXT_PUBLIC_TELLER_ENV as TellerEnvironment) ?? 'sandbox', - selectAccount: 'disabled' as TellerAccountSelection, - ...(institutionId !== undefined ? { institution: institutionId } : {}), - onInit: () => { - logger.debug(`Teller Connect has initialized`) - }, - onSuccess: (enrollment: TellerConnectEnrollment) => { - logger.debug(`User enrolled successfully`, enrollment) - }, - onExit: () => { - logger.debug(`Teller Connect exited`) - }, - onFailure: (failure: TellerConnectFailure) => { - logger.error(`Teller Connect exited with error`, failure) - Sentry.captureEvent({ - level: 'error', - message: 'TELLER_CONNECT_ERROR', - tags: { - 'teller.error.code': failure.code, - 'teller.error.message': failure.message, - }, - }) - }, - } as TellerConnectOptions -} diff --git a/libs/server/features/src/providers/teller/teller.service.ts b/libs/server/features/src/providers/teller/teller.service.ts index a5847041..931e6721 100644 --- a/libs/server/features/src/providers/teller/teller.service.ts +++ b/libs/server/features/src/providers/teller/teller.service.ts @@ -6,10 +6,11 @@ import type { IAccountConnectionProvider, } from '../../account-connection' import { SharedUtil } from '@maybe-finance/shared' +import type { SharedType } from '@maybe-finance/shared' import type { SyncConnectionOptions, CryptoService, IETL } from '@maybe-finance/server/shared' import _ from 'lodash' import { ErrorUtil, etl } from '@maybe-finance/server/shared' -import type { TellerApi } from '@maybe-finance/teller-api' +import type { TellerApi, TellerTypes } from '@maybe-finance/teller-api' export interface ITellerConnect { generateConnectUrl(userId: User['id'], institutionId: string): Promise<{ link: string }> @@ -67,13 +68,22 @@ export class TellerService implements IAccountConnectionProvider, IInstitutionPr async delete(connection: AccountConnection) { // purge teller data - if (connection.tellerAccessToken && connection.tellerAccountId) { - await this.teller.deleteAccount({ - accessToken: this.crypto.decrypt(connection.tellerAccessToken), - accountId: connection.tellerAccountId, + if (connection.tellerAccessToken && connection.tellerEnrollmentId) { + const accounts = await this.prisma.account.findMany({ + where: { accountConnectionId: connection.id }, }) - this.logger.info(`Item ${connection.tellerAccountId} removed`) + for (const account of accounts) { + if (!account.tellerAccountId) continue + await this.teller.deleteAccount({ + accessToken: this.crypto.decrypt(connection.tellerAccessToken), + accountId: account.tellerAccountId, + }) + + this.logger.info(`Teller account ${account.id} removed`) + } + + this.logger.info(`Teller enrollment ${connection.tellerEnrollmentId} removed`) } } @@ -124,4 +134,51 @@ export class TellerService implements IAccountConnectionProvider, IInstitutionPr } }) } + + async handleEnrollment( + userId: User['id'], + institution: Pick, + enrollment: TellerTypes.Enrollment + ) { + const connections = await this.prisma.accountConnection.findMany({ + where: { userId }, + }) + + if (connections.length > 40) { + throw new Error('MAX_ACCOUNT_CONNECTIONS') + } + + const accounts = await this.teller.getAccounts({ accessToken: enrollment.accessToken }) + + this.logger.info(`Teller accounts retrieved for enrollment ${enrollment.enrollment.id}`) + + // If all the accounts are Non-USD, throw an error + if (accounts.every((a) => a.currency !== 'USD')) { + throw new Error('USD_ONLY') + } + + // Create account connection on exchange; accounts + txns will sync later with webhook + const [accountConnection] = await this.prisma.$transaction([ + this.prisma.accountConnection.create({ + data: { + name: enrollment.enrollment.institution.name, + type: 'teller' as SharedType.AccountConnectionType, + tellerEnrollmentId: enrollment.enrollment.id, + tellerInstitutionId: institution.id, + tellerAccessToken: this.crypto.encrypt(enrollment.accessToken), + userId, + syncStatus: 'PENDING', + }, + }), + ]) + + await this.prisma.user.update({ + where: { id: userId }, + data: { + tellerUserId: enrollment.user.id, + }, + }) + + return accountConnection + } } diff --git a/libs/teller-api/src/teller-api.ts b/libs/teller-api/src/teller-api.ts index 16b982f7..b6fe48d0 100644 --- a/libs/teller-api/src/teller-api.ts +++ b/libs/teller-api/src/teller-api.ts @@ -137,12 +137,12 @@ export class TellerApi { } private async getApi(accessToken: string): Promise { - const cert = fs.readFileSync('./certs/certificate.pem', 'utf8') - const key = fs.readFileSync('./certs/private_key.pem', 'utf8') + const cert = fs.readFileSync('./certs/certificate.pem') + const key = fs.readFileSync('./certs/private_key.pem') const agent = new https.Agent({ - cert, - key, + cert: cert, + key: key, }) if (!this.api) { @@ -153,15 +153,10 @@ export class TellerApi { headers: { Accept: 'application/json', }, - }) - - this.api.interceptors.request.use((config) => { - // Add the access_token to the auth object - config.auth = { - username: 'ACCESS_TOKEN', - password: accessToken, - } - return config + auth: { + username: accessToken, + password: '', + }, }) } diff --git a/libs/teller-api/src/types/enrollment.ts b/libs/teller-api/src/types/enrollment.ts new file mode 100644 index 00000000..e85b2552 --- /dev/null +++ b/libs/teller-api/src/types/enrollment.ts @@ -0,0 +1,13 @@ +export type Enrollment = { + accessToken: string + user: { + id: string + } + enrollment: { + id: string + institution: { + name: string + } + } + signatures?: string[] +} diff --git a/libs/teller-api/src/types/index.ts b/libs/teller-api/src/types/index.ts index ca90d347..863d6f9e 100644 --- a/libs/teller-api/src/types/index.ts +++ b/libs/teller-api/src/types/index.ts @@ -3,6 +3,7 @@ export * from './account-balance' export * from './account-details' export * from './authentication' export * from './error' +export * from './enrollment' export * from './identity' export * from './institutions' export * from './transactions' diff --git a/package.json b/package.json index 6b62db4a..0ddb5222 100644 --- a/package.json +++ b/package.json @@ -147,6 +147,7 @@ "react-popper": "^2.3.0", "react-ranger": "^2.1.0", "react-responsive": "^9.0.0-beta.10", + "react-script-hook": "^1.7.2", "regenerator-runtime": "0.13.7", "sanitize-html": "^2.8.1", "smooth-scroll-into-view-if-needed": "^1.1.33", diff --git a/prisma/migrations/20240116224800_add_enrollment_id_for_teller/migration.sql b/prisma/migrations/20240116224800_add_enrollment_id_for_teller/migration.sql new file mode 100644 index 00000000..e9dddb32 --- /dev/null +++ b/prisma/migrations/20240116224800_add_enrollment_id_for_teller/migration.sql @@ -0,0 +1,9 @@ +/* + Warnings: + + - You are about to drop the column `teller_account_id` on the `account_connection` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "account_connection" DROP COLUMN "teller_account_id", +ADD COLUMN "teller_enrollment_id" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 918f1349..19299720 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -71,8 +71,8 @@ model AccountConnection { finicityError Json? @map("finicity_error") // teller data - tellerAccountId String? @map("teller_account_id") tellerAccessToken String? @map("teller_access_token") + tellerEnrollmentId String? @map("teller_enrollment_id") tellerInstitutionId String? @map("teller_institution_id") tellerError Json? @map("teller_error") From 2575ccf311be0744fd17ea211137455381754c86 Mon Sep 17 00:00:00 2001 From: Tyler Myracle Date: Tue, 16 Jan 2024 22:03:50 -0600 Subject: [PATCH 05/32] add fix etl and test Teller sandbox --- .env.example | 2 +- apps/workers/src/main.ts | 2 +- libs/client/shared/src/api/useTellerApi.ts | 5 +++ libs/client/shared/src/hooks/useTeller.ts | 2 +- .../src/providers/teller/teller.etl.ts | 13 +++---- .../src/providers/teller/teller.service.ts | 38 +++++++++++-------- libs/server/shared/src/utils/teller-utils.ts | 25 ++++-------- libs/teller-api/src/types/accounts.ts | 2 +- 8 files changed, 45 insertions(+), 44 deletions(-) diff --git a/.env.example b/.env.example index abd0167b..56a62b67 100644 --- a/.env.example +++ b/.env.example @@ -57,4 +57,4 @@ NX_POSTMARK_API_TOKEN= ######################################################################## NX_PLAID_SECRET= NX_FINICITY_APP_KEY= -NX_FINICITY_PARTNER_SECRET= \ No newline at end of file +NX_FINICITY_PARTNER_SECRET= diff --git a/apps/workers/src/main.ts b/apps/workers/src/main.ts index 7db31bb0..a0cf5e82 100644 --- a/apps/workers/src/main.ts +++ b/apps/workers/src/main.ts @@ -140,7 +140,7 @@ syncInstitutionQueue.add( 'sync-teller-institutions', {}, { - repeat: { cron: '* */24 * * *' }, // Run every 24 hours + repeat: { cron: '0 0 */1 * *' }, // Run every 24 hours } ) diff --git a/libs/client/shared/src/api/useTellerApi.ts b/libs/client/shared/src/api/useTellerApi.ts index 2a323da7..a59287a1 100644 --- a/libs/client/shared/src/api/useTellerApi.ts +++ b/libs/client/shared/src/api/useTellerApi.ts @@ -6,6 +6,7 @@ import type { SharedType } from '@maybe-finance/shared' import { invalidateAccountQueries } from '../utils' import type { AxiosInstance } from 'axios' import type { TellerTypes } from '@maybe-finance/teller-api' +import { useAccountConnectionApi } from './useAccountConnectionApi' type TellerInstitution = { name: string @@ -30,6 +31,9 @@ export function useTellerApi() { const { axios } = useAxiosWithAuth() const api = useMemo(() => TellerApi(axios), [axios]) + const { useSyncConnection } = useAccountConnectionApi() + const syncConnection = useSyncConnection() + const addConnectionToState = (connection: SharedType.AccountConnection) => { const accountsData = queryClient.getQueryData(['accounts']) if (!accountsData) @@ -50,6 +54,7 @@ export function useTellerApi() { useMutation(api.handleEnrollment, { onSuccess: (_connection) => { addConnectionToState(_connection) + syncConnection.mutate(_connection.id) toast.success(`Account connection added!`) }, onSettled: () => { diff --git a/libs/client/shared/src/hooks/useTeller.ts b/libs/client/shared/src/hooks/useTeller.ts index b20c056b..6e09567f 100644 --- a/libs/client/shared/src/hooks/useTeller.ts +++ b/libs/client/shared/src/hooks/useTeller.ts @@ -58,7 +58,7 @@ export const useTellerConnect = (options: TellerConnectOptions, logger: Logger) { ...options, onSuccess: async (enrollment: TellerConnectEnrollment) => { - logger.debug(`User enrolled successfully`, enrollment) + logger.debug('User enrolled successfully') try { await handleEnrollment.mutateAsync({ institution: { diff --git a/libs/server/features/src/providers/teller/teller.etl.ts b/libs/server/features/src/providers/teller/teller.etl.ts index 241934bb..524659c9 100644 --- a/libs/server/features/src/providers/teller/teller.etl.ts +++ b/libs/server/features/src/providers/teller/teller.etl.ts @@ -1,6 +1,6 @@ import type { AccountConnection, PrismaClient } from '@prisma/client' import type { Logger } from 'winston' -import { SharedUtil, AccountUtil, type SharedType } from '@maybe-finance/shared' +import { SharedUtil, type SharedType } from '@maybe-finance/shared' import type { TellerApi, TellerTypes } from '@maybe-finance/teller-api' import { DbUtil, TellerUtil, type IETL, type ICryptoService } from '@maybe-finance/server/shared' import { Prisma } from '@prisma/client' @@ -117,8 +117,6 @@ export class TellerETL implements IETL { return [ // upsert accounts ...accounts.map((tellerAccount) => { - const type = TellerUtil.getType(tellerAccount.type) - const classification = AccountUtil.getClassification(type) return this.prisma.account.upsert({ where: { accountConnectionId_tellerAccountId: { @@ -132,12 +130,13 @@ export class TellerETL implements IETL { categoryProvider: TellerUtil.tellerTypesToCategory(tellerAccount.type), subcategoryProvider: tellerAccount.subtype ?? 'other', accountConnectionId: connection.id, + userId: connection.userId, tellerAccountId: tellerAccount.id, name: tellerAccount.name, tellerType: tellerAccount.type, tellerSubtype: tellerAccount.subtype, mask: tellerAccount.last_four, - ...TellerUtil.getAccountBalanceData(tellerAccount, classification), + ...TellerUtil.getAccountBalanceData(tellerAccount), }, update: { type: TellerUtil.getType(tellerAccount.type), @@ -145,7 +144,7 @@ export class TellerETL implements IETL { subcategoryProvider: tellerAccount.subtype ?? 'other', tellerType: tellerAccount.type, tellerSubtype: tellerAccount.subtype, - ..._.omit(TellerUtil.getAccountBalanceData(tellerAccount, classification), [ + ..._.omit(TellerUtil.getAccountBalanceData(tellerAccount), [ 'currentBalanceStrategy', 'availableBalanceStrategy', ]), @@ -226,13 +225,13 @@ export class TellerETL implements IETL { } AND teller_account_id = ${account_id.toString()}), ${id}, ${date}::date, - ${[description].filter(Boolean).join(' ')}, + ${description}, ${DbUtil.toDecimal(-amount)}, ${status === 'pending'}, ${'USD'}, ${details.counterparty.name ?? ''}, ${type}, - ${details.category ?? ''}, + ${details.category ?? ''} )` }) )} diff --git a/libs/server/features/src/providers/teller/teller.service.ts b/libs/server/features/src/providers/teller/teller.service.ts index 931e6721..7ce65280 100644 --- a/libs/server/features/src/providers/teller/teller.service.ts +++ b/libs/server/features/src/providers/teller/teller.service.ts @@ -45,6 +45,7 @@ export class TellerService implements IAccountConnectionProvider, IInstitutionPr where: { id: connection.id }, data: { status: 'OK', + syncStatus: 'IDLE', }, }) break @@ -157,21 +158,6 @@ export class TellerService implements IAccountConnectionProvider, IInstitutionPr throw new Error('USD_ONLY') } - // Create account connection on exchange; accounts + txns will sync later with webhook - const [accountConnection] = await this.prisma.$transaction([ - this.prisma.accountConnection.create({ - data: { - name: enrollment.enrollment.institution.name, - type: 'teller' as SharedType.AccountConnectionType, - tellerEnrollmentId: enrollment.enrollment.id, - tellerInstitutionId: institution.id, - tellerAccessToken: this.crypto.encrypt(enrollment.accessToken), - userId, - syncStatus: 'PENDING', - }, - }), - ]) - await this.prisma.user.update({ where: { id: userId }, data: { @@ -179,6 +165,28 @@ export class TellerService implements IAccountConnectionProvider, IInstitutionPr }, }) + const accountConnection = await this.prisma.accountConnection.create({ + data: { + name: enrollment.enrollment.institution.name, + type: 'teller' as SharedType.AccountConnectionType, + tellerEnrollmentId: enrollment.enrollment.id, + tellerInstitutionId: institution.id, + tellerAccessToken: this.crypto.encrypt(enrollment.accessToken), + userId, + syncStatus: 'PENDING', + }, + }) + + await this.sync(accountConnection, { type: 'teller', initialSync: true }) + + await this.prisma.accountConnection.update({ + where: { id: accountConnection.id }, + data: { + status: 'OK', + syncStatus: 'IDLE', + }, + }) + return accountConnection } } diff --git a/libs/server/shared/src/utils/teller-utils.ts b/libs/server/shared/src/utils/teller-utils.ts index a8e8e3ee..0b741583 100644 --- a/libs/server/shared/src/utils/teller-utils.ts +++ b/libs/server/shared/src/utils/teller-utils.ts @@ -1,10 +1,4 @@ -import { - Prisma, - AccountCategory, - AccountType, - type AccountClassification, - type Account, -} from '@prisma/client' +import { Prisma, AccountCategory, AccountType, type Account } from '@prisma/client' import type { TellerTypes } from '@maybe-finance/teller-api' import { Duration } from 'luxon' @@ -13,10 +7,10 @@ import { Duration } from 'luxon' */ export const TELLER_WINDOW_MAX = Duration.fromObject({ years: 1 }) -export function getAccountBalanceData( - { balances, currency }: Pick, - classification: AccountClassification -): Pick< +export function getAccountBalanceData({ + balance, + currency, +}: Pick): Pick< Account, | 'currentBalanceProvider' | 'currentBalanceStrategy' @@ -24,16 +18,11 @@ export function getAccountBalanceData( | 'availableBalanceStrategy' | 'currencyCode' > { - // Flip balance values to positive for liabilities - const sign = classification === 'liability' ? -1 : 1 - return { - currentBalanceProvider: new Prisma.Decimal( - balances.ledger ? sign * Number(balances.ledger) : 0 - ), + currentBalanceProvider: new Prisma.Decimal(balance.ledger ? Number(balance.ledger) : 0), currentBalanceStrategy: 'current', availableBalanceProvider: new Prisma.Decimal( - balances.available ? sign * Number(balances.available) : 0 + balance.available ? Number(balance.available) : 0 ), availableBalanceStrategy: 'available', currencyCode: currency, diff --git a/libs/teller-api/src/types/accounts.ts b/libs/teller-api/src/types/accounts.ts index 5df29953..d1ccbcc1 100644 --- a/libs/teller-api/src/types/accounts.ts +++ b/libs/teller-api/src/types/accounts.ts @@ -50,7 +50,7 @@ interface CreditAccount extends BaseAccount { export type Account = DepositoryAccount | CreditAccount export type AccountWithBalances = Account & { - balances: AccountBalance + balance: AccountBalance } export type GetAccountsResponse = Account[] From 7d62c6e68819b3ae60b164b116d6f1e8e36424f0 Mon Sep 17 00:00:00 2001 From: Tyler Myracle Date: Tue, 16 Jan 2024 22:20:56 -0600 Subject: [PATCH 06/32] clean up --- .env.example | 1 - 1 file changed, 1 deletion(-) diff --git a/.env.example b/.env.example index 56a62b67..58b320fe 100644 --- a/.env.example +++ b/.env.example @@ -57,4 +57,3 @@ NX_POSTMARK_API_TOKEN= ######################################################################## NX_PLAID_SECRET= NX_FINICITY_APP_KEY= -NX_FINICITY_PARTNER_SECRET= From f38c7be1a6cbd65c2ca98e09299510deb5783f58 Mon Sep 17 00:00:00 2001 From: Tyler Myracle Date: Tue, 16 Jan 2024 22:22:23 -0600 Subject: [PATCH 07/32] add new teller envs to example --- .env.example | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index 58b320fe..73888889 100644 --- a/.env.example +++ b/.env.example @@ -35,8 +35,9 @@ NX_POLYGON_API_KEY= # We use Teller.io for automated banking data. You can sign up for a free # account and get a free API key at https://teller.io NX_TELLER_SIGNING_SECRET= -NX_TELLER_APP_ID= NX_TELLER_ENV=sandbox +NEXT_PUBLIC_TELLER_ENV=sandbox +NEXT_PUBLIC_TELLER_APP_ID= ######################################################################## # EMAIL From faabe6a3d44268f76b2d3f15fcc5ab5ac307c274 Mon Sep 17 00:00:00 2001 From: Tyler Myracle Date: Tue, 16 Jan 2024 22:23:17 -0600 Subject: [PATCH 08/32] add back --- .env.example | 1 + 1 file changed, 1 insertion(+) diff --git a/.env.example b/.env.example index 73888889..12baaedb 100644 --- a/.env.example +++ b/.env.example @@ -58,3 +58,4 @@ NX_POSTMARK_API_TOKEN= ######################################################################## NX_PLAID_SECRET= NX_FINICITY_APP_KEY= +NX_FINICITY_PARTNER_SECRET= From 836a0e157c3177422278589fb66ba5d5a35c98e2 Mon Sep 17 00:00:00 2001 From: Tyler Myracle Date: Tue, 16 Jan 2024 22:35:00 -0600 Subject: [PATCH 09/32] remove unused --- .../features/src/providers/teller/teller.service.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/libs/server/features/src/providers/teller/teller.service.ts b/libs/server/features/src/providers/teller/teller.service.ts index 7ce65280..defa153d 100644 --- a/libs/server/features/src/providers/teller/teller.service.ts +++ b/libs/server/features/src/providers/teller/teller.service.ts @@ -12,15 +12,6 @@ import _ from 'lodash' import { ErrorUtil, etl } from '@maybe-finance/server/shared' import type { TellerApi, TellerTypes } from '@maybe-finance/teller-api' -export interface ITellerConnect { - generateConnectUrl(userId: User['id'], institutionId: string): Promise<{ link: string }> - - generateFixConnectUrl( - userId: User['id'], - accountConnectionId: AccountConnection['id'] - ): Promise<{ link: string }> -} - export class TellerService implements IAccountConnectionProvider, IInstitutionProvider { constructor( private readonly logger: Logger, From 3cb1d5eaed4c662ab1554244e49b1445e1c7b46d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jan 2024 08:34:14 +0000 Subject: [PATCH 10/32] Bump decode-uri-component from 0.2.0 to 0.2.2 Bumps [decode-uri-component](https://github.com/SamVerschueren/decode-uri-component) from 0.2.0 to 0.2.2. - [Release notes](https://github.com/SamVerschueren/decode-uri-component/releases) - [Commits](https://github.com/SamVerschueren/decode-uri-component/compare/v0.2.0...v0.2.2) --- updated-dependencies: - dependency-name: decode-uri-component dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index e301ab27..ad626291 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8670,9 +8670,9 @@ decimal.js@^10.3.1, decimal.js@^10.4.2: integrity sha512-ic1yEvwT6GuvaYwBLLY6/aFFgjZdySKTE8en/fkU3QICTmRtgtSlFn0u0BXN06InZwtfCelR7j8LRiDI/02iGA== decode-uri-component@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545" - integrity sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og== + version "0.2.2" + resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" + integrity sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ== decompress-response@^6.0.0: version "6.0.0" From b144f6cb30096312f1dfda7484b2b5a734a5f9e2 Mon Sep 17 00:00:00 2001 From: Sascha Ronnie Daoudia <85792632+Dadudidas@users.noreply.github.com> Date: Wed, 17 Jan 2024 09:34:30 +0100 Subject: [PATCH 11/32] Create snyk-security.yml Signed-off-by: Sascha Ronnie Daoudia <85792632+Dadudidas@users.noreply.github.com> --- .github/workflows/snyk-security.yml | 79 +++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 .github/workflows/snyk-security.yml diff --git a/.github/workflows/snyk-security.yml b/.github/workflows/snyk-security.yml new file mode 100644 index 00000000..e1fbaa66 --- /dev/null +++ b/.github/workflows/snyk-security.yml @@ -0,0 +1,79 @@ +# This workflow uses actions that are not certified by GitHub. +# They are provided by a third-party and are governed by +# separate terms of service, privacy policy, and support +# documentation. + +# A sample workflow which sets up Snyk to analyze the full Snyk platform (Snyk Open Source, Snyk Code, +# Snyk Container and Snyk Infrastructure as Code) +# The setup installs the Snyk CLI - for more details on the possible commands +# check https://docs.snyk.io/snyk-cli/cli-reference +# The results of Snyk Code are then uploaded to GitHub Security Code Scanning +# +# In order to use the Snyk Action you will need to have a Snyk API token. +# More details in https://github.com/snyk/actions#getting-your-snyk-token +# or you can signup for free at https://snyk.io/login +# +# For more examples, including how to limit scans to only high-severity issues +# and fail PR checks, see https://github.com/snyk/actions/ + +name: Snyk Security + +on: + push: + branches: ["main" ] + pull_request: + branches: ["main"] + +permissions: + contents: read + +jobs: + snyk: + permissions: + contents: read # for actions/checkout to fetch code + security-events: write # for github/codeql-action/upload-sarif to upload SARIF results + actions: read # only required for a private repository by github/codeql-action/upload-sarif to get the Action run status + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Set up Snyk CLI to check for security issues + # Snyk can be used to break the build when it detects security issues. + # In this case we want to upload the SAST issues to GitHub Code Scanning + uses: snyk/actions/setup@806182742461562b67788a64410098c9d9b96adb + + # For Snyk Open Source you must first set up the development environment for your application's dependencies + # For example for Node + #- uses: actions/setup-node@v3 + # with: + # node-version: 16 + + env: + # This is where you will need to introduce the Snyk API token created with your Snyk account + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + + # Runs Snyk Code (SAST) analysis and uploads result into GitHub. + # Use || true to not fail the pipeline + - name: Snyk Code test + run: snyk code test --sarif > snyk-code.sarif # || true + + # Runs Snyk Open Source (SCA) analysis and uploads result to Snyk. + - name: Snyk Open Source monitor + run: snyk monitor --all-projects + + # Runs Snyk Infrastructure as Code (IaC) analysis and uploads result to Snyk. + # Use || true to not fail the pipeline. + - name: Snyk IaC test and report + run: snyk iac test --report # || true + + # Build the docker image for testing + - name: Build a Docker image + run: docker build -t your/image-to-test . + # Runs Snyk Container (Container and SCA) analysis and uploads result to Snyk. + - name: Snyk Container monitor + run: snyk container monitor your/image-to-test --file=Dockerfile + + # Push the Snyk Code results into GitHub Code Scanning tab + - name: Upload result to GitHub Code Scanning + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: snyk-code.sarif From c1393991cdd6a6c42811cef9ba90ce7b27ea3849 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jan 2024 08:34:36 +0000 Subject: [PATCH 12/32] Bump jsonwebtoken from 8.5.1 to 9.0.0 Bumps [jsonwebtoken](https://github.com/auth0/node-jsonwebtoken) from 8.5.1 to 9.0.0. - [Changelog](https://github.com/auth0/node-jsonwebtoken/blob/master/CHANGELOG.md) - [Commits](https://github.com/auth0/node-jsonwebtoken/compare/v8.5.1...v9.0.0) --- updated-dependencies: - dependency-name: jsonwebtoken dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 25 ++++++++++++++----------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 96c52336..e0a003b3 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "http-errors": "^2.0.0", "ioredis": "^5.2.4", "is-ci": "^3.0.1", - "jsonwebtoken": "^8.5.1", + "jsonwebtoken": "^9.0.0", "jwk-to-pem": "^2.0.5", "jwks-rsa": "^3.0.0", "jwt-decode": "^3.1.2", diff --git a/yarn.lock b/yarn.lock index e301ab27..99238f72 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13102,6 +13102,16 @@ jsonwebtoken@^8.5.1: ms "^2.1.1" semver "^5.6.0" +jsonwebtoken@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz#d0faf9ba1cc3a56255fe49c0961a67e520c1926d" + integrity sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw== + dependencies: + jws "^3.2.2" + lodash "^4.17.21" + ms "^2.1.1" + semver "^7.3.8" + jsprim@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-2.0.2.tgz#77ca23dbcd4135cd364800d22ff82c2185803d4d" @@ -17431,10 +17441,10 @@ semver@7.3.4: dependencies: lru-cache "^6.0.0" -semver@7.x: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== +semver@7.x, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" @@ -17443,13 +17453,6 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: - version "7.3.7" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" - integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== - dependencies: - lru-cache "^6.0.0" - send@0.17.2, send@latest: version "0.17.2" resolved "https://registry.yarnpkg.com/send/-/send-0.17.2.tgz#926622f76601c41808012c8bf1688fe3906f7820" From 42927ffdde58db66f671a832980b018421ff7bff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jan 2024 08:34:43 +0000 Subject: [PATCH 13/32] Bump loader-utils from 1.4.0 to 1.4.2 Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.0 to 1.4.2. - [Release notes](https://github.com/webpack/loader-utils/releases) - [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.2/CHANGELOG.md) - [Commits](https://github.com/webpack/loader-utils/compare/v1.4.0...v1.4.2) --- updated-dependencies: - dependency-name: loader-utils dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/yarn.lock b/yarn.lock index e301ab27..eb035949 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13414,24 +13414,15 @@ loader-runner@^4.2.0: integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw== loader-utils@^1.2.3, loader-utils@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613" - integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA== + version "1.4.2" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.2.tgz#29a957f3a63973883eb684f10ffd3d151fec01a3" + integrity sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg== dependencies: big.js "^5.2.2" emojis-list "^3.0.0" json5 "^1.0.1" -loader-utils@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.2.tgz#d6e3b4fb81870721ae4e0868ab11dd638368c129" - integrity sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A== - dependencies: - big.js "^5.2.2" - emojis-list "^3.0.0" - json5 "^2.1.2" - -loader-utils@^2.0.3, loader-utils@^2.0.4: +loader-utils@^2.0.0, loader-utils@^2.0.3, loader-utils@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== @@ -13441,9 +13432,9 @@ loader-utils@^2.0.3, loader-utils@^2.0.4: json5 "^2.1.2" loader-utils@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.0.tgz#bcecc51a7898bee7473d4bc6b845b23af8304d4f" - integrity sha512-HVl9ZqccQihZ7JM85dco1MvO9G+ONvxoGa9rkhzFsneGLKSUg1gJf9bWzhRhcvm2qChhWpebQhP44qxjKIUCaQ== + version "3.2.1" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-3.2.1.tgz#4fb104b599daafd82ef3e1a41fb9265f87e1f576" + integrity sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw== locate-path@^2.0.0: version "2.0.0" From 3ca8277fe0386e4c22952a116f770e8a227236fd Mon Sep 17 00:00:00 2001 From: Sascha Ronnie Daoudia <85792632+Dadudidas@users.noreply.github.com> Date: Wed, 17 Jan 2024 09:38:35 +0100 Subject: [PATCH 14/32] Create codeql.yml Signed-off-by: Sascha Ronnie Daoudia <85792632+Dadudidas@users.noreply.github.com> --- .github/workflows/codeql.yml | 84 ++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 .github/workflows/codeql.yml diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..b0d2eed5 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,84 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '20 21 * * 1' + +jobs: + analyze: + name: Analyze + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners + # Consider using larger runners for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} + permissions: + # required for all workflows + security-events: write + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + language: [ 'javascript-typescript' ] + # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] + # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + + # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs + # queries: security-extended,security-and-quality + + + # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v3 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun + + # If the Autobuild fails above, remove it and uncomment the following three lines. + # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + + # - run: | + # echo "Run, Build Application using script" + # ./location_of_script_within_repo/buildscript.sh + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:${{matrix.language}}" From 7351680a8cb7c636a2592395eefae7c65a4692e5 Mon Sep 17 00:00:00 2001 From: Sascha Ronnie Daoudia <85792632+Dadudidas@users.noreply.github.com> Date: Wed, 17 Jan 2024 14:07:27 +0100 Subject: [PATCH 15/32] Update issue templates --- .github/ISSUE_TEMPLATE/custom.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/custom.md diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 00000000..48d5f81f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,10 @@ +--- +name: Custom issue template +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- + + From a37a975048d5747725a7dc93a0a3f3ccab75dfc3 Mon Sep 17 00:00:00 2001 From: Sascha Ronnie Daoudia <85792632+Dadudidas@users.noreply.github.com> Date: Wed, 17 Jan 2024 14:10:18 +0100 Subject: [PATCH 16/32] Create SECURITY.md Signed-off-by: Sascha Ronnie Daoudia <85792632+Dadudidas@users.noreply.github.com> --- SECURITY.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..034e8480 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,21 @@ +# Security Policy + +## Supported Versions + +Use this section to tell people about which versions of your project are +currently being supported with security updates. + +| Version | Supported | +| ------- | ------------------ | +| 5.1.x | :white_check_mark: | +| 5.0.x | :x: | +| 4.0.x | :white_check_mark: | +| < 4.0 | :x: | + +## Reporting a Vulnerability + +Use this section to tell people how to report a vulnerability. + +Tell them where to go, how often they can expect to get an update on a +reported vulnerability, what to expect if the vulnerability is accepted or +declined, etc. From 7aa9bc4e3d50f53df8819d68105e0cf5da598546 Mon Sep 17 00:00:00 2001 From: Karan Handa Date: Wed, 17 Jan 2024 21:32:38 +0530 Subject: [PATCH 17/32] fix repeated data-testid --- libs/design-system/src/lib/DatePicker/DatePickerCalendar.tsx | 2 +- .../lib/DatePicker/DatePickerRange/DatePickerRangeCalendar.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/design-system/src/lib/DatePicker/DatePickerCalendar.tsx b/libs/design-system/src/lib/DatePicker/DatePickerCalendar.tsx index 34f307bc..c9cd8257 100644 --- a/libs/design-system/src/lib/DatePicker/DatePickerCalendar.tsx +++ b/libs/design-system/src/lib/DatePicker/DatePickerCalendar.tsx @@ -73,7 +73,7 @@ export function DatePickerCalendar({