Compare commits

...

2 Commits

Author SHA1 Message Date
ekzyis c8975038bd
Never update author of item on edit (#1401)
* Never update author of item on edit

* Only show option to edit via hmac if anonymous

* Only send hash+hmac if anonymous
2024-09-13 11:19:54 -05:00
ekzyis 30d5eb9801
Catch s3 upload errors (#1400)
* Catch s3 upload errors

* Include file name in error message

* More renaming from image to file
2024-09-13 10:41:07 -05:00
5 changed files with 37 additions and 25 deletions

View File

@ -1315,16 +1315,19 @@ export const updateItem = async (parent, { sub: subName, forward, hash, hmac, ..
if (old.bio) { if (old.bio) {
// prevent editing a bio like a regular item // prevent editing a bio like a regular item
item = { id: Number(item.id), text: item.text, title: `@${user.name}'s bio`, userId: meId } item = { id: Number(item.id), text: item.text, title: `@${user.name}'s bio` }
} else if (old.parentId) { } else if (old.parentId) {
// prevent editing a comment like a post // prevent editing a comment like a post
item = { id: Number(item.id), text: item.text, userId: meId } item = { id: Number(item.id), text: item.text }
} else { } else {
item = { subName, userId: meId, ...item } item = { subName, ...item }
item.forwardUsers = await getForwardUsers(models, forward) item.forwardUsers = await getForwardUsers(models, forward)
} }
item.uploadIds = uploadIdsFromText(item.text, { models }) item.uploadIds = uploadIdsFromText(item.text, { models })
// never change author of item
item.userId = old.userId
const resultItem = await performPaidAction('ITEM_UPDATE', item, { models, me, lnd }) const resultItem = await performPaidAction('ITEM_UPDATE', item, { models, me, lnd })
resultItem.comments = [] resultItem.comments = []

View File

@ -29,11 +29,12 @@ export default {
} }
} }
// width and height is 0 for videos
if (width * height > IMAGE_PIXELS_MAX) { if (width * height > IMAGE_PIXELS_MAX) {
throw new GqlInputError(`image must be less than ${IMAGE_PIXELS_MAX} pixels`) throw new GqlInputError(`image must be less than ${IMAGE_PIXELS_MAX} pixels`)
} }
const imgParams = { const fileParams = {
type, type,
size, size,
width, width,
@ -44,10 +45,10 @@ export default {
if (avatar) { if (avatar) {
if (!me) throw new GqlAuthenticationError() if (!me) throw new GqlAuthenticationError()
imgParams.paid = undefined fileParams.paid = undefined
} }
const upload = await models.upload.create({ data: { ...imgParams } }) const upload = await models.upload.create({ data: { ...fileParams } })
return createPresignedPost({ key: String(upload.id), type, size }) return createPresignedPost({ key: String(upload.id), type, size })
} }
} }

View File

@ -39,8 +39,8 @@ export const FileUpload = forwardRef(({ children, className, onSelect, onUpload,
try { try {
({ data } = await getSignedPOST({ variables })) ({ data } = await getSignedPOST({ variables }))
} catch (e) { } catch (e) {
toaster.danger('error initiating upload: ' + e.message || e.toString?.())
onError?.({ ...variables, name: file.name, file }) onError?.({ ...variables, name: file.name, file })
reject(e)
return return
} }
@ -58,10 +58,8 @@ export const FileUpload = forwardRef(({ children, className, onSelect, onUpload,
if (!res.ok) { if (!res.ok) {
// TODO make sure this is actually a helpful error message and does not expose anything to the user we don't want // TODO make sure this is actually a helpful error message and does not expose anything to the user we don't want
const err = res.statusText
toaster.danger('error uploading: ' + err)
onError?.({ ...variables, name: file.name, file }) onError?.({ ...variables, name: file.name, file })
reject(err) reject(new Error(res.statusText))
return return
} }
@ -96,16 +94,20 @@ export const FileUpload = forwardRef(({ children, className, onSelect, onUpload,
onChange={async (e) => { onChange={async (e) => {
const fileList = e.target.files const fileList = e.target.files
for (const file of Array.from(fileList)) { for (const file of Array.from(fileList)) {
try {
if (accept.indexOf(file.type) === -1) { if (accept.indexOf(file.type) === -1) {
toaster.danger(`image must be ${accept.map(t => t.replace('image/', '').replace('video/', '')).join(', ')}`) throw new Error(`file must be ${accept.map(t => t.replace(/^(image|video)\//, '')).join(', ')}`)
continue
} }
if (onSelect) await onSelect?.(file, s3Upload) if (onSelect) await onSelect?.(file, s3Upload)
else await s3Upload(file) else await s3Upload(file)
} catch (e) {
toaster.danger(`upload of '${file.name}' failed: ` + e.message || e.toString?.())
continue
}
}
// reset file input // reset file input
// see https://bobbyhadz.com/blog/react-reset-file-input#reset-a-file-input-in-react // see https://bobbyhadz.com/blog/react-reset-file-input#reset-a-file-input-in-react
e.target.value = null e.target.value = null
}
}} }}
/> />
<div <div

View File

@ -49,9 +49,11 @@ export default function ItemInfo ({
}, [item]) }, [item])
useEffect(() => { useEffect(() => {
const invoice = window.localStorage.getItem(`item:${item.id}:hash:hmac`) const authorEdit = item.mine
setCanEdit((item.mine || invoice) && (Date.now() < editThreshold)) const invParams = window.localStorage.getItem(`item:${item.id}:hash:hmac`)
}, [item.id, item.mine, editThreshold]) const hmacEdit = !!invParams && !me && Number(item.user.id) === USER_ID.anon
setCanEdit((authorEdit || hmacEdit) && (Date.now() < editThreshold))
}, [me, item.id, item.mine, editThreshold])
// territory founders can pin any post in their territory // territory founders can pin any post in their territory
// and OPs can pin any root reply in their post // and OPs can pin any root reply in their post

View File

@ -6,6 +6,8 @@ import { useCallback } from 'react'
import { normalizeForwards, toastUpsertSuccessMessages } from '@/lib/form' import { normalizeForwards, toastUpsertSuccessMessages } from '@/lib/form'
import { RETRY_PAID_ACTION } from '@/fragments/paidAction' import { RETRY_PAID_ACTION } from '@/fragments/paidAction'
import gql from 'graphql-tag' import gql from 'graphql-tag'
import { USER_ID } from '@/lib/constants'
import { useMe } from './me'
// this is intented to be compatible with upsert item mutations // this is intented to be compatible with upsert item mutations
// so that it can be reused for all post types and comments and we don't have // so that it can be reused for all post types and comments and we don't have
@ -19,6 +21,7 @@ export default function useItemSubmit (mutation,
const toaster = useToast() const toaster = useToast()
const crossposter = useCrossposter() const crossposter = useCrossposter()
const [upsertItem] = usePaidMutation(mutation) const [upsertItem] = usePaidMutation(mutation)
const { me } = useMe()
return useCallback( return useCallback(
async ({ boost, crosspost, title, options, bounty, maxBid, start, stop, ...values }, { resetForm }) => { async ({ boost, crosspost, title, options, bounty, maxBid, start, stop, ...values }, { resetForm }) => {
@ -27,10 +30,11 @@ export default function useItemSubmit (mutation,
options = options.slice(item?.poll?.options?.length || 0).filter(o => o.trim().length > 0) options = options.slice(item?.poll?.options?.length || 0).filter(o => o.trim().length > 0)
} }
if (item?.id) { const hmacEdit = item?.id && Number(item.user.id) === USER_ID.anon && !me
const invoiceData = window.localStorage.getItem(`item:${item.id}:hash:hmac`) if (hmacEdit) {
if (invoiceData) { const invParams = window.localStorage.getItem(`item:${item.id}:hash:hmac`)
const [hash, hmac] = invoiceData.split(':') if (invParams) {
const [hash, hmac] = invParams.split(':')
values.hash = hash values.hash = hash
values.hmac = hmac values.hmac = hmac
} }
@ -89,7 +93,7 @@ export default function useItemSubmit (mutation,
await router.push(sub ? `/~${sub.name}/recent` : '/recent') await router.push(sub ? `/~${sub.name}/recent` : '/recent')
} }
} }
}, [upsertItem, router, crossposter, item, sub, onSuccessfulSubmit, }, [me, upsertItem, router, crossposter, item, sub, onSuccessfulSubmit,
navigateOnSubmit, extraValues, paidMutationOptions] navigateOnSubmit, extraValues, paidMutationOptions]
) )
} }