diff --git a/app/Http/Controllers/Api/ApiController.php b/app/Http/Controllers/Api/ApiController.php
index 33b96f8a..c85adc06 100644
--- a/app/Http/Controllers/Api/ApiController.php
+++ b/app/Http/Controllers/Api/ApiController.php
@@ -10,6 +10,8 @@
use App\Models\User;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Str;
class ApiController extends Controller
{
@@ -320,4 +322,27 @@ public function users(): JsonResponse
'users' => $users,
]);
}
+
+ /**
+ * Generate a presigned URL for avatar upload
+ */
+ public function generatePresignedUrlForAvatarUpload(Request $request): JsonResponse
+ {
+ $request->validate([
+ 'avatar' => 'required|image',
+ ]);
+
+ $uuid = Str::uuid()->toString();
+ $fileName = $uuid . '.' . $request->avatar->extension();
+ $path = 'avatars/' . $fileName;
+ $presignedUrl = Storage::disk('s3')->temporaryUploadUrl(
+ $path,
+ now()->addMinutes(5)
+ );
+
+ return response()->json([
+ 'presignedUrl' => $presignedUrl,
+ 'path' => $path,
+ ]);
+ }
}
diff --git a/app/Http/Controllers/DashboardAdminController.php b/app/Http/Controllers/DashboardAdminController.php
index 4cce2bce..6c1cca5f 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;
@@ -70,13 +69,13 @@ public function editUser(IlluminateRequest $request): RedirectResponse
$validated = Request::validate([
'firstname' => ['required', 'string', 'min:2', 'max:255'],
'lastname' => ['required', 'string', 'min:2', 'max:255'],
- 'email' => ['required', 'string', 'email', 'min:3', 'max:255', 'unique:users,email,'.$user->id],
+ 'email' => ['required', 'string', 'email', 'min:3', 'max:255', 'unique:users,email,' . $user->id],
'email_confirm' => ['required', 'string', 'email', 'min:3', 'max:255', 'same:email'],
'course_id' => ['required', 'integer', 'exists:courses,id'],
'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
@@ -134,7 +121,7 @@ public function editUser(IlluminateRequest $request): RedirectResponse
$user->syncRoles($roles);
}
- Session::flash('success', 'Der Account '.$user->email.' wurde erfolgreich bearbeitet. Die Tabelle aktualisiert sich in wenigen Sekunden automatisch.');
+ Session::flash('success', 'Der Account ' . $user->email . ' wurde erfolgreich bearbeitet. Die Tabelle aktualisiert sich in wenigen Sekunden automatisch.');
return Redirect::back();
}
@@ -171,7 +158,7 @@ public function deleteUser(IlluminateRequest $request): RedirectResponse
Storage::disk('s3')->delete($userTemp->avatar);
}
- Session::flash('success', 'Der Account '.$userTemp->email.' wurde erfolgreich gelöscht. Die Tabelle aktualisiert sich in wenigen Sekunden automatisch.');
+ Session::flash('success', 'Der Account ' . $userTemp->email . ' wurde erfolgreich gelöscht. Die Tabelle aktualisiert sich in wenigen Sekunden automatisch.');
return Redirect::back();
}
@@ -277,8 +264,8 @@ public function eventExecuteSubmit(IlluminateRequest $request): RedirectResponse
if (count($groups) > 0) {
// get max_groups and max_participants for course by request
- $maxGroups = $request->input('max_groups_'.$course->id);
- $maxParticipants = $request->input('max_participants_'.$course->id);
+ $maxGroups = $request->input('max_groups_' . $course->id);
+ $maxParticipants = $request->input('max_participants_' . $course->id);
$groupCourseDivision = new GroupCourseDivision($event, $course, $event->consider_alcohol, (int) $maxGroups, (int) $maxParticipants);
$groupCourseDivision->assign();
@@ -352,31 +339,16 @@ 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);
- Session::flash('success', 'Der Account '.$user->email.' wurde erfolgreich erstellt.');
+ Session::flash('success', 'Der Account ' . $user->email . ' wurde erfolgreich erstellt.');
return Redirect::back();
}
@@ -469,7 +441,7 @@ public function assignUser(): RedirectResponse
'queue_position' => $queuePosition,
]);
- Session::flash('success', 'Der Account '.$user->email.' wurde erfolgreich für das Event '.$event->name.''.(array_key_exists('slot_id', $userRegistration) ? ' zu dem Slot '.$slot->name.'' : '').' zugewiesen.');
+ Session::flash('success', 'Der Account ' . $user->email . ' wurde erfolgreich für das Event ' . $event->name . '' . (array_key_exists('slot_id', $userRegistration) ? ' zu dem Slot ' . $slot->name . '' : '') . ' zugewiesen.');
return Redirect::back();
}
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;
diff --git a/resources/js/components/user/EditModal.vue b/resources/js/components/user/EditModal.vue
index f4286202..b1a4219b 100644
--- a/resources/js/components/user/EditModal.vue
+++ b/resources/js/components/user/EditModal.vue
@@ -197,11 +197,53 @@ const selectFormCourseOptions = useSelectFormCourseOptions(courses, true);
const selectFormRoleOptions = useSelectFormRoleOptions(roles);
const randomPlaceholderPerson = usePlaceholderPerson();
+const { uploadFileByPresignedUrl } = useS3();
+
const close = () => {
emits("close");
};
const editSubmitHandler = async () => {
- Inertia.post(`/dashboard/admin/user/${user.id}`, editForm.value);
+ 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/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/user/${user.id}`, {
+ ...editForm.value,
+ avatar: avatarPath.value,
+ });
emits("submit");
};
diff --git a/resources/js/composables/useS3.ts b/resources/js/composables/useS3.ts
new file mode 100644
index 00000000..62f42ed3
--- /dev/null
+++ b/resources/js/composables/useS3.ts
@@ -0,0 +1,16 @@
+export default function () {
+ const uploadFileByPresignedUrl = async (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/resources/js/pages/Dashboard/Admin/Register.vue b/resources/js/pages/Dashboard/Admin/Register.vue
index 1293577f..bbac39c3 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/resources/js/types/auto-imports.d.ts b/resources/js/types/auto-imports.d.ts
index c61fc46e..32da3350 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/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;
diff --git a/routes/web.php b/routes/web.php
index 6151987a..0b914278 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');
});
@@ -84,13 +84,13 @@
Route::post('/event/{event}/submit', [DashboardAdminController::class, 'eventExecuteSubmit'])->name('dashboard.admin.event.executeSubmit');
});
- Route::middleware(ActiveModule::class.':randomGenerator', 'can:manage random generator')->group(function () {
+ Route::middleware(ActiveModule::class . ':randomGenerator', 'can:manage random generator')->group(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');
Route::get('/random-generator/display', [DashboardAdminRandomGeneratorController::class, 'display'])->name('dashboard.admin.randomGenerator.display');
});
- Route::middleware(ActiveModule::class.':scoreSystem', 'can:manage score system')->group(function () {
+ Route::middleware(ActiveModule::class . ':scoreSystem', 'can:manage score system')->group(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');
Route::get('/score-system/display', [DashboardAdminScoreSystemController::class, 'display'])->name('dashboard.admin.scoreSystem.display');
@@ -123,16 +123,17 @@
Route::middleware('can:manage users')->group(function () {
Route::get('/users', [ApiController::class, 'users'])->name('api.users');
+ Route::post('/user/presigned-avatar-url', [ApiController::class, 'generatePresignedUrlForAvatarUpload'])->name('api.user.presignedAvatarUrl');
});
});
Route::get('/registrations/{registration}', [ApiController::class, 'registrationsShow'])->name('api.registrations.show');
- Route::middleware(ActiveModule::class.':randomGenerator', 'can:manage random generator')->group(function () {
+ Route::middleware(ActiveModule::class . ':randomGenerator', 'can:manage random generator')->group(function () {
Route::get('/random-generator/state', [ApiController::class, 'randomGeneratorState'])->name('api.randomGeneratorState');
});
- Route::middleware(ActiveModule::class.':scoreSystem', 'can:manage score system')->group(function () {
+ Route::middleware(ActiveModule::class . ':scoreSystem', 'can:manage score system')->group(function () {
Route::get('/score-system/state', [ApiController::class, 'scoreSystemState'])->name('api.scoreSystemState');
});
});