mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-09 07:25:19 +02:00
Merge remote-tracking branch 'upstream/main' into support-more-email-providers
This commit is contained in:
commit
d121681b1f
21 changed files with 123 additions and 163 deletions
|
@ -2,7 +2,8 @@ import NextAuth from 'next-auth'
|
|||
import type { SessionStrategy, NextAuthOptions } from 'next-auth'
|
||||
import CredentialsProvider from 'next-auth/providers/credentials'
|
||||
import { z } from 'zod'
|
||||
import { PrismaClient, type Prisma } from '@prisma/client'
|
||||
import { PrismaClient, AuthUserRole } from '@prisma/client'
|
||||
import type { Prisma } from '@prisma/client'
|
||||
import { PrismaAdapter } from '@auth/prisma-adapter'
|
||||
import type { SharedType } from '@maybe-finance/shared'
|
||||
import bcrypt from 'bcrypt'
|
||||
|
@ -36,6 +37,7 @@ async function validateCredentials(credentials: any): Promise<z.infer<typeof aut
|
|||
lastName: z.string().optional(),
|
||||
email: z.string().email({ message: 'Invalid email address.' }),
|
||||
password: z.string().min(6),
|
||||
isAdmin: z.boolean().default(false),
|
||||
})
|
||||
|
||||
const parsed = authSchema.safeParse(credentials)
|
||||
|
@ -51,13 +53,15 @@ async function createNewAuthUser(credentials: {
|
|||
lastName: string
|
||||
email: string
|
||||
password: string
|
||||
isAdmin: boolean
|
||||
}): Promise<SharedType.AuthUser> {
|
||||
const { firstName, lastName, email, password } = credentials
|
||||
const { firstName, lastName, email, password, isAdmin } = credentials
|
||||
|
||||
if (!firstName || !lastName) {
|
||||
throw new Error('Both first name and last name are required.')
|
||||
}
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV === 'development'
|
||||
const hashedPassword = await bcrypt.hash(password, 10)
|
||||
return createAuthUser({
|
||||
firstName,
|
||||
|
@ -65,6 +69,7 @@ async function createNewAuthUser(credentials: {
|
|||
name: `${firstName} ${lastName}`,
|
||||
email,
|
||||
password: hashedPassword,
|
||||
role: isAdmin && isDevelopment ? AuthUserRole.admin : AuthUserRole.user,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -94,10 +99,14 @@ export const authOptions = {
|
|||
lastName: { label: 'Last name', type: 'text', placeholder: 'Last name' },
|
||||
email: { label: 'Email', type: 'email', placeholder: 'hello@maybe.co' },
|
||||
password: { label: 'Password', type: 'password' },
|
||||
isAdmin: { label: 'Admin', type: 'checkbox' },
|
||||
},
|
||||
async authorize(credentials) {
|
||||
const { firstName, lastName, email, password } = await validateCredentials(
|
||||
credentials
|
||||
const { firstName, lastName, email, password, isAdmin } = await validateCredentials(
|
||||
{
|
||||
...credentials,
|
||||
isAdmin: Boolean(credentials?.isAdmin),
|
||||
}
|
||||
)
|
||||
|
||||
const existingUser = await getAuthUserByEmail(email)
|
||||
|
@ -114,7 +123,7 @@ export const authOptions = {
|
|||
throw new Error('Invalid credentials provided.')
|
||||
}
|
||||
|
||||
return createNewAuthUser({ firstName, lastName, email, password })
|
||||
return createNewAuthUser({ firstName, lastName, email, password, isAdmin })
|
||||
},
|
||||
}),
|
||||
],
|
||||
|
@ -126,6 +135,7 @@ export const authOptions = {
|
|||
token.firstName = authUser.firstName
|
||||
token.lastName = authUser.lastName
|
||||
token.name = authUser.name
|
||||
token.role = authUser.role
|
||||
}
|
||||
return token
|
||||
},
|
||||
|
@ -136,6 +146,7 @@ export const authOptions = {
|
|||
session.firstName = token.firstName
|
||||
session.lastName = token.lastName
|
||||
session.name = token.name
|
||||
session.role = token.role
|
||||
return session
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, type ReactElement } from 'react'
|
||||
import { Input, InputPassword, Button } from '@maybe-finance/design-system'
|
||||
import { Input, InputPassword, Button, Checkbox } from '@maybe-finance/design-system'
|
||||
import { FullPageLayout } from '@maybe-finance/client/features'
|
||||
import { signIn, useSession } from 'next-auth/react'
|
||||
import { useRouter } from 'next/router'
|
||||
|
@ -15,6 +15,7 @@ export default function RegisterPage() {
|
|||
const [isValid, setIsValid] = useState(false)
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isAdmin, setIsAdmin] = useState(false)
|
||||
|
||||
const { data: session } = useSession()
|
||||
const router = useRouter()
|
||||
|
@ -38,6 +39,7 @@ export default function RegisterPage() {
|
|||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
isAdmin,
|
||||
redirect: false,
|
||||
})
|
||||
|
||||
|
@ -108,6 +110,8 @@ export default function RegisterPage() {
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
<AuthDevTools isAdmin={isAdmin} setIsAdmin={setIsAdmin} />
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!isValid}
|
||||
|
@ -135,6 +139,28 @@ export default function RegisterPage() {
|
|||
)
|
||||
}
|
||||
|
||||
type AuthDevToolsProps = {
|
||||
isAdmin: boolean
|
||||
setIsAdmin: (isAdmin: boolean) => void
|
||||
}
|
||||
|
||||
function AuthDevTools({ isAdmin, setIsAdmin }: AuthDevToolsProps) {
|
||||
return process.env.NODE_ENV === 'development' ? (
|
||||
<div className="my-2 p-2 border border-red-300 rounded-md">
|
||||
<h6 className="flex text-red">
|
||||
Dev Tools <i className="ri-tools-fill ml-1.5" />
|
||||
</h6>
|
||||
<p className="text-sm my-2">
|
||||
This section will NOT show in production and is solely for making testing easier.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<Checkbox checked={isAdmin} onChange={setIsAdmin} label="Create Admin user?" />
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
RegisterPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <FullPageLayout>{page}</FullPageLayout>
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { User } from '@prisma/client'
|
||||
import { InvestmentTransactionCategory, Prisma, PrismaClient } from '@prisma/client'
|
||||
import { AssetClass, InvestmentTransactionCategory, Prisma, PrismaClient } from '@prisma/client'
|
||||
import { createLogger, transports } from 'winston'
|
||||
import { DateTime } from 'luxon'
|
||||
import type {
|
||||
|
@ -202,19 +202,25 @@ describe('insight service', () => {
|
|||
holdings: {
|
||||
create: [
|
||||
{
|
||||
security: { create: { symbol: 'AAPL', plaidType: 'equity' } },
|
||||
security: {
|
||||
create: { symbol: 'AAPL', assetClass: AssetClass.stocks },
|
||||
},
|
||||
quantity: 1,
|
||||
costBasisUser: 100,
|
||||
value: 200,
|
||||
},
|
||||
{
|
||||
security: { create: { symbol: 'NFLX', plaidType: 'equity' } },
|
||||
security: {
|
||||
create: { symbol: 'NFLX', assetClass: AssetClass.stocks },
|
||||
},
|
||||
quantity: 10,
|
||||
costBasisUser: 200,
|
||||
value: 300,
|
||||
},
|
||||
{
|
||||
security: { create: { symbol: 'SHOP', plaidType: 'equity' } },
|
||||
security: {
|
||||
create: { symbol: 'SHOP', assetClass: AssetClass.stocks },
|
||||
},
|
||||
quantity: 2,
|
||||
costBasisUser: 100,
|
||||
value: 50,
|
||||
|
|
|
@ -2,7 +2,6 @@ import type { Subjects } from '@casl/prisma'
|
|||
import { PrismaAbility, accessibleBy } from '@casl/prisma'
|
||||
import type { AbilityClass } from '@casl/ability'
|
||||
import { AbilityBuilder, ForbiddenError } from '@casl/ability'
|
||||
import type { SharedType } from '@maybe-finance/shared'
|
||||
import type {
|
||||
User,
|
||||
Account,
|
||||
|
@ -18,6 +17,7 @@ import type {
|
|||
ProviderInstitution,
|
||||
Plan,
|
||||
} from '@prisma/client'
|
||||
import { AuthUserRole } from '@prisma/client'
|
||||
|
||||
type CRUDActions = 'create' | 'read' | 'update' | 'delete'
|
||||
type AppActions = CRUDActions | 'manage'
|
||||
|
@ -41,13 +41,11 @@ type AppSubjects = PrismaSubjects | 'all'
|
|||
|
||||
type AppAbility = PrismaAbility<[AppActions, AppSubjects]>
|
||||
|
||||
export default function defineAbilityFor(
|
||||
user: (Pick<User, 'id'> & { roles: SharedType.UserRole[] }) | null
|
||||
) {
|
||||
export default function defineAbilityFor(user: (Pick<User, 'id'> & { role: AuthUserRole }) | null) {
|
||||
const { can, build } = new AbilityBuilder(PrismaAbility as AbilityClass<AppAbility>)
|
||||
|
||||
if (user) {
|
||||
if (user.roles.includes('Admin')) {
|
||||
if (user.role === AuthUserRole.admin) {
|
||||
can('manage', 'Account')
|
||||
can('manage', 'AccountConnection')
|
||||
can('manage', 'Valuation')
|
||||
|
|
|
@ -300,15 +300,11 @@ async function getCurrentUser(jwt: NonNullable<Request['user']>) {
|
|||
|
||||
return {
|
||||
...user,
|
||||
role: jwt.role,
|
||||
// TODO: Replace Auth0 concepts with next-auth
|
||||
roles: [],
|
||||
primaryIdentity: {},
|
||||
userMetadata: {},
|
||||
appMetadata: {},
|
||||
// roles: jwt[SharedType.Auth0CustomNamespace.Roles] ?? [],
|
||||
// primaryIdentity: jwt[SharedType.Auth0CustomNamespace.PrimaryIdentity] ?? {},
|
||||
// userMetadata: jwt[SharedType.Auth0CustomNamespace.UserMetadata] ?? {},
|
||||
// appMetadata: jwt[SharedType.Auth0CustomNamespace.AppMetadata] ?? {},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
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 { BullQueue } from '@maybe-finance/server/shared'
|
||||
import { queueService } from '../lib/endpoint'
|
||||
import env from '../../env'
|
||||
import { validateAuthJwt } from '../middleware'
|
||||
|
||||
const router = Router()
|
||||
|
|
|
@ -27,10 +27,10 @@ router.post(
|
|||
resolve: async ({ ctx }) => {
|
||||
ctx.ability.throwUnlessCan('update', 'Institution')
|
||||
|
||||
// Sync all Plaid institutions
|
||||
// Sync all Teller institutions
|
||||
await ctx.queueService
|
||||
.getQueue('sync-institution')
|
||||
.addBulk([{ name: 'sync-plaid-institutions', data: {} }])
|
||||
.addBulk([{ name: 'sync-teller-institutions', data: {} }])
|
||||
|
||||
return { success: true }
|
||||
},
|
||||
|
|
|
@ -1,23 +1,16 @@
|
|||
import Link from 'next/link'
|
||||
import {
|
||||
useAccountConnectionApi,
|
||||
useInstitutionApi,
|
||||
usePlaidApi,
|
||||
BrowserUtil,
|
||||
} from '@maybe-finance/client/shared'
|
||||
import { useAccountConnectionApi, useInstitutionApi } from '@maybe-finance/client/shared'
|
||||
|
||||
export function AccountDevTools() {
|
||||
const { useSandboxQuickAdd } = usePlaidApi()
|
||||
const { useDeleteAllConnections } = useAccountConnectionApi()
|
||||
const { useSyncInstitutions, useDeduplicateInstitutions } = useInstitutionApi()
|
||||
|
||||
const sandboxQuickAdd = useSandboxQuickAdd()
|
||||
const deleteAllConnections = useDeleteAllConnections()
|
||||
const syncInstitutions = useSyncInstitutions()
|
||||
const deduplicateInstitutions = useDeduplicateInstitutions()
|
||||
|
||||
return process.env.NODE_ENV === 'development' ? (
|
||||
<div className="relative mb-12 mx-2 sm:mx-0 p-4 bg-gray-700 rounded-md">
|
||||
<div className="relative mb-12 mx-2 sm:mx-0 p-4 bg-gray-700 rounded-md z-10">
|
||||
<h6 className="flex text-red">
|
||||
Dev Tools <i className="ri-tools-fill ml-1.5" />
|
||||
</h6>
|
||||
|
@ -27,12 +20,6 @@ export function AccountDevTools() {
|
|||
</p>
|
||||
<div className="flex items-center text-sm mt-4">
|
||||
<p className="font-bold">Actions:</p>
|
||||
<button
|
||||
className="underline text-red ml-4"
|
||||
onClick={() => sandboxQuickAdd.mutate()}
|
||||
>
|
||||
Quick add item
|
||||
</button>
|
||||
<button
|
||||
className="underline text-red ml-4"
|
||||
onClick={() => deleteAllConnections.mutate()}
|
||||
|
@ -42,12 +29,6 @@ export function AccountDevTools() {
|
|||
<Link href="http://localhost:3333/admin/bullmq" className="underline text-red ml-4">
|
||||
BullMQ Dashboard
|
||||
</Link>
|
||||
<button
|
||||
className="underline text-red ml-4"
|
||||
onClick={() => BrowserUtil.copyToClipboard((window as any).JWT)}
|
||||
>
|
||||
Copy JWT
|
||||
</button>
|
||||
<button
|
||||
className="underline text-red ml-4"
|
||||
onClick={() => syncInstitutions.mutate()}
|
||||
|
|
|
@ -57,7 +57,7 @@ export default function AccountTypeSelector({
|
|||
type="text"
|
||||
placeholder="Search for an institution"
|
||||
fixedLeftOverride={<RiSearchLine className="w-5 h-5" />}
|
||||
inputClassName="pl-10"
|
||||
inputClassName="pl-11"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
ref={inputRef}
|
||||
|
|
|
@ -641,14 +641,7 @@ export class InsightService implements IInsightService {
|
|||
INNER JOIN (
|
||||
SELECT
|
||||
id,
|
||||
CASE
|
||||
-- plaid
|
||||
WHEN plaid_type IN ('equity', 'etf', 'mutual fund', 'derivative') THEN 'stocks'
|
||||
WHEN plaid_type IN ('fixed income') THEN 'fixed_income'
|
||||
WHEN plaid_type IN ('cash', 'loan') THEN 'cash'
|
||||
WHEN plaid_type IN ('cryptocurrency') THEN 'crypto'
|
||||
ELSE 'other'
|
||||
END AS "asset_class"
|
||||
asset_class
|
||||
FROM
|
||||
"security"
|
||||
) s ON s.id = h.security_id
|
||||
|
@ -694,14 +687,7 @@ export class InsightService implements IInsightService {
|
|||
INNER JOIN security s ON s.id = h.security_id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT
|
||||
CASE
|
||||
-- plaid
|
||||
WHEN s.plaid_type IN ('equity', 'etf', 'mutual fund', 'derivative') THEN 'stocks'
|
||||
WHEN s.plaid_type IN ('fixed income') THEN 'fixed_income'
|
||||
WHEN s.plaid_type IN ('cash', 'loan') THEN 'cash'
|
||||
WHEN s.plaid_type IN ('cryptocurrency') THEN 'crypto'
|
||||
ELSE 'other'
|
||||
END AS "category"
|
||||
asset_class AS "category"
|
||||
) x ON TRUE
|
||||
WHERE
|
||||
h.account_id IN ${accountIds}
|
||||
|
@ -828,28 +814,21 @@ export class InsightService implements IInsightService {
|
|||
UNION ALL
|
||||
-- investment accounts
|
||||
SELECT
|
||||
s.asset_type,
|
||||
s.asset_class AS "asset_type",
|
||||
SUM(h.value) AS "amount"
|
||||
FROM
|
||||
holdings_enriched h
|
||||
INNER JOIN (
|
||||
SELECT
|
||||
id,
|
||||
CASE
|
||||
-- plaid
|
||||
WHEN plaid_type IN ('equity', 'etf', 'mutual fund', 'derivative') THEN 'stocks'
|
||||
WHEN plaid_type IN ('fixed income') THEN 'bonds'
|
||||
WHEN plaid_type IN ('cash', 'loan') THEN 'cash'
|
||||
WHEN plaid_type IN ('cryptocurrency') THEN 'crypto'
|
||||
ELSE 'other'
|
||||
END AS "asset_type"
|
||||
asset_class
|
||||
FROM
|
||||
"security"
|
||||
) s ON s.id = h.security_id
|
||||
WHERE
|
||||
h.account_id IN ${pAccountIds}
|
||||
GROUP BY
|
||||
s.asset_type
|
||||
s.asset_class
|
||||
) x
|
||||
GROUP BY
|
||||
1
|
||||
|
|
|
@ -33,7 +33,7 @@ const PROJECTION_ASSET_PARAMS: {
|
|||
[type in SharedType.ProjectionAssetType]: [mean: Decimal.Value, stddev: Decimal.Value]
|
||||
} = {
|
||||
stocks: ['0.05', '0.186'],
|
||||
bonds: ['0.02', '0.052'],
|
||||
fixed_income: ['0.02', '0.052'],
|
||||
cash: ['-0.02', '0.05'],
|
||||
crypto: ['1.0', '1.0'],
|
||||
property: ['0.1', '0.2'],
|
||||
|
|
|
@ -100,10 +100,7 @@ export class TellerETL implements IETL<Connection, TellerRawData, TellerData> {
|
|||
|
||||
const accounts = await this._extractAccounts(accessToken)
|
||||
|
||||
const transactions = await this._extractTransactions(
|
||||
accessToken,
|
||||
accounts.map((a) => a.id)
|
||||
)
|
||||
const transactions = await this._extractTransactions(accessToken, accounts)
|
||||
|
||||
this.logger.info(
|
||||
`Extracted Teller data for customer ${user.tellerUserId} accounts=${accounts.length} transactions=${transactions.length}`,
|
||||
|
@ -196,26 +193,26 @@ export class TellerETL implements IETL<Connection, TellerRawData, TellerData> {
|
|||
]
|
||||
}
|
||||
|
||||
private async _extractTransactions(accessToken: string, accountIds: string[]) {
|
||||
private async _extractTransactions(
|
||||
accessToken: string,
|
||||
tellerAccounts: TellerTypes.GetAccountsResponse
|
||||
) {
|
||||
const accountTransactions = await Promise.all(
|
||||
accountIds.map(async (accountId) => {
|
||||
const account = await this.prisma.account.findFirst({
|
||||
where: {
|
||||
tellerAccountId: accountId,
|
||||
},
|
||||
})
|
||||
tellerAccounts.map(async (tellerAccount) => {
|
||||
const type = TellerUtil.getType(tellerAccount.type)
|
||||
const classification = AccountUtil.getClassification(type)
|
||||
|
||||
const transactions = await SharedUtil.withRetry(
|
||||
() =>
|
||||
this.teller.getTransactions({
|
||||
accountId,
|
||||
accountId: tellerAccount.id,
|
||||
accessToken,
|
||||
}),
|
||||
{
|
||||
maxRetries: 3,
|
||||
}
|
||||
)
|
||||
if (account!.classification === AccountClassification.asset) {
|
||||
if (classification === AccountClassification.asset) {
|
||||
transactions.forEach((t) => {
|
||||
t.amount = String(Number(t.amount) * -1)
|
||||
})
|
||||
|
@ -277,7 +274,7 @@ export class TellerETL implements IETL<Connection, TellerRawData, TellerData> {
|
|||
pending = EXCLUDED.pending,
|
||||
merchant_name = EXCLUDED.merchant_name,
|
||||
teller_type = EXCLUDED.teller_type,
|
||||
teller_category = EXCLUDED.teller_category;
|
||||
teller_category = EXCLUDED.teller_category,
|
||||
category = EXCLUDED.category;
|
||||
`
|
||||
})
|
||||
|
|
|
@ -70,6 +70,7 @@ export class InMemoryQueueFactory implements IQueueFactory {
|
|||
constructor(
|
||||
private readonly ignoreJobNames: string[] = [
|
||||
'sync-all-securities',
|
||||
'sync-teller-institutions',
|
||||
'sync-plaid-institutions',
|
||||
'trial-reminders',
|
||||
'send-email',
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
import jwks from 'jwks-rsa'
|
||||
import jwt from 'jsonwebtoken'
|
||||
import { SharedType } from '@maybe-finance/shared'
|
||||
|
||||
export const verifyRoleClaims = (claims, role: SharedType.UserRole) => {
|
||||
const customRoleClaim = claims[SharedType.Auth0CustomNamespace.Roles]
|
||||
|
||||
return customRoleClaim && Array.isArray(customRoleClaim) && customRoleClaim.includes(role)
|
||||
}
|
||||
|
||||
export async function validateRS256JWT(
|
||||
token: string,
|
||||
domain: string,
|
||||
audience: string
|
||||
): Promise<{
|
||||
authId: string
|
||||
userMetadata: SharedType.MaybeUserMetadata
|
||||
appMetadata: SharedType.MaybeAppMetadata
|
||||
}> {
|
||||
const jwksClient = jwks({
|
||||
rateLimit: true,
|
||||
jwksUri: `https://${domain}/.well-known/jwks.json`,
|
||||
})
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!token) reject('No token provided')
|
||||
|
||||
const parts = token.split(' ')
|
||||
|
||||
if (!parts || parts.length !== 2) reject('JWT must be in format: Bearer <token>')
|
||||
if (parts[0] !== 'Bearer') reject('JWT must be in format: Bearer <token>')
|
||||
|
||||
const rawToken = parts[1]
|
||||
|
||||
jwt.verify(
|
||||
rawToken,
|
||||
(header, cb) => {
|
||||
jwksClient
|
||||
.getSigningKey(header.kid)
|
||||
.then((key) => cb(null, key.getPublicKey()))
|
||||
.catch((err) => cb(err))
|
||||
},
|
||||
{
|
||||
audience,
|
||||
issuer: `https://${domain}/`,
|
||||
algorithms: ['RS256'],
|
||||
},
|
||||
(err, payload) => {
|
||||
if (err) return reject(err)
|
||||
if (typeof payload !== 'object') return reject('payload not an object')
|
||||
|
||||
resolve({
|
||||
authId: payload.sub!,
|
||||
appMetadata: payload[SharedType.Auth0CustomNamespace.AppMetadata] ?? {},
|
||||
userMetadata: payload[SharedType.Auth0CustomNamespace.UserMetadata] ?? {},
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
export * as AuthUtil from './auth-utils'
|
||||
export * as DbUtil from './db-utils'
|
||||
export * as PlaidUtil from './plaid-utils'
|
||||
export * as TellerUtil from './teller-utils'
|
||||
|
|
|
@ -56,7 +56,13 @@ export type PlanProjectionResponse = {
|
|||
}[]
|
||||
}
|
||||
|
||||
export type ProjectionAssetType = 'stocks' | 'bonds' | 'cash' | 'crypto' | 'property' | 'other'
|
||||
export type ProjectionAssetType =
|
||||
| 'stocks'
|
||||
| 'fixed_income'
|
||||
| 'cash'
|
||||
| 'crypto'
|
||||
| 'property'
|
||||
| 'other'
|
||||
export type ProjectionLiabilityType = 'credit' | 'loan' | 'other'
|
||||
|
||||
export type PlanInsights = {
|
||||
|
|
|
@ -170,9 +170,6 @@ export type MaybeUserMetadata = Partial<{
|
|||
// Maybe's "normalized" Auth0 `user.app_metadata` object
|
||||
export type MaybeAppMetadata = {}
|
||||
|
||||
// The custom roles we have defined in Auth0
|
||||
export type UserRole = 'Admin' | 'CIUser'
|
||||
|
||||
export type PrimaryAuth0Identity = Partial<{
|
||||
connection: string
|
||||
provider: string
|
||||
|
@ -183,7 +180,6 @@ export type PrimaryAuth0Identity = Partial<{
|
|||
export type MaybeCustomClaims = {
|
||||
[Auth0CustomNamespace.Email]?: string | null
|
||||
[Auth0CustomNamespace.Picture]?: string | null
|
||||
[Auth0CustomNamespace.Roles]?: UserRole[]
|
||||
[Auth0CustomNamespace.UserMetadata]?: MaybeUserMetadata
|
||||
[Auth0CustomNamespace.AppMetadata]?: MaybeAppMetadata
|
||||
[Auth0CustomNamespace.PrimaryIdentity]?: PrimaryAuth0Identity
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
-- CreateEnum
|
||||
CREATE TYPE "AssetClass" AS ENUM ('cash', 'crypto', 'fixed_income', 'stocks', 'other');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "security"
|
||||
ADD COLUMN "asset_class" "AssetClass" NOT NULL DEFAULT 'other';
|
|
@ -0,0 +1,5 @@
|
|||
-- CreateEnum
|
||||
CREATE TYPE "AuthUserRole" AS ENUM ('user', 'admin', 'ci');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "auth_user" ADD COLUMN "role" "AuthUserRole" NOT NULL DEFAULT 'user';
|
|
@ -243,18 +243,27 @@ model InvestmentTransaction {
|
|||
@@map("investment_transaction")
|
||||
}
|
||||
|
||||
enum AssetClass {
|
||||
cash
|
||||
crypto
|
||||
fixed_income
|
||||
stocks
|
||||
other
|
||||
}
|
||||
|
||||
model Security {
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||
id Int @id @default(autoincrement())
|
||||
createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||
name String?
|
||||
symbol String?
|
||||
cusip String?
|
||||
isin String?
|
||||
sharesPerContract Decimal? @map("shares_per_contract") @db.Decimal(36, 18)
|
||||
currencyCode String @default("USD") @map("currency_code")
|
||||
pricingLastSyncedAt DateTime? @map("pricing_last_synced_at") @db.Timestamptz(6)
|
||||
isBrokerageCash Boolean @default(false) @map("is_brokerage_cash")
|
||||
sharesPerContract Decimal? @map("shares_per_contract") @db.Decimal(36, 18)
|
||||
currencyCode String @default("USD") @map("currency_code")
|
||||
pricingLastSyncedAt DateTime? @map("pricing_last_synced_at") @db.Timestamptz(6)
|
||||
isBrokerageCash Boolean @default(false) @map("is_brokerage_cash")
|
||||
assetClass AssetClass @default(other) @map("asset_class")
|
||||
|
||||
// plaid data
|
||||
plaidSecurityId String? @unique @map("plaid_security_id")
|
||||
|
@ -564,6 +573,12 @@ model AuthAccount {
|
|||
@@map("auth_account")
|
||||
}
|
||||
|
||||
enum AuthUserRole {
|
||||
user
|
||||
admin
|
||||
ci
|
||||
}
|
||||
|
||||
model AuthUser {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
|
@ -573,6 +588,7 @@ model AuthUser {
|
|||
emailVerified DateTime? @map("email_verified")
|
||||
password String?
|
||||
image String?
|
||||
role AuthUserRole @default(user)
|
||||
accounts AuthAccount[]
|
||||
sessions AuthSession[]
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue