From b51e432d32223035c69f6024fc79f705252ae60d Mon Sep 17 00:00:00 2001 From: provokateurin Date: Thu, 25 Jul 2024 13:14:49 +0200 Subject: [PATCH] refactor(provisioning_api): Replace security annotations with respective attributes Signed-off-by: provokateurin --- .../lib/Controller/AppConfigController.php | 9 +- .../lib/Controller/AppsController.php | 7 +- .../lib/Controller/GroupsController.php | 29 +++---- .../lib/Controller/PreferencesController.php | 9 +- .../lib/Controller/UsersController.php | 82 ++++++++----------- .../lib/Controller/VerificationController.php | 11 ++- 6 files changed, 68 insertions(+), 79 deletions(-) diff --git a/apps/provisioning_api/lib/Controller/AppConfigController.php b/apps/provisioning_api/lib/Controller/AppConfigController.php index e26e04a2f8e77..65b301245b396 100644 --- a/apps/provisioning_api/lib/Controller/AppConfigController.php +++ b/apps/provisioning_api/lib/Controller/AppConfigController.php @@ -11,6 +11,8 @@ use OC\AppConfig; use OC\AppFramework\Middleware\Security\Exceptions\NotAdminException; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; use OCP\IAppConfig; @@ -93,9 +95,7 @@ public function getValue(string $app, string $key, string $defaultValue = ''): D } /** - * @PasswordConfirmationRequired * @NoSubAdminRequired - * @NoAdminRequired * * Update the config value of an app * @@ -107,6 +107,8 @@ public function getValue(string $app, string $key, string $defaultValue = ''): D * 200: Value updated successfully * 403: App or key is not allowed */ + #[PasswordConfirmationRequired] + #[NoAdminRequired] public function setValue(string $app, string $key, string $value): DataResponse { $user = $this->userSession->getUser(); if ($user === null) { @@ -130,8 +132,6 @@ public function setValue(string $app, string $key, string $value): DataResponse } /** - * @PasswordConfirmationRequired - * * Delete a config key of an app * * @param string $app ID of the app @@ -141,6 +141,7 @@ public function setValue(string $app, string $key, string $value): DataResponse * 200: Key deleted successfully * 403: App or key is not allowed */ + #[PasswordConfirmationRequired] public function deleteKey(string $app, string $key): DataResponse { try { $this->verifyAppId($app); diff --git a/apps/provisioning_api/lib/Controller/AppsController.php b/apps/provisioning_api/lib/Controller/AppsController.php index 1471b13cd3180..d60a85f374039 100644 --- a/apps/provisioning_api/lib/Controller/AppsController.php +++ b/apps/provisioning_api/lib/Controller/AppsController.php @@ -12,6 +12,7 @@ use OCP\App\AppPathNotFoundException; use OCP\App\IAppManager; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSException; use OCP\AppFramework\OCSController; @@ -84,8 +85,6 @@ public function getAppInfo(string $app): DataResponse { } /** - * @PasswordConfirmationRequired - * * Enable an app * * @param string $app ID of the app @@ -94,6 +93,7 @@ public function getAppInfo(string $app): DataResponse { * * 200: App enabled successfully */ + #[PasswordConfirmationRequired] public function enable(string $app): DataResponse { try { $this->appManager->enableApp($app); @@ -104,8 +104,6 @@ public function enable(string $app): DataResponse { } /** - * @PasswordConfirmationRequired - * * Disable an app * * @param string $app ID of the app @@ -113,6 +111,7 @@ public function enable(string $app): DataResponse { * * 200: App disabled successfully */ + #[PasswordConfirmationRequired] public function disable(string $app): DataResponse { $this->appManager->disableApp($app); return new DataResponse(); diff --git a/apps/provisioning_api/lib/Controller/GroupsController.php b/apps/provisioning_api/lib/Controller/GroupsController.php index 97480058fd1f4..4b05f772e8f27 100644 --- a/apps/provisioning_api/lib/Controller/GroupsController.php +++ b/apps/provisioning_api/lib/Controller/GroupsController.php @@ -9,10 +9,13 @@ namespace OCA\Provisioning_API\Controller; use OCA\Provisioning_API\ResponseDefinitions; +use OCA\Settings\Settings\Admin\Sharing; use OCA\Settings\Settings\Admin\Users; use OCP\Accounts\IAccountManager; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSException; use OCP\AppFramework\OCS\OCSForbiddenException; @@ -60,8 +63,6 @@ public function __construct(string $appName, } /** - * @NoAdminRequired - * * Get a list of groups * * @param string $search Text to search for @@ -71,6 +72,7 @@ public function __construct(string $appName, * * 200: Groups returned */ + #[NoAdminRequired] public function getGroups(string $search = '', ?int $limit = null, int $offset = 0): DataResponse { $groups = $this->groupManager->search($search, $limit, $offset); $groups = array_map(function ($group) { @@ -82,9 +84,6 @@ public function getGroups(string $search = '', ?int $limit = null, int $offset = } /** - * @NoAdminRequired - * @AuthorizedAdminSetting(settings=OCA\Settings\Settings\Admin\Sharing) - * * Get a list of groups details * * @param string $search Text to search for @@ -94,6 +93,8 @@ public function getGroups(string $search = '', ?int $limit = null, int $offset = * * 200: Groups details returned */ + #[NoAdminRequired] + #[AuthorizedAdminSetting(settings: Sharing::class)] public function getGroupsDetails(string $search = '', ?int $limit = null, int $offset = 0): DataResponse { $groups = $this->groupManager->search($search, $limit, $offset); $groups = array_map(function ($group) { @@ -112,8 +113,6 @@ public function getGroupsDetails(string $search = '', ?int $limit = null, int $o } /** - * @NoAdminRequired - * * Get a list of users in the specified group * * @param string $groupId ID of the group @@ -124,13 +123,12 @@ public function getGroupsDetails(string $search = '', ?int $limit = null, int $o * * 200: Group users returned */ + #[NoAdminRequired] public function getGroup(string $groupId): DataResponse { return $this->getGroupUsers($groupId); } /** - * @NoAdminRequired - * * Get a list of users in the specified group * * @param string $groupId ID of the group @@ -141,6 +139,7 @@ public function getGroup(string $groupId): DataResponse { * * 200: User IDs returned */ + #[NoAdminRequired] public function getGroupUsers(string $groupId): DataResponse { $groupId = urldecode($groupId); @@ -173,8 +172,6 @@ public function getGroupUsers(string $groupId): DataResponse { } /** - * @NoAdminRequired - * * Get a list of users details in the specified group * * @param string $groupId ID of the group @@ -187,6 +184,7 @@ public function getGroupUsers(string $groupId): DataResponse { * * 200: Group users details returned */ + #[NoAdminRequired] public function getGroupUsersDetails(string $groupId, string $search = '', ?int $limit = null, int $offset = 0): DataResponse { $groupId = urldecode($groupId); $currentUser = $this->userSession->getUser(); @@ -231,8 +229,6 @@ public function getGroupUsersDetails(string $groupId, string $search = '', ?int } /** - * @PasswordConfirmationRequired - * * Create a new group * * @param string $groupid ID of the group @@ -243,6 +239,7 @@ public function getGroupUsersDetails(string $groupId, string $search = '', ?int * 200: Group created successfully */ #[AuthorizedAdminSetting(settings:Users::class)] + #[PasswordConfirmationRequired] public function addGroup(string $groupid, string $displayname = ''): DataResponse { // Validate name if (empty($groupid)) { @@ -264,8 +261,6 @@ public function addGroup(string $groupid, string $displayname = ''): DataRespons } /** - * @PasswordConfirmationRequired - * * Update a group * * @param string $groupId ID of the group @@ -277,6 +272,7 @@ public function addGroup(string $groupid, string $displayname = ''): DataRespons * 200: Group updated successfully */ #[AuthorizedAdminSetting(settings:Users::class)] + #[PasswordConfirmationRequired] public function updateGroup(string $groupId, string $key, string $value): DataResponse { $groupId = urldecode($groupId); @@ -296,8 +292,6 @@ public function updateGroup(string $groupId, string $key, string $value): DataRe } /** - * @PasswordConfirmationRequired - * * Delete a group * * @param string $groupId ID of the group @@ -307,6 +301,7 @@ public function updateGroup(string $groupId, string $key, string $value): DataRe * 200: Group deleted successfully */ #[AuthorizedAdminSetting(settings:Users::class)] + #[PasswordConfirmationRequired] public function deleteGroup(string $groupId): DataResponse { $groupId = urldecode($groupId); diff --git a/apps/provisioning_api/lib/Controller/PreferencesController.php b/apps/provisioning_api/lib/Controller/PreferencesController.php index 521e2f039fe5a..affacb4fb3238 100644 --- a/apps/provisioning_api/lib/Controller/PreferencesController.php +++ b/apps/provisioning_api/lib/Controller/PreferencesController.php @@ -10,6 +10,7 @@ namespace OCA\Provisioning_API\Controller; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCSController; use OCP\Config\BeforePreferenceDeletedEvent; @@ -39,7 +40,6 @@ public function __construct( } /** - * @NoAdminRequired * @NoSubAdminRequired * * Update multiple preference values of an app @@ -52,6 +52,7 @@ public function __construct( * 200: Preferences updated successfully * 400: Preference invalid */ + #[NoAdminRequired] public function setMultiplePreferences(string $appId, array $configs): DataResponse { $userId = $this->userSession->getUser()->getUID(); @@ -84,7 +85,6 @@ public function setMultiplePreferences(string $appId, array $configs): DataRespo } /** - * @NoAdminRequired * @NoSubAdminRequired * * Update a preference value of an app @@ -97,6 +97,7 @@ public function setMultiplePreferences(string $appId, array $configs): DataRespo * 200: Preference updated successfully * 400: Preference invalid */ + #[NoAdminRequired] public function setPreference(string $appId, string $configKey, string $configValue): DataResponse { $userId = $this->userSession->getUser()->getUID(); @@ -125,7 +126,6 @@ public function setPreference(string $appId, string $configKey, string $configVa } /** - * @NoAdminRequired * @NoSubAdminRequired * * Delete multiple preferences for an app @@ -137,6 +137,7 @@ public function setPreference(string $appId, string $configKey, string $configVa * 200: Preferences deleted successfully * 400: Preference invalid */ + #[NoAdminRequired] public function deleteMultiplePreference(string $appId, array $configKeys): DataResponse { $userId = $this->userSession->getUser()->getUID(); @@ -167,7 +168,6 @@ public function deleteMultiplePreference(string $appId, array $configKeys): Data } /** - * @NoAdminRequired * @NoSubAdminRequired * * Delete a preference for an app @@ -179,6 +179,7 @@ public function deleteMultiplePreference(string $appId, array $configKeys): Data * 200: Preference deleted successfully * 400: Preference invalid */ + #[NoAdminRequired] public function deletePreference(string $appId, string $configKey): DataResponse { $userId = $this->userSession->getUser()->getUID(); diff --git a/apps/provisioning_api/lib/Controller/UsersController.php b/apps/provisioning_api/lib/Controller/UsersController.php index 5ac8d23cf77be..46773f2f6a551 100644 --- a/apps/provisioning_api/lib/Controller/UsersController.php +++ b/apps/provisioning_api/lib/Controller/UsersController.php @@ -22,6 +22,9 @@ use OCP\Accounts\PropertyDoesNotExistException; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\AuthorizedAdminSetting; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; +use OCP\AppFramework\Http\Attribute\UserRateLimit; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSException; use OCP\AppFramework\OCS\OCSForbiddenException; @@ -85,8 +88,6 @@ public function __construct( } /** - * @NoAdminRequired - * * Get a list of users * * @param string $search Text to search for @@ -96,6 +97,7 @@ public function __construct( * * 200: Users returned */ + #[NoAdminRequired] public function getUsers(string $search = '', ?int $limit = null, int $offset = 0): DataResponse { $user = $this->userSession->getUser(); $users = []; @@ -128,8 +130,6 @@ public function getUsers(string $search = '', ?int $limit = null, int $offset = } /** - * @NoAdminRequired - * * Get a list of users and their details * * @param string $search Text to search for @@ -139,6 +139,7 @@ public function getUsers(string $search = '', ?int $limit = null, int $offset = * * 200: Users details returned */ + #[NoAdminRequired] public function getUsersDetails(string $search = '', ?int $limit = null, int $offset = 0): DataResponse { $currentUser = $this->userSession->getUser(); $users = []; @@ -191,8 +192,6 @@ public function getUsersDetails(string $search = '', ?int $limit = null, int $of } /** - * @NoAdminRequired - * * Get the list of disabled users and their details * * @param string $search Text to search for @@ -202,6 +201,7 @@ public function getUsersDetails(string $search = '', ?int $limit = null, int $of * * 200: Disabled users details returned */ + #[NoAdminRequired] public function getDisabledUsersDetails(string $search = '', ?int $limit = null, int $offset = 0): DataResponse { $currentUser = $this->userSession->getUser(); if ($currentUser === null) { @@ -332,7 +332,6 @@ public function getLastLoggedInUsers(string $search = '', /** - * @NoAdminRequired * @NoSubAdminRequired * * Search users by their phone numbers @@ -344,6 +343,7 @@ public function getLastLoggedInUsers(string $search = '', * 200: Users returned * 400: Invalid location */ + #[NoAdminRequired] public function searchByPhoneNumbers(string $location, array $search): DataResponse { if ($this->phoneNumberUtil->getCountryCodeForRegion($location) === null) { // Not a valid region code @@ -423,9 +423,6 @@ private function createNewUserId(): string { } /** - * @PasswordConfirmationRequired - * @NoAdminRequired - * * Create a new user * * @param string $userid ID of the user @@ -443,6 +440,8 @@ private function createNewUserId(): string { * * 200: User added successfully */ + #[PasswordConfirmationRequired] + #[NoAdminRequired] public function addUser( string $userid, string $password = '', @@ -633,7 +632,6 @@ public function addUser( } /** - * @NoAdminRequired * @NoSubAdminRequired * * Get the details of a user @@ -644,6 +642,7 @@ public function addUser( * * 200: User returned */ + #[NoAdminRequired] public function getUser(string $userId): DataResponse { $includeScopes = false; $currentUser = $this->userSession->getUser(); @@ -660,7 +659,6 @@ public function getUser(string $userId): DataResponse { } /** - * @NoAdminRequired * @NoSubAdminRequired * * Get the details of the current user @@ -670,6 +668,7 @@ public function getUser(string $userId): DataResponse { * * 200: Current user returned */ + #[NoAdminRequired] public function getCurrentUser(): DataResponse { $user = $this->userSession->getUser(); if ($user) { @@ -682,7 +681,6 @@ public function getCurrentUser(): DataResponse { } /** - * @NoAdminRequired * @NoSubAdminRequired * * Get a list of fields that are editable for the current user @@ -692,6 +690,7 @@ public function getCurrentUser(): DataResponse { * * 200: Editable fields returned */ + #[NoAdminRequired] public function getEditableFields(): DataResponse { $currentLoggedInUser = $this->userSession->getUser(); if (!$currentLoggedInUser instanceof IUser) { @@ -702,7 +701,6 @@ public function getEditableFields(): DataResponse { } /** - * @NoAdminRequired * @NoSubAdminRequired * * Get a list of fields that are editable for a user @@ -713,6 +711,7 @@ public function getEditableFields(): DataResponse { * * 200: Editable fields for user returned */ + #[NoAdminRequired] public function getEditableFieldsForUser(string $userId): DataResponse { $currentLoggedInUser = $this->userSession->getUser(); if (!$currentLoggedInUser instanceof IUser) { @@ -767,10 +766,7 @@ public function getEditableFieldsForUser(string $userId): DataResponse { } /** - * @NoAdminRequired * @NoSubAdminRequired - * @PasswordConfirmationRequired - * @UserRateThrottle(limit=5, period=60) * * Update multiple values of the user's details * @@ -783,6 +779,9 @@ public function getEditableFieldsForUser(string $userId): DataResponse { * * 200: User values edited successfully */ + #[PasswordConfirmationRequired] + #[NoAdminRequired] + #[UserRateLimit(limit: 5, period: 60)] public function editUserMultiValue( string $userId, string $collectionName, @@ -870,10 +869,7 @@ public function editUserMultiValue( } /** - * @NoAdminRequired * @NoSubAdminRequired - * @PasswordConfirmationRequired - * @UserRateThrottle(limit=50, period=600) * * Update a value of the user's details * @@ -885,6 +881,9 @@ public function editUserMultiValue( * * 200: User value edited successfully */ + #[PasswordConfirmationRequired] + #[NoAdminRequired] + #[UserRateLimit(limit: 50, period: 60)] public function editUser(string $userId, string $key, string $value): DataResponse { $currentLoggedInUser = $this->userSession->getUser(); @@ -1206,9 +1205,6 @@ public function editUser(string $userId, string $key, string $value): DataRespon } /** - * @PasswordConfirmationRequired - * @NoAdminRequired - * * Wipe all devices of a user * * @param string $userId ID of the user @@ -1219,6 +1215,8 @@ public function editUser(string $userId, string $key, string $value): DataRespon * * 200: Wiped all user devices successfully */ + #[PasswordConfirmationRequired] + #[NoAdminRequired] public function wipeUserDevices(string $userId): DataResponse { /** @var IUser $currentLoggedInUser */ $currentLoggedInUser = $this->userSession->getUser(); @@ -1247,9 +1245,6 @@ public function wipeUserDevices(string $userId): DataResponse { } /** - * @PasswordConfirmationRequired - * @NoAdminRequired - * * Delete a user * * @param string $userId ID of the user @@ -1258,6 +1253,8 @@ public function wipeUserDevices(string $userId): DataResponse { * * 200: User deleted successfully */ + #[PasswordConfirmationRequired] + #[NoAdminRequired] public function deleteUser(string $userId): DataResponse { $currentLoggedInUser = $this->userSession->getUser(); @@ -1288,9 +1285,6 @@ public function deleteUser(string $userId): DataResponse { } /** - * @PasswordConfirmationRequired - * @NoAdminRequired - * * Disable a user * * @param string $userId ID of the user @@ -1299,14 +1293,13 @@ public function deleteUser(string $userId): DataResponse { * * 200: User disabled successfully */ + #[PasswordConfirmationRequired] + #[NoAdminRequired] public function disableUser(string $userId): DataResponse { return $this->setEnabled($userId, false); } /** - * @PasswordConfirmationRequired - * @NoAdminRequired - * * Enable a user * * @param string $userId ID of the user @@ -1315,6 +1308,8 @@ public function disableUser(string $userId): DataResponse { * * 200: User enabled successfully */ + #[PasswordConfirmationRequired] + #[NoAdminRequired] public function enableUser(string $userId): DataResponse { return $this->setEnabled($userId, true); } @@ -1347,7 +1342,6 @@ private function setEnabled(string $userId, bool $value): DataResponse { } /** - * @NoAdminRequired * @NoSubAdminRequired * * Get a list of groups the user belongs to @@ -1358,6 +1352,7 @@ private function setEnabled(string $userId, bool $value): DataResponse { * * 200: Users groups returned */ + #[NoAdminRequired] public function getUsersGroups(string $userId): DataResponse { $loggedInUser = $this->userSession->getUser(); @@ -1398,9 +1393,6 @@ public function getUsersGroups(string $userId): DataResponse { } /** - * @PasswordConfirmationRequired - * @NoAdminRequired - * * Add a user to a group * * @param string $userId ID of the user @@ -1410,6 +1402,8 @@ public function getUsersGroups(string $userId): DataResponse { * * 200: User added to group successfully */ + #[PasswordConfirmationRequired] + #[NoAdminRequired] public function addToGroup(string $userId, string $groupid = ''): DataResponse { if ($groupid === '') { throw new OCSException('', 101); @@ -1439,9 +1433,6 @@ public function addToGroup(string $userId, string $groupid = ''): DataResponse { } /** - * @PasswordConfirmationRequired - * @NoAdminRequired - * * Remove a user from a group * * @param string $userId ID of the user @@ -1451,6 +1442,8 @@ public function addToGroup(string $userId, string $groupid = ''): DataResponse { * * 200: User removed from group successfully */ + #[PasswordConfirmationRequired] + #[NoAdminRequired] public function removeFromGroup(string $userId, string $groupid): DataResponse { $loggedInUser = $this->userSession->getUser(); @@ -1507,8 +1500,6 @@ public function removeFromGroup(string $userId, string $groupid): DataResponse { } /** - * @PasswordConfirmationRequired - * * Make a user a subadmin of a group * * @param string $userId ID of the user @@ -1519,6 +1510,7 @@ public function removeFromGroup(string $userId, string $groupid): DataResponse { * 200: User added as group subadmin successfully */ #[AuthorizedAdminSetting(settings:Users::class)] + #[PasswordConfirmationRequired] public function addSubAdmin(string $userId, string $groupid): DataResponse { $group = $this->groupManager->get($groupid); $user = $this->userManager->get($userId); @@ -1548,8 +1540,6 @@ public function addSubAdmin(string $userId, string $groupid): DataResponse { } /** - * @PasswordConfirmationRequired - * * Remove a user from the subadmins of a group * * @param string $userId ID of the user @@ -1560,6 +1550,7 @@ public function addSubAdmin(string $userId, string $groupid): DataResponse { * 200: User removed as group subadmin successfully */ #[AuthorizedAdminSetting(settings:Users::class)] + #[PasswordConfirmationRequired] public function removeSubAdmin(string $userId, string $groupid): DataResponse { $group = $this->groupManager->get($groupid); $user = $this->userManager->get($userId); @@ -1599,9 +1590,6 @@ public function getUserSubAdminGroups(string $userId): DataResponse { } /** - * @NoAdminRequired - * @PasswordConfirmationRequired - * * Resend the welcome message * * @param string $userId ID if the user @@ -1610,6 +1598,8 @@ public function getUserSubAdminGroups(string $userId): DataResponse { * * 200: Resent welcome message successfully */ + #[PasswordConfirmationRequired] + #[NoAdminRequired] public function resendWelcomeMessage(string $userId): DataResponse { $currentLoggedInUser = $this->userSession->getUser(); diff --git a/apps/provisioning_api/lib/Controller/VerificationController.php b/apps/provisioning_api/lib/Controller/VerificationController.php index ade97331a9634..18113484c8a4e 100644 --- a/apps/provisioning_api/lib/Controller/VerificationController.php +++ b/apps/provisioning_api/lib/Controller/VerificationController.php @@ -13,6 +13,9 @@ use OC\Security\Crypto; use OCP\Accounts\IAccountManager; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\BruteForceProtection; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\Attribute\OpenAPI; use OCP\AppFramework\Http\TemplateResponse; use OCP\IL10N; @@ -58,10 +61,10 @@ public function __construct( } /** - * @NoCSRFRequired - * @NoAdminRequired * @NoSubAdminRequired */ + #[NoAdminRequired] + #[NoCSRFRequired] public function showVerifyMail(string $token, string $userId, string $key): TemplateResponse { if ($this->userSession->getUser()->getUID() !== $userId) { // not a public page, hence getUser() must return an IUser @@ -78,10 +81,10 @@ public function showVerifyMail(string $token, string $userId, string $key): Temp } /** - * @NoAdminRequired * @NoSubAdminRequired - * @BruteForceProtection(action=emailVerification) */ + #[NoAdminRequired] + #[BruteForceProtection(action: 'emailVerification')] public function verifyMail(string $token, string $userId, string $key): TemplateResponse { $throttle = false; try {