diff --git a/CHANGELOG.md b/CHANGELOG.md index 6091cf7f4..ccafc0383 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improved error handling when a room requires an access code but none was provided ([#3035]) +- Improved 404 error messages for better readability and clarity ([#86], [#3036]) +- Standardized 404 error handling on admin pages ([#1676], [#3036]) +- Improved and standardized room not found error handling in the room view ([#3036]) ## [v4.14.2] - 2026-04-10 @@ -544,6 +547,7 @@ You can find the changelog for older versions there [here](https://github.com/TH [#31]: https://github.com/THM-Health/PILOS/issues/31 [#75]: https://github.com/THM-Health/PILOS/issues/75 [#77]: https://github.com/THM-Health/PILOS/issues/77 +[#86]: https://github.com/THM-Health/PILOS/issues/86 [#300]: https://github.com/THM-Health/PILOS/issues/300 [#315]: https://github.com/THM-Health/PILOS/issues/315 [#372]: https://github.com/THM-Health/PILOS/issues/372 @@ -640,6 +644,7 @@ You can find the changelog for older versions there [here](https://github.com/TH [#1636]: https://github.com/THM-Health/PILOS/issues/1636 [#1651]: https://github.com/THM-Health/PILOS/issues/1651 [#1675]: https://github.com/THM-Health/PILOS/issues/1675 +[#1676]: https://github.com/THM-Health/PILOS/issues/1676 [#1677]: https://github.com/THM-Health/PILOS/issues/1677 [#1678]: https://github.com/THM-Health/PILOS/pull/1678 [#1679]: https://github.com/THM-Health/PILOS/issues/1679 @@ -774,6 +779,7 @@ You can find the changelog for older versions there [here](https://github.com/TH [#3028]: https://github.com/THM-Health/PILOS/issues/3028 [#3029]: https://github.com/THM-Health/PILOS/pull/3029 [#3035]: https://github.com/THM-Health/PILOS/pull/3035 +[#3036]: https://github.com/THM-Health/PILOS/pull/3036 [#3039]: https://github.com/THM-Health/PILOS/issues/3039 [#3040]: https://github.com/THM-Health/PILOS/pull/3040 [unreleased]: https://github.com/THM-Health/PILOS/compare/v4.14.2...develop diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index f100a15d8..a57133302 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -4,12 +4,15 @@ namespace App\Exceptions; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler; use Illuminate\Foundation\ViteException; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Str; use Psr\Log\LogLevel; use Spatie\LaravelIgnition\Exceptions\ViewException; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Throwable; class Handler extends ExceptionHandler @@ -77,5 +80,21 @@ public function register(): void return null; }); + + $this->renderable(function (NotFoundHttpException $e, Request $request) { + if ($request->expectsJson()) { + $modelNotFoundException = $e->getPrevious() instanceof ModelNotFoundException ? $e->getPrevious() : null; + + if ($modelNotFoundException) { + $json = [ + 'message' => 'model_not_found', + 'model' => Str::snake(class_basename($modelNotFoundException->getModel())), + 'ids' => $modelNotFoundException->getIds(), + ]; + + return response()->json($json, 404); + } + } + }); } } diff --git a/app/Http/Controllers/api/v1/RoomMemberController.php b/app/Http/Controllers/api/v1/RoomMemberController.php index 35d8aa78d..3a255cadf 100644 --- a/app/Http/Controllers/api/v1/RoomMemberController.php +++ b/app/Http/Controllers/api/v1/RoomMemberController.php @@ -122,14 +122,11 @@ public function bulkImport(Room $room, BulkImportRequest $request) * * @return Response */ - public function update(Room $room, User $user, UpdateRoomMemberRequest $request) + public function update(Room $room, User $member, UpdateRoomMemberRequest $request) { - if (! $room->members->contains($user)) { - abort(410, __('app.errors.not_member_of_room')); - } - $room->members()->updateExistingPivot($user, ['role' => $request->role]); + $room->members()->updateExistingPivot($member, ['role' => $request->role]); - Log::info('Changed role for member {member} to {role} in room {room}', ['room' => $room->getLogLabel(), 'role' => RoomUserRole::from($request->role)->label(), 'member' => $user->getLogLabel()]); + Log::info('Changed role for member {member} to {role} in room {room}', ['room' => $room->getLogLabel(), 'role' => RoomUserRole::from($request->role)->label(), 'member' => $member->getLogLabel()]); return response()->noContent(); } @@ -155,14 +152,11 @@ public function bulkUpdate(Room $room, BulkUpdateRequest $request) * * @return Response */ - public function destroy(Room $room, User $user) + public function destroy(Room $room, User $member) { - if (! $room->members->contains($user)) { - abort(410, __('app.errors.not_member_of_room')); - } - $room->members()->detach($user); + $room->members()->detach($member); - Log::info('Removed member {member} from room {room}', ['room' => $room->getLogLabel(), 'member' => $user->getLogLabel()]); + Log::info('Removed member {member} from room {room}', ['room' => $room->getLogLabel(), 'member' => $member->getLogLabel()]); return response()->noContent(); } diff --git a/app/Http/Controllers/api/v1/RoomPersonalizedLinkController.php b/app/Http/Controllers/api/v1/RoomPersonalizedLinkController.php index e7158bb51..4b29b87a8 100644 --- a/app/Http/Controllers/api/v1/RoomPersonalizedLinkController.php +++ b/app/Http/Controllers/api/v1/RoomPersonalizedLinkController.php @@ -103,20 +103,16 @@ public function store(Room $room, RoomPersonalizedLinkRequest $request) * * @return RoomPersonalizedLinkResource */ - public function update(Room $room, RoomPersonalizedLink $link, RoomPersonalizedLinkRequest $request) + public function update(Room $room, RoomPersonalizedLink $personalizedLink, RoomPersonalizedLinkRequest $request) { - if (! $link->room->is($room)) { - abort(404, __('app.errors.personalized_link_not_found')); - } - - $link->firstname = $request->firstname; - $link->lastname = $request->lastname; - $link->role = $request->role; - $link->save(); + $personalizedLink->firstname = $request->firstname; + $personalizedLink->lastname = $request->lastname; + $personalizedLink->role = $request->role; + $personalizedLink->save(); - Log::info('Updated personalized room link for guest {name} with the role {role} for room {room}', ['room' => $room->getLogLabel(), 'role' => $link->role->label(), 'name' => $link->fullname]); + Log::info('Updated personalized room link for guest {name} with the role {role} for room {room}', ['room' => $room->getLogLabel(), 'role' => $personalizedLink->role->label(), 'name' => $personalizedLink->fullname]); - return new RoomPersonalizedLinkResource($link); + return new RoomPersonalizedLinkResource($personalizedLink); } /** @@ -126,15 +122,11 @@ public function update(Room $room, RoomPersonalizedLink $link, RoomPersonalizedL * * @throws \Exception */ - public function destroy(Room $room, RoomPersonalizedLink $link) + public function destroy(Room $room, RoomPersonalizedLink $personalizedLink) { - if (! $link->room->is($room)) { - abort(404, __('app.errors.personalized_link_not_found')); - } - - $link->delete(); + $personalizedLink->delete(); - Log::info('Removed personalized room link for guest {name} with the role {role} for room {room}', ['room' => $room->getLogLabel(), 'role' => $link->role->label(), 'name' => $link->fullname]); + Log::info('Removed personalized room link for guest {name} with the role {role} for room {room}', ['room' => $room->getLogLabel(), 'role' => $personalizedLink->role->label(), 'name' => $personalizedLink->fullname]); return response()->noContent(); } diff --git a/lang/en/app.php b/lang/en/app.php index 3782829de..fe001afa4 100644 --- a/lang/en/app.php +++ b/lang/en/app.php @@ -43,7 +43,7 @@ 'membership_disabled' => 'Membership failed! Membership for this room is currently not available.', 'no_room_access' => 'You does not have the necessary permissions, to edit this room.', 'no_server_available' => 'Currently there are no servers available.', - 'not_member_of_room' => 'The person is not a member of this room (anymore).', + 'not_member_of_room' => 'The user is not a member of this room.', 'not_running' => 'Joining the room has failed as it is currently closed.', 'personalized_link_not_found' => 'The personalized room link could not be found.', 'record_agreement_missing' => 'Consent to the recording is required.', @@ -66,6 +66,10 @@ 'flash' => [ 'client_error' => 'An unknown error occurred in the application!', 'guests_only' => 'The request can only be made by guests!', + 'model_not_found' => [ + 'title' => 'The :model was not found!', + 'details' => 'Ids: :ids', + ], 'server_error' => [ 'empty_message' => 'An error occurred on the server during request!', 'error_code' => 'Error code: :statusCode', @@ -97,10 +101,21 @@ 'fr' => 'French', ], 'model' => [ + 'meeting' => 'Meeting', + 'recording' => 'Recording', + 'recording_format' => 'Recording format', + 'role' => 'Role', 'roles' => 'role', + 'room' => 'Room', + 'room_file' => 'Room file', + 'room_personalized_link' => 'Room personalized link', + 'room_type' => 'Room type', 'room_types' => 'room type', + 'server' => 'Server', + 'server_pool' => 'Server pool', 'server_pools' => 'server pool', 'servers' => 'server', + 'user' => 'User', 'users' => 'user', ], 'model_name' => 'Name', diff --git a/resources/js/components/RoomCard.vue b/resources/js/components/RoomCard.vue index af48193fe..93617d18d 100644 --- a/resources/js/components/RoomCard.vue +++ b/resources/js/components/RoomCard.vue @@ -35,6 +35,7 @@ @@ -72,6 +73,7 @@
diff --git a/resources/js/components/RoomFavoriteButton.vue b/resources/js/components/RoomFavoriteButton.vue index 0e64e9ef8..fd0439d7b 100644 --- a/resources/js/components/RoomFavoriteButton.vue +++ b/resources/js/components/RoomFavoriteButton.vue @@ -33,6 +33,10 @@ const props = defineProps({ type: Boolean, default: true, }, + redirectOnRoomModelNotFound: { + type: Boolean, + default: true, + }, }); const isLoading = ref(false); @@ -57,6 +61,7 @@ function toggleFavorite() { .catch((error) => { api.error(error, { redirectOnUnauthenticated: props.redirectOnUnauthenticated, + redirectOnRoomModelNotFound: props.redirectOnRoomModelNotFound, }); }) .finally(() => { diff --git a/resources/js/components/RoomTabFilesDeleteButton.vue b/resources/js/components/RoomTabFilesDeleteButton.vue index 184663335..e383b1471 100644 --- a/resources/js/components/RoomTabFilesDeleteButton.vue +++ b/resources/js/components/RoomTabFilesDeleteButton.vue @@ -53,6 +53,7 @@ import { useApi } from "../composables/useApi.js"; import { ref } from "vue"; import { useToast } from "../composables/useToast.js"; import { useI18n } from "vue-i18n"; +import { ROOM_FILE } from "../constants/modelNames.js"; const props = defineProps({ roomId: { @@ -101,7 +102,10 @@ function deleteFile() { // deleting failed if (error.response) { // file not found - if (error.response.status === env.HTTP_NOT_FOUND) { + if ( + error.response.status === env.HTTP_NOT_FOUND && + error.response.data?.model === ROOM_FILE + ) { toast.error(t("rooms.flash.file_gone")); emit("notFound"); modalVisible.value = false; diff --git a/resources/js/components/RoomTabFilesEditButton.vue b/resources/js/components/RoomTabFilesEditButton.vue index 25362cdb5..f602dbc31 100644 --- a/resources/js/components/RoomTabFilesEditButton.vue +++ b/resources/js/components/RoomTabFilesEditButton.vue @@ -100,6 +100,7 @@ import { ref, watch } from "vue"; import { useFormErrors } from "../composables/useFormErrors.js"; import { useToast } from "../composables/useToast.js"; import { useI18n } from "vue-i18n"; +import { ROOM_FILE } from "../constants/modelNames.js"; const props = defineProps({ roomId: { @@ -195,7 +196,10 @@ function save() { // editing failed if (error.response) { // file not found - if (error.response.status === env.HTTP_NOT_FOUND) { + if ( + error.response.status === env.HTTP_NOT_FOUND && + error.response.data?.model === ROOM_FILE + ) { toast.error(t("rooms.flash.file_gone")); emit("notFound"); modalVisible.value = false; diff --git a/resources/js/components/RoomTabMembersDeleteButton.vue b/resources/js/components/RoomTabMembersDeleteButton.vue index cf30ba516..06c481399 100644 --- a/resources/js/components/RoomTabMembersDeleteButton.vue +++ b/resources/js/components/RoomTabMembersDeleteButton.vue @@ -56,6 +56,9 @@ import env from "../env"; import { useApi } from "../composables/useApi.js"; import { ref } from "vue"; +import { USER } from "../constants/modelNames.js"; +import { useToast } from "../composables/useToast.js"; +import { useI18n } from "vue-i18n"; const props = defineProps({ roomId: { @@ -83,6 +86,8 @@ const props = defineProps({ const emit = defineEmits(["deleted", "gone"]); const api = useApi(); +const toast = useToast(); +const { t } = useI18n(); const modalVisible = ref(false); const isLoadingAction = ref(false); @@ -106,9 +111,14 @@ function deleteMember() { // editing failed if (error.response) { // user not found - if (error.response.status === env.HTTP_GONE) { + if ( + error.response.status === env.HTTP_NOT_FOUND && + error.response.data?.model === USER + ) { + toast.error(t("app.errors.not_member_of_room")); emit("gone"); modalVisible.value = false; + return; } } api.error(error, { redirectOnUnauthenticated: false }); diff --git a/resources/js/components/RoomTabMembersEditButton.vue b/resources/js/components/RoomTabMembersEditButton.vue index 8a02bed35..518837fca 100644 --- a/resources/js/components/RoomTabMembersEditButton.vue +++ b/resources/js/components/RoomTabMembersEditButton.vue @@ -99,6 +99,9 @@ import env from "../env"; import { useApi } from "../composables/useApi.js"; import { useFormErrors } from "../composables/useFormErrors.js"; import { ref } from "vue"; +import { USER } from "../constants/modelNames.js"; +import { useToast } from "../composables/useToast.js"; +import { useI18n } from "vue-i18n"; const props = defineProps({ roomId: { @@ -131,6 +134,8 @@ const emit = defineEmits(["edited", "gone"]); const api = useApi(); const formErrors = useFormErrors(); +const toast = useToast(); +const { t } = useI18n(); const modalVisible = ref(false); const newRole = ref(null); @@ -168,9 +173,14 @@ function save() { // editing failed if (error.response) { // user not found - if (error.response.status === env.HTTP_GONE) { + if ( + error.response.status === env.HTTP_NOT_FOUND && + error.response.data?.model === USER + ) { + toast.error(t("app.errors.not_member_of_room")); emit("gone"); modalVisible.value = false; + return; } // failed due to form validation errors if (error.response.status === env.HTTP_UNPROCESSABLE_ENTITY) { diff --git a/resources/js/components/RoomTabPersonalizedLinksDeleteButton.vue b/resources/js/components/RoomTabPersonalizedLinksDeleteButton.vue index cd2e998ad..aa7f1ca78 100644 --- a/resources/js/components/RoomTabPersonalizedLinksDeleteButton.vue +++ b/resources/js/components/RoomTabPersonalizedLinksDeleteButton.vue @@ -59,6 +59,7 @@ import { useApi } from "../composables/useApi.js"; import { ref } from "vue"; import { useToast } from "../composables/useToast.js"; import { useI18n } from "vue-i18n"; +import { ROOM_PERSONALIZED_LINK } from "../constants/modelNames.js"; const props = defineProps({ roomId: { @@ -120,7 +121,10 @@ function deleteLink() { // deleting failed if (error.response) { // personalized link not found - if (error.response.status === env.HTTP_NOT_FOUND) { + if ( + error.response.status === env.HTTP_NOT_FOUND && + error.response.data?.model === ROOM_PERSONALIZED_LINK + ) { toast.error(t("rooms.flash.personalized_link_gone")); modalVisible.value = false; emit("notFound"); diff --git a/resources/js/components/RoomTabPersonalizedLinksEditButton.vue b/resources/js/components/RoomTabPersonalizedLinksEditButton.vue index 7dde482ad..937988c60 100644 --- a/resources/js/components/RoomTabPersonalizedLinksEditButton.vue +++ b/resources/js/components/RoomTabPersonalizedLinksEditButton.vue @@ -110,6 +110,7 @@ import { ref } from "vue"; import env from "../env.js"; import { useToast } from "../composables/useToast.js"; import { useI18n } from "vue-i18n"; +import { ROOM_PERSONALIZED_LINK } from "../constants/modelNames.js"; const props = defineProps({ roomId: { @@ -189,7 +190,10 @@ function save() { // editing failed if (error.response) { // token not found - if (error.response.status === env.HTTP_NOT_FOUND) { + if ( + error.response.status === env.HTTP_NOT_FOUND && + error.response.data?.model === ROOM_PERSONALIZED_LINK + ) { toast.error(t("rooms.flash.personalized_link_gone")); modalVisible.value = false; emit("notFound"); diff --git a/resources/js/components/RoomTabRecordingsDeleteButton.vue b/resources/js/components/RoomTabRecordingsDeleteButton.vue index 326f8f533..553d4a80f 100644 --- a/resources/js/components/RoomTabRecordingsDeleteButton.vue +++ b/resources/js/components/RoomTabRecordingsDeleteButton.vue @@ -53,6 +53,7 @@ import { useApi } from "../composables/useApi.js"; import { ref } from "vue"; import { useToast } from "../composables/useToast.js"; import { useI18n } from "vue-i18n"; +import { RECORDING } from "../constants/modelNames.js"; const props = defineProps({ recordingId: { @@ -97,7 +98,10 @@ function deleteRecording() { // editing failed if (error.response) { // recording not found - if (error.response.status === env.HTTP_NOT_FOUND) { + if ( + error.response.status === env.HTTP_NOT_FOUND && + error.response.data?.model === RECORDING + ) { toast.error(t("rooms.flash.recording_gone")); emit("notFound"); return; diff --git a/resources/js/components/RoomTabRecordingsEditButton.vue b/resources/js/components/RoomTabRecordingsEditButton.vue index f07787a4d..654a0d005 100644 --- a/resources/js/components/RoomTabRecordingsEditButton.vue +++ b/resources/js/components/RoomTabRecordingsEditButton.vue @@ -130,6 +130,7 @@ import * as _ from "lodash-es"; import { useSettingsStore } from "../stores/settings.js"; import { useToast } from "../composables/useToast.js"; import { useI18n } from "vue-i18n"; +import { RECORDING } from "../constants/modelNames.js"; const props = defineProps({ recordingId: { @@ -236,7 +237,10 @@ function save() { // editing failed if (error.response) { // recording not found - if (error.response.status === env.HTTP_NOT_FOUND) { + if ( + error.response.status === env.HTTP_NOT_FOUND && + error.response.data?.model === RECORDING + ) { toast.error(t("rooms.flash.recording_gone")); modalVisible.value = false; emit("notFound"); diff --git a/resources/js/components/SettingsRolesDeleteButton.vue b/resources/js/components/SettingsRolesDeleteButton.vue index 61f2a9e1d..76a6afa7a 100644 --- a/resources/js/components/SettingsRolesDeleteButton.vue +++ b/resources/js/components/SettingsRolesDeleteButton.vue @@ -46,6 +46,7 @@