Throw errors which extend GraphQLError (#1386)

This commit is contained in:
ekzyis 2024-09-10 18:35:25 +02:00 committed by GitHub
parent ec5241ad29
commit 821ac60de5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 153 additions and 132 deletions

View File

@ -1,7 +1,7 @@
import { GraphQLError } from 'graphql'
import { GqlAuthorizationError } from '@/lib/error'
export default function assertApiKeyNotPermitted ({ me }) {
if (me?.apiKey === true) {
throw new GraphQLError('this operation is not allowed to be performed via API Key', { extensions: { code: 'FORBIDDEN' } })
throw new GqlAuthorizationError('this operation is not allowed to be performed via API Key')
}
}

View File

@ -1,13 +1,13 @@
import { GraphQLError } from 'graphql'
import { inviteSchema, ssValidate } from '@/lib/validate'
import { msatsToSats } from '@/lib/format'
import assertApiKeyNotPermitted from './apiKey'
import { GqlAuthenticationError } from '@/lib/error'
export default {
Query: {
invites: async (parent, args, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
throw new GqlAuthenticationError()
}
return await models.invite.findMany({
@ -31,7 +31,7 @@ export default {
Mutation: {
createInvite: async (parent, { gift, limit }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
throw new GqlAuthenticationError()
}
assertApiKeyNotPermitted({ me })
@ -43,7 +43,7 @@ export default {
},
revokeInvite: async (parent, { id }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
throw new GqlAuthenticationError()
}
return await models.invite.update({

View File

@ -1,4 +1,3 @@
import { GraphQLError } from 'graphql'
import { ensureProtocol, removeTracking, stripTrailingSlash } from '@/lib/url'
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
import { getMetadata, metadataRuleSets } from 'page-metadata-parser'
@ -20,6 +19,7 @@ import { uploadIdsFromText } from './image'
import assertGofacYourself from './ofac'
import assertApiKeyNotPermitted from './apiKey'
import performPaidAction from '../paidAction'
import { GqlAuthenticationError, GqlInputError } from '@/lib/error'
function commentsOrderByClause (me, models, sort) {
if (sort === 'recent') {
@ -333,12 +333,12 @@ export default {
switch (sort) {
case 'user':
if (!name) {
throw new GraphQLError('must supply name', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('must supply name')
}
user ??= await models.user.findUnique({ where: { name } })
if (!user) {
throw new GraphQLError('no user has that name', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('no user has that name')
}
table = type === 'bookmarks' ? 'Bookmark' : 'Item'
@ -657,7 +657,7 @@ export default {
},
pinItem: async (parent, { id }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
throw new GqlAuthenticationError()
}
const [item] = await models.$queryRawUnsafe(
@ -671,7 +671,7 @@ export default {
// OPs can only pin top level replies
if (item.path.split('.').length > 2) {
throw new GraphQLError('can only pin root replies', { extensions: { code: 'FORBIDDEN' } })
throw new GqlInputError('can only pin root replies')
}
const root = await models.item.findUnique({
@ -682,7 +682,7 @@ export default {
})
if (root.userId !== Number(me.id)) {
throw new GraphQLError('not your post', { extensions: { code: 'FORBIDDEN' } })
throw new GqlInputError('not your post')
}
} else if (item.subName) {
args.push(item.subName)
@ -690,10 +690,10 @@ export default {
// only territory founder can pin posts
const sub = await models.sub.findUnique({ where: { name: item.subName } })
if (Number(me.id) !== sub.userId) {
throw new GraphQLError('not your sub', { extensions: { code: 'FORBIDDEN' } })
throw new GqlInputError('not your sub')
}
} else {
throw new GraphQLError('item must have subName or parentId', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('item must have subName or parentId')
}
let pinId
@ -723,7 +723,7 @@ export default {
}`, ...args)
if (npins >= 3) {
throw new GraphQLError('max 3 pins allowed', { extensions: { code: 'FORBIDDEN' } })
throw new GqlInputError('max 3 pins allowed')
}
const [{ pinId: newPinId }] = await models.$queryRawUnsafe(`
@ -757,10 +757,10 @@ export default {
deleteItem: async (parent, { id }, { me, models }) => {
const old = await models.item.findUnique({ where: { id: Number(id) } })
if (Number(old.userId) !== Number(me?.id)) {
throw new GraphQLError('item does not belong to you', { extensions: { code: 'FORBIDDEN' } })
throw new GqlInputError('item does not belong to you')
}
if (old.bio) {
throw new GraphQLError('cannot delete bio', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('cannot delete bio')
}
return await deleteItemByAuthor({ models, id, item: old })
@ -812,7 +812,7 @@ export default {
},
upsertJob: async (parent, { id, ...item }, { me, models, lnd }) => {
if (!me) {
throw new GraphQLError('you must be logged in to create job', { extensions: { code: 'FORBIDDEN' } })
throw new GqlAuthenticationError()
}
item.location = item.location?.toLowerCase() === 'remote' ? undefined : item.location
@ -841,7 +841,7 @@ export default {
},
updateNoteId: async (parent, { id, noteId }, { me, models }) => {
if (!id) {
throw new GraphQLError('id required', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('id required')
}
await models.item.update({
@ -853,7 +853,7 @@ export default {
},
pollVote: async (parent, { id }, { me, models, lnd }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
throw new GqlAuthenticationError()
}
return await performPaidAction('POLL_VOTE', { id }, { me, models, lnd })
@ -869,24 +869,24 @@ export default {
WHERE id = $1`, Number(id))
if (item.deletedAt) {
throw new GraphQLError('item is deleted', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('item is deleted')
}
if (item.invoiceActionState && item.invoiceActionState !== 'PAID') {
throw new GraphQLError('cannot act on unpaid item', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('cannot act on unpaid item')
}
// disallow self tips except anons
if (me) {
if (Number(item.userId) === Number(me.id)) {
throw new GraphQLError('cannot zap your self', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('cannot zap yourself')
}
// Disallow tips if me is one of the forward user recipients
if (act === 'TIP') {
const existingForwards = await models.itemForward.findMany({ where: { itemId: Number(id) } })
if (existingForwards.some(fwd => Number(fwd.userId) === Number(me.id))) {
throw new GraphQLError('cannot zap a post for which you are forwarded zaps', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('cannot zap a post for which you are forwarded zaps')
}
}
}
@ -896,12 +896,12 @@ export default {
} else if (act === 'DONT_LIKE_THIS') {
return await performPaidAction('DOWN_ZAP', { id, sats }, { me, models, lnd })
} else {
throw new GraphQLError('unknown act', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('unknown act')
}
},
toggleOutlaw: async (parent, { id }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
throw new GqlAuthenticationError()
}
const item = await models.item.findUnique({
@ -919,7 +919,7 @@ export default {
const sub = item.sub || item.root?.sub
if (Number(sub.userId) !== Number(me.id)) {
throw new GraphQLError('you cant do this broh', { extensions: { code: 'FORBIDDEN' } })
throw new GqlInputError('you cant do this broh')
}
if (item.outlawed) {
@ -1262,11 +1262,11 @@ export const updateItem = async (parent, { sub: subName, forward, ...item }, { m
const old = await models.item.findUnique({ where: { id: Number(item.id) }, include: { sub: true } })
if (old.deletedAt) {
throw new GraphQLError('item is deleted', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('item is deleted')
}
if (old.invoiceActionState && old.invoiceActionState !== 'PAID') {
throw new GraphQLError('cannot edit unpaid item', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('cannot edit unpaid item')
}
// author can always edit their own item
@ -1278,14 +1278,14 @@ export const updateItem = async (parent, { sub: subName, forward, ...item }, { m
const adminEdit = SN_USER_IDS.includes(mid) && allowEdit
if (!isMine && !adminEdit) {
throw new GraphQLError('item does not belong to you', { extensions: { code: 'FORBIDDEN' } })
throw new GqlInputError('item does not belong to you')
}
const differentSub = subName && old.subName !== subName
if (differentSub) {
const sub = await models.sub.findUnique({ where: { name: subName } })
if (sub.baseCost > old.sub.baseCost) {
throw new GraphQLError('cannot change to a more expensive sub', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('cannot change to a more expensive sub')
}
}
@ -1299,7 +1299,7 @@ export const updateItem = async (parent, { sub: subName, forward, ...item }, { m
const timer = Date.now() < new Date(old.invoicePaidAt ?? old.createdAt).getTime() + 10 * 60_000
if (!allowEdit && !myBio && !timer && !isJob(item)) {
throw new GraphQLError('item can no longer be editted', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('item can no longer be edited')
}
if (item.url && !isJob(item)) {
@ -1343,7 +1343,7 @@ export const createItem = async (parent, { forward, ...item }, { me, models, lnd
if (item.parentId) {
const parent = await models.item.findUnique({ where: { id: parseInt(item.parentId) } })
if (parent.invoiceActionState && parent.invoiceActionState !== 'PAID') {
throw new GraphQLError('cannot comment on unpaid item', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('cannot comment on unpaid item')
}
}

View File

@ -1,8 +1,8 @@
import { randomBytes } from 'crypto'
import { bech32 } from 'bech32'
import { GraphQLError } from 'graphql'
import assertGofacYourself from './ofac'
import assertApiKeyNotPermitted from './apiKey'
import { GqlAuthenticationError } from '@/lib/error'
function encodedUrl (iurl, tag, k1) {
const url = new URL(iurl)
@ -35,7 +35,7 @@ export default {
await assertGofacYourself({ models, headers })
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
assertApiKeyNotPermitted({ me })

View File

@ -1,4 +1,4 @@
import { GraphQLError } from 'graphql'
import { GqlInputError } from '@/lib/error'
export default {
Query: {
@ -11,7 +11,7 @@ export default {
Mutation: {
createMessage: async (parent, { text }, { me, models }) => {
if (!text) {
throw new GraphQLError('Must have text', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('must have text')
}
return await models.message.create({

View File

@ -1,17 +1,17 @@
import { GraphQLError } from 'graphql'
import { decodeCursor, LIMIT, nextNoteCursorEncoded } from '@/lib/cursor'
import { getItem, filterClause, whereClause, muteClause, activeOrMine } from './item'
import { getInvoice, getWithdrawl } from './wallet'
import { pushSubscriptionSchema, ssValidate } from '@/lib/validate'
import { replyToSubscription } from '@/lib/webPush'
import { getSub } from './sub'
import { GqlAuthenticationError, GqlInputError } from '@/lib/error'
export default {
Query: {
notifications: async (parent, { cursor, inc }, { me, models }) => {
const decodedCursor = decodeCursor(cursor)
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
const meFull = await models.user.findUnique({ where: { id: me.id } })
@ -382,7 +382,7 @@ export default {
Mutation: {
savePushSubscription: async (parent, { endpoint, p256dh, auth, oldEndpoint }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
await ssValidate(pushSubscriptionSchema, { endpoint, p256dh, auth })
@ -406,12 +406,12 @@ export default {
},
deletePushSubscription: async (parent, { endpoint }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
const subscription = await models.pushSubscription.findFirst({ where: { endpoint, userId: Number(me.id) } })
if (!subscription) {
throw new GraphQLError('endpoint not found', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('endpoint not found')
}
const deletedSubscription = await models.pushSubscription.delete({ where: { id: subscription.id } })
console.log(`[webPush] deleted subscription ${deletedSubscription.id} of user ${deletedSubscription.userId} due to client request`)

View File

@ -1,13 +1,11 @@
import { GraphQLError } from 'graphql'
import { GqlAuthorizationError } from '@/lib/error'
// this function makes america more secure apparently
export default async function assertGofacYourself ({ models, headers, ip }) {
const country = await gOFACYourself({ models, headers, ip })
if (!country) return
throw new GraphQLError(
`Your IP address is in ${country}. We cannot provide financial services to residents of ${country}.`,
{ extensions: { code: 'FORBIDDEN' } })
throw new GqlAuthorizationError(`Your IP address is in ${country}. We cannot provide financial services to residents of ${country}.`)
}
export async function gOFACYourself ({ models, headers = {}, ip }) {

View File

@ -1,12 +1,12 @@
import { GraphQLError } from 'graphql'
import { timeUnitForRange, whenRange } from '@/lib/time'
import { viewGroup } from './growth'
import { GqlAuthenticationError } from '@/lib/error'
export default {
Query: {
referrals: async (parent, { when, from, to }, { models, me }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
const range = whenRange(when, from, to)

View File

@ -1,8 +1,8 @@
import { GraphQLError } from 'graphql'
import { amountSchema, ssValidate } from '@/lib/validate'
import { getItem } from './item'
import { topUsers } from './user'
import performPaidAction from '../paidAction'
import { GqlInputError } from '@/lib/error'
let rewardCache
@ -63,21 +63,21 @@ async function getMonthlyRewards (when, models) {
async function getRewards (when, models) {
if (when) {
if (when.length > 1) {
throw new GraphQLError('too many dates', { extensions: { code: 'BAD_USER_INPUT' } })
throw new GqlInputError('too many dates')
}
when.forEach(w => {
if (isNaN(new Date(w))) {
throw new GraphQLError('invalid date', { extensions: { code: 'BAD_USER_INPUT' } })
throw new GqlInputError('invalid date')
}
})
if (new Date(when[0]) > new Date(when[when.length - 1])) {
throw new GraphQLError('bad date range', { extensions: { code: 'BAD_USER_INPUT' } })
throw new GqlInputError('bad date range')
}
if (new Date(when[0]).getTime() > new Date('2024-03-01').getTime() && new Date(when[0]).getTime() < new Date('2024-05-02').getTime()) {
// after 3/1/2024 and until 5/1/2024, we reward monthly on the 1st
if (new Date(when[0]).getUTCDate() !== 1) {
throw new GraphQLError('invalid reward date', { extensions: { code: 'BAD_USER_INPUT' } })
throw new GqlInputError('bad reward date')
}
return await getMonthlyRewards(when, models)
@ -119,11 +119,11 @@ export default {
}
if (!when || when.length > 2) {
throw new GraphQLError('invalid date range', { extensions: { code: 'BAD_USER_INPUT' } })
throw new GqlInputError('bad date range')
}
for (const w of when) {
if (isNaN(new Date(w))) {
throw new GraphQLError('invalid date', { extensions: { code: 'BAD_USER_INPUT' } })
throw new GqlInputError('invalid date')
}
}

View File

@ -1,8 +1,8 @@
import { GraphQLError } from 'graphql'
import retry from 'async-retry'
import Prisma from '@prisma/client'
import { msatsToSats, numWithUnits } from '@/lib/format'
import { BALANCE_LIMIT_MSATS } from '@/lib/constants'
import { GqlInputError } from '@/lib/error'
export default async function serialize (trx, { models, lnd }) {
// wrap first argument in array if not array already
@ -28,7 +28,7 @@ export default async function serialize (trx, { models, lnd }) {
// have to check the error message
if (error.message.includes('SN_INSUFFICIENT_FUNDS') ||
error.message.includes('\\"users\\" violates check constraint \\"msats_positive\\"')) {
bail(new GraphQLError('insufficient funds', { extensions: { code: 'BAD_INPUT' } }))
bail(new GqlInputError('insufficient funds'))
}
if (error.message.includes('SN_NOT_SERIALIZABLE')) {
bail(new Error('wallet balance transaction is not serializable'))

View File

@ -1,10 +1,10 @@
import { GraphQLError } from 'graphql'
import { whenRange } from '@/lib/time'
import { ssValidate, territorySchema } from '@/lib/validate'
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
import { viewGroup } from './growth'
import { notifyTerritoryTransfer } from '@/lib/webPush'
import performPaidAction from '../paidAction'
import { GqlAuthenticationError, GqlInputError } from '@/lib/error'
export async function getSub (parent, { name }, { models, me }) {
if (!name) return null
@ -108,12 +108,12 @@ export default {
},
userSubs: async (_parent, { name, cursor, when, by, from, to, limit = LIMIT }, { models }) => {
if (!name) {
throw new GraphQLError('must supply user name', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('must supply user name')
}
const user = await models.user.findUnique({ where: { name } })
if (!user) {
throw new GraphQLError('no user has that name', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('no user has that name')
}
const decodedCursor = decodeCursor(cursor)
@ -154,7 +154,7 @@ export default {
Mutation: {
upsertSub: async (parent, { ...data }, { me, models, lnd }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
await ssValidate(territorySchema, data, { models, me, sub: { name: data.oldName } })
@ -174,11 +174,11 @@ export default {
})
if (!sub) {
throw new GraphQLError('sub not found', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('sub not found')
}
if (sub.userId !== me.id) {
throw new GraphQLError('you do not own this sub', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('you do not own this sub')
}
if (sub.status === 'ACTIVE') {
@ -189,7 +189,7 @@ export default {
},
toggleMuteSub: async (parent, { name }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
const lookupData = { userId: Number(me.id), subName: name }
@ -205,7 +205,7 @@ export default {
},
toggleSubSubscription: async (sub, { name }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
const lookupData = { userId: me.id, subName: name }
@ -221,7 +221,7 @@ export default {
},
transferTerritory: async (parent, { subName, userName }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
const sub = await models.sub.findUnique({
@ -230,18 +230,18 @@ export default {
}
})
if (!sub) {
throw new GraphQLError('sub not found', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('sub not found')
}
if (sub.userId !== me.id) {
throw new GraphQLError('you do not own this sub', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('you do not own this sub')
}
const user = await models.user.findFirst({ where: { name: userName } })
if (!user) {
throw new GraphQLError('user not found', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('user not found')
}
if (user.id === me.id) {
throw new GraphQLError('cannot transfer territory to yourself', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('cannot transfer territory to yourself')
}
const [, updatedSub] = await models.$transaction([
@ -255,7 +255,7 @@ export default {
},
unarchiveTerritory: async (parent, { ...data }, { me, models, lnd }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
const { name } = data
@ -264,16 +264,16 @@ export default {
const oldSub = await models.sub.findUnique({ where: { name } })
if (!oldSub) {
throw new GraphQLError('sub not found', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('sub not found')
}
if (oldSub.status !== 'STOPPED') {
throw new GraphQLError('sub is not archived', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('sub is not archived')
}
if (oldSub.billingType === 'ONCE') {
// sanity check. this should never happen but leaving this comment here
// to stop error propagation just in case and document that this should never happen.
// #defensivecode
throw new GraphQLError('sub should not be archived', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('sub should not be archived')
}
return await performPaidAction('TERRITORY_UNARCHIVE', data, { me, models, lnd })
@ -319,7 +319,7 @@ async function createSub (parent, data, { me, models, lnd }) {
return await performPaidAction('TERRITORY_CREATE', data, { me, models, lnd })
} catch (error) {
if (error.code === 'P2002') {
throw new GraphQLError('name taken', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('name taken')
}
throw error
}
@ -339,14 +339,14 @@ async function updateSub (parent, { oldName, ...data }, { me, models, lnd }) {
})
if (!oldSub) {
throw new GraphQLError('sub not found', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('sub not found')
}
try {
return await performPaidAction('TERRITORY_UPDATE', { oldName, ...data }, { me, models, lnd })
} catch (error) {
if (error.code === 'P2002') {
throw new GraphQLError('name taken', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('name taken')
}
throw error
}

View File

@ -1,24 +1,24 @@
import { GraphQLError } from 'graphql'
import { USER_ID, IMAGE_PIXELS_MAX, UPLOAD_SIZE_MAX, UPLOAD_SIZE_MAX_AVATAR, UPLOAD_TYPES_ALLOW } from '@/lib/constants'
import { createPresignedPost } from '@/api/s3'
import { GqlAuthenticationError, GqlInputError } from '@/lib/error'
export default {
Mutation: {
getSignedPOST: async (parent, { type, size, width, height, avatar }, { models, me }) => {
if (UPLOAD_TYPES_ALLOW.indexOf(type) === -1) {
throw new GraphQLError(`image must be ${UPLOAD_TYPES_ALLOW.map(t => t.replace('image/', '')).join(', ')}`, { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError(`image must be ${UPLOAD_TYPES_ALLOW.map(t => t.replace('image/', '')).join(', ')}`)
}
if (size > UPLOAD_SIZE_MAX) {
throw new GraphQLError(`image must be less than ${UPLOAD_SIZE_MAX / (1024 ** 2)} megabytes`, { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError(`image must be less than ${UPLOAD_SIZE_MAX / (1024 ** 2)} megabytes`)
}
if (avatar && size > UPLOAD_SIZE_MAX_AVATAR) {
throw new GraphQLError(`image must be less than ${UPLOAD_SIZE_MAX_AVATAR / (1024 ** 2)} megabytes`, { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError(`image must be less than ${UPLOAD_SIZE_MAX_AVATAR / (1024 ** 2)} megabytes`)
}
if (width * height > IMAGE_PIXELS_MAX) {
throw new GraphQLError(`image must be less than ${IMAGE_PIXELS_MAX} pixels`, { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError(`image must be less than ${IMAGE_PIXELS_MAX} pixels`)
}
const imgParams = {
@ -31,7 +31,7 @@ export default {
}
if (avatar) {
if (!me) throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
if (!me) throw new GqlAuthenticationError()
imgParams.paid = undefined
}

View File

@ -1,6 +1,5 @@
import { readFile } from 'fs/promises'
import { join, resolve } from 'path'
import { GraphQLError } from 'graphql'
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
import { msatsToSats } from '@/lib/format'
import { bioSchema, emailSchema, settingsSchema, ssValidate, userSchema } from '@/lib/validate'
@ -11,6 +10,7 @@ import { timeUnitForRange, whenRange } from '@/lib/time'
import assertApiKeyNotPermitted from './apiKey'
import { hashEmail } from '@/lib/crypto'
import { isMuted } from '@/lib/user'
import { GqlAuthenticationError, GqlAuthorizationError, GqlInputError } from '@/lib/error'
const contributors = new Set()
@ -125,7 +125,7 @@ export default {
},
settings: async (parent, args, { models, me }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
return await models.user.findUnique({ where: { id: me.id } })
@ -144,7 +144,7 @@ export default {
},
mySubscribedUsers: async (parent, { cursor }, { models, me }) => {
if (!me) {
throw new GraphQLError('You must be logged in to view subscribed users', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
const decodedCursor = decodeCursor(cursor)
@ -165,7 +165,7 @@ export default {
},
myMutedUsers: async (parent, { cursor }, { models, me }) => {
if (!me) {
throw new GraphQLError('You must be logged in to view muted users', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
const decodedCursor = decodeCursor(cursor)
@ -624,7 +624,7 @@ export default {
Mutation: {
disableFreebies: async (parent, args, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
// disable freebies if it hasn't been set yet
@ -644,7 +644,7 @@ export default {
},
setName: async (parent, data, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
await ssValidate(userSchema, data, { models })
@ -654,14 +654,14 @@ export default {
return data.name
} catch (error) {
if (error.code === 'P2002') {
throw new GraphQLError('name taken', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('name taken')
}
throw error
}
},
setSettings: async (parent, { settings: { nostrRelays, ...data } }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
await ssValidate(settingsSchema, { nostrRelays, ...data })
@ -687,7 +687,7 @@ export default {
},
setWalkthrough: async (parent, { upvotePopover, tipPopover }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
await models.user.update({ where: { id: me.id }, data: { upvotePopover, tipPopover } })
@ -696,7 +696,7 @@ export default {
},
setPhoto: async (parent, { photoId }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
await models.user.update({
@ -708,7 +708,7 @@ export default {
},
upsertBio: async (parent, { bio }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
await ssValidate(bioSchema, { bio })
@ -725,12 +725,12 @@ export default {
},
generateApiKey: async (parent, { id }, { models, me }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
const user = await models.user.findUnique({ where: { id: me.id } })
if (!user.apiKeyEnabled) {
throw new GraphQLError('you are not allowed to generate api keys', { extensions: { code: 'FORBIDDEN' } })
throw new GqlAuthorizationError('you are not allowed to generate api keys')
}
// I trust postgres CSPRNG more than the one from JS
@ -745,14 +745,14 @@ export default {
},
deleteApiKey: async (parent, { id }, { models, me }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
return await models.user.update({ where: { id: me.id }, data: { apiKeyHash: null } })
},
unlinkAuth: async (parent, { authType }, { models, me }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
assertApiKeyNotPermitted({ me })
@ -761,7 +761,7 @@ export default {
user = await models.user.findUnique({ where: { id: me.id } })
const account = await models.account.findFirst({ where: { userId: me.id, provider: authType } })
if (!account) {
throw new GraphQLError('no such account', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('no such account')
}
await models.account.delete({ where: { id: account.id } })
if (authType === 'twitter') {
@ -776,14 +776,14 @@ export default {
} else if (authType === 'email') {
user = await models.user.update({ where: { id: me.id }, data: { email: null, emailVerified: null, emailHash: null } })
} else {
throw new GraphQLError('no such account', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('no such account')
}
return await authMethods(user, undefined, { models, me })
},
linkUnverifiedEmail: async (parent, { email }, { models, me }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
assertApiKeyNotPermitted({ me })
@ -796,7 +796,7 @@ export default {
})
} catch (error) {
if (error.code === 'P2002') {
throw new GraphQLError('email taken', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('email taken')
}
throw error
}
@ -809,12 +809,12 @@ export default {
const muted = await isMuted({ models, muterId: me?.id, mutedId: id })
if (existing) {
if (muted && !existing.postsSubscribedAt) {
throw new GraphQLError("you can't subscribe to a stacker that you've muted", { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError("you can't subscribe to a stacker that you've muted")
}
await models.userSubscription.update({ where: { followerId_followeeId: lookupData }, data: { postsSubscribedAt: existing.postsSubscribedAt ? null : new Date() } })
} else {
if (muted) {
throw new GraphQLError("you can't subscribe to a stacker that you've muted", { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError("you can't subscribe to a stacker that you've muted")
}
await models.userSubscription.create({ data: { ...lookupData, postsSubscribedAt: new Date() } })
}
@ -826,12 +826,12 @@ export default {
const muted = await isMuted({ models, muterId: me?.id, mutedId: id })
if (existing) {
if (muted && !existing.commentsSubscribedAt) {
throw new GraphQLError("you can't subscribe to a stacker that you've muted", { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError("you can't subscribe to a stacker that you've muted")
}
await models.userSubscription.update({ where: { followerId_followeeId: lookupData }, data: { commentsSubscribedAt: existing.commentsSubscribedAt ? null : new Date() } })
} else {
if (muted) {
throw new GraphQLError("you can't subscribe to a stacker that you've muted", { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError("you can't subscribe to a stacker that you've muted")
}
await models.userSubscription.create({ data: { ...lookupData, commentsSubscribedAt: new Date() } })
}
@ -854,7 +854,7 @@ export default {
}
})
if (subscription?.postsSubscribedAt || subscription?.commentsSubscribedAt) {
throw new GraphQLError("you can't mute a stacker to whom you've subscribed", { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError("you can't mute a stacker to whom you've subscribed")
}
await models.mute.create({ data: { ...lookupData } })
}
@ -862,7 +862,7 @@ export default {
},
hideWelcomeBanner: async (parent, data, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
await models.user.update({ where: { id: me.id }, data: { hideWelcomeBanner: true } })

View File

@ -1,5 +1,4 @@
import { createHodlInvoice, createInvoice, decodePaymentRequest, payViaPaymentRequest, getInvoice as getInvoiceFromLnd, getNode, deletePayment, getPayment, getIdentity } from 'ln-service'
import { GraphQLError } from 'graphql'
import crypto, { timingSafeEqual } from 'crypto'
import serialize from './serial'
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
@ -15,6 +14,7 @@ import { finalizeHodlInvoice } from 'worker/wallet'
import walletDefs from 'wallets/server'
import { generateResolverName, generateTypeDefName } from '@/lib/wallet'
import { lnAddrOptions } from '@/lib/lnurl'
import { GqlAuthenticationError, GqlAuthorizationError, GqlInputError } from '@/lib/error'
function injectResolvers (resolvers) {
console.group('injected GraphQL resolvers:')
@ -54,17 +54,17 @@ export async function getInvoice (parent, { id }, { me, models, lnd }) {
})
if (!inv) {
throw new GraphQLError('invoice not found', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('invoice not found')
}
if (inv.user.id === USER_ID.anon) {
return inv
}
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
throw new GqlAuthenticationError()
}
if (inv.user.id !== me.id) {
throw new GraphQLError('not ur invoice', { extensions: { code: 'FORBIDDEN' } })
throw new GqlInputError('not ur invoice')
}
try {
@ -85,7 +85,7 @@ export async function getInvoice (parent, { id }, { me, models, lnd }) {
export async function getWithdrawl (parent, { id }, { me, models, lnd }) {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
throw new GqlAuthenticationError()
}
const wdrwl = await models.withdrawl.findUnique({
@ -99,11 +99,11 @@ export async function getWithdrawl (parent, { id }, { me, models, lnd }) {
})
if (!wdrwl) {
throw new GraphQLError('withdrawal not found', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('withdrawal not found')
}
if (wdrwl.user.id !== me.id) {
throw new GraphQLError('not ur withdrawal', { extensions: { code: 'FORBIDDEN' } })
throw new GqlInputError('not ur withdrawal')
}
return wdrwl
@ -119,7 +119,7 @@ const resolvers = {
invoice: getInvoice,
wallet: async (parent, { id }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
throw new GqlAuthenticationError()
}
return await models.wallet.findUnique({
@ -131,7 +131,7 @@ const resolvers = {
},
walletByType: async (parent, { type }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
throw new GqlAuthenticationError()
}
const wallet = await models.wallet.findFirst({
@ -144,7 +144,7 @@ const resolvers = {
},
wallets: async (parent, args, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
throw new GqlAuthenticationError()
}
return await models.wallet.findMany({
@ -156,7 +156,7 @@ const resolvers = {
withdrawl: getWithdrawl,
numBolt11s: async (parent, args, { me, models, lnd }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
throw new GqlAuthenticationError()
}
return await models.withdrawl.count({
@ -172,7 +172,7 @@ const resolvers = {
walletHistory: async (parent, { cursor, inc }, { me, models, lnd }) => {
const decodedCursor = decodeCursor(cursor)
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
throw new GqlAuthenticationError()
}
const include = new Set(inc?.split(','))
@ -337,7 +337,7 @@ const resolvers = {
},
walletLogs: async (parent, args, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
throw new GqlAuthenticationError()
}
return await models.walletLog.findMany({
@ -413,14 +413,14 @@ const resolvers = {
cancelInvoice: async (parent, { hash, hmac }, { models, lnd, boss }) => {
const hmac2 = createHmac(hash)
if (!timingSafeEqual(Buffer.from(hmac), Buffer.from(hmac2))) {
throw new GraphQLError('bad hmac', { extensions: { code: 'FORBIDDEN' } })
throw new GqlAuthorizationError('bad hmac')
}
await finalizeHodlInvoice({ data: { hash }, lnd, models, boss })
return await models.invoice.findFirst({ where: { hash } })
},
dropBolt11: async (parent, { id }, { me, models, lnd }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
const retention = `${INVOICE_RETENTION_DAYS} days`
@ -450,19 +450,19 @@ const resolvers = {
where: { id: invoice.id },
data: { hash: invoice.hash, bolt11: invoice.bolt11 }
})
throw new GraphQLError('failed to drop bolt11 from lnd', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('failed to drop bolt11 from lnd')
}
}
return { id }
},
removeWallet: async (parent, { id }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
const wallet = await models.wallet.findUnique({ where: { userId: me.id, id: Number(id) } })
if (!wallet) {
throw new GraphQLError('wallet not found', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('wallet not found')
}
await models.$transaction([
@ -475,7 +475,7 @@ const resolvers = {
},
deleteWalletLogs: async (parent, { wallet }, { me, models }) => {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
await models.walletLog.deleteMany({ where: { userId: me.id, wallet } })
@ -575,7 +575,7 @@ export const addWalletLog = async ({ wallet, level, message }, { models }) => {
async function upsertWallet (
{ wallet, testCreateInvoice }, { settings, data, priorityOnly }, { me, models }) {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
throw new GqlAuthenticationError()
}
assertApiKeyNotPermitted({ me })
@ -588,7 +588,7 @@ async function upsertWallet (
wallet = { ...wallet, userId: me.id }
await addWalletLog({ wallet, level: 'ERROR', message }, { models })
await addWalletLog({ wallet, level: 'INFO', message: 'receives disabled' }, { models })
throw new GraphQLError(message, { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError(message)
}
}
@ -674,7 +674,7 @@ export async function createWithdrawal (parent, { invoice, maxFee }, { me, model
decoded = await decodePaymentRequest({ lnd, request: invoice })
} catch (error) {
console.log(error)
throw new GraphQLError('could not decode invoice', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('could not decode invoice')
}
try {
@ -692,7 +692,7 @@ export async function createWithdrawal (parent, { invoice, maxFee }, { me, model
}
if (!decoded.mtokens || BigInt(decoded.mtokens) <= 0) {
throw new GraphQLError('your invoice must specify an amount', { extensions: { code: 'BAD_INPUT' } })
throw new GqlInputError('your invoice must specify an amount')
}
const msatsFee = Number(maxFee) * 1000
@ -721,7 +721,7 @@ export async function createWithdrawal (parent, { invoice, maxFee }, { me, model
export async function sendToLnAddr (parent, { addr, amount, maxFee, comment, ...payer },
{ me, models, lnd, headers }) {
if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
throw new GqlAuthenticationError()
}
assertApiKeyNotPermitted({ me })

23
lib/error.js Normal file
View File

@ -0,0 +1,23 @@
import { GraphQLError } from 'graphql'
export const E_FORBIDDEN = 'E_FORBIDDEN'
export const E_UNAUTHENTICATED = 'E_UNAUTHENTICATED'
export const E_BAD_INPUT = 'E_BAD_INPUT'
export class GqlAuthorizationError extends GraphQLError {
constructor (message) {
super(message, { extensions: { code: E_FORBIDDEN } })
}
}
export class GqlAuthenticationError extends GraphQLError {
constructor () {
super('you must be logged in', { extensions: { code: E_UNAUTHENTICATED } })
}
}
export class GqlInputError extends GraphQLError {
constructor (message) {
super(message, { extensions: { code: E_BAD_INPUT } })
}
}