From 43d7cada34ff6a205067c050a62c743d81e69815 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Fri, 16 Jan 2026 11:56:44 +0800 Subject: [PATCH] fix: tenancy middleware order and ui consistency for user profile --- .../Landlord/ProfileController.php | 55 +++++ app/Http/Controllers/ProfileController.php | 54 +++++ app/Http/Middleware/HandleInertiaRequests.php | 1 + bootstrap/app.php | 5 +- resources/js/Layouts/AuthenticatedLayout.tsx | 14 +- resources/js/Layouts/LandlordLayout.tsx | 30 ++- resources/js/Pages/Landlord/Dashboard.tsx | 4 +- resources/js/Pages/Landlord/Profile/Edit.tsx | 195 +++++++++++++++++ resources/js/Pages/Landlord/Tenant/Create.tsx | 4 +- resources/js/Pages/Landlord/Tenant/Edit.tsx | 4 +- resources/js/Pages/Landlord/Tenant/Index.tsx | 4 +- resources/js/Pages/Landlord/Tenant/Show.tsx | 4 +- resources/js/Pages/Profile/Edit.tsx | 205 ++++++++++++++++++ resources/js/types/global.d.ts | 1 + routes/landlord.php | 6 + routes/web.php | 6 + 16 files changed, 576 insertions(+), 16 deletions(-) create mode 100644 app/Http/Controllers/Landlord/ProfileController.php create mode 100644 app/Http/Controllers/ProfileController.php create mode 100644 resources/js/Pages/Landlord/Profile/Edit.tsx create mode 100644 resources/js/Pages/Profile/Edit.tsx diff --git a/app/Http/Controllers/Landlord/ProfileController.php b/app/Http/Controllers/Landlord/ProfileController.php new file mode 100644 index 0000000..7bfc423 --- /dev/null +++ b/app/Http/Controllers/Landlord/ProfileController.php @@ -0,0 +1,55 @@ + $request->user(), + ]); + } + + /** + * 更新使用者基本資料 + */ + public function update(Request $request) + { + $validated = $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'username' => ['required', 'string', 'max:255', 'unique:users,username,' . $request->user()->id], + 'email' => ['nullable', 'string', 'email', 'max:255', 'unique:users,email,' . $request->user()->id], + ]); + + $request->user()->update($validated); + + return back()->with('success', '個人資料已更新'); + } + + /** + * 更新密碼 + */ + public function updatePassword(Request $request) + { + $validated = $request->validate([ + 'current_password' => ['required', 'current_password'], + 'password' => ['required', 'confirmed', Password::defaults()], + ]); + + $request->user()->update([ + 'password' => Hash::make($validated['password']), + ]); + + return back()->with('success', '密碼已更新'); + } +} diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php new file mode 100644 index 0000000..d95076d --- /dev/null +++ b/app/Http/Controllers/ProfileController.php @@ -0,0 +1,54 @@ + $request->user(), + ]); + } + + /** + * 更新使用者基本資料 + */ + public function update(Request $request) + { + $validated = $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'username' => ['required', 'string', 'max:255', 'unique:users,username,' . $request->user()->id], + 'email' => ['nullable', 'string', 'email', 'max:255', 'unique:users,email,' . $request->user()->id], + ]); + + $request->user()->update($validated); + + return back()->with('success', '個人資料已更新'); + } + + /** + * 更新密碼 + */ + public function updatePassword(Request $request) + { + $validated = $request->validate([ + 'current_password' => ['required', 'current_password'], + 'password' => ['required', 'confirmed', Password::defaults()], + ]); + + $request->user()->update([ + 'password' => Hash::make($validated['password']), + ]); + + return back()->with('success', '密碼已更新'); + } +} diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 3e13687..231a939 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -47,6 +47,7 @@ class HandleInertiaRequests extends Middleware 'username' => $user->username ?? null, // 權限資料 'roles' => $user->getRoleNames(), + 'role_labels' => $user->roles->pluck('display_name'), 'permissions' => $user->getAllPermissions()->pluck('name')->toArray(), ] : null, ], diff --git a/bootstrap/app.php b/bootstrap/app.php index 73e4de9..3721148 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -18,8 +18,11 @@ return Application::configure(basePath: dirname(__DIR__)) health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { - $middleware->web(append: [ + // Tenancy 必須最先執行,確保資料庫連線在 Session 讀取之前建立 + $middleware->web(prepend: [ \App\Http\Middleware\UniversalTenancy::class, + ]); + $middleware->web(append: [ \App\Http\Middleware\HandleInertiaRequests::class, ]); diff --git a/resources/js/Layouts/AuthenticatedLayout.tsx b/resources/js/Layouts/AuthenticatedLayout.tsx index 914f552..8415661 100644 --- a/resources/js/Layouts/AuthenticatedLayout.tsx +++ b/resources/js/Layouts/AuthenticatedLayout.tsx @@ -351,7 +351,7 @@ export default function AuthenticatedLayout({ {user.name} - {user.username || 'Administrator'} + {user.role_labels?.[0] || user.roles?.[0] || '一般用戶'}
@@ -359,7 +359,17 @@ export default function AuthenticatedLayout({
- 我的帳號 + {user.name} ({user.username}) + + + + + 使用者設定 + +
- + {user.name} - 系統管理員 + + {user.role_labels?.[0] || user.roles?.[0] || '系統管理員'} +
-
- +
+
- - 我的帳號 + + {user.name} ({user.username}) + + + + + 使用者設定 + + 登出系統 diff --git a/resources/js/Pages/Landlord/Dashboard.tsx b/resources/js/Pages/Landlord/Dashboard.tsx index 998f067..14c76dc 100644 --- a/resources/js/Pages/Landlord/Dashboard.tsx +++ b/resources/js/Pages/Landlord/Dashboard.tsx @@ -39,7 +39,9 @@ export default function Dashboard({ totalTenants, activeTenants, recentTenants } ]; return ( - +
{/* Stats Cards */}
diff --git a/resources/js/Pages/Landlord/Profile/Edit.tsx b/resources/js/Pages/Landlord/Profile/Edit.tsx new file mode 100644 index 0000000..0891bb5 --- /dev/null +++ b/resources/js/Pages/Landlord/Profile/Edit.tsx @@ -0,0 +1,195 @@ +import LandlordLayout from "@/Layouts/LandlordLayout"; +import { Head, useForm } from "@inertiajs/react"; +import { User, Lock, Mail } from "lucide-react"; +import { FormEvent } from "react"; +import { toast } from "sonner"; + +interface User { + id: number; + name: string; + email: string; + username: string; +} + +interface Props { + user: User; +} + +export default function Edit({ user }: Props) { + // 個人資料表單 + const { data: profileData, setData: setProfileData, patch: patchProfile, processing: profileProcessing, errors: profileErrors } = useForm({ + name: user.name, + username: user.username || "", + email: user.email || "", + }); + + // 密碼表單 + const { data: passwordData, setData: setPasswordData, put: putPassword, processing: passwordProcessing, errors: passwordErrors, reset: resetPassword } = useForm({ + current_password: "", + password: "", + password_confirmation: "", + }); + + const handleProfileSubmit = (e: FormEvent) => { + e.preventDefault(); + patchProfile(route('landlord.profile.update'), { + onSuccess: () => toast.success('個人資料已更新'), + onError: () => toast.error('更新失敗,請檢查輸入內容'), + }); + }; + + const handlePasswordSubmit = (e: FormEvent) => { + e.preventDefault(); + putPassword(route('landlord.profile.password'), { + onSuccess: () => { + toast.success('密碼已更新'); + resetPassword(); + }, + onError: () => toast.error('密碼更新失敗'), + }); + }; + + return ( + + + +
+ {/* 頁面標題 */} +
+

+ + 使用者設定 +

+

管理您的個人資料與帳號安全

+
+ + {/* 個人資料區塊 */} +
+
+

+ + 個人資料 +

+
+
+
+ + setProfileData("username", e.target.value)} + className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main" + placeholder="請輸入登入帳號" + /> + {profileErrors.username &&

{profileErrors.username}

} +
+ +
+ + setProfileData("name", e.target.value)} + className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main" + /> + {profileErrors.name &&

{profileErrors.name}

} +
+ +
+ +
+ + setProfileData("email", e.target.value)} + className="w-full pl-10 pr-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main" + placeholder="example@mail.com" + /> +
+ {profileErrors.email &&

{profileErrors.email}

} +
+ +
+ +
+
+
+ + {/* 密碼變更區塊 */} +
+
+

+ + 變更密碼 +

+
+
+
+ + setPasswordData("current_password", e.target.value)} + className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main" + /> + {passwordErrors.current_password &&

{passwordErrors.current_password}

} +
+ +
+ + setPasswordData("password", e.target.value)} + className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main" + /> + {passwordErrors.password &&

{passwordErrors.password}

} +

密碼至少需要 8 個字元

+
+ +
+ + setPasswordData("password_confirmation", e.target.value)} + className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main" + /> +
+ +
+ +
+
+
+
+
+ ); +} diff --git a/resources/js/Pages/Landlord/Tenant/Create.tsx b/resources/js/Pages/Landlord/Tenant/Create.tsx index 75a2778..47277b3 100644 --- a/resources/js/Pages/Landlord/Tenant/Create.tsx +++ b/resources/js/Pages/Landlord/Tenant/Create.tsx @@ -16,7 +16,9 @@ export default function TenantCreate() { }; return ( - +

新增客戶

diff --git a/resources/js/Pages/Landlord/Tenant/Edit.tsx b/resources/js/Pages/Landlord/Tenant/Edit.tsx index 92eb2e0..d580755 100644 --- a/resources/js/Pages/Landlord/Tenant/Edit.tsx +++ b/resources/js/Pages/Landlord/Tenant/Edit.tsx @@ -26,7 +26,9 @@ export default function TenantEdit({ tenant }: Props) { }; return ( - +

編輯客戶

diff --git a/resources/js/Pages/Landlord/Tenant/Index.tsx b/resources/js/Pages/Landlord/Tenant/Index.tsx index 1d6d064..bd4f5fd 100644 --- a/resources/js/Pages/Landlord/Tenant/Index.tsx +++ b/resources/js/Pages/Landlord/Tenant/Index.tsx @@ -37,7 +37,9 @@ export default function TenantIndex({ tenants }: Props) { }; return ( - +
{/* Header */}
diff --git a/resources/js/Pages/Landlord/Tenant/Show.tsx b/resources/js/Pages/Landlord/Tenant/Show.tsx index b2b39b1..efb5673 100644 --- a/resources/js/Pages/Landlord/Tenant/Show.tsx +++ b/resources/js/Pages/Landlord/Tenant/Show.tsx @@ -45,7 +45,9 @@ export default function TenantShow({ tenant }: Props) { }; return ( - +
{/* Back Link */} { + e.preventDefault(); + patchProfile(route('profile.update'), { + onSuccess: () => toast.success('個人資料已更新'), + onError: () => toast.error('更新失敗,請檢查輸入內容'), + }); + }; + + const handlePasswordSubmit = (e: FormEvent) => { + e.preventDefault(); + putPassword(route('profile.password'), { + onSuccess: () => { + toast.success('密碼已更新'); + resetPassword(); + }, + onError: () => toast.error('密碼更新失敗'), + }); + }; + + return ( + + + +
+ {/* 頁面標題 */} +
+

+ + 使用者設定 +

+

管理您的個人資料與帳號安全

+
+ +
+ {/* 個人資料區塊 */} +
+
+

+ + 個人資料 +

+
+
+
+
+ + setProfileData("username", e.target.value)} + className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main outline-none transition-all" + placeholder="請輸入登入帳號" + /> + {profileErrors.username &&

{profileErrors.username}

} +
+ +
+ + setProfileData("name", e.target.value)} + className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main outline-none transition-all" + placeholder="請輸入姓名" + /> + {profileErrors.name &&

{profileErrors.name}

} +
+ +
+ +
+ + setProfileData("email", e.target.value)} + className="w-full pl-10 pr-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main outline-none transition-all" + placeholder="example@mail.com" + /> +
+ {profileErrors.email &&

{profileErrors.email}

} +
+
+ +
+ +
+
+
+ + {/* 密碼變更區塊 */} +
+
+

+ + 安全性與密碼 +

+
+
+
+
+ + setPasswordData("current_password", e.target.value)} + className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main outline-none transition-all" + /> + {passwordErrors.current_password &&

{passwordErrors.current_password}

} +
+ +
+ + setPasswordData("password", e.target.value)} + className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main outline-none transition-all" + /> + {passwordErrors.password &&

{passwordErrors.password}

} +

建議使用 8 個字元以上包含數字與符號的密碼

+
+ +
+ + setPasswordData("password_confirmation", e.target.value)} + className="w-full px-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-main focus:border-primary-main outline-none transition-all" + /> +
+
+ +
+ +
+
+
+
+
+
+ ); +} diff --git a/resources/js/types/global.d.ts b/resources/js/types/global.d.ts index 3854a8b..f352836 100644 --- a/resources/js/types/global.d.ts +++ b/resources/js/types/global.d.ts @@ -7,6 +7,7 @@ export interface AuthUser { email: string; username?: string; roles: string[]; + role_labels: string[]; permissions: string[]; } diff --git a/routes/landlord.php b/routes/landlord.php index 45f2b41..699e17b 100644 --- a/routes/landlord.php +++ b/routes/landlord.php @@ -3,6 +3,7 @@ use Illuminate\Support\Facades\Route; use App\Http\Controllers\Landlord\DashboardController; use App\Http\Controllers\Landlord\TenantController; +use App\Http\Controllers\Landlord\ProfileController; /* |-------------------------------------------------------------------------- @@ -21,6 +22,11 @@ Route::prefix('landlord')->name('landlord.')->middleware(['web', 'auth', \App\Ht // 租戶管理 CRUD Route::resource('tenants', TenantController::class); + // 使用者設定 + Route::get('profile', [ProfileController::class, 'edit'])->name('profile.edit'); + Route::patch('profile', [ProfileController::class, 'update'])->name('profile.update'); + Route::put('profile/password', [ProfileController::class, 'updatePassword'])->name('profile.password'); + // 租戶域名管理 Route::post('tenants/{tenant}/domains', [TenantController::class, 'addDomain'])->name('tenants.domains.store'); Route::delete('tenants/{tenant}/domains/{domain}', [TenantController::class, 'removeDomain'])->name('tenants.domains.destroy'); diff --git a/routes/web.php b/routes/web.php index b11c5e5..02f6616 100644 --- a/routes/web.php +++ b/routes/web.php @@ -16,6 +16,7 @@ use App\Http\Controllers\TransferOrderController; use App\Http\Controllers\UnitController; use App\Http\Controllers\Admin\RoleController; use App\Http\Controllers\Admin\UserController; +use App\Http\Controllers\ProfileController; use Stancl\Tenancy\Middleware\InitializeTenancyByDomainOrSubdomain; // 登入/登出路由 @@ -27,6 +28,11 @@ Route::middleware('auth')->group(function () { // 儀表板 - 所有登入使用者皆可存取 Route::get('/', [DashboardController::class, 'index'])->name('dashboard'); + // 使用者帳號設定 + Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); + Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); + Route::put('/profile/password', [ProfileController::class, 'updatePassword'])->name('profile.password'); + // 類別管理 (用於商品對話框) - 需要商品權限 Route::middleware('permission:products.view')->group(function () { Route::get('/categories', [CategoryController::class, 'index'])->name('categories.index');