Commit graph

6 commits

Author SHA1 Message Date
Paulo Castellano
676afb15ba
fix: chunked upload session collision + workspace name i18n (#263)
* fix: give each chunked upload attempt a unique server-side identifier

The upload session identifier was derived only from user+filename+size
(ChunkedAssetReceiver::receive), with no per-attempt nonce. Two genuinely
concurrent attempts of the same file (e.g. closing and reopening the media
picker mid-upload, then re-uploading the same file) collided on the same
Redis cache key / temp file, producing RuntimeException("Chunked cloud
upload session expired or missing.") on the multipart/cloud path and
silent byte corruption on the local-assemble path.

The frontend now mints a UUID per upload attempt (X-Upload-Id header) that
gets folded into the identifier. Falls back to the old formula when the
header is absent, so any already-loaded frontend bundle keeps working.

Also guards the media picker's dropzone against re-triggering an upload
while one is in flight, and aborts the in-flight fetch when the dialog
unmounts mid-upload.

Fixes Nightwatch issue #23.

* fix: explicitly type upload_id when passing to receive()

Matches the existing explicit (int) casts on the sibling validated()
calls in the same method — validated() returns mixed, so this keeps
the nullable-string contract explicit instead of relying on an
implicit runtime type.

* style: inline the upload_id null-safe cast

Drop the intermediate variable so all receive() arguments read as a
single expression each, matching the sibling validated() casts.

* fix: require X-Upload-Id instead of falling back to the legacy identifier

Nullable upload_id only preserved the old (collision-prone) formula for
clients that omit the header — it didn't actually protect them. Making it
required closes that gap outright: a request without the header now fails
loud (422) instead of silently falling back to the vulnerable identifier.

ChunkedAssetReceiver::receive() now takes a required $attemptId. Updated
every existing test hitting app.assets.store-chunked (ChunkedCloudUploadTest,
ChunkedAssetReceiverTest, ChunkedUploadFilenameEncodingTest, AssetControllerTest)
to send a real upload id, and added a regression test asserting the endpoint
rejects a request with no X-Upload-Id header.

* fix: localize hardcoded workspace name validation messages

StoreWorkspaceRequest had its custom messages() hardcoded in pt-BR
regardless of the user's locale; UpdateWorkspaceRequest had the same
bug hardcoded in English. Both now go through __('validation.required'
/ 'validation.max.string') with the already-localized
workspaces.create.name attribute label (present in all 16 lang/
directories), matching the pattern already used by
StoreWorkspaceInviteRequest.

Unrelated to the chunked upload fix, but caught while reviewing this
file's messages() convention.

* simplify: drop messages() override on workspace name validation

Laravel already localizes the generic required/max messages from
lang/{locale}/validation.php automatically — no need to hand-roll
messages() for standard rules with no custom copy.

* fix: localize StoreChunkedAssetRequest validation messages

Drop the hardcoded English messages for required/ends_with rules —
Laravel's own localized validation.php messages already cover them
adequately (ends_with's generic message is actually more useful, since
it lists the accepted extensions). total_size.max still needs a custom
message (the rule is in raw bytes, unreadable without MB conversion),
so it now goes through __('assets.upload.file_too_large') with the key
added to all 16 lang/ locales.

Also fixed test flakiness discovered while touching this file:
ChunkedCloudUploadTest used random_bytes() for the first mp4 chunk,
which occasionally collides with an unrelated magic number (MZ/PE,
SIMH tape, ...) and makes finfo misdetect the mime type. Replaced with
real mp4 header bytes padded with nulls, so detection is deterministic.

* fix: address final code review findings

- ChunkedAssetReceiver: use double-quoted interpolation instead of
  concatenation for the identifier hash, per project convention.
- AssetControllerTest: two chunked-upload rejection tests didn't send
  X-Upload-Id, so their 422 assertions could pass for the wrong reason
  (upload_id.required) instead of the field they claim to cover. Added
  the header and asserted the specific validation error field.
- GalleryBrowser: centralize the upload-in-progress guard as a single
  check at the top of uploadFiles() instead of three separate checks
  at each entry point (click/select/drop) — matches the single-source-
  of-truth pattern already used in PhotoUpload.vue.
- GalleryBrowser: show a toast when an in-flight upload is aborted
  (dialog closed mid-upload) instead of silently discarding it with no
  feedback. New assets.upload.cancelled key added to all 16 lang/
  locales.
2026-08-09 14:06:47 -03:00
Paulo Castellano
f0e2907ab5 fix(accounts,media): address PR review findings
Connect flow:
- surface OAuth failures: useOAuthPopup forwards {success, message}; the
  accounts grid toasts the error instead of silently reloading
- detect a blocked popup and toast a "allow popups" message
- PopupCallback only promises auto-close when it has an opener, otherwise
  tells the user to close it; adds role=status/aria-live and aria-hidden icons
- drop the dead LinkedInPage connect-route entry and the unsent access_token
  field on the Facebook page-select type

Media/gallery:
- delete dead code: MediaDropzone.vue and useDragAndDrop.ts (no references)
- GalleryBrowser toasts per-file upload failures instead of swallowing them;
  cards are always clickable now that videos open in the lightbox
- formatDurationMs rounds the sub-second branch and tolerates undefined
- advertise PDF in the gallery's accepted-formats hint

i18n: popup_blocked, manual_close (accounts) and upload.failed (assets) in all locales.
2026-06-25 19:06:43 -03:00
Paulo Castellano
dafdd5da43 feat: media gallery picker, custom emoji picker, preview tabs, real platform logos
- gallery: extract /assets tabs (uploads, Unsplash, Giphy) into shared
  GalleryBrowser used by both /assets and a new MediaPickerDialog inside the
  post editor; add JSON search endpoint for workspace assets with tests
- emoji: replace broken emoji-picker-element web component with a custom
  EmojiPicker (full Unicode set, search, categories, recently-used,
  light/dark, i18n)
- preview tab: platform selector pills, variant tabs (data-driven from
  content_types map) so the user can switch Feed/Reel/Story etc. and have
  it autosave through the same handler ScheduleTab uses
- platform logos: shared usePlatformLogo composable (logo + label + content
  types); replaces inline maps across 5 components, fixes
  instagram-facebook falling back to default.png
- tooltips: hover details (display_name · @username + platform label) on
  platform avatars across editor, posts list and calendar
- settings cards: show ` · @username` in the title bar so multiple accounts
  on the same network are distinguishable
- routes: drop the throttle:6,1 group middleware on social connect routes
  (was 429ing legitimate OAuth retries) and rely on the default limiter
2026-05-01 14:53:49 -03:00
Paulo Castellano
0f6ae9a4e6 feat: add PostCommentCreated broadcast event 2026-04-15 20:11:36 -03:00
Paulo Castellano
e1965f9f7c feat: integrate Giphy support and add trending endpoints for media assets 2026-04-15 11:02:10 -03:00
Paulo Castellano
0d88ced20b feat: implement asset management system with Unsplash integration and chunked uploads 2026-04-15 10:20:37 -03:00