Migrate profile-edit page to Vue + /api/v2/users/me (Group 2.2, all 5 tabs)#868
Open
edwh wants to merge 18 commits into
Open
Migrate profile-edit page to Vue + /api/v2/users/me (Group 2.2, all 5 tabs)#868edwh wants to merge 18 commits into
edwh wants to merge 18 commits into
Conversation
…Group 2.2 partial 1/5)
The profile-edit page is a tabbed shell with 5 partials (profile, account,
email-preferences, calendars, repair-directory). This commit migrates the
email-preferences partial — the simplest — as the first slice.
- GET/PATCH /api/v2/users/me/preferences (auth:api, OpenAPI annotated)
returning {data: {invites: bool}}
- tests/Feature/Users/APIv2UserPreferencesTest: 7 cases covering auth,
shape, validation, and persistence in both directions
- resources/js/components/EmailPreferencesTab.vue: small Vue component
rendering the existing UI and POSTing via the new endpoint
- resources/views/user/profile/email-preferences.blade.php now mounts
the Vue component; the existing /profile/edit-preferences POST handler
is unchanged and remains a fallback
- lang/{en,fr,fr-BE}/general.php: error_occurred key for the toast
Follow-ups: migrate profile/account/calendars/repair-directory tabs
incrementally on the same branch.
edwh
added a commit
that referenced
this pull request
May 30, 2026
…oup 2.2 2/5) - GET /api/v2/users/me/calendars (auth:api) returns user + per-group + admin all-events + group-area calendar URLs with OpenAPI - tests/Feature/Users/APIv2UserCalendarsTest: 3 cases (auth, host shape, admin flag/url) - CalendarsTab.vue: lists subscription URLs, copy buttons, area select; uses clipboard API instead of the old jQuery btn-copy-input-text - calendars.blade.php now mounts the Vue component
…ir-directory-* (Group 2.2 3/5)
- GET /api/v2/users/{id}/repair-directory-options: per-option `selected`
+ `disabled` flags computed against existing UserPolicy::changeRepairDirRole.
- PATCH /api/v2/users/{id}/repair-directory-role: policy-gated save.
- APIv2RepairDirectoryTest: 7 cases (auth/404/super-admin enabled/non-admin
disabled/forbidden update/persistence/validation enum).
- RepairDirectoryTab.vue: replicates the existing select with disabled
options and submit button.
…up 2.2 4/5 partial) The account tab contains 4 sub-forms (password, language, admin matrix, soft-delete). This commit migrates the language sub-form only — non-sensitive and isolated. Password change, admin matrix, and soft-delete remain on existing CSRF-protected POST routes pending adversarial review of those security boundaries. - GET /api/v2/users/me/language returns current language + supported locales - PATCH /api/v2/users/me/language validates against supported list, fires UserLanguageUpdated event, persists to user - APIv2UserLanguageTest: 5 cases (get auth/shape, update auth/validation/persist) - LanguageTab.vue: select + save, reloads after save to apply translations
…ed key
The PR's new v2 endpoint tests exposed three real issues:
1. Unauthenticated auth:api requests returned 500 instead of 401. The custom
JSON exception handler mapped any exception without getStatusCode() to 500,
and Laravel's AuthenticationException has none. Added an explicit
AuthenticationException -> 401 branch. This is correct API behaviour and was
simply never asserted before (these are the suite's first 401 tests), so it
fixed every *RequiresAuth test across preferences/calendars/language/
repair-directory at once.
2. getMyCalendarsv2 read the admin all-events hash via env('CALENDAR_HASH')
directly, which returns null under config:cache in production (a latent bug)
and can't be overridden in tests. Moved it to config('restarters.calendar_hash')
and updated the test to set it via config() instead of putenv() (which
Laravel's env() does not read).
3. email_alerts_pref1 ('monthly newsletter') was only ever referenced inside a
commented-out block in the old blade, so translations:check passed on the
stray string. Migrating that tab to Vue removed the last textual reference,
orphaning the key. The newsletter checkbox has long been disabled (handled by
the community platform), so removed the dead key from en/fr/fr-BE.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PHPUnit is green after the previous commit; the only failure was tests/Integration/event.test.js "Invite volunteers modal opens from Event Actions dropdown" timing out clicking the dropdown item. That test is untouched by this PR, the branch is level with develop, and the test has a long history of timeout/hang flake-fixes (Google Maps hang in Docker noted in its own comments). Empty commit to re-run the suite. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The event Playwright suite failed deterministically today (last day of May) on "Invite volunteers modal opens from Event Actions dropdown". Root cause is in the shared createEvent() helper, not the test or this PR's code: createEvent(past=false) selected the last day of the *current* month at 13:00-14:00. On the last day of a month that date is "today"; when CI runs after 14:00 local the event has already finished, so the API reports upcoming=false and EventActions correctly hides the "Invite Volunteers" item (gated on isAttending && upcoming && approved). The test then times out waiting for an item that will never appear. develop passed only because its run happened earlier in the day. Fix: for future events, advance to the next month before selecting the day (mirroring the past branch's "Previous month"), so the event is always comfortably upcoming regardless of when the suite runs. Only the two past=false callers in event.test.js are affected; device.test.js uses past. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Migrates the name/country/email/townCity/age/gender/biography form and the repair-skills selector to Vue, backed by new /api/v2/users/me/profile and /api/v2/users/me/skills endpoints (mirroring the auth:api + Auth::user() pattern used by the language/calendars/preferences endpoints). - getMyProfilev2 / updateMyProfilev2: replicate postProfileInfoEdit's validation and geocoding behaviour (geocode townCity+country, clear lat/long on empty location or geocode failure). - getMySkillsv2 / updateMySkillsv2: replicate postProfileTagsEdit's skillsold() sync and host-role promotion when a category-1 skill is selected. - ProfileInfoTab.vue / SkillsTab.vue: new components mounted from profile.blade.php, replacing the edit-info and edit-tags forms. - APIv2UserProfileTest: 15 cases covering auth, validation, persistence, geocoding (success/empty/failure), skills replace, and PII hiding (api_token/calendar_hash/latitude/longitude absent from responses). - EditProfileTest::test_tags_update updated to assert persisted skill IDs directly rather than scraping a server-rendered <option selected>, since the skills selector is now populated client-side via the API. The "change photo" upload form is intentionally left as legacy Blade, pending a security review of the upload surface.
The profile tab migration replaced profile.blade.php (which was the only user of
`@lang('general.other_profile')`) with the ProfileInfoTab/SkillsTab Vue mounts,
leaving the key unused. `php artisan translations:check` fails on unused keys, so
remove `other_profile` from every locale's general.php.
Verified: translations:check exits 0.
SonarCloud flagged the area <select> and the read-only area-URL <input> in CalendarsTab.vue as form controls with no associated label. Add ids and visually-hidden (sr-only) <label> elements so each control is properly labelled for assistive tech.
edwh
added a commit
that referenced
this pull request
Jul 3, 2026
…sified Mark Group 2.2 (profile-edit, all 5 tabs) done via #868, note the four PRs are rebased onto current develop and CI-green, record 3.2 dead-code removal (#891), and flag that the only remaining items are security-sensitive (group/view permission flags; profile-photo upload; account password/admin/delete) or deliberately server-rendered iframe widgets.
…min)
Completes the profile-edit page migration. These three surfaces are
security-sensitive; each API endpoint replicates the existing web handler's
authorization exactly:
- Change password: PATCH /api/v2/users/me/password. Operates only on
Auth::user() (no id param — cannot target another user); requires
Hash::check(current_password); enforces new === confirmation; resets the
recovery token and fires PasswordChanged. Response returns only {success}.
- Admin settings: GET/PATCH /api/v2/users/{id}/admin-settings. Administrator
only — abort(403) for any non-admin (matches postAdminEdit). role set
directly (not mass-assignable); NetworkCoordinator demotion detaches
networks; soft-deleted group memberships restored before sync of
groups/preferences/permissions.
- Profile photo: POST /api/v2/users/me/photo. Auth::user() only; validates
required|image|mimes:jpeg,png,gif,webp|max:2048; stores via the existing
FixometerFile helper.
Also: Handler::render() now maps AuthorizationException -> 403 (it lacks
getStatusCode() and previously fell through to 500), so authorize() failures
return 403 for JSON API clients.
New Vue: PasswordTab, AdminSettingsTab (admin-only UI), ProfilePhotoTab.
Tests: APIv2UserPasswordTest (6), APIv2UserAdminSettingsTest (11),
APIv2UserPhotoTest (6) — cover 401/403/422/200, the non-admin privilege-
escalation guard, wrong-current-password, non-image upload, and PII hiding.
NOTE: these are sensitive surfaces (credentials, role assignment, file upload)
and warrant a security review pass.
FixometerFile::upload() only recognises jpeg/png/gif (finfo mime map), so a webp that passed the API's mimes:...,webp validation would then be silently rejected at storage. Align the contract: remove webp from the validation rule and the file input's accept attribute.
The test scraped the CSRF token from the admin-settings <form> on /user/edit and
asserted a server-rendered <option selected> for the re-added group. That form is
now the AdminSettingsTab Vue component (PATCH /api/v2/users/{id}/admin-settings),
so both the token input and the option markup are gone. Re-add the host via the
API endpoint and assert membership via the groups() relationship instead of
scraping HTML.
Add DELETE /api/v2/users/me (auth:api) mirroring postSoftDeleteUser: authorizes via UserPolicy::delete then soft-deletes Auth::user() (existing event handlers anonymise the record). Replace the last server-rendered form on the account page with DeleteAccountTab.vue - a danger alert plus a confirm modal that calls the new endpoint and redirects to /login. Add APIv2UserDeleteAccountTest covering unauthenticated access, a successful self-delete (soft-deleted + trashed), and that the id cannot be smuggled in to delete another user's account.
…server Replace the multipart POST /api/v2/users/me/photo transport with the tus protocol: Uppy (core + dashboard + tus + compressor) uploads to a new /api/tus route backed by ankitpokhrel/tus-php (TusController wraps TusPhp\Tus\Server, storage under storage/app/tus + storage/app/tus-cache, both gitignored). The tus route is deliberately outside auth:api (tus's chunked PATCH requests can't carry an API token cleanly) and CSRF-exempt, capped at 10MB per upload as a basic anti-abuse limit. updateMyPhotov2 now takes an upload_key instead of a file: it looks the key up in the shared tus cache (App\Helpers\Tus::buildCache(), kept separate from buildServer() because tus-php's Server eagerly builds a Request from PHP's real superglobals - fatal under this worktree's elevated CLI error_reporting), confirms the upload is complete and under 2MB, sniffs the real MIME type, then hands the assembled file to a new FixometerFile::uploadLocalFile() (factored out of upload() so the same thumbnailing/DB-record pipeline runs without requiring a move_uploaded_file()-eligible source). The tus temp file and cache entry are deleted after every attempt, successful or not, so a key can't be replayed. ProfilePhotoTab.vue is rewritten around a plain-JS Uppy Dashboard (Vue 2 has no supported @uppy/vue release - every version requires Vue 3 - so the framework-agnostic core/dashboard/tus/compressor packages are mounted directly via a template ref instead). On upload completion it derives the tus key from the last path segment of file.tus.uploadUrl, same convention as Freegle's OurUploader.vue, and posts it to /photo. Tests: APIv2UserPhotoTest is rewritten around the upload_key contract, seeding completed uploads directly into the real tus cache/upload dir (same factory the controller uses) so updateMyPhotov2 itself runs unmocked. APIv2TusUploadTest drives the real /api/tus route's creation step through the actual HTTP test client (with PHP's real $_SERVER superglobals seeded, since tus-php reads those directly rather than Laravel's request object) - the PATCH byte-transfer step can't be driven the same way because tus-php hardcodes php://input as its source, which isn't reachable from a single PHPUnit process without a real HTTP server in front of it.
…ration
Like testAdminRemoveReaddHost, this scraped input[name=_token] from /user/edit and
POSTed to /profile/edit-admin-settings. The admin-settings form is now the
AdminSettingsTab Vue component; once the account soft-delete form was also removed
the page had no _token input at all, so $tokens[0] was undefined. Demote the
coordinator via PATCH /api/v2/users/{id}/admin-settings instead.
The tus endpoint is registered in routes/api.php, which uses the "api" middleware group — that group does not include VerifyCsrfToken at all, so the api/tus entries in VerifyCsrfToken::$except were redundant. Removing them restores $except to empty (no behaviour change) and clears SonarCloud's S4502 "disabling CSRF protection" finding. Comment updated to explain why CSRF simply doesn't apply here.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
Migrates the entire profile-edit page (Group 2.2 of the Blade-to-Vue plan) from Blade to Vue components backed by
/api/v2/users/me/*endpoints. All five tabs are now Vue:EmailPreferencesTab.vue+GET/PATCH /api/v2/users/me/preferencesLanguageTab.vue+PATCH /api/v2/users/me/languageCalendarsTab.vue+GET /api/v2/users/me/calendarsRepairDirectoryTab.vue+GET/PATCH /api/v2/users/me/repair-directory-*ProfileInfoTab.vue+SkillsTab.vue+GET/PATCH /api/v2/users/me/profileand/skillsEach endpoint has OpenAPI annotations and a dedicated
APIv2*Test. Responses return an explicit field whitelist (noapi_token/calendar_hash/ lat-lng leakage;User::$hiddenasserted in tests).Deferred (needs security review)
The profile photo upload form is intentionally left as legacy Blade/jQuery, pending an adversarial review of the upload surface (file handling, validation, storage). It is clearly commented in
profile.blade.php.Behavioural note
The migrated profile/language tabs operate on
Auth::user()("me") only — the legacy/profile/edit-info//profile/edit-tagsweb routes still exist and still support an admin editing another user'sid, but there is no longer UI for that on this page (consistent precedent across the migrated tabs). Flag if admin-edits-another-user's-profile is a required surface.Test plan
APIv2UserProfileTest(15),APIv2UserPreferencesTest,APIv2UserCalendarsTest,APIv2UserLanguageTest,APIv2RepairDirectoryTest— full tab set green locally (35 tests)EditProfileTest,ProfileTest,PrivilegeEscalationTestpass (skills assertion updated to check persistedUsersSkillsrows rather than server-rendered HTML)vite buildsucceeds