| @@ -4,8 +4,9 @@ | ||
| 4 | 4 | |
| 5 | 5 | use FluentBoards\App\Models\Board; |
| 6 | 6 | use FluentBoards\App\Models\User; |
| 7 | 7 | use FluentBoards\App\Services\Helper; |
| 8 | +use FluentBoards\App\Services\MCPAccessService; | |
| 8 | 9 | use FluentBoards\App\Services\PermissionManager; |
| 9 | 10 | use FluentBoards\App\Services\UserService; |
| 10 | 11 | use FluentBoards\Framework\Http\Request\Request; |
| 11 | 12 | use FluentCrm\App\Models\Subscriber; |
| @@ -80,8 +81,10 @@ | ||
| 80 | 81 | $boardIds = []; |
| 81 | 82 | if (is_array($rawBoardIds)) { |
| 82 | 83 | $boardIds = array_filter(array_map('intval', $rawBoardIds)); |
| 83 | 84 | } |
| 85 | + | |
| 86 | + $perPage = max(1, min(absint($request->getSafe('per_page', 'intval', 15)), 50)); | |
| 84 | 87 | |
| 85 | 88 | $requestData = [ |
| 86 | 89 | 'page' => $request->getSafe('page', 'intval', 1), |
| 87 | 90 | 'taskType' => $request->getSafe('taskType', 'sanitize_text_field'), |
| @@ -87,8 +90,9 @@ | ||
| 87 | 90 | 'taskType' => $request->getSafe('taskType', 'sanitize_text_field'), |
| 88 | 91 | 'boardIds' => $boardIds, |
| 89 | 92 | 'orderBy' => $request->getSafe('orderBy', 'sanitize_text_field'), |
| 90 | 93 | 'order' => $request->getSafe('order', 'sanitize_text_field'), |
| 94 | + 'per_page' => $perPage | |
| 91 | 95 | ]; |
| 92 | 96 | try { |
| 93 | 97 | return $this->sendSuccess( |
| 94 | 98 | $this->userService->getMemberAssociatedTasks($user_id, $requestData) |
| @@ -97,8 +101,28 @@ | ||
| 97 | 101 | return $this->sendError($e->getMessage(), 404); |
| 98 | 102 | } |
| 99 | 103 | } |
| 100 | 104 | |
| 105 | + /** | |
| 106 | + * Return profile task counts using the same categories shown on the dashboard. | |
| 107 | + */ | |
| 108 | + public function getMemberTaskCounts(Request $request, $user_id) | |
| 109 | + { | |
| 110 | + $user_id = absint($user_id); | |
| 111 | + $rawBoardIds = $request->getSafe('boardIds'); | |
| 112 | + $boardIds = is_array($rawBoardIds) | |
| 113 | + ? array_filter(array_map('intval', $rawBoardIds)) | |
| 114 | + : []; | |
| 115 | + | |
| 116 | + try { | |
| 117 | + return $this->sendSuccess([ | |
| 118 | + 'counts' => $this->userService->getMemberTaskCounts($user_id, $boardIds), | |
| 119 | + ], 200); | |
| 120 | + } catch (\Exception $e) { | |
| 121 | + return $this->sendError($e->getMessage(), 404); | |
| 122 | + } | |
| 123 | + } | |
| 124 | + | |
| 101 | 125 | public function getMemberRelatedAcitivies(Request $request, $user_id) |
| 102 | 126 | { |
| 103 | 127 | $user_id = absint($user_id); |
| 104 | 128 | $page = $request->getSafe('page', 'intval', 1); |
| @@ -155,149 +179,84 @@ | ||
| 155 | 179 | return $this->sendError($e->getMessage(), 404); |
| 156 | 180 | } |
| 157 | 181 | } |
| 158 | 182 | |
| 159 | - public function updateDisplayName(Request $request, $user_id) | |
| 183 | + public function getMemberStats($user_id) | |
| 160 | 184 | { |
| 161 | 185 | $user_id = absint($user_id); |
| 162 | - $currentUserId = get_current_user_id(); | |
| 163 | 186 | |
| 164 | - if ($currentUserId !== $user_id && !PermissionManager::isAdmin($currentUserId)) { | |
| 165 | - return $this->sendError( | |
| 166 | - __('You do not have permission to update this display name', 'fluent-boards'), | |
| 167 | - 403 | |
| 168 | - ); | |
| 169 | - } | |
| 170 | - | |
| 171 | - $displayName = $request->getSafe('display_name', 'sanitize_text_field'); | |
| 172 | - | |
| 173 | - if(!$displayName) { | |
| 174 | - return $this->sendError('Display name is required', 400); | |
| 175 | - } | |
| 176 | - | |
| 177 | - $updateResult = wp_update_user([ | |
| 178 | - 'ID' => $user_id, | |
| 179 | - 'display_name' => $displayName, | |
| 180 | - ]); | |
| 181 | - | |
| 182 | - if (is_wp_error($updateResult)) { | |
| 183 | - return $this->sendError( | |
| 184 | - $updateResult->get_error_message(), | |
| 185 | - 400 | |
| 187 | + return $this->sendSuccess( | |
| 188 | + $this->userService->getMemberStats($user_id), | |
| 189 | + 200 | |
| 186 | 190 | ); |
| 187 | - } | |
| 191 | + } | |
| 188 | 192 | |
| 189 | - $user = User::findOrFail($user_id); | |
| 190 | - $user = Helper::sanitizeUserCollections($user); | |
| 191 | - $user->fbs_role = PermissionManager::isFluentBoardsAdmin($user_id) ? 'fbs_admin' : 'member'; | |
| 192 | - $user->is_wp_admin = user_can($user_id, 'manage_options') ? 'yes' : 'no'; | |
| 193 | - | |
| 194 | - if (defined('FLUENTCRM')) { | |
| 195 | - $subscriber = Subscriber::where('user_id', $user_id)->first(); | |
| 196 | - $user->fluentcrm_subscriber = $subscriber ?? null; | |
| 193 | + /** Update the authenticated member's display name. */ | |
| 194 | + public function updateDisplayName(Request $request, $user_id) | |
| 195 | + { | |
| 196 | + try { | |
| 197 | + return $this->sendSuccess([ | |
| 198 | + 'message' => __('Display name has been updated', 'fluent-boards'), | |
| 199 | + 'user' => $this->userService->updateDisplayName($user_id, $request->get('display_name')), | |
| 200 | + ]); | |
| 201 | + } catch (\Exception $e) { | |
| 202 | + return $this->sendError(['message' => $e->getMessage()], $e->getCode() === 403 ? 403 : 400); | |
| 197 | 203 | } |
| 198 | - | |
| 199 | - return $this->sendSuccess([ | |
| 200 | - 'message' => __('Display name has been updated', 'fluent-boards'), | |
| 201 | - 'user' => $user, | |
| 202 | - ], 200); | |
| 203 | - | |
| 204 | 204 | } |
| 205 | 205 | |
| 206 | + /** Upload a profile photo for the authenticated member. */ | |
| 206 | 207 | public function updateProfilePhoto(Request $request, $user_id) |
| 207 | -{ | |
| 208 | - $user_id = absint($user_id); | |
| 209 | - $currentUserId = get_current_user_id(); | |
| 210 | - | |
| 211 | - // Only the user themself OR an admin can change the profile picture | |
| 212 | - if ($currentUserId !== $user_id && !PermissionManager::isAdmin($currentUserId)) { | |
| 213 | - return $this->sendError( | |
| 214 | - __('You do not have permission to update this profile photo', 'fluent-boards'), | |
| 215 | - 403 | |
| 216 | - ); | |
| 208 | + { | |
| 209 | + try { | |
| 210 | + return $this->sendSuccess([ | |
| 211 | + 'message' => __('Profile photo has been updated', 'fluent-boards'), | |
| 212 | + 'user' => $this->userService->updateProfilePhoto($user_id, $_FILES['photo'] ?? []), | |
| 213 | + ]); | |
| 214 | + } catch (\Exception $e) { | |
| 215 | + return $this->sendError(['message' => $e->getMessage()], $e->getCode() === 403 ? 403 : 400); | |
| 216 | + } | |
| 217 | 217 | } |
| 218 | 218 | |
| 219 | - // We’ll use native WordPress upload handling | |
| 220 | - if (empty($_FILES['photo']) || !empty($_FILES['photo']['error'])) { | |
| 221 | - return $this->sendError( | |
| 222 | - __('No photo uploaded or upload error', 'fluent-boards'), | |
| 223 | - 400 | |
| 224 | - ); | |
| 225 | - } | |
| 219 | + public function getMcpConnections(Request $request, $id) | |
| 220 | + { | |
| 221 | + if (!current_user_can('list_app_passwords', (int) $id)) { | |
| 222 | + return $this->sendError(['message' => __('You are not allowed to list application passwords.', 'fluent-boards')], 403); | |
| 223 | + } | |
| 226 | 224 | |
| 227 | - // Limit file size to 2MB for profile photos | |
| 228 | - $maxSize = 2 * 1024 * 1024; | |
| 229 | - if ($_FILES['photo']['size'] > $maxSize) { | |
| 230 | - return $this->sendError( | |
| 231 | - __('Photo must be under 2MB', 'fluent-boards'), | |
| 232 | - 400 | |
| 233 | - ); | |
| 225 | + return $this->sendSuccess(MCPAccessService::getProfileState((int) $id)); | |
| 234 | 226 | } |
| 235 | 227 | |
| 236 | - $file = $_FILES['photo']; | |
| 228 | + /** | |
| 229 | + * Create an MCP connection key. The password is only ever in this response. | |
| 230 | + */ | |
| 231 | + public function createMcpConnection(Request $request, $id) | |
| 232 | + { | |
| 233 | + $result = MCPAccessService::createConnection((int) $id, (string) $request->get('name')); | |
| 237 | 234 | |
| 238 | - // Validate MIME type server-side (client-sent type is spoofable) | |
| 239 | - $fileType = wp_check_filetype($file['name'], [ | |
| 240 | - 'jpg|jpeg|jpe' => 'image/jpeg', | |
| 241 | - 'gif' => 'image/gif', | |
| 242 | - 'png' => 'image/png', | |
| 243 | - 'webp' => 'image/webp', | |
| 244 | - ]); | |
| 235 | + if (is_wp_error($result)) { | |
| 236 | + $data = $result->get_error_data(); | |
| 237 | + return $this->sendError(['message' => $result->get_error_message()], $data['status'] ?? 400); | |
| 238 | + } | |
| 245 | 239 | |
| 246 | - if (!$fileType['type']) { | |
| 247 | - return $this->sendError( | |
| 248 | - __('Invalid image type', 'fluent-boards'), | |
| 249 | - 400 | |
| 250 | - ); | |
| 251 | - } | |
| 240 | + nocache_headers(); | |
| 252 | 241 | |
| 253 | - // Load WordPress upload helpers | |
| 254 | - if (!function_exists('wp_handle_upload')) { | |
| 255 | - require_once ABSPATH . 'wp-admin/includes/file.php'; | |
| 242 | + return $this->sendSuccess($result); | |
| 256 | 243 | } |
| 257 | 244 | |
| 258 | - $overrides = [ | |
| 259 | - 'test_form' => false, | |
| 260 | - 'mimes' => [ | |
| 261 | - 'jpg|jpeg|jpe' => 'image/jpeg', | |
| 262 | - 'gif' => 'image/gif', | |
| 263 | - 'png' => 'image/png', | |
| 264 | - 'webp' => 'image/webp', | |
| 265 | - ], | |
| 266 | - ]; | |
| 245 | + public function revokeMcpConnection(Request $request, $id, $uuid) | |
| 246 | + { | |
| 247 | + if (!current_user_can('delete_app_password', (int) $id, $uuid)) { | |
| 248 | + return $this->sendError(['message' => __('You are not allowed to delete this application password.', 'fluent-boards')], 403); | |
| 249 | + } | |
| 267 | 250 | |
| 268 | - $uploaded = wp_handle_upload($file, $overrides); | |
| 251 | + $result = MCPAccessService::revokeConnection((int) $id, sanitize_text_field($uuid)); | |
| 269 | 252 | |
| 270 | - if (isset($uploaded['error'])) { | |
| 271 | - return $this->sendError( | |
| 272 | - $uploaded['error'], | |
| 273 | - 400 | |
| 274 | - ); | |
| 275 | - } | |
| 253 | + if (is_wp_error($result)) { | |
| 254 | + $data = $result->get_error_data(); | |
| 255 | + return $this->sendError(['message' => $result->get_error_message()], $data['status'] ?? 400); | |
| 256 | + } | |
| 276 | 257 | |
| 277 | - $photoUrl = esc_url_raw($uploaded['url']); | |
| 278 | - | |
| 279 | - // Store custom profile photo in user meta | |
| 280 | - update_user_meta($user_id, 'fbs_profile_photo', $photoUrl); | |
| 281 | - | |
| 282 | - // Reload user and sanitize same as in getMemberInfo() | |
| 283 | - $user = User::findOrFail($user_id); | |
| 284 | - $user = Helper::sanitizeUserCollections($user); | |
| 285 | - | |
| 286 | - // Override photo field if your sanitizer does not already use the meta | |
| 287 | - $user->photo = $photoUrl; | |
| 288 | - | |
| 289 | - $user->fbs_role = PermissionManager::isFluentBoardsAdmin($user_id) ? 'fbs_admin' : 'member'; | |
| 290 | - $user->is_wp_admin = user_can($user_id, 'manage_options') ? 'yes' : 'no'; | |
| 291 | - | |
| 292 | - if (defined('FLUENTCRM')) { | |
| 293 | - $subscriber = Subscriber::where('user_id', $user_id)->first(); | |
| 294 | - $user->fluentcrm_subscriber = $subscriber ?? null; | |
| 258 | + return $this->sendSuccess([ | |
| 259 | + 'message' => __('Connection revoked.', 'fluent-boards'), | |
| 260 | + ]); | |
| 295 | 261 | } |
| 296 | - | |
| 297 | - return $this->sendSuccess([ | |
| 298 | - 'message' => __('Profile photo has been updated', 'fluent-boards'), | |
| 299 | - 'photo_url' => $photoUrl, | |
| 300 | - 'user' => $user, | |
| 301 | - ], 200); | |
| 302 | -} | |
| 303 | 262 | } |