mirror of
https://github.com/maybe-finance/maybe.git
synced 2025-08-09 07:25:19 +02:00
fix dev tools and add admin role
This commit is contained in:
parent
0d5d7d5a7f
commit
f9768f7e08
14 changed files with 67 additions and 109 deletions
|
@ -2,7 +2,8 @@ import NextAuth from 'next-auth'
|
||||||
import type { SessionStrategy, NextAuthOptions } from 'next-auth'
|
import type { SessionStrategy, NextAuthOptions } from 'next-auth'
|
||||||
import CredentialsProvider from 'next-auth/providers/credentials'
|
import CredentialsProvider from 'next-auth/providers/credentials'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { PrismaClient, type Prisma } from '@prisma/client'
|
import { PrismaClient, AuthUserRole } from '@prisma/client'
|
||||||
|
import type { Prisma } from '@prisma/client'
|
||||||
import { PrismaAdapter } from '@auth/prisma-adapter'
|
import { PrismaAdapter } from '@auth/prisma-adapter'
|
||||||
import type { SharedType } from '@maybe-finance/shared'
|
import type { SharedType } from '@maybe-finance/shared'
|
||||||
import bcrypt from 'bcrypt'
|
import bcrypt from 'bcrypt'
|
||||||
|
@ -36,6 +37,7 @@ async function validateCredentials(credentials: any): Promise<z.infer<typeof aut
|
||||||
lastName: z.string().optional(),
|
lastName: z.string().optional(),
|
||||||
email: z.string().email({ message: 'Invalid email address.' }),
|
email: z.string().email({ message: 'Invalid email address.' }),
|
||||||
password: z.string().min(6),
|
password: z.string().min(6),
|
||||||
|
isAdmin: z.boolean().default(false),
|
||||||
})
|
})
|
||||||
|
|
||||||
const parsed = authSchema.safeParse(credentials)
|
const parsed = authSchema.safeParse(credentials)
|
||||||
|
@ -51,13 +53,15 @@ async function createNewAuthUser(credentials: {
|
||||||
lastName: string
|
lastName: string
|
||||||
email: string
|
email: string
|
||||||
password: string
|
password: string
|
||||||
|
isAdmin: boolean
|
||||||
}): Promise<SharedType.AuthUser> {
|
}): Promise<SharedType.AuthUser> {
|
||||||
const { firstName, lastName, email, password } = credentials
|
const { firstName, lastName, email, password, isAdmin } = credentials
|
||||||
|
|
||||||
if (!firstName || !lastName) {
|
if (!firstName || !lastName) {
|
||||||
throw new Error('Both first name and last name are required.')
|
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)
|
const hashedPassword = await bcrypt.hash(password, 10)
|
||||||
return createAuthUser({
|
return createAuthUser({
|
||||||
firstName,
|
firstName,
|
||||||
|
@ -65,6 +69,7 @@ async function createNewAuthUser(credentials: {
|
||||||
name: `${firstName} ${lastName}`,
|
name: `${firstName} ${lastName}`,
|
||||||
email,
|
email,
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
|
role: isAdmin && isDevelopment ? AuthUserRole.admin : AuthUserRole.user,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,10 +99,15 @@ export const authOptions = {
|
||||||
lastName: { label: 'Last name', type: 'text', placeholder: 'Last name' },
|
lastName: { label: 'Last name', type: 'text', placeholder: 'Last name' },
|
||||||
email: { label: 'Email', type: 'email', placeholder: 'hello@maybe.co' },
|
email: { label: 'Email', type: 'email', placeholder: 'hello@maybe.co' },
|
||||||
password: { label: 'Password', type: 'password' },
|
password: { label: 'Password', type: 'password' },
|
||||||
|
isAdmin: { label: 'Admin', type: 'checkbox' },
|
||||||
},
|
},
|
||||||
async authorize(credentials) {
|
async authorize(credentials) {
|
||||||
const { firstName, lastName, email, password } = await validateCredentials(
|
// Take credentials and convert the isAdmin string to a boolean
|
||||||
credentials
|
const { firstName, lastName, email, password, isAdmin } = await validateCredentials(
|
||||||
|
{
|
||||||
|
...credentials,
|
||||||
|
isAdmin: Boolean(credentials?.isAdmin),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const existingUser = await getAuthUserByEmail(email)
|
const existingUser = await getAuthUserByEmail(email)
|
||||||
|
@ -114,7 +124,7 @@ export const authOptions = {
|
||||||
throw new Error('Invalid credentials provided.')
|
throw new Error('Invalid credentials provided.')
|
||||||
}
|
}
|
||||||
|
|
||||||
return createNewAuthUser({ firstName, lastName, email, password })
|
return createNewAuthUser({ firstName, lastName, email, password, isAdmin })
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
@ -126,6 +136,7 @@ export const authOptions = {
|
||||||
token.firstName = authUser.firstName
|
token.firstName = authUser.firstName
|
||||||
token.lastName = authUser.lastName
|
token.lastName = authUser.lastName
|
||||||
token.name = authUser.name
|
token.name = authUser.name
|
||||||
|
token.role = authUser.role
|
||||||
}
|
}
|
||||||
return token
|
return token
|
||||||
},
|
},
|
||||||
|
@ -136,6 +147,7 @@ export const authOptions = {
|
||||||
session.firstName = token.firstName
|
session.firstName = token.firstName
|
||||||
session.lastName = token.lastName
|
session.lastName = token.lastName
|
||||||
session.name = token.name
|
session.name = token.name
|
||||||
|
session.role = token.role
|
||||||
return session
|
return session
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, type ReactElement } from 'react'
|
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 { FullPageLayout } from '@maybe-finance/client/features'
|
||||||
import { signIn, useSession } from 'next-auth/react'
|
import { signIn, useSession } from 'next-auth/react'
|
||||||
import { useRouter } from 'next/router'
|
import { useRouter } from 'next/router'
|
||||||
|
@ -15,6 +15,7 @@ export default function RegisterPage() {
|
||||||
const [isValid, setIsValid] = useState(false)
|
const [isValid, setIsValid] = useState(false)
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
const [errorMessage, setErrorMessage] = useState<string | null>(null)
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [isAdmin, setIsAdmin] = useState(false)
|
||||||
|
|
||||||
const { data: session } = useSession()
|
const { data: session } = useSession()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
@ -38,6 +39,7 @@ export default function RegisterPage() {
|
||||||
password,
|
password,
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
|
isAdmin,
|
||||||
redirect: false,
|
redirect: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -108,6 +110,8 @@ export default function RegisterPage() {
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
<AuthDevTools isAdmin={isAdmin} setIsAdmin={setIsAdmin} />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={!isValid}
|
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) {
|
RegisterPage.getLayout = function getLayout(page: ReactElement) {
|
||||||
return <FullPageLayout>{page}</FullPageLayout>
|
return <FullPageLayout>{page}</FullPageLayout>
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ import type { Subjects } from '@casl/prisma'
|
||||||
import { PrismaAbility, accessibleBy } from '@casl/prisma'
|
import { PrismaAbility, accessibleBy } from '@casl/prisma'
|
||||||
import type { AbilityClass } from '@casl/ability'
|
import type { AbilityClass } from '@casl/ability'
|
||||||
import { AbilityBuilder, ForbiddenError } from '@casl/ability'
|
import { AbilityBuilder, ForbiddenError } from '@casl/ability'
|
||||||
import type { SharedType } from '@maybe-finance/shared'
|
|
||||||
import type {
|
import type {
|
||||||
User,
|
User,
|
||||||
Account,
|
Account,
|
||||||
|
@ -18,6 +17,7 @@ import type {
|
||||||
ProviderInstitution,
|
ProviderInstitution,
|
||||||
Plan,
|
Plan,
|
||||||
} from '@prisma/client'
|
} from '@prisma/client'
|
||||||
|
import { AuthUserRole } from '@prisma/client'
|
||||||
|
|
||||||
type CRUDActions = 'create' | 'read' | 'update' | 'delete'
|
type CRUDActions = 'create' | 'read' | 'update' | 'delete'
|
||||||
type AppActions = CRUDActions | 'manage'
|
type AppActions = CRUDActions | 'manage'
|
||||||
|
@ -41,13 +41,11 @@ type AppSubjects = PrismaSubjects | 'all'
|
||||||
|
|
||||||
type AppAbility = PrismaAbility<[AppActions, AppSubjects]>
|
type AppAbility = PrismaAbility<[AppActions, AppSubjects]>
|
||||||
|
|
||||||
export default function defineAbilityFor(
|
export default function defineAbilityFor(user: (Pick<User, 'id'> & { role: AuthUserRole }) | null) {
|
||||||
user: (Pick<User, 'id'> & { roles: SharedType.UserRole[] }) | null
|
|
||||||
) {
|
|
||||||
const { can, build } = new AbilityBuilder(PrismaAbility as AbilityClass<AppAbility>)
|
const { can, build } = new AbilityBuilder(PrismaAbility as AbilityClass<AppAbility>)
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
if (user.roles.includes('Admin')) {
|
if (user.role === AuthUserRole.admin) {
|
||||||
can('manage', 'Account')
|
can('manage', 'Account')
|
||||||
can('manage', 'AccountConnection')
|
can('manage', 'AccountConnection')
|
||||||
can('manage', 'Valuation')
|
can('manage', 'Valuation')
|
||||||
|
|
|
@ -301,15 +301,11 @@ async function getCurrentUser(jwt: NonNullable<Request['user']>) {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...user,
|
...user,
|
||||||
|
role: jwt.role,
|
||||||
// TODO: Replace Auth0 concepts with next-auth
|
// TODO: Replace Auth0 concepts with next-auth
|
||||||
roles: [],
|
|
||||||
primaryIdentity: {},
|
primaryIdentity: {},
|
||||||
userMetadata: {},
|
userMetadata: {},
|
||||||
appMetadata: {},
|
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 { Router } from 'express'
|
||||||
import { auth, claimCheck } from 'express-openid-connect'
|
|
||||||
import { createBullBoard } from '@bull-board/api'
|
import { createBullBoard } from '@bull-board/api'
|
||||||
import { BullAdapter } from '@bull-board/api/bullAdapter'
|
import { BullAdapter } from '@bull-board/api/bullAdapter'
|
||||||
import { ExpressAdapter } from '@bull-board/express'
|
import { ExpressAdapter } from '@bull-board/express'
|
||||||
import { AuthUtil, BullQueue } from '@maybe-finance/server/shared'
|
import { BullQueue } from '@maybe-finance/server/shared'
|
||||||
import { SharedType } from '@maybe-finance/shared'
|
|
||||||
import { queueService } from '../lib/endpoint'
|
import { queueService } from '../lib/endpoint'
|
||||||
import env from '../../env'
|
|
||||||
import { validateAuthJwt } from '../middleware'
|
import { validateAuthJwt } from '../middleware'
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
|
@ -27,10 +27,10 @@ router.post(
|
||||||
resolve: async ({ ctx }) => {
|
resolve: async ({ ctx }) => {
|
||||||
ctx.ability.throwUnlessCan('update', 'Institution')
|
ctx.ability.throwUnlessCan('update', 'Institution')
|
||||||
|
|
||||||
// Sync all Plaid institutions
|
// Sync all Teller institutions
|
||||||
await ctx.queueService
|
await ctx.queueService
|
||||||
.getQueue('sync-institution')
|
.getQueue('sync-institution')
|
||||||
.addBulk([{ name: 'sync-plaid-institutions', data: {} }])
|
.addBulk([{ name: 'sync-teller-institutions', data: {} }])
|
||||||
|
|
||||||
return { success: true }
|
return { success: true }
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,23 +1,16 @@
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import {
|
import { useAccountConnectionApi, useInstitutionApi } from '@maybe-finance/client/shared'
|
||||||
useAccountConnectionApi,
|
|
||||||
useInstitutionApi,
|
|
||||||
usePlaidApi,
|
|
||||||
BrowserUtil,
|
|
||||||
} from '@maybe-finance/client/shared'
|
|
||||||
|
|
||||||
export function AccountDevTools() {
|
export function AccountDevTools() {
|
||||||
const { useSandboxQuickAdd } = usePlaidApi()
|
|
||||||
const { useDeleteAllConnections } = useAccountConnectionApi()
|
const { useDeleteAllConnections } = useAccountConnectionApi()
|
||||||
const { useSyncInstitutions, useDeduplicateInstitutions } = useInstitutionApi()
|
const { useSyncInstitutions, useDeduplicateInstitutions } = useInstitutionApi()
|
||||||
|
|
||||||
const sandboxQuickAdd = useSandboxQuickAdd()
|
|
||||||
const deleteAllConnections = useDeleteAllConnections()
|
const deleteAllConnections = useDeleteAllConnections()
|
||||||
const syncInstitutions = useSyncInstitutions()
|
const syncInstitutions = useSyncInstitutions()
|
||||||
const deduplicateInstitutions = useDeduplicateInstitutions()
|
const deduplicateInstitutions = useDeduplicateInstitutions()
|
||||||
|
|
||||||
return process.env.NODE_ENV === 'development' ? (
|
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">
|
<h6 className="flex text-red">
|
||||||
Dev Tools <i className="ri-tools-fill ml-1.5" />
|
Dev Tools <i className="ri-tools-fill ml-1.5" />
|
||||||
</h6>
|
</h6>
|
||||||
|
@ -27,12 +20,6 @@ export function AccountDevTools() {
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center text-sm mt-4">
|
<div className="flex items-center text-sm mt-4">
|
||||||
<p className="font-bold">Actions:</p>
|
<p className="font-bold">Actions:</p>
|
||||||
<button
|
|
||||||
className="underline text-red ml-4"
|
|
||||||
onClick={() => sandboxQuickAdd.mutate()}
|
|
||||||
>
|
|
||||||
Quick add item
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
className="underline text-red ml-4"
|
className="underline text-red ml-4"
|
||||||
onClick={() => deleteAllConnections.mutate()}
|
onClick={() => deleteAllConnections.mutate()}
|
||||||
|
@ -42,12 +29,6 @@ export function AccountDevTools() {
|
||||||
<Link href="http://localhost:3333/admin/bullmq" className="underline text-red ml-4">
|
<Link href="http://localhost:3333/admin/bullmq" className="underline text-red ml-4">
|
||||||
BullMQ Dashboard
|
BullMQ Dashboard
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
|
||||||
className="underline text-red ml-4"
|
|
||||||
onClick={() => BrowserUtil.copyToClipboard((window as any).JWT)}
|
|
||||||
>
|
|
||||||
Copy JWT
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
className="underline text-red ml-4"
|
className="underline text-red ml-4"
|
||||||
onClick={() => syncInstitutions.mutate()}
|
onClick={() => syncInstitutions.mutate()}
|
||||||
|
|
|
@ -57,7 +57,7 @@ export default function AccountTypeSelector({
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search for an institution"
|
placeholder="Search for an institution"
|
||||||
fixedLeftOverride={<RiSearchLine className="w-5 h-5" />}
|
fixedLeftOverride={<RiSearchLine className="w-5 h-5" />}
|
||||||
inputClassName="pl-10"
|
inputClassName="pl-11"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
|
|
|
@ -70,6 +70,7 @@ export class InMemoryQueueFactory implements IQueueFactory {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly ignoreJobNames: string[] = [
|
private readonly ignoreJobNames: string[] = [
|
||||||
'sync-all-securities',
|
'sync-all-securities',
|
||||||
|
'sync-teller-institutions',
|
||||||
'sync-plaid-institutions',
|
'sync-plaid-institutions',
|
||||||
'trial-reminders',
|
'trial-reminders',
|
||||||
'send-email',
|
'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 DbUtil from './db-utils'
|
||||||
export * as PlaidUtil from './plaid-utils'
|
export * as PlaidUtil from './plaid-utils'
|
||||||
export * as TellerUtil from './teller-utils'
|
export * as TellerUtil from './teller-utils'
|
||||||
|
|
|
@ -170,9 +170,6 @@ export type MaybeUserMetadata = Partial<{
|
||||||
// Maybe's "normalized" Auth0 `user.app_metadata` object
|
// Maybe's "normalized" Auth0 `user.app_metadata` object
|
||||||
export type MaybeAppMetadata = {}
|
export type MaybeAppMetadata = {}
|
||||||
|
|
||||||
// The custom roles we have defined in Auth0
|
|
||||||
export type UserRole = 'Admin' | 'CIUser'
|
|
||||||
|
|
||||||
export type PrimaryAuth0Identity = Partial<{
|
export type PrimaryAuth0Identity = Partial<{
|
||||||
connection: string
|
connection: string
|
||||||
provider: string
|
provider: string
|
||||||
|
@ -183,7 +180,6 @@ export type PrimaryAuth0Identity = Partial<{
|
||||||
export type MaybeCustomClaims = {
|
export type MaybeCustomClaims = {
|
||||||
[Auth0CustomNamespace.Email]?: string | null
|
[Auth0CustomNamespace.Email]?: string | null
|
||||||
[Auth0CustomNamespace.Picture]?: string | null
|
[Auth0CustomNamespace.Picture]?: string | null
|
||||||
[Auth0CustomNamespace.Roles]?: UserRole[]
|
|
||||||
[Auth0CustomNamespace.UserMetadata]?: MaybeUserMetadata
|
[Auth0CustomNamespace.UserMetadata]?: MaybeUserMetadata
|
||||||
[Auth0CustomNamespace.AppMetadata]?: MaybeAppMetadata
|
[Auth0CustomNamespace.AppMetadata]?: MaybeAppMetadata
|
||||||
[Auth0CustomNamespace.PrimaryIdentity]?: PrimaryAuth0Identity
|
[Auth0CustomNamespace.PrimaryIdentity]?: PrimaryAuth0Identity
|
||||||
|
|
|
@ -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';
|
|
@ -564,6 +564,12 @@ model AuthAccount {
|
||||||
@@map("auth_account")
|
@@map("auth_account")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AuthUserRole {
|
||||||
|
user
|
||||||
|
admin
|
||||||
|
ci
|
||||||
|
}
|
||||||
|
|
||||||
model AuthUser {
|
model AuthUser {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
name String?
|
name String?
|
||||||
|
@ -573,6 +579,7 @@ model AuthUser {
|
||||||
emailVerified DateTime? @map("email_verified")
|
emailVerified DateTime? @map("email_verified")
|
||||||
password String?
|
password String?
|
||||||
image String?
|
image String?
|
||||||
|
role AuthUserRole @default(user)
|
||||||
accounts AuthAccount[]
|
accounts AuthAccount[]
|
||||||
sessions AuthSession[]
|
sessions AuthSession[]
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue