Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Admin User Role #107

Merged
merged 4 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion apps/api/app/api/markets/[id]/options/[optionId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { auth } from '@play-money/auth'
import { CommentNotFoundError } from '@play-money/comments/lib/exceptions'
import { getMarket } from '@play-money/markets/lib/getMarket'
import { updateMarketOption } from '@play-money/markets/lib/updateMarketOption'
import { canModifyMarket } from '@play-money/markets/rules'
import { getUserById } from '@play-money/users/lib/getUserById'
import schema from './schema'

export const dynamic = 'force-dynamic'
Expand All @@ -24,7 +26,9 @@ export async function PATCH(
const { name, color } = schema.patch.requestBody.transform(stripUndefined).parse(body)

const market = await getMarket({ id })
if (market.createdBy !== session.user.id) {
const user = await getUserById({ id: session.user.id })

if (!canModifyMarket({ market, user })) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

Expand Down
10 changes: 10 additions & 0 deletions apps/api/app/api/markets/[id]/resolve/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { NextResponse } from 'next/server'
import type { SchemaResponse } from '@play-money/api-helpers'
import { auth } from '@play-money/auth'
import { getMarket } from '@play-money/markets/lib/getMarket'
import { resolveMarket } from '@play-money/markets/lib/resolveMarket'
import { canModifyMarket } from '@play-money/markets/rules'
import { getUserById } from '@play-money/users/lib/getUserById'
import schema from './schema'

export const dynamic = 'force-dynamic'
Expand All @@ -22,6 +25,13 @@ export async function POST(
const body = (await req.json()) as unknown
const { optionId, supportingLink } = schema.post.requestBody.parse(body)

const market = await getMarket({ id, extended: true })
const resolvingUser = await getUserById({ id: session.user.id })

if (!canModifyMarket({ market, user: resolvingUser })) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

await resolveMarket({
resolverId: session.user.id,
marketId: id,
Expand Down
10 changes: 7 additions & 3 deletions apps/api/app/api/markets/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { auth } from '@play-money/auth'
import { CommentNotFoundError } from '@play-money/comments/lib/exceptions'
import { getMarket } from '@play-money/markets/lib/getMarket'
import { updateMarket } from '@play-money/markets/lib/updateMarket'
import { canModifyMarket } from '@play-money/markets/rules'
import { getUserById } from '@play-money/users/lib/getUserById'
import schema from './schema'

export const dynamic = 'force-dynamic'
Expand Down Expand Up @@ -49,13 +51,15 @@ export async function PATCH(
const { question, description, closeDate, tags } = schema.patch.requestBody.transform(stripUndefined).parse(body)

const market = await getMarket({ id })
if (market.createdBy !== session.user.id) {
const user = await getUserById({ id: session.user.id })

if (!canModifyMarket({ market, user })) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}

const updatedComment = await updateMarket({ id, question, description, closeDate, tags })
const updatedMarket = await updateMarket({ id, question, description, closeDate, tags })

return NextResponse.json(updatedComment)
return NextResponse.json(updatedMarket)
} catch (error) {
if (error instanceof CommentNotFoundError) {
return NextResponse.json({ error: error.message }, { status: 404 })
Expand Down
2 changes: 2 additions & 0 deletions apps/api/app/api/users/me/resource-viewed/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export async function POST(req: Request): Promise<SchemaResponse<typeof schema.p

if (resourceType === 'MARKET') {
await updateNotificationsRead({ userId: session.user.id, marketId: resourceId })
} else if (resourceType === 'LIST') {
await updateNotificationsRead({ userId: session.user.id, listId: resourceId })
}

return NextResponse.json({ success: true })
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- CreateEnum
CREATE TYPE "UserRole" AS ENUM ('USER', 'ADMIN');

-- AlterTable
ALTER TABLE "User" ADD COLUMN "role" "UserRole" NOT NULL DEFAULT 'USER';
1 change: 1 addition & 0 deletions packages/database/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export function mockUser(overrides?: Partial<User>): User {
createdAt: faker.date.past(),
updatedAt: faker.date.recent(),
primaryAccountId: faker.string.uuid(),
role: 'USER',
timezone: faker.helpers.arrayElement(Intl.supportedValuesOf('timeZone')),
...overrides,
}
Expand Down
6 changes: 6 additions & 0 deletions packages/database/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ datasource db {
directUrl = env("POSTGRES_URL_NON_POOLING")
}

enum UserRole {
USER
ADMIN
}

model User {
id String @id @default(cuid())
username String @unique
Expand All @@ -38,6 +43,7 @@ model User {
primaryAccount Account @relation("UserPrimaryAccount", fields: [primaryAccountId], references: [id])
accounts Account[]
transactions Transaction[]
role UserRole @default(USER)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt

Expand Down
1 change: 1 addition & 0 deletions packages/database/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ async function main() {
email: process.env.DEV_DB_SEED_EMAIL,
username: 'dev',
displayName: 'Dev User',
role: 'ADMIN' as const,
}
: {
email: faker.internet.email(),
Expand Down
7 changes: 7 additions & 0 deletions packages/database/zod/inputTypeSchemas/UserRoleSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { z } from 'zod';

export const UserRoleSchema = z.enum(['USER','ADMIN']);

export type UserRoleType = `${z.infer<typeof UserRoleSchema>}`

export default UserRoleSchema;
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from 'zod';

export const UserScalarFieldEnumSchema = z.enum(['id','username','displayName','avatarUrl','twitterHandle','discordHandle','website','bio','timezone','primaryAccountId','createdAt','updatedAt','email','emailVerified']);
export const UserScalarFieldEnumSchema = z.enum(['id','username','displayName','avatarUrl','twitterHandle','discordHandle','website','bio','timezone','primaryAccountId','role','createdAt','updatedAt','email','emailVerified']);

export default UserScalarFieldEnumSchema;
1 change: 1 addition & 0 deletions packages/database/zod/inputTypeSchemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export { JsonNullValueInputSchema } from './JsonNullValueInputSchema';
export { QueryModeSchema } from './QueryModeSchema';
export { NullsOrderSchema } from './NullsOrderSchema';
export { JsonNullValueFilterSchema } from './JsonNullValueFilterSchema';
export { UserRoleSchema } from './UserRoleSchema';
export { CommentEntityTypeSchema } from './CommentEntityTypeSchema';
export { TransactionTypeSchema } from './TransactionTypeSchema';
export { AssetTypeSchema } from './AssetTypeSchema';
Expand Down
2 changes: 2 additions & 0 deletions packages/database/zod/modelSchema/UserSchema.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { z } from 'zod';
import { UserRoleSchema } from '../inputTypeSchemas/UserRoleSchema'

/////////////////////////////////////////
// USER SCHEMA
/////////////////////////////////////////

export const UserSchema = z.object({
role: UserRoleSchema,
id: z.string().cuid(),
username: z.string(),
displayName: z.string(),
Expand Down
4 changes: 2 additions & 2 deletions packages/lists/components/ListMarketRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { EllipsisVerticalIcon } from 'lucide-react'
import React from 'react'
import { MarketProbabilityDetail } from '@play-money/markets/components/MarketProbabilityDetail'
import { ResolveMarketDialog } from '@play-money/markets/components/ResolveMarketDialog'
import { canResolveMarket } from '@play-money/markets/lib/helpers'
import { canModifyMarket } from '@play-money/markets/rules'
import { ExtendedMarket } from '@play-money/markets/types'
import { useSearchParam } from '@play-money/ui'
import { Button } from '@play-money/ui/button'
Expand Down Expand Up @@ -43,7 +43,7 @@ export function ListMarketRow({
}) {
const { user } = useUser()
const [isResolving, setResolving] = useSearchParam('resolve')
const canResolve = canResolveMarket({ market, userId: user?.id })
const canResolve = user ? canModifyMarket({ market, user: user }) : false

return (
<div className={cn('flex cursor-pointer items-center hover:bg-muted/50', active && 'bg-muted/50', className)}>
Expand Down
6 changes: 3 additions & 3 deletions packages/lists/components/ListTradePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { MarketBuyForm } from '@play-money/markets/components/MarketBuyForm'
import { MarketLeaderboardPanel } from '@play-money/markets/components/MarketLeaderboardPanel'
import { MarketSellForm } from '@play-money/markets/components/MarketSellForm'
import { useSidebar } from '@play-money/markets/components/SidebarContext'
import { isMarketResolved, isMarketTradable } from '@play-money/markets/lib/helpers'
import { isMarketResolved, isMarketTradable } from '@play-money/markets/rules'
import { useSelectedItems } from '@play-money/ui'
import { Card, CardContent, CardHeader } from '@play-money/ui/card'
import { Combobox } from '@play-money/ui/combobox'
Expand All @@ -23,8 +23,8 @@ export function ListTradePanel({ list, onTradeComplete }: { list: ExtendedList;
const { data: balance, mutate: revalidate } = useListBalance({ listId: list.id })
const selectedMarket = list.markets.find((m) => m.market.id === selected[0])

const isTradable = selectedMarket ? isMarketTradable(selectedMarket.market) : false
const isResolved = selectedMarket ? isMarketResolved(selectedMarket.market) : false
const isTradable = selectedMarket ? isMarketTradable(selectedMarket) : false
const isResolved = selectedMarket ? isMarketResolved(selectedMarket) : false

const handleComplete = async () => {
void mutate(MY_BALANCE_PATH)
Expand Down
7 changes: 4 additions & 3 deletions packages/markets/components/MarketOverviewPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { UserLink } from '@play-money/users/components/UserLink'
import { useUser } from '@play-money/users/context/UserContext'
import { useSelectedItems } from '../../ui/src/contexts/SelectedItemContext'
import { useSearchParam } from '../../ui/src/hooks/useSearchParam'
import { canModifyMarket } from '../rules'
import { ExtendedMarket } from '../types'
import { EditMarketDialog } from './EditMarketDialog'
import { EditMarketOptionDialog } from './EditMarketOptionDialog'
Expand Down Expand Up @@ -79,7 +80,7 @@ export function MarketOverviewPage({
<Card className="flex-1">
<MarketToolbar
market={market}
canEdit={isCreator}
canEdit={user ? canModifyMarket({ market, user }) : false}
onInitiateEdit={() => setIsEditing('true')}
onInitiateBoost={() => setIsBoosting('true')}
onRevalidate={handleRevalidateBalance}
Expand Down Expand Up @@ -170,7 +171,7 @@ export function MarketOverviewPage({
active={option.id === selected[0]}
probability={probabilities[option.id] || option.probability || 0}
className={i > 0 ? 'border-t' : ''}
canEdit={user?.id === market.createdBy}
canEdit={user ? canModifyMarket({ market, user }) : false}
onEdit={() => setIsEditOption(option.id)}
onSelect={() => {
setSelected([option.id])
Expand All @@ -192,7 +193,7 @@ export function MarketOverviewPage({
active={option.id === selected[0]}
probability={probabilities[option.id] || option.probability || 0}
className={i > 0 ? 'border-t' : ''}
canEdit={user?.id === market.createdBy}
canEdit={user ? canModifyMarket({ market, user }) : false}
onEdit={() => setIsEditOption(option.id)}
onSelect={() => {
setSelected([option.id])
Expand Down
6 changes: 3 additions & 3 deletions packages/markets/components/MarketPageSidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import React from 'react'
import { isMarketResolved, isMarketTradable } from '../lib/helpers'
import { isMarketResolved, isMarketTradable } from '../rules'
import { ExtendedMarket } from '../types'
import { MarketTradePanel } from './MarketTradePanel'
import { RelatedMarkets } from './RelatedMarkets'
Expand All @@ -17,8 +17,8 @@ export function MarketPageSidebar({
<div className="space-y-8">
<MarketTradePanel
market={market}
isTradable={isMarketTradable(market)}
isResolved={isMarketResolved(market)}
isTradable={isMarketTradable({ market })}
isResolved={isMarketResolved({ market })}
onTradeComplete={onTradeComplete}
/>

Expand Down
4 changes: 2 additions & 2 deletions packages/markets/components/MarketToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
import { Tooltip, TooltipContent, TooltipTrigger } from '@play-money/ui/tooltip'
import { toast } from '@play-money/ui/use-toast'
import { useUser } from '@play-money/users/context/UserContext'
import { canResolveMarket } from '../lib/helpers'
import { canModifyMarket } from '../rules'
import { ExtendedMarket } from '../types'
import { ResolveMarketDialog } from './ResolveMarketDialog'

Expand Down Expand Up @@ -55,7 +55,7 @@ export function MarketToolbar({
}) {
const { user } = useUser()
const [isResolving, setResolving] = useQueryString('resolve')
const canResolve = canResolveMarket({ market, userId: user?.id })
const canResolve = user ? canModifyMarket({ market, user: user }) : false

const handleCopyLink = async () => {
try {
Expand Down
4 changes: 2 additions & 2 deletions packages/markets/lib/addLiquidity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import { createNotification } from '@play-money/notifications/lib/createNotifica
import { createDailyLiquidityBonusTransaction } from '@play-money/quests/lib/createDailyLiquidityBonusTransaction'
import { hasBoostedLiquidityToday } from '@play-money/quests/lib/helpers'
import { getUserPrimaryAccount } from '@play-money/users/lib/getUserPrimaryAccount'
import { isMarketResolved } from '../rules'
import { createMarketLiquidityTransaction } from './createMarketLiquidityTransaction'
import { getMarket } from './getMarket'
import { isMarketResolved } from './helpers'

export async function addLiquidity({
userId,
Expand All @@ -31,7 +31,7 @@ export async function addLiquidity({
throw new Error('User does not have enough balance')
}

if (isMarketResolved(market)) {
if (isMarketResolved({ market })) {
throw new Error('Market already resolved')
}

Expand Down
16 changes: 0 additions & 16 deletions packages/markets/lib/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,3 @@
import { Market } from '@play-money/database'
import { ExtendedMarket } from '../types'

export function canResolveMarket({ market, userId }: { market: ExtendedMarket; userId?: string }) {
return market.createdBy === userId && !isMarketResolved(market)
}

export function isMarketTradable(market: Market): boolean {
const now = new Date()
return !market.resolvedAt && (!market.closeDate || new Date(market.closeDate) > now)
}

export function isMarketResolved(market: ExtendedMarket): boolean {
return Boolean(market.marketResolution)
}

// Based on django's slugify:
// https://github.com/django/django/blob/a21a63cc288ba51bcf8c227a49de6f5bb9a72cc3/django/utils/text.py#L362
export function slugifyTitle(title: string, maxLen = 50) {
Expand Down
4 changes: 2 additions & 2 deletions packages/markets/lib/marketBuy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import { createNotification } from '@play-money/notifications/lib/createNotifica
import { createDailyTradeBonusTransaction } from '@play-money/quests/lib/createDailyTradeBonusTransaction'
import { hasPlacedMarketTradeToday } from '@play-money/quests/lib/helpers'
import { getUserPrimaryAccount } from '@play-money/users/lib/getUserPrimaryAccount'
import { isMarketTradable } from '../rules'
import { createLiquidityVolumeBonusTransaction } from './createLiquidityVolumeBonusTransaction'
import { createMarketBuyTransaction } from './createMarketBuyTransaction'
import { createMarketLiquidityTransaction } from './createMarketLiquidityTransaction'
import { createMarketTraderBonusTransactions } from './createMarketTraderBonusTransactions'
import { getMarket } from './getMarket'
import { isMarketTradable } from './helpers'

export async function marketBuy({
marketId,
Expand All @@ -27,7 +27,7 @@ export async function marketBuy({
}) {
const [market, userAccount] = await Promise.all([getMarket({ id: marketId }), getUserPrimaryAccount({ userId })])

if (!isMarketTradable(market)) {
if (!isMarketTradable({ market })) {
throw new Error('Market is closed')
}

Expand Down
4 changes: 2 additions & 2 deletions packages/markets/lib/marketSell.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import Decimal from 'decimal.js'
import { getUserPrimaryAccount } from '@play-money/users/lib/getUserPrimaryAccount'
import { isMarketTradable } from '../rules'
import { createMarketSellTransaction } from './createMarketSellTransaction'
import { getMarket } from './getMarket'
import { isMarketTradable } from './helpers'

export async function marketSell({
marketId,
Expand All @@ -19,7 +19,7 @@ export async function marketSell({
getMarket({ id: marketId }),
getUserPrimaryAccount({ userId: userId }),
])
if (!isMarketTradable(market)) {
if (!isMarketTradable({ market })) {
throw new Error('Market is closed')
}

Expand Down
12 changes: 4 additions & 8 deletions packages/markets/lib/resolveMarket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import db from '@play-money/database'
import { getUniqueTraderIds } from '@play-money/markets/lib/getUniqueTraderIds'
import { createNotification } from '@play-money/notifications/lib/createNotification'
import { getUserById } from '@play-money/users/lib/getUserById'
import { canModifyMarket, isMarketResolved } from '../rules'
import { createMarketExcessLiquidityTransactions } from './createMarketExcessLiquidityTransactions'
import { createMarketResolveLossTransactions } from './createMarketResolveLossTransactions'
import { createMarketResolveWinTransactions } from './createMarketResolveWinTransactions'
import { getMarket } from './getMarket'
import { canResolveMarket } from './helpers'

export async function resolveMarket({
resolverId,
Expand All @@ -20,15 +20,12 @@ export async function resolveMarket({
supportingLink?: string
}) {
const market = await getMarket({ id: marketId, extended: true })
const resolvingUser = await getUserById({ id: resolverId })

if (market.resolvedAt) {
if (isMarketResolved({ market })) {
throw new Error('Market already resolved')
}

if (!canResolveMarket({ market, userId: resolverId })) {
throw new Error('User cannot resolve market')
}

await db.$transaction(
async (tx) => {
const now = new Date()
Expand Down Expand Up @@ -76,8 +73,7 @@ export async function resolveMarket({

await createMarketExcessLiquidityTransactions({ marketId, initiatorId: resolverId })

const user = await getUserById({ id: resolverId })
const recipientIds = await getUniqueTraderIds(marketId, [user.id])
const recipientIds = await getUniqueTraderIds(marketId, [resolvingUser.id])

await Promise.all(
recipientIds.map((recipientId) =>
Expand Down
Loading