all(), [ 'email' => 'required|string|email', 'password' => 'required|string', ]); if ($validator->fails()) { return response()->json([ 'errors' => $validator->errors(), ], 422); } $user = User::where('email', $request->email)->first(); if (! $user || ! Hash::check($request->password, $user->password)) { return response()->json([ 'message' => 'Invalid credentials', ], 401); } if (! $user->active) { return response()->json([ 'message' => 'Account is inactive', ], 403); } $accessToken = $this->generateAccessToken($user); $refreshToken = $this->generateRefreshToken($user); return response()->json([ 'access_token' => $accessToken, 'refresh_token' => $refreshToken, 'token_type' => 'bearer', 'expires_in' => 3600, 'user' => [ 'id' => $user->id, 'name' => $user->name, 'email' => $user->email, 'role' => $user->role, ], ]); } public function refresh(Request $request): JsonResponse { $refreshToken = $request->input('refresh_token'); $userId = $this->getUserIdFromRefreshToken($refreshToken); if (! $userId) { return response()->json([ 'message' => 'Invalid or expired refresh token', ], 401); } $user = User::find($userId); if (! $user) { return response()->json([ 'message' => 'Invalid or expired refresh token', ], 401); } $this->invalidateRefreshToken($refreshToken, $userId); $accessToken = $this->generateAccessToken($user); $newRefreshToken = $this->generateRefreshToken($user); return response()->json([ 'access_token' => $accessToken, 'refresh_token' => $newRefreshToken, 'token_type' => 'bearer', 'expires_in' => 3600, ]); } public function logout(Request $request): JsonResponse { $user = $request->user(); $refreshToken = $request->input('refresh_token'); if ($refreshToken) { $this->invalidateRefreshToken($refreshToken, $user->id); } return response()->json([ 'message' => 'Logged out successfully', ]); } protected function generateAccessToken(User $user): string { $payload = [ 'iss' => config('app.url', 'headroom'), 'sub' => $user->id, 'iat' => time(), 'exp' => time() + 3600, 'role' => $user->role, 'permissions' => $this->getPermissions($user->role), 'jti' => uniqid('token_', true), ]; return $this->encodeJWT($payload); } protected function generateRefreshToken(User $user): string { $token = bin2hex(random_bytes(32)); // Store with token as the key part for easy lookup $key = "refresh_token:{$token}"; Redis::setex($key, 604800, $user->id); return $token; } protected function getUserIdFromRefreshToken(string $token): ?string { return Redis::get("refresh_token:{$token}") ?: null; } protected function invalidateRefreshToken(string $token, string $userId): void { Redis::del("refresh_token:{$token}"); } protected function getPermissions(string $role): array { return match ($role) { 'superuser' => [ 'manage_users', 'manage_team_members', 'manage_projects', 'manage_allocations', 'manage_actuals', 'view_reports', 'configure_system', 'view_audit_logs', ], 'manager' => [ 'manage_projects', 'manage_allocations', 'manage_actuals', 'view_reports', 'manage_team_members', ], 'developer' => [ 'manage_actuals', 'view_own_allocations', 'view_own_actuals', 'log_hours', ], 'top_brass' => [ 'view_reports', 'view_allocations', 'view_actuals', 'view_capacity', ], default => [], }; } protected function encodeJWT(array $payload): string { $header = json_encode(['typ' => 'JWT', 'alg' => 'HS256']); $header = base64_encode($header); $header = str_replace(['+', '/', '='], ['-', '_', ''], $header); $payload = json_encode($payload); $payload = base64_encode($payload); $payload = str_replace(['+', '/', '='], ['-', '_', ''], $payload); $signature = hash_hmac('sha256', $header . '.' . $payload, config('app.key'), true); $signature = base64_encode($signature); $signature = str_replace(['+', '/', '='], ['-', '_', ''], $signature); return $header . '.' . $payload . '.' . $signature; } }