diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index d68cea6..62b3633 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -3,6 +3,10 @@ namespace App\Http\Controllers; use Illuminate\Http\Request; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; +use Illuminate\Validation\Rule; +use Illuminate\Validation\ValidationException; class ProfileController extends Controller { @@ -10,4 +14,194 @@ class ProfileController extends Controller { return view('components/users/viewProfile'); } + + /** + * Show change password form + */ + public function changePasswordForm() + { + return view('profile.change-password'); + } + + /** + * Update user password + */ + public function updatePassword(Request $request) + { + $request->validate([ + 'current_password' => ['required', 'string'], + 'password' => [ + 'required', + 'string', + 'min:8', + 'confirmed', + 'regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{8,}$/', + ], + ], [ + 'current_password.required' => 'Password saat ini wajib diisi.', + 'password.min' => 'Password baru minimal 8 karakter.', + 'password.confirmed' => 'Konfirmasi password tidak cocok.', + 'password.regex' => 'Password harus mengandung huruf besar, huruf kecil, angka, dan simbol khusus.', + ]); + + $user = Auth::user(); + + // Verify current password + if (!Hash::check($request->current_password, $user->password)) { + throw ValidationException::withMessages([ + 'current_password' => ['Password saat ini tidak benar.'], + ]); + } + + // Update password + $user->update([ + 'password' => Hash::make($request->password), + ]); + + // Revoke all API tokens for security (if using Sanctum) + if (method_exists($user, 'tokens')) { + $user->tokens()->delete(); + } + + // Logout from current session for security + Auth::logout(); + + // Invalidate the session + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect()->route('login.index') + ->with('success', 'Password berhasil diubah. Silakan login kembali dengan password baru.'); + } + + /** + * Show change email form + */ + public function changeEmailForm() + { + return view('profile.change-email'); + } + + /** + * Update user email + */ + public function updateEmail(Request $request) + { + $user = Auth::user(); + + $request->validate([ + 'current_password' => ['required', 'string'], + 'email' => [ + 'required', + 'string', + 'email', + 'max:255', + Rule::unique('users')->ignore($user->id), + ], + ], [ + 'current_password.required' => 'Password saat ini wajib diisi untuk konfirmasi.', + 'email.required' => 'Email baru wajib diisi.', + 'email.email' => 'Format email tidak valid.', + 'email.unique' => 'Email sudah digunakan oleh pengguna lain.', + ]); + + // Verify current password + if (!Hash::check($request->current_password, $user->password)) { + throw ValidationException::withMessages([ + 'current_password' => ['Password saat ini tidak benar.'], + ]); + } + + // Update email + $user->update([ + 'email' => $request->email, + ]); + + return redirect()->route('profile.change-email') + ->with('success', 'Email berhasil diubah.'); + } + + /** + * Show edit profile form + */ + public function edit() + { + $user = Auth::user(); + return view('profile.edit', compact('user')); + } + + /** + * Update user profile + */ + public function update(Request $request) + { + $user = Auth::user(); + + $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'username' => [ + 'required', + 'string', + 'max:255', + Rule::unique('users')->ignore($user->id), + ], + ], [ + 'name.required' => 'Nama wajib diisi.', + 'username.required' => 'Username wajib diisi.', + 'username.unique' => 'Username sudah digunakan oleh pengguna lain.', + ]); + + $user->update([ + 'name' => $request->name, + 'username' => $request->username, + ]); + + return redirect()->route('profile.edit') + ->with('success', 'Profil berhasil diperbarui.'); + } + + /** + * Update user profile photo + */ + public function updatePhoto(Request $request) + { + $request->validate([ + 'profile_photo' => ['required', 'image', 'mimes:jpeg,png,jpg,gif', 'max:2048'], + ], [ + 'profile_photo.required' => 'Foto profil wajib dipilih.', + 'profile_photo.image' => 'File harus berupa gambar.', + 'profile_photo.mimes' => 'Format foto harus jpeg, png, jpg, atau gif.', + 'profile_photo.max' => 'Ukuran foto maksimal 2MB.', + ]); + + $user = Auth::user(); + + // Delete old photo if exists + if ($user->profile_photo && file_exists(public_path('storage/profile_photos/' . $user->profile_photo))) { + unlink(public_path('storage/profile_photos/' . $user->profile_photo)); + } + + // Store new photo + $file = $request->file('profile_photo'); + $filename = time() . '_' . $user->id . '.' . $file->getClientOriginalExtension(); + + // Create directory if not exists + $uploadPath = public_path('storage/profile_photos'); + if (!file_exists($uploadPath)) { + mkdir($uploadPath, 0755, true); + } + + $file->move($uploadPath, $filename); + + // Update user profile photo + $user->update([ + 'profile_photo' => $filename, + ]); + + return response()->json([ + 'success' => true, + 'message' => 'Foto profil berhasil diperbarui.', + 'photo_url' => asset('storage/profile_photos/' . $filename) + ]); + } } diff --git a/app/Models/User.php b/app/Models/User.php index b61a608..c05eea9 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -3,6 +3,7 @@ namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; +use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; use Laravel\Sanctum\HasApiTokens; use Illuminate\Foundation\Auth\User as Authenticatable; @@ -12,7 +13,21 @@ use Spatie\Permission\Traits\HasRoles; class User extends Authenticatable { /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasApiTokens, HasFactory, Notifiable, HasRoles; + use HasApiTokens, HasFactory, HasUuids, Notifiable, HasRoles; + + /** + * The primary key type. + * + * @var string + */ + protected $keyType = 'string'; + + /** + * Indicates if the IDs are auto-incrementing. + * + * @var bool + */ + public $incrementing = false; /** * The attributes that are mass assignable. @@ -24,6 +39,7 @@ class User extends Authenticatable 'email', 'username', 'password', + 'profile_photo', ]; /** diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 452e6b6..1263291 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use Illuminate\Support\Facades\URL; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -19,6 +20,9 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { - // + if ($this->app->environment('production')) { + URL::forceScheme('https'); + } + } } diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php index 05fb5d9..266f8d0 100644 --- a/database/migrations/0001_01_01_000000_create_users_table.php +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -12,11 +12,13 @@ return new class extends Migration public function up(): void { Schema::create('users', function (Blueprint $table) { - $table->id(); + $table->uuid('id')->primary(); $table->string('name'); $table->string('email')->unique(); + $table->string('username')->nullable()->unique(); $table->timestamp('email_verified_at')->nullable(); $table->string('password'); + $table->string('profile_photo')->nullable(); $table->rememberToken(); $table->timestamps(); }); @@ -29,7 +31,7 @@ return new class extends Migration Schema::create('sessions', function (Blueprint $table) { $table->string('id')->primary(); - $table->foreignId('user_id')->nullable()->index(); + $table->uuid('user_id')->nullable()->index(); $table->string('ip_address', 45)->nullable(); $table->text('user_agent')->nullable(); $table->longText('payload'); diff --git a/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php b/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php index 40ff706..78f1eb7 100644 --- a/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php +++ b/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php @@ -13,7 +13,9 @@ return new class extends Migration { Schema::create('personal_access_tokens', function (Blueprint $table) { $table->id(); - $table->morphs('tokenable'); + $table->string('tokenable_type'); + $table->string('tokenable_id'); + $table->index(['tokenable_type', 'tokenable_id']); $table->text('name'); $table->string('token', 64)->unique(); $table->text('abilities')->nullable(); diff --git a/database/migrations/2025_09_10_000000_add_username_to_users_table.php b/database/migrations/2025_09_10_000000_add_username_to_users_table.php deleted file mode 100644 index fa4bcbb..0000000 --- a/database/migrations/2025_09_10_000000_add_username_to_users_table.php +++ /dev/null @@ -1,24 +0,0 @@ -string('username')->nullable()->unique()->after('email'); - }); - } - - public function down(): void - { - Schema::table('users', function (Blueprint $table) { - $table->dropUnique(['username']); - $table->dropColumn('username'); - }); - } -}; - diff --git a/database/migrations/2025_09_10_152414_create_permission_tables.php b/database/migrations/2025_09_10_152414_create_permission_tables.php index ce4d9d2..7c628a4 100644 --- a/database/migrations/2025_09_10_152414_create_permission_tables.php +++ b/database/migrations/2025_09_10_152414_create_permission_tables.php @@ -51,7 +51,7 @@ return new class extends Migration $table->unsignedBigInteger($pivotPermission); $table->string('model_type'); - $table->unsignedBigInteger($columnNames['model_morph_key']); + $table->string($columnNames['model_morph_key']); $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index'); $table->foreign($pivotPermission) @@ -62,20 +62,23 @@ return new class extends Migration $table->unsignedBigInteger($columnNames['team_foreign_key']); $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index'); - $table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'], - 'model_has_permissions_permission_model_type_primary'); + $table->primary( + [$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary' + ); } else { - $table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'], - 'model_has_permissions_permission_model_type_primary'); + $table->primary( + [$pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary' + ); } - }); Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) { $table->unsignedBigInteger($pivotRole); $table->string('model_type'); - $table->unsignedBigInteger($columnNames['model_morph_key']); + $table->string($columnNames['model_morph_key']); $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index'); $table->foreign($pivotRole) @@ -86,11 +89,15 @@ return new class extends Migration $table->unsignedBigInteger($columnNames['team_foreign_key']); $table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index'); - $table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'], - 'model_has_roles_role_model_type_primary'); + $table->primary( + [$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary' + ); } else { - $table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'], - 'model_has_roles_role_model_type_primary'); + $table->primary( + [$pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary' + ); } }); diff --git a/resources/views/admin/users/edit.blade.php b/resources/views/admin/users/edit.blade.php index afcc2c9..d4fbbe6 100644 --- a/resources/views/admin/users/edit.blade.php +++ b/resources/views/admin/users/edit.blade.php @@ -6,112 +6,177 @@ @endphp @section('content') -