security(auth): P1 Token Refresh + sessionStorage

- 新增三個 refresh 端點(member/provider/admin),revoke 舊 token 發行新 7 天 token
- axios.js / coachAxios.js:401 interceptor 改為 refresh-then-retry,含 isRefreshing queue 防並發、isRedirecting flag 防雙重 redirect
- auth.js / coachAuth.js / AuthCallbackView.vue:localStorage 全改為 sessionStorage
- echo.js / notificationAxios.js:同步改為 sessionStorage
- notifications.js:401 時停止 polling(stopPolling),不印 error log
- 新增 TokenRefreshTest.php(11 tests / 24 assertions):refresh 成功/失效/跨角色 403

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 04:33:35 +08:00
parent a94c2462b8
commit d9f0fe11c6
25 changed files with 787 additions and 48 deletions
@@ -1051,6 +1051,63 @@ class AuthController extends Controller
]);
}
/**
* 會員 Token Refresh
*/
public function refreshMember(Request $request): \Illuminate\Http\JsonResponse
{
$user = $request->user();
if ($user->role !== self::ROLE_MEMBER) {
return $this->getUnauthorizedResponse();
}
$request->user()->currentAccessToken()->delete();
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json([
'status' => true,
'data' => ['token' => $token, 'token_type' => 'Bearer'],
]);
}
/**
* 服務提供者 Token Refresh
*/
public function refreshProvider(Request $request): \Illuminate\Http\JsonResponse
{
$user = $request->user();
if ($user->role !== self::ROLE_PROVIDER) {
return $this->getUnauthorizedResponse();
}
$request->user()->currentAccessToken()->delete();
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json([
'status' => true,
'data' => ['token' => $token, 'token_type' => 'Bearer'],
]);
}
/**
* 管理員 Token Refresh
*/
public function refreshAdmin(Request $request): \Illuminate\Http\JsonResponse
{
$user = $request->user();
if ($user->role !== self::ROLE_ADMIN) {
return $this->getUnauthorizedResponse();
}
$request->user()->currentAccessToken()->delete();
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json([
'status' => true,
'data' => ['token' => $token, 'token_type' => 'Bearer'],
]);
}
/**
* 查詢會員資料
* 只有管理員可以使用這個方法