From 7b866c85e83f295c4085c6f21acff829db320685 Mon Sep 17 00:00:00 2001 From: Simon Ostendorf Date: Tue, 20 Aug 2024 15:50:53 +0200 Subject: [PATCH 1/9] feat: use presigned urls for s3 --- app/Models/User.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/app/Models/User.php b/app/Models/User.php index 4595f0a0..b8e27ac8 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -62,11 +62,13 @@ public function course(): BelongsTo */ public function avatarUrl(): ?string { - // check if avatar is set - if ($this->avatar) { - - // get immage from S3 - return Storage::disk('s3')->url($this->avatar); + // check if avatar is set and file exists + if ($this->avatar && Storage::disk('s3')->exists($this->avatar)) { + // create presigned url + return Storage::disk('s3')->temporaryUrl( + $this->avatar, + now()->addMinutes(60) + ); } return null; From d67bdd206af0aaa815e2dd72727ba4ffbecf4cad Mon Sep 17 00:00:00 2001 From: Simon Ostendorf Date: Tue, 20 Aug 2024 23:38:06 +0200 Subject: [PATCH 2/9] feat: upload avatar using presigned url --- .../Controllers/DashboardAdminController.php | 20 +++++++++++++++++ resources/js/components/user/EditModal.vue | 2 ++ resources/js/composables/useS3.ts | 22 +++++++++++++++++++ routes/web.php | 11 +++++----- 4 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 resources/js/composables/useS3.ts diff --git a/app/Http/Controllers/DashboardAdminController.php b/app/Http/Controllers/DashboardAdminController.php index e146bf49..a3b99013 100644 --- a/app/Http/Controllers/DashboardAdminController.php +++ b/app/Http/Controllers/DashboardAdminController.php @@ -473,4 +473,24 @@ public function assignUser(): RedirectResponse return Redirect::back(); } + + /** + * Generate a presigned URL for avatar upload + */ + public function generatePresignedUrlForAvatarUpload(IlluminateRequest $request): array + { + $user = User::find($request->user); + + $request->validate([ + 'avatar' => 'required|image', + ]); + + $fileName = uniqid() . '.' . $request->avatar->extension(); + $path = 'avatars/' . $user->id . '/' . $fileName; + $presignedUrl = Storage::disk('s3')->temporaryUploadUrl( + $path, + now()->addMinutes(5) + ); + return $presignedUrl; + } } diff --git a/resources/js/components/user/EditModal.vue b/resources/js/components/user/EditModal.vue index f4286202..e596549e 100644 --- a/resources/js/components/user/EditModal.vue +++ b/resources/js/components/user/EditModal.vue @@ -197,6 +197,8 @@ const selectFormCourseOptions = useSelectFormCourseOptions(courses, true); const selectFormRoleOptions = useSelectFormRoleOptions(roles); const randomPlaceholderPerson = usePlaceholderPerson(); +const { uploadFileByPresignedUrl } = useS3(); + const close = () => { emits("close"); }; diff --git a/resources/js/composables/useS3.ts b/resources/js/composables/useS3.ts new file mode 100644 index 00000000..8c0556ed --- /dev/null +++ b/resources/js/composables/useS3.ts @@ -0,0 +1,22 @@ +export default function () { + const uploadFileByPresignedUrl = async ({ + file, + presignedUrl, + }: { + file: File; + presignedUrl: string; + }) => { + const response = await fetch(presignedUrl, { + method: "PUT", + body: file, + }); + + if (!response.ok) { + throw new Error("Failed to upload file"); + } + }; + + return { + uploadFileByPresignedUrl, + }; +} diff --git a/routes/web.php b/routes/web.php index cebed491..1695be10 100644 --- a/routes/web.php +++ b/routes/web.php @@ -32,7 +32,7 @@ Route::get('/login', [AppController::class, 'login'])->name('app.login'); Route::post('/login', [AppController::class, 'loginUser'])->name('app.loginUser'); - Route::middleware(ActiveModule::class.':registration')->group(function () { + Route::middleware(ActiveModule::class . ':registration')->group(function () { Route::get('/register', [AppController::class, 'register'])->name('app.register'); Route::post('/register', [AppController::class, 'registerUser'])->name('app.registerUser'); }); @@ -85,7 +85,7 @@ }); Route::group([ - 'middleware' => [ActiveModule::class.':randomGenerator', 'can:manage random generator'], + 'middleware' => [ActiveModule::class . ':randomGenerator', 'can:manage random generator'], ], function () { Route::get('/random-generator', [DashboardAdminRandomGeneratorController::class, 'index'])->name('dashboard.admin.randomGenerator.index'); Route::post('/random-generator', [DashboardAdminRandomGeneratorController::class, 'indexExecuteSubmit'])->name('dashboard.admin.randomGenerator.indexExecuteSubmit'); @@ -93,7 +93,7 @@ }); Route::group([ - 'middleware' => [ActiveModule::class.':scoreSystem', 'can:manage score system'], + 'middleware' => [ActiveModule::class . ':scoreSystem', 'can:manage score system'], ], function () { Route::get('/score-system', [DashboardAdminScoreSystemController::class, 'index'])->name('dashboard.admin.scoreSystem.index'); Route::post('/score-system', [DashboardAdminScoreSystemController::class, 'indexExecuteSubmit'])->name('dashboard.admin.scoreSystem.indexExecuteSubmit'); @@ -127,19 +127,20 @@ Route::group(['middleware' => ['can:manage users']], function () { Route::get('/users', [ApiController::class, 'users'])->name('api.users'); + Route::get('/user/{user}/presigned-avatar-url', [DashboardAdminController::class, 'generatePresignedUrlForAvatarUpload'])->name('api.user.presignedAvatarUrl'); }); }); Route::get('/registrations/{registration}', [ApiController::class, 'registrationsShow'])->name('api.registrations.show'); Route::group([ - 'middleware' => [ActiveModule::class.':randomGenerator', 'can:manage random generator'], + 'middleware' => [ActiveModule::class . ':randomGenerator', 'can:manage random generator'], ], function () { Route::get('/random-generator/state', [ApiController::class, 'randomGeneratorState'])->name('api.randomGeneratorState'); }); Route::group([ - 'middleware' => [ActiveModule::class.':scoreSystem', 'can:manage score system'], + 'middleware' => [ActiveModule::class . ':scoreSystem', 'can:manage score system'], ], function () { Route::get('/score-system/state', [ApiController::class, 'scoreSystemState'])->name('api.scoreSystemState'); }); From 455476c8c2938dfb28649e5d787825f19c9d473e Mon Sep 17 00:00:00 2001 From: Simon Ostendorf Date: Wed, 21 Aug 2024 09:26:47 +0200 Subject: [PATCH 3/9] feat: upload avatar using presigned url --- .../Controllers/DashboardAdminController.php | 5 +- resources/js/types/models.d.ts | 188 +++++++++--------- 2 files changed, 99 insertions(+), 94 deletions(-) diff --git a/app/Http/Controllers/DashboardAdminController.php b/app/Http/Controllers/DashboardAdminController.php index a3b99013..78eb254b 100644 --- a/app/Http/Controllers/DashboardAdminController.php +++ b/app/Http/Controllers/DashboardAdminController.php @@ -20,6 +20,7 @@ use Inertia\Inertia; use Inertia\Response; use Spatie\Permission\Models\Role; +use Illuminate\Http\JsonResponse; class DashboardAdminController extends Controller { @@ -477,7 +478,7 @@ public function assignUser(): RedirectResponse /** * Generate a presigned URL for avatar upload */ - public function generatePresignedUrlForAvatarUpload(IlluminateRequest $request): array + public function generatePresignedUrlForAvatarUpload(IlluminateRequest $request): JsonResponse { $user = User::find($request->user); @@ -491,6 +492,6 @@ public function generatePresignedUrlForAvatarUpload(IlluminateRequest $request): $path, now()->addMinutes(5) ); - return $presignedUrl; + return response()->json($presignedUrl); } } diff --git a/resources/js/types/models.d.ts b/resources/js/types/models.d.ts index 7ee3a7b5..41888e38 100644 --- a/resources/js/types/models.d.ts +++ b/resources/js/types/models.d.ts @@ -5,34 +5,6 @@ */ declare namespace App.Models { - export interface GroupTutor { - id: number; - created_at: string | null; - updated_at: string | null; - user_id: number; - group_id: number; - user?: Models.User | null; - group?: App.Models.Group | null; - } - - export interface User { - id: number; - created_at: string | null; - updated_at: string | null; - firstname: string; - lastname: string; - email: string; - course_id: number; - remember_token: string | null; - station_tutors?: Array | null; - group_tutors?: Array | null; - registrations?: Array | null; - course?: App.Models.Course | null; - station_tutors_count?: number | null; - group_tutors_count?: number | null; - registrations_count?: number | null; - } - export interface Course { id: number; created_at: string | null; @@ -42,53 +14,34 @@ declare namespace App.Models { icon: string; show_on_registration: boolean; classes: string; - users?: Array | null; + users?: Array | null; groups?: Array | null; users_count?: number | null; groups_count?: number | null; } - export interface StationTutor { - id: number; - created_at: string | null; - updated_at: string | null; - user_id: number; - station_id: number; - user?: Models.User | null; - station?: App.Models.Station | null; - } - - export interface Event { + export interface Group { id: number; + name: string; created_at: string | null; updated_at: string | null; - name: string; - description: string | null; - registration_from: string | null; - registration_to: string | null; - type: string; - has_requirements: boolean; - consider_alcohol: boolean; - form: string | null; - sort_order: number; - groups?: Array | null; + event_id: number; + course_id: number | null; + telegram_group_link: string | null; + group_tutors?: Array | null; registrations?: Array | null; - slots?: Array | null; + stops?: Array | null; + course?: App.Models.Course | null; + event?: App.Models.Event | null; + tutors?: Array | null; stations?: Array | null; - groups_count?: number | null; + group_tutors_count?: number | null; registrations_count?: number | null; - slots_count?: number | null; + stops_count?: number | null; + tutors_count?: number | null; stations_count?: number | null; } - export interface Module { - id: number; - key: string; - active: boolean; - created_at: string | null; - updated_at: string | null; - } - export interface Registration { id: number; created_at: string | null; @@ -100,46 +53,34 @@ declare namespace App.Models { drinks_alcohol: boolean | null; fulfils_requirements: boolean | null; is_present: boolean; - form_responses: string | null; + form_responses: Array | any | null; queue_position: number | null; event?: App.Models.Event | null; - user?: Models.User | null; + user?: App.Models.User | null; slot?: App.Models.Slot | null; group?: App.Models.Group | null; } - export interface Slot { + export interface Event { id: number; - name: string; created_at: string | null; updated_at: string | null; - event_id: number; - has_requirements: boolean; - maximum_participants: number | null; - form: string | null; - registrations?: Array | null; - event?: App.Models.Event | null; - registrations_count?: number | null; - } - - export interface Group { - id: number; name: string; - created_at: string | null; - updated_at: string | null; - event_id: number; - course_id: number | null; - group_tutors?: Array | null; + description: string | null; + registration_from: string | null; + registration_to: string | null; + type: string; + has_requirements: boolean; + consider_alcohol: boolean; + form: Array | any | null; + sort_order: number; + groups?: Array | null; registrations?: Array | null; - stops?: Array | null; - course?: App.Models.Course | null; - event?: App.Models.Event | null; - tutors?: Array | null; + slots?: Array | null; stations?: Array | null; - group_tutors_count?: number | null; + groups_count?: number | null; registrations_count?: number | null; - stops_count?: number | null; - tutors_count?: number | null; + slots_count?: number | null; stations_count?: number | null; } @@ -151,13 +92,29 @@ declare namespace App.Models { event_id: number; stops?: Array | null; event?: App.Models.Event | null; - tutors?: Array | null; + tutors?: Array | null; groups?: Array | null; stops_count?: number | null; tutors_count?: number | null; groups_count?: number | null; } + export interface State { + id: number; + key: string; + value: Array | any | null; + created_at: string | null; + updated_at: string | null; + } + + export interface Module { + id: number; + key: string; + active: boolean; + created_at: string | null; + updated_at: string | null; + } + export interface Page { id: number; created_at: string | null; @@ -168,12 +125,39 @@ declare namespace App.Models { sort_order: number; } - export interface State { + export interface StationTutor { id: number; - key: string; - value: string | null; created_at: string | null; updated_at: string | null; + user_id: number; + station_id: number; + user?: App.Models.User | null; + station?: App.Models.Station | null; + } + + export interface Slot { + id: number; + name: string; + created_at: string | null; + updated_at: string | null; + event_id: number; + has_requirements: boolean; + maximum_participants: number | null; + form: Array | any | null; + telegram_group_link: string | null; + registrations?: Array | null; + event?: App.Models.Event | null; + registrations_count?: number | null; + } + + export interface GroupTutor { + id: number; + created_at: string | null; + updated_at: string | null; + user_id: number; + group_id: number; + user?: App.Models.User | null; + group?: App.Models.Group | null; } export interface Stop { @@ -187,4 +171,24 @@ declare namespace App.Models { group?: App.Models.Group | null; station?: App.Models.Station | null; } + + export interface User { + id: number; + created_at: string | null; + updated_at: string | null; + firstname: string; + lastname: string; + email: string; + course_id: number; + remember_token: string | null; + is_disabled: boolean; + avatar: string | null; + station_tutors?: Array | null; + group_tutors?: Array | null; + registrations?: Array | null; + course?: App.Models.Course | null; + station_tutors_count?: number | null; + group_tutors_count?: number | null; + registrations_count?: number | null; + } } From 22ec46764d8f46472ad3e495b81f93dc16c515cc Mon Sep 17 00:00:00 2001 From: Simon Ostendorf Date: Wed, 21 Aug 2024 09:47:07 +0200 Subject: [PATCH 4/9] feat: upload avatar using presigned url --- app/Http/Controllers/Api/ApiController.php | 21 +++++++++++++++++++ .../Controllers/DashboardAdminController.php | 21 ------------------- resources/js/types/auto-imports.d.ts | 1 + routes/web.php | 2 +- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/app/Http/Controllers/Api/ApiController.php b/app/Http/Controllers/Api/ApiController.php index 33b96f8a..25cddc3a 100644 --- a/app/Http/Controllers/Api/ApiController.php +++ b/app/Http/Controllers/Api/ApiController.php @@ -10,6 +10,7 @@ use App\Models\User; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Storage; class ApiController extends Controller { @@ -320,4 +321,24 @@ public function users(): JsonResponse 'users' => $users, ]); } + + /** + * Generate a presigned URL for avatar upload + */ + public function generatePresignedUrlForAvatarUpload(Request $request): JsonResponse + { + $user = User::find($request->user); + + $request->validate([ + 'avatar' => 'required|image', + ]); + + $fileName = uniqid() . '.' . $request->avatar->extension(); + $path = 'avatars/' . $user->id . '/' . $fileName; + $presignedUrl = Storage::disk('s3')->temporaryUploadUrl( + $path, + now()->addMinutes(5) + ); + return response()->json($presignedUrl); + } } diff --git a/app/Http/Controllers/DashboardAdminController.php b/app/Http/Controllers/DashboardAdminController.php index 78eb254b..e146bf49 100644 --- a/app/Http/Controllers/DashboardAdminController.php +++ b/app/Http/Controllers/DashboardAdminController.php @@ -20,7 +20,6 @@ use Inertia\Inertia; use Inertia\Response; use Spatie\Permission\Models\Role; -use Illuminate\Http\JsonResponse; class DashboardAdminController extends Controller { @@ -474,24 +473,4 @@ public function assignUser(): RedirectResponse return Redirect::back(); } - - /** - * Generate a presigned URL for avatar upload - */ - public function generatePresignedUrlForAvatarUpload(IlluminateRequest $request): JsonResponse - { - $user = User::find($request->user); - - $request->validate([ - 'avatar' => 'required|image', - ]); - - $fileName = uniqid() . '.' . $request->avatar->extension(); - $path = 'avatars/' . $user->id . '/' . $fileName; - $presignedUrl = Storage::disk('s3')->temporaryUploadUrl( - $path, - now()->addMinutes(5) - ); - return response()->json($presignedUrl); - } } diff --git a/resources/js/types/auto-imports.d.ts b/resources/js/types/auto-imports.d.ts index a4197bef..a14bd76c 100644 --- a/resources/js/types/auto-imports.d.ts +++ b/resources/js/types/auto-imports.d.ts @@ -9,6 +9,7 @@ declare global { const useColorMode: typeof import("../composables/useColorMode")["default"]; const usePagesAsNavigation: typeof import("../composables/usePagesAsNavigation")["default"]; const usePlaceholderPerson: typeof import("../composables/usePlaceholderPerson")["default"]; + const useS3: typeof import("../composables/useS3")["default"]; const useSelectFormCourseOptions: typeof import("../composables/useSelectFormCourseOptions")["default"]; const useSelectFormEventOptions: typeof import("../composables/useSelectFormEventOptions")["default"]; const useSelectFormRoleOptions: typeof import("../composables/useSelectFormRoleOptions")["default"]; diff --git a/routes/web.php b/routes/web.php index 1695be10..719303b3 100644 --- a/routes/web.php +++ b/routes/web.php @@ -127,7 +127,7 @@ Route::group(['middleware' => ['can:manage users']], function () { Route::get('/users', [ApiController::class, 'users'])->name('api.users'); - Route::get('/user/{user}/presigned-avatar-url', [DashboardAdminController::class, 'generatePresignedUrlForAvatarUpload'])->name('api.user.presignedAvatarUrl'); + Route::get('/user/{user}/presigned-avatar-url', [ApiController::class, 'generatePresignedUrlForAvatarUpload'])->name('api.user.presignedAvatarUrl'); }); }); From 8b3167fb0b66e70da6e9634c0268e9c4bba7c7b6 Mon Sep 17 00:00:00 2001 From: Simon Ostendorf Date: Thu, 22 Aug 2024 08:42:02 +0200 Subject: [PATCH 5/9] feat: upload avatar using presigned url --- app/Http/Controllers/Api/ApiController.php | 17 +++++--- .../Controllers/DashboardAdminController.php | 32 +------------- resources/js/components/user/EditModal.vue | 43 ++++++++++++++++++- routes/web.php | 2 +- 4 files changed, 56 insertions(+), 38 deletions(-) diff --git a/app/Http/Controllers/Api/ApiController.php b/app/Http/Controllers/Api/ApiController.php index 25cddc3a..b8fdb41a 100644 --- a/app/Http/Controllers/Api/ApiController.php +++ b/app/Http/Controllers/Api/ApiController.php @@ -11,6 +11,7 @@ use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Str; class ApiController extends Controller { @@ -325,20 +326,24 @@ public function users(): JsonResponse /** * Generate a presigned URL for avatar upload */ - public function generatePresignedUrlForAvatarUpload(Request $request): JsonResponse + public function generatePresignedUrlForAvatarUpload(Request $request, User $user): JsonResponse { - $user = User::find($request->user); - $request->validate([ 'avatar' => 'required|image', ]); - $fileName = uniqid() . '.' . $request->avatar->extension(); - $path = 'avatars/' . $user->id . '/' . $fileName; + $uuid = Str::uuid()->toString(); + $fileName = $uuid . '.' . $request->avatar->extension(); + //$path = 'avatars/' . $user->id . '/' . $fileName; + $path = 'avatars/' . $fileName; $presignedUrl = Storage::disk('s3')->temporaryUploadUrl( $path, now()->addMinutes(5) ); - return response()->json($presignedUrl); + + return response()->json([ + 'presignedUrl' => $presignedUrl, + 'path' => $path, + ]); } } diff --git a/app/Http/Controllers/DashboardAdminController.php b/app/Http/Controllers/DashboardAdminController.php index e146bf49..96032e26 100644 --- a/app/Http/Controllers/DashboardAdminController.php +++ b/app/Http/Controllers/DashboardAdminController.php @@ -16,7 +16,6 @@ use Illuminate\Support\Facades\Request; use Illuminate\Support\Facades\Session; use Illuminate\Support\Facades\Storage; -use Illuminate\Support\Str; use Inertia\Inertia; use Inertia\Response; use Spatie\Permission\Models\Role; @@ -76,7 +75,7 @@ public function editUser(IlluminateRequest $request): RedirectResponse 'role_id' => ['array'], 'is_disabled' => ['boolean'], 'remove_avatar' => ['boolean'], - 'avatar.*.file' => ['nullable', 'image', 'mimes:jpeg,png,jpg,gif'], + 'avatar' => ['nullable', 'string'], ]); // check if all roles exists and not super admin if so add to roles array @@ -107,18 +106,6 @@ public function editUser(IlluminateRequest $request): RedirectResponse // set avatar to null $validated['avatar'] = null; - } elseif (array_key_exists('avatar', $validated) && $validated['avatar'][0]) { - // get avatar file - $avatarFile = Request::file('avatar')[0]['file']; - - // generate a uuid - $uuid = Str::uuid()->toString(); - - // store file in s3 bucket - $path = Storage::disk('s3')->put('/avatars/' . $uuid, $avatarFile); - - // add avatar to validated array - $validated['avatar'] = $path; } // remove email_confirm, role_id and remove_avatar from array @@ -352,27 +339,12 @@ public function registerUser(): RedirectResponse 'email' => ['required', 'string', 'email', 'min:3', 'max:255', 'unique:users'], 'email_confirm' => ['required', 'string', 'email', 'min:3', 'max:255', 'same:email'], 'course_id' => ['required', 'integer', 'exists:courses,id'], - 'avatar.*.file' => ['nullable', 'image', 'mimes:jpeg,png,jpg,gif'], + 'avatar' => ['nullable', 'string'], ]); // remove email_confirm from array unset($validated['email_confirm']); - // check if avatar is set - if (array_key_exists('avatar', $validated) && $validated['avatar'][0]) { - // get avatar file - $avatarFile = Request::file('avatar')[0]['file']; - - // generate a uuid - $uuid = Str::uuid()->toString(); - - // store file in s3 bucket - $path = Storage::disk('s3')->put('/avatars/' . $uuid, $avatarFile); - - // add avatar to validated array - $validated['avatar'] = $path; - } - // create the user $user = User::create($validated); diff --git a/resources/js/components/user/EditModal.vue b/resources/js/components/user/EditModal.vue index e596549e..03b7a515 100644 --- a/resources/js/components/user/EditModal.vue +++ b/resources/js/components/user/EditModal.vue @@ -203,7 +203,48 @@ const close = () => { emits("close"); }; const editSubmitHandler = async () => { - Inertia.post(`/dashboard/admin/user/${user.id}`, editForm.value); + var avatarPath = null; + + if (editForm.value.avatar) { + const formData = new FormData(); + formData.append("avatar", editForm.value.avatar[0].file); + + const response = await fetch(`/api/user/${user.id}/presigned-avatar-url`, { + method: "POST", + credentials: "include", + headers: { + "X-CSRF-TOKEN": + document + .querySelector("meta[name='csrf-token']") + ?.getAttribute("content") || "", + }, + body: formData, + }); + + if (!response.ok) { + console.error("Failed to get presigned URL for avatar upload"); + return; + } + + const data = await response.json(); + + try { + await uploadFileByPresignedUrl({ + file: formData.get("avatar"), + presignedUrl: data.presignedUrl.url, + }); + avatarPath = data.path; + console.info("Avatar uploaded"); + } catch (error) { + console.error("Failed to upload avatar", error); + return; + } + } + + Inertia.post(`/dashboard/admin/user/${user.id}`, { + ...editForm.value, + avatar: avatarPath, + }); emits("submit"); }; diff --git a/routes/web.php b/routes/web.php index 719303b3..e1fbc432 100644 --- a/routes/web.php +++ b/routes/web.php @@ -127,7 +127,7 @@ Route::group(['middleware' => ['can:manage users']], function () { Route::get('/users', [ApiController::class, 'users'])->name('api.users'); - Route::get('/user/{user}/presigned-avatar-url', [ApiController::class, 'generatePresignedUrlForAvatarUpload'])->name('api.user.presignedAvatarUrl'); + Route::post('/user/{user}/presigned-avatar-url', [ApiController::class, 'generatePresignedUrlForAvatarUpload'])->name('api.user.presignedAvatarUrl'); }); }); From fb1635f8825e14b72a1b838e095a29d770f08a8a Mon Sep 17 00:00:00 2001 From: Simon Ostendorf Date: Thu, 22 Aug 2024 13:30:50 +0200 Subject: [PATCH 6/9] fix: pr comments --- resources/js/components/user/EditModal.vue | 16 ++++++++-------- resources/js/composables/useS3.ts | 8 +------- 2 files changed, 9 insertions(+), 15 deletions(-) diff --git a/resources/js/components/user/EditModal.vue b/resources/js/components/user/EditModal.vue index 03b7a515..c51effc6 100644 --- a/resources/js/components/user/EditModal.vue +++ b/resources/js/components/user/EditModal.vue @@ -203,9 +203,9 @@ const close = () => { emits("close"); }; const editSubmitHandler = async () => { - var avatarPath = null; + const avatarPath = ref(null); - if (editForm.value.avatar) { + if (editForm.value.avatar?.length) { const formData = new FormData(); formData.append("avatar", editForm.value.avatar[0].file); @@ -229,11 +229,11 @@ const editSubmitHandler = async () => { const data = await response.json(); try { - await uploadFileByPresignedUrl({ - file: formData.get("avatar"), - presignedUrl: data.presignedUrl.url, - }); - avatarPath = data.path; + await uploadFileByPresignedUrl( + formData.get("avatar"), + data.presignedUrl.url + ); + avatarPath.value = data.path; console.info("Avatar uploaded"); } catch (error) { console.error("Failed to upload avatar", error); @@ -243,7 +243,7 @@ const editSubmitHandler = async () => { Inertia.post(`/dashboard/admin/user/${user.id}`, { ...editForm.value, - avatar: avatarPath, + avatar: avatarPath.value, }); emits("submit"); }; diff --git a/resources/js/composables/useS3.ts b/resources/js/composables/useS3.ts index 8c0556ed..62f42ed3 100644 --- a/resources/js/composables/useS3.ts +++ b/resources/js/composables/useS3.ts @@ -1,11 +1,5 @@ export default function () { - const uploadFileByPresignedUrl = async ({ - file, - presignedUrl, - }: { - file: File; - presignedUrl: string; - }) => { + const uploadFileByPresignedUrl = async (file: File, presignedUrl: string) => { const response = await fetch(presignedUrl, { method: "PUT", body: file, From e8bd5727967e93518037205c99aac1a265a690b3 Mon Sep 17 00:00:00 2001 From: Simon Ostendorf Date: Fri, 23 Aug 2024 08:48:22 +0200 Subject: [PATCH 7/9] fix: pr comments and remove unused userId from presignedUrl api call --- app/Http/Controllers/Api/ApiController.php | 3 +- resources/js/components/user/EditModal.vue | 5 +-- .../js/pages/Dashboard/Admin/Register.vue | 45 ++++++++++++++++++- routes/web.php | 2 +- 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/app/Http/Controllers/Api/ApiController.php b/app/Http/Controllers/Api/ApiController.php index b8fdb41a..c85adc06 100644 --- a/app/Http/Controllers/Api/ApiController.php +++ b/app/Http/Controllers/Api/ApiController.php @@ -326,7 +326,7 @@ public function users(): JsonResponse /** * Generate a presigned URL for avatar upload */ - public function generatePresignedUrlForAvatarUpload(Request $request, User $user): JsonResponse + public function generatePresignedUrlForAvatarUpload(Request $request): JsonResponse { $request->validate([ 'avatar' => 'required|image', @@ -334,7 +334,6 @@ public function generatePresignedUrlForAvatarUpload(Request $request, User $user $uuid = Str::uuid()->toString(); $fileName = $uuid . '.' . $request->avatar->extension(); - //$path = 'avatars/' . $user->id . '/' . $fileName; $path = 'avatars/' . $fileName; $presignedUrl = Storage::disk('s3')->temporaryUploadUrl( $path, diff --git a/resources/js/components/user/EditModal.vue b/resources/js/components/user/EditModal.vue index c51effc6..d5089cca 100644 --- a/resources/js/components/user/EditModal.vue +++ b/resources/js/components/user/EditModal.vue @@ -203,13 +203,13 @@ const close = () => { emits("close"); }; const editSubmitHandler = async () => { - const avatarPath = ref(null); + const avatarPath = ref(); if (editForm.value.avatar?.length) { const formData = new FormData(); formData.append("avatar", editForm.value.avatar[0].file); - const response = await fetch(`/api/user/${user.id}/presigned-avatar-url`, { + const response = await fetch(`/api/user/presigned-avatar-url`, { method: "POST", credentials: "include", headers: { @@ -234,7 +234,6 @@ const editSubmitHandler = async () => { data.presignedUrl.url ); avatarPath.value = data.path; - console.info("Avatar uploaded"); } catch (error) { console.error("Failed to upload avatar", error); return; diff --git a/resources/js/pages/Dashboard/Admin/Register.vue b/resources/js/pages/Dashboard/Admin/Register.vue index 1293577f..f4d95b6e 100644 --- a/resources/js/pages/Dashboard/Admin/Register.vue +++ b/resources/js/pages/Dashboard/Admin/Register.vue @@ -201,6 +201,9 @@ const getEventById = (id: number) => { const selectFormCourseOptions = useSelectFormCourseOptions(courses, true); const selectFormEventOptions = useSelectFormEventOptions(events); + +const { uploadFileByPresignedUrl } = useS3(); + const selectFormSlotOptions = computed(() => { const event = getEventById(assignForm.value.event_id); @@ -213,7 +216,47 @@ const selectFormSlotOptions = computed(() => { const randomPlaceholderPerson = usePlaceholderPerson(); const registerSubmitHandler = async () => { - Inertia.post("/dashboard/admin/register", registerForm.value); + const avatarPath = ref(); + + if (registerForm.value.avatar?.length) { + const formData = new FormData(); + formData.append("avatar", registerForm.value.avatar[0].file); + + const response = await fetch(`/api/user/presigned-avatar-url`, { + method: "POST", + credentials: "include", + headers: { + "X-CSRF-TOKEN": + document + .querySelector("meta[name='csrf-token']") + ?.getAttribute("content") || "", + }, + body: formData, + }); + + if (!response.ok) { + console.error("Failed to get presigned URL for avatar upload"); + return; + } + + const data = await response.json(); + + try { + await uploadFileByPresignedUrl( + formData.get("avatar"), + data.presignedUrl.url + ); + avatarPath.value = data.path; + } catch (error) { + console.error("Failed to upload avatar", error); + return; + } + } + + Inertia.post("/dashboard/admin/register", { + ...registerForm.value, + avatar: avatarPath.value, + }); }; const assignSubmitHandler = async () => { diff --git a/routes/web.php b/routes/web.php index e1fbc432..748d3e70 100644 --- a/routes/web.php +++ b/routes/web.php @@ -127,7 +127,7 @@ Route::group(['middleware' => ['can:manage users']], function () { Route::get('/users', [ApiController::class, 'users'])->name('api.users'); - Route::post('/user/{user}/presigned-avatar-url', [ApiController::class, 'generatePresignedUrlForAvatarUpload'])->name('api.user.presignedAvatarUrl'); + Route::post('/user/presigned-avatar-url', [ApiController::class, 'generatePresignedUrlForAvatarUpload'])->name('api.user.presignedAvatarUrl'); }); }); From 1fe603f36d7d3cba02458f87524938a03a010e8b Mon Sep 17 00:00:00 2001 From: Simon Ostendorf Date: Tue, 27 Aug 2024 16:44:17 +0200 Subject: [PATCH 8/9] style: fix linting --- resources/js/components/user/EditModal.vue | 2 +- resources/js/pages/Dashboard/Admin/Register.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/js/components/user/EditModal.vue b/resources/js/components/user/EditModal.vue index d5089cca..b1a4219b 100644 --- a/resources/js/components/user/EditModal.vue +++ b/resources/js/components/user/EditModal.vue @@ -231,7 +231,7 @@ const editSubmitHandler = async () => { try { await uploadFileByPresignedUrl( formData.get("avatar"), - data.presignedUrl.url + data.presignedUrl.url, ); avatarPath.value = data.path; } catch (error) { diff --git a/resources/js/pages/Dashboard/Admin/Register.vue b/resources/js/pages/Dashboard/Admin/Register.vue index f4d95b6e..bbac39c3 100644 --- a/resources/js/pages/Dashboard/Admin/Register.vue +++ b/resources/js/pages/Dashboard/Admin/Register.vue @@ -244,7 +244,7 @@ const registerSubmitHandler = async () => { try { await uploadFileByPresignedUrl( formData.get("avatar"), - data.presignedUrl.url + data.presignedUrl.url, ); avatarPath.value = data.path; } catch (error) { From b90678adbe396f8b29ebf46df26dc78a29bdce44 Mon Sep 17 00:00:00 2001 From: Simon Ostendorf Date: Tue, 27 Aug 2024 16:44:28 +0200 Subject: [PATCH 9/9] fix: regenerate ts types --- resources/js/types/group.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/resources/js/types/group.d.ts b/resources/js/types/group.d.ts index 92bfb2e2..29fbb3f1 100644 --- a/resources/js/types/group.d.ts +++ b/resources/js/types/group.d.ts @@ -4,7 +4,6 @@ declare namespace App.Models { created_at: string /* Date */ | null; updated_at: string /* Date */ | null; event_id: number; - course_id: number | null; name: string; group_tutors?: GroupTutor[] | null; registrations?: Registration[] | null;