diff --git a/backend/api/src/app.ts b/backend/api/src/app.ts index 8f9384254e..9473644c30 100644 --- a/backend/api/src/app.ts +++ b/backend/api/src/app.ts @@ -71,7 +71,8 @@ import { } from './get-dashboard-from-slug' import { unresolve } from './unresolve' import { referuser } from 'api/refer-user' -import { banuser } from 'api/ban-user' +import { banUserFromPosting } from 'api/ban-user-from-posting' +import { banUserFromMana } from './ban-user-from-mana' import { updateMarket } from 'api/update-market' import { createprivateusermessage } from 'api/create-private-user-message' import { createprivateusermessagechannel } from 'api/create-private-user-message-channel' @@ -192,6 +193,7 @@ import { getUserLimitOrdersWithContracts } from 'api/get-user-limit-orders-with- import { getInterestingGroupsFromViews } from 'api/get-interesting-groups-from-views' import { completeCashoutSession } from 'api/gidx/complete-cashout-session' import { getCashouts } from './get-cashouts' +import { banUserFromSweepcash } from './ban-user-from-sweepcash' const allowCorsUnrestricted: RequestHandler = cors({}) @@ -512,7 +514,9 @@ app.post('/updatedashboard', ...apiRoute(updatedashboard)) app.post('/delete-dashboard', ...apiRoute(deletedashboard)) app.get('/get-news-dashboards', ...apiRoute(getnews)) app.post('/getdashboardfromslug', ...apiRoute(getdashboardfromslug)) -app.post('/ban-user', ...apiRoute(banuser)) +app.post('/ban-user-from-posting', ...apiRoute(banUserFromPosting)) +app.post('/ban-user-from-mana', ...apiRoute(banUserFromMana)) +app.post('/ban-user-from-sweepcash', ...apiRoute(banUserFromSweepcash)) app.post('/create-private-user-message', ...apiRoute(createprivateusermessage)) app.post( '/create-private-user-message-channel', diff --git a/backend/api/src/ban-user-from-mana.ts b/backend/api/src/ban-user-from-mana.ts new file mode 100644 index 0000000000..6324ae9737 --- /dev/null +++ b/backend/api/src/ban-user-from-mana.ts @@ -0,0 +1,30 @@ +import { APIError, authEndpoint, validate } from 'api/helpers/endpoint' +import { z } from 'zod' +import { trackPublicEvent } from 'shared/analytics' +import { throwErrorIfNotMod } from 'shared/helpers/auth' +import { isAdminId } from 'common/envs/constants' +import { log } from 'shared/utils' +import { createSupabaseDirectClient } from 'shared/supabase/init' +import { updateUser } from 'shared/supabase/users' + +const bodySchema = z + .object({ + userId: z.string(), + unban: z.boolean().optional(), + }) + .strict() + +export const banUserFromMana = authEndpoint(async (req, auth) => { + const { userId, unban } = validate(bodySchema, req.body) + const pg = createSupabaseDirectClient() + await throwErrorIfNotMod(auth.uid) + if (isAdminId(userId)) throw new APIError(403, 'Cannot ban admin') + await trackPublicEvent(auth.uid, 'ban user from trading mana', { + userId, + }) + await updateUser(pg, userId, { + isBannedFromMana: !unban, + }) + log(`Updated trading ban status for user ${userId}`) + return { success: true } +}) diff --git a/backend/api/src/ban-user.ts b/backend/api/src/ban-user-from-posting.ts similarity index 85% rename from backend/api/src/ban-user.ts rename to backend/api/src/ban-user-from-posting.ts index 504f35851c..1ebf0b339f 100644 --- a/backend/api/src/ban-user.ts +++ b/backend/api/src/ban-user-from-posting.ts @@ -14,15 +14,15 @@ const bodySchema = z }) .strict() -export const banuser = authEndpoint(async (req, auth) => { +export const banUserFromPosting = authEndpoint(async (req, auth) => { const { userId, unban } = validate(bodySchema, req.body) - const db = createSupabaseDirectClient() + const pg = createSupabaseDirectClient() await throwErrorIfNotMod(auth.uid) if (isAdminId(userId)) throw new APIError(403, 'Cannot ban admin') await trackPublicEvent(auth.uid, 'ban user', { userId, }) - await updateUser(db, userId, { + await updateUser(pg, userId, { isBannedFromPosting: !unban, }) log('updated user') diff --git a/backend/api/src/ban-user-from-sweepcash.ts b/backend/api/src/ban-user-from-sweepcash.ts new file mode 100644 index 0000000000..ec7dbab18a --- /dev/null +++ b/backend/api/src/ban-user-from-sweepcash.ts @@ -0,0 +1,30 @@ +import { APIError, authEndpoint, validate } from 'api/helpers/endpoint' +import { z } from 'zod' +import { trackPublicEvent } from 'shared/analytics' +import { throwErrorIfNotMod } from 'shared/helpers/auth' +import { isAdminId } from 'common/envs/constants' +import { log } from 'shared/utils' +import { createSupabaseDirectClient } from 'shared/supabase/init' +import { updateUser } from 'shared/supabase/users' + +const bodySchema = z + .object({ + userId: z.string(), + unban: z.boolean().optional(), + }) + .strict() + +export const banUserFromSweepcash = authEndpoint(async (req, auth) => { + const { userId, unban } = validate(bodySchema, req.body) + const pg = createSupabaseDirectClient() + await throwErrorIfNotMod(auth.uid) + if (isAdminId(userId)) throw new APIError(403, 'Cannot ban admin') + await trackPublicEvent(auth.uid, 'ban user from trading sweepcash', { + userId, + }) + await updateUser(pg, userId, { + isBannedFromSweepcash: !unban, + }) + log(`Updated trading ban status for user ${userId}`) + return { success: true } +}) diff --git a/backend/api/src/delete-me.ts b/backend/api/src/delete-me.ts index 0670d9a8b9..886beae71b 100644 --- a/backend/api/src/delete-me.ts +++ b/backend/api/src/delete-me.ts @@ -20,7 +20,6 @@ export const deleteMe: APIHandler<'me/delete'> = async (body, auth) => { const pg = createSupabaseDirectClient() await updateUser(pg, auth.uid, { userDeleted: true, - isBannedFromPosting: true, }) await updatePrivateUser(pg, auth.uid, { email: FieldVal.delete(), diff --git a/backend/api/src/get-mod-reports.ts b/backend/api/src/get-mod-reports.ts index 223f6196c7..4cc32b276e 100644 --- a/backend/api/src/get-mod-reports.ts +++ b/backend/api/src/get-mod-reports.ts @@ -15,7 +15,9 @@ export const getModReports: APIHandler<'get-mod-reports'> = async () => { owner.username as owner_username, owner.data->>'avatarUrl' as owner_avatar_url, owner.name as owner_name, - (owner.data->>'isBannedFromPosting')::boolean as owner_is_banned_from_posting + (owner.data->>'isBannedFromPosting')::boolean as owner_is_banned_from_posting, + (owner.data->>'isBannedFromMana')::boolean as owner_is_banned_from_mana, + (owner.data->>'isBannedFromSweepcash')::boolean as owner_is_banned_from_sweepcash from mod_reports mr join contract_comments cc on cc.comment_id = mr.comment_id join contracts c on c.id = mr.contract_id diff --git a/backend/api/src/get-user.ts b/backend/api/src/get-user.ts index 6314dd9506..3388f9497b 100644 --- a/backend/api/src/get-user.ts +++ b/backend/api/src/get-user.ts @@ -22,9 +22,16 @@ export const getLiteUser = async ( ) => { const pg = createSupabaseDirectClient() const liteUser = await pg.oneOrNone( - `select id, name, username, data->>'avatarUrl' as "avatarUrl", data->'isBannedFromPosting' as "isBannedFromPosting" - from users - where ${'id' in props ? 'id' : 'username'} = $1`, + `select + id, + name, + username, + data->>'avatarUrl' as "avatarUrl", + (data->>'isBannedFromPosting')::boolean as "isBannedFromPosting", + (data->>'isBannedFromMana')::boolean as "isBannedFromMana", + (data->>'isBannedFromSweepcash')::boolean as "isBannedFromSweepcash" + from users + where ${'id' in props ? 'id' : 'username'} = $1`, ['id' in props ? props.id : props.username] ) if (!liteUser) throw new APIError(404, 'User not found') diff --git a/backend/api/src/place-bet.ts b/backend/api/src/place-bet.ts index 69307fbb77..abf87bc644 100644 --- a/backend/api/src/place-bet.ts +++ b/backend/api/src/place-bet.ts @@ -16,11 +16,15 @@ import { Answer } from 'common/answer' import { CpmmState, getCpmmProbability } from 'common/calculate-cpmm' import { ValidatedAPIParams } from 'common/api/schema' import { onCreateBets } from 'api/on-create-bet' +<<<<<<< trading-ban +import { isAdminId, TWOMBA_ENABLED } from 'common/envs/constants' +======= import { BANNED_TRADING_USER_IDS, isAdminId, TWOMBA_ENABLED, } from 'common/envs/constants' +>>>>>>> main import * as crypto from 'crypto' import { formatMoneyWithDecimals } from 'common/util/format' import { @@ -294,7 +298,15 @@ export const fetchContractBetDataAndValidate = async ( contract.token === 'CASH' ? bet.cash_balance : bet.balance, ]) ) + const unfilledBetUserIds = Object.keys(balanceByUserId) + if ( + (isAdminId(uid) && contract.token === 'CASH') || + (user.isBannedFromSweepcash && contract.token === 'CASH') + ) { + throw new APIError(403, 'Banned from trading on sweepstakes markets.') + } + const balance = contract.token === 'CASH' ? user.cashBalance : user.balance if (amount !== undefined && balance < amount) throw new APIError(403, 'Insufficient balance.') @@ -307,11 +319,16 @@ export const fetchContractBetDataAndValidate = async ( 'You must be kyc verified to trade on sweepstakes markets.' ) } +<<<<<<< trading-ban + if (user.userDeleted) { + throw new APIError(403, 'You are banned or deleted.') +======= if (isAdminId(user.id) && contract.token === 'CASH' && isProd()) { throw new APIError(403, 'Admins cannot trade on sweepstakes markets.') } if (BANNED_TRADING_USER_IDS.includes(user.id) || user.userDeleted) { throw new APIError(403, 'You are banned or deleted. And not #blessed.') +>>>>>>> main } log( `Loaded user ${user.username} with id ${user.id} betting on slug ${contract.slug} with contract id: ${contract.id}.` @@ -322,7 +339,12 @@ export const fetchContractBetDataAndValidate = async ( log( `Loaded user ${user.username} with id ${user.id} betting on slug ${contract.slug} with contract id: ${contract.id}.` ) - + if (user.isBannedFromMana && contract.token !== 'CASH') { + throw new APIError(403, 'You are banned from trading mana (or deleted).') + } + log( + `Loaded user ${user.username} with id ${user.id} betting on slug ${contract.slug} with contract id: ${contract.id}.` + ) return { user, contract, diff --git a/common/src/api/user-types.ts b/common/src/api/user-types.ts index 1705eee70d..7739bf9a9e 100644 --- a/common/src/api/user-types.ts +++ b/common/src/api/user-types.ts @@ -8,6 +8,8 @@ export type DisplayUser = { username: string avatarUrl: string isBannedFromPosting?: boolean + isBannedFromMana?: boolean + isBannedFromSweepcash?: boolean } export type FullUser = User & { diff --git a/common/src/envs/constants.ts b/common/src/envs/constants.ts index f3cf8f4799..1035d3702a 100644 --- a/common/src/envs/constants.ts +++ b/common/src/envs/constants.ts @@ -267,13 +267,6 @@ export const VERIFIED_USERNAMES = [ 'DanHendrycks', ] -export const BANNED_TRADING_USER_IDS = [ - 'zgCIqq8AmRUYVu6AdQ9vVEJN8On1', //firstuserhere aka _deleted_ - 'LIBAoi7tpqeNLYM1xxJ1QJBQqW32', //lastuserhere - 'p3ADzwIUS3fk0ka80XYEE3OM3S32', //PC - '4JuXgDx47xPagH5mcLDqLzUSN5g2', // BTE -] - export const PARTNER_USER_IDS: string[] = [ 'sTUV8ejuM2byukNZp7qKP2OKXMx2', // NFL 'rFJu0EIdR6RP8d1vHKSh62pbnbH2', // SimonGrayson diff --git a/common/src/mod-report.ts b/common/src/mod-report.ts index 74a40efbc6..93aa57f584 100644 --- a/common/src/mod-report.ts +++ b/common/src/mod-report.ts @@ -17,5 +17,7 @@ export type ModReport = { owner_username: string owner_avatar_url: string owner_is_banned_from_posting: boolean + owner_is_banned_from_mana: boolean + owner_is_banned_from_sweepcash: boolean owner_name: string } diff --git a/common/src/user.ts b/common/src/user.ts index 6f1ba5c6c3..3e9607afe7 100644 --- a/common/src/user.ts +++ b/common/src/user.ts @@ -63,6 +63,8 @@ export type User = { hasSeenLoanModal?: boolean hasSeenContractFollowModal?: boolean isBannedFromPosting?: boolean + isBannedFromMana?: boolean + isBannedFromSweepcash?: boolean userDeleted?: boolean optOutBetWarnings?: boolean freeQuestionsCreated?: number diff --git a/web/components/auth-context.tsx b/web/components/auth-context.tsx index 50d1be9c3b..8a47748d36 100644 --- a/web/components/auth-context.tsx +++ b/web/components/auth-context.tsx @@ -7,11 +7,7 @@ import { createUser } from 'web/lib/api/api' import { randomString } from 'common/util/random' import { identifyUser, setUserProperty } from 'web/lib/service/analytics' import { useStateCheckEquality } from 'web/hooks/use-state-check-equality' -import { - AUTH_COOKIE_NAME, - BANNED_TRADING_USER_IDS, - TEN_YEARS_SECS, -} from 'common/envs/constants' +import { AUTH_COOKIE_NAME, TEN_YEARS_SECS } from 'common/envs/constants' import { getCookie, setCookie } from 'web/lib/util/cookie' import { type PrivateUser, @@ -114,12 +110,13 @@ export function AuthProvider(props: { useEffect(() => { if (authUser) { if ( - BANNED_TRADING_USER_IDS.includes(authUser.user.id) || + (authUser.user.isBannedFromPosting && + authUser.user.isBannedFromMana) && authUser.user.isBannedFromSweepcash || authUser.user.userDeleted ) { const message = authUser.user.userDeleted ? 'You have deleted the account associated with this email. To restore your account please email info@manifold.markets' - : 'You are banned from trading. To learn more please email info@manifold.markets' + : 'You are banned from Manifold. To learn more please email info@manifold.markets' firebaseLogout().then(() => { alert(message) diff --git a/web/components/buttons/user-settings-button.tsx b/web/components/buttons/user-settings-button.tsx index d667f29e9d..33efa01a1c 100644 --- a/web/components/buttons/user-settings-button.tsx +++ b/web/components/buttons/user-settings-button.tsx @@ -22,7 +22,11 @@ import { Referrals, useReferralCount, } from 'web/components/buttons/referrals-button' -import { banUser } from 'web/lib/api/api' +import { + banUserFromPosting, + banUserFromMana, + banUserFromSweepcash, +} from 'web/lib/api/api' import SuperBanControl from '../SuperBanControl' import { buildArray } from 'common/util/array' import { AccountSettings } from '../profile/settings' @@ -80,19 +84,50 @@ export function UserSettingButton(props: { user: User }) {
{name} {(isAdmin || isTrusted) && ( - + + + + )} diff --git a/web/components/mod-report-item.tsx b/web/components/mod-report-item.tsx index 3a9bf3d640..59c34c474e 100644 --- a/web/components/mod-report-item.tsx +++ b/web/components/mod-report-item.tsx @@ -50,8 +50,18 @@ const ModReportItem: React.FC = ({ avatarUrl={report.owner_avatar_url || ''} size="sm" /> + - {report.owner_is_banned_from_posting && } + {(report.owner_is_banned_from_posting || + report.owner_is_banned_from_mana) || report.owner_is_banned_from_sweepcash && ( + + + + )}
commented: diff --git a/web/components/profile/blocked-user.tsx b/web/components/profile/blocked-user.tsx index a461381c66..ec3822ebd8 100644 --- a/web/components/profile/blocked-user.tsx +++ b/web/components/profile/blocked-user.tsx @@ -39,7 +39,13 @@ export function BlockedUser(props: { user: User; privateUser: PrivateUser }) { {user.name} {' (Blocked) '} {} - {user.isBannedFromPosting && } + + + @{user.username} diff --git a/web/components/widgets/user-link.tsx b/web/components/widgets/user-link.tsx index b4e0423bc0..65d2e3a47e 100644 --- a/web/components/widgets/user-link.tsx +++ b/web/components/widgets/user-link.tsx @@ -136,14 +136,54 @@ function BotBadge() { ) } -export function BannedBadge() { +export function BannedBadge({ + isBannedFromPosting, + isBannedFromMana, + isBannedFromSweepcash, +}: { + isBannedFromPosting: boolean + isBannedFromMana: boolean + isBannedFromSweepcash: boolean +}) { + let badgeText = '' + let tooltipText = '' + + if (isBannedFromPosting && isBannedFromMana && isBannedFromSweepcash) { + badgeText = 'Banned from posting & trading' + tooltipText = + "Can't create comments, messages, questions, or trade with mana or sweepcash" + } else if (isBannedFromPosting && isBannedFromMana) { + badgeText = 'Banned from posting & mana' + tooltipText = + "Can't create comments, messages, questions, or trade with mana" + } else if (isBannedFromPosting && isBannedFromSweepcash) { + badgeText = 'Banned from posting & sweepcash' + tooltipText = + "Can't create comments, messages, questions, or trade with sweepcash" + } else if (isBannedFromMana && isBannedFromSweepcash) { + badgeText = 'Banned from trading' + tooltipText = "Can't participate in trading mana or sweepcash" + } else if (isBannedFromPosting) { + badgeText = 'Banned from posting' + tooltipText = "Can't create comments, messages, or questions" + } else if (isBannedFromMana) { + badgeText = 'Banned from mana' + tooltipText = "Can't participate in trading mana" + } else if (isBannedFromSweepcash) { + badgeText = 'Banned from sweepcash' + tooltipText = "Can't participate in trading sweepcash" + } + + if (!badgeText) return null + return ( - - - Banned + + + {badgeText} ) @@ -260,6 +300,8 @@ export const StackedUserNames = (props: { username: string createdTime: number isBannedFromPosting?: boolean + isBannedFromMana?: boolean + isBannedFromSweepcash?: boolean } followsYou?: boolean className?: string @@ -277,8 +319,13 @@ export const StackedUserNames = (props: { fresh={isFresh(user.createdTime)} /> } - {user.isBannedFromPosting && } + + @{user.username}{' '} diff --git a/web/lib/api/api.ts b/web/lib/api/api.ts index 1b5b8f742e..e3794251f0 100644 --- a/web/lib/api/api.ts +++ b/web/lib/api/api.ts @@ -312,8 +312,11 @@ export const updateMarket = curriedAPI('market/:contractId/update') export const updateUser = curriedAPI('me/update') -export function banUser(params: { userId: string; unban?: boolean }) { - return call(getApiUrl('ban-user'), 'POST', params) +export function banUserFromPosting(params: { + userId: string + unban?: boolean +}) { + return call(getApiUrl('ban-user-from-posting'), 'POST', params) } export function createPrivateMessageChannelWithUsers(params: { userIds: string[] @@ -321,6 +324,17 @@ export function createPrivateMessageChannelWithUsers(params: { return call(getApiUrl('create-private-user-message-channel'), 'POST', params) } +export function banUserFromMana(params: { userId: string; unban?: boolean }) { + return call(getApiUrl('ban-user-from-mana'), 'POST', params) +} + +export function banUserFromSweepcash(params: { + userId: string + unban?: boolean +}) { + return call(getApiUrl('ban-user-from-sweepcash'), 'POST', params) +} + export function sendUserPrivateMessage(params: { channelId: number content: JSONContent diff --git a/web/lib/supabase/super-ban-user.ts b/web/lib/supabase/super-ban-user.ts index dbada2cc81..36a5b3a650 100644 --- a/web/lib/supabase/super-ban-user.ts +++ b/web/lib/supabase/super-ban-user.ts @@ -1,11 +1,11 @@ -import { api, banUser } from 'web/lib/api/api' +import { api, banUserFromPosting } from 'web/lib/api/api' async function superBanUser(userId: string) { let marketsStatus = "could not be unlisted nor N/A'd due to an unknown error" let commentsStatus = 'could not be hidden due to an unknown error' try { - await banUser({ userId }) + await banUserFromPosting({ userId }) await api('unlist-and-cancel-user-contracts', { userId }) marketsStatus = "successfully unlisted & NA'd" } catch (error) { diff --git a/web/lib/supabase/users.ts b/web/lib/supabase/users.ts index 448edba2a9..f5401c23cc 100644 --- a/web/lib/supabase/users.ts +++ b/web/lib/supabase/users.ts @@ -50,10 +50,14 @@ export async function searchUsers(prompt: string, limit: number) { export async function getDisplayUsers(userIds: string[]) { // note: random order const { data } = await run( - selectFrom(db, 'users', ...defaultFields, 'isBannedFromPosting').in( - 'id', - userIds - ) + selectFrom( + db, + 'users', + ...defaultFields, + 'isBannedFromPosting', + 'isBannedFromMana', + 'isBannedFromSweepcash' + ).in('id', userIds) ) return data diff --git a/web/pages/admin/reports.tsx b/web/pages/admin/reports.tsx index 33c171a1fa..fe9f7b4f02 100644 --- a/web/pages/admin/reports.tsx +++ b/web/pages/admin/reports.tsx @@ -69,7 +69,17 @@ export default function Reports(props: { reports: LiteReport[] }) { size="sm" /> - {owner.isBannedFromPosting && } + diff --git a/web/pages/messages/[channelId].tsx b/web/pages/messages/[channelId].tsx index 3d94065640..14894caecc 100644 --- a/web/pages/messages/[channelId].tsx +++ b/web/pages/messages/[channelId].tsx @@ -218,9 +218,14 @@ export const PrivateChat = (props: { {members.length > 2 && ` & ${members.length - 2} more`} )} - {members?.length == 1 && members[0].isBannedFromPosting && ( - + {members?.length >= 1 && ( + )} + 2 && ` & ${otherUsers.length - 2} more`} )} - {isBanned && } + + {otherUsers?.[0] && ( + + + + )} {chat && }