From d74a97e571379fb03b9d01acf73fe195ca13d644 Mon Sep 17 00:00:00 2001 From: Chen Asraf Date: Wed, 21 Jan 2026 01:40:29 +0200 Subject: [PATCH] feat: admin section with repair seeds+add role helpers --- appinfo/info.xml | 4 + lib/Controller/AdminController.php | 204 +++++++++++++ lib/Sections/AdminSection.php | 32 ++ lib/Settings/AdminSettings.php | 42 +++ openapi.json | 454 +++++++++++++++++++++++++++++ src/AdminSettings.vue | 277 ++++++++++++++++++ src/admin.ts | 5 + templates/settings.php | 12 + vite.config.ts | 1 + 9 files changed, 1031 insertions(+) create mode 100644 lib/Sections/AdminSection.php create mode 100644 lib/Settings/AdminSettings.php create mode 100644 src/AdminSettings.vue create mode 100644 src/admin.ts create mode 100644 templates/settings.php diff --git a/appinfo/info.xml b/appinfo/info.xml index 94db10b..17e6922 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -67,6 +67,10 @@ The forum integrates seamlessly with your Nextcloud instance, using your existin OCA\Forum\Command\SetRole OCA\Forum\Command\TestNotifier + + OCA\Forum\Settings\AdminSettings + OCA\Forum\Sections\AdminSection + Forum diff --git a/lib/Controller/AdminController.php b/lib/Controller/AdminController.php index 95b4d6b..db680ec 100644 --- a/lib/Controller/AdminController.php +++ b/lib/Controller/AdminController.php @@ -11,9 +11,12 @@ use OCA\Forum\Attribute\RequirePermission; use OCA\Forum\Db\CategoryMapper; use OCA\Forum\Db\ForumUserMapper; use OCA\Forum\Db\PostMapper; +use OCA\Forum\Db\RoleMapper; use OCA\Forum\Db\ThreadMapper; use OCA\Forum\Db\UserRoleMapper; +use OCA\Forum\Migration\SeedHelper; use OCA\Forum\Service\AdminSettingsService; +use OCA\Forum\Service\UserRoleService; use OCA\Forum\Service\UserService; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\ApiRoute; @@ -23,6 +26,7 @@ use OCP\AppFramework\OCSController; use OCP\IRequest; use OCP\IUserManager; use OCP\IUserSession; +use OCP\Migration\IOutput; use Psr\Log\LoggerInterface; class AdminController extends OCSController { @@ -36,6 +40,8 @@ class AdminController extends OCSController { private PostMapper $postMapper, private CategoryMapper $categoryMapper, private UserRoleMapper $userRoleMapper, + private RoleMapper $roleMapper, + private UserRoleService $userRoleService, private IUserManager $userManager, private IUserSession $userSession, private AdminSettingsService $settingsService, @@ -228,4 +234,202 @@ class AdminController extends OCSController { return false; } } + + /** + * Run the repair seeds command to restore default forum data + * + * @return DataResponse + * + * 200: Seeds repaired successfully + */ + #[NoAdminRequired] + #[RequirePermission('canAccessAdminTools')] + #[ApiRoute(verb: 'POST', url: '/api/admin/repair-seeds')] + public function repairSeeds(): DataResponse { + try { + $messages = []; + $migrationOutput = new class($messages) implements IOutput { + /** @var array */ + private array $messages; + + public function __construct(array &$messages) { + $this->messages = &$messages; + } + + public function info($message): void { + $this->messages[] = $message; + } + + public function warning($message): void { + $this->messages[] = '[Warning] ' . $message; + } + + public function debug($message): void { + $this->messages[] = '[Debug] ' . $message; + } + + public function startProgress($max = 0): void { + } + + public function advance($step = 1, $description = ''): void { + } + + public function finishProgress(): void { + } + }; + + SeedHelper::seedAll($migrationOutput, true); + + $this->logger->info('Forum repair seeds completed successfully'); + return new DataResponse([ + 'success' => true, + 'message' => implode("\n", $messages), + ]); + } catch (\Exception $e) { + $this->logger->error('Error running repair seeds: ' . $e->getMessage()); + return new DataResponse([ + 'success' => false, + 'message' => 'Failed to repair seeds: ' . $e->getMessage(), + ], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + /** + * Get all available roles + * + * @return DataResponse>}, array{}> + * + * 200: Roles list returned + */ + #[NoAdminRequired] + #[RequirePermission('canAccessAdminTools')] + #[ApiRoute(verb: 'GET', url: '/api/admin/roles')] + public function getRoles(): DataResponse { + try { + $roles = $this->roleMapper->findAll(); + $rolesData = array_map(fn ($role) => [ + 'id' => $role->getId(), + 'name' => $role->getName(), + 'roleType' => $role->getRoleType(), + ], $roles); + return new DataResponse(['roles' => $rolesData]); + } catch (\Exception $e) { + $this->logger->error('Error fetching roles: ' . $e->getMessage()); + return new DataResponse(['error' => 'Failed to fetch roles'], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + /** + * Assign a role to a user + * + * @param string $userId The user ID + * @param int $roleId The role ID to assign + * @return DataResponse + * + * 200: Role assigned successfully + */ + #[NoAdminRequired] + #[RequirePermission('canAccessAdminTools')] + #[ApiRoute(verb: 'POST', url: '/api/admin/users/{userId}/roles')] + public function assignRole(string $userId, int $roleId): DataResponse { + try { + // Check if user exists + $user = $this->userManager->get($userId); + if ($user === null) { + return new DataResponse([ + 'success' => false, + 'message' => "User '$userId' does not exist.", + ], Http::STATUS_NOT_FOUND); + } + + // Check if role exists + try { + $role = $this->roleMapper->find($roleId); + } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { + return new DataResponse([ + 'success' => false, + 'message' => "Role with ID '$roleId' does not exist.", + ], Http::STATUS_NOT_FOUND); + } + + // Check if user already has this role + if ($this->userRoleService->hasRole($userId, $roleId)) { + return new DataResponse([ + 'success' => true, + 'message' => "User '$userId' already has the role '{$role->getName()}'.", + ]); + } + + // Assign the role + $this->userRoleService->assignRole($userId, $roleId, skipIfExists: false); + $this->logger->info("Assigned role '{$role->getName()}' to user '$userId'"); + + return new DataResponse([ + 'success' => true, + 'message' => "Successfully assigned role '{$role->getName()}' to user '$userId'.", + ]); + } catch (\Exception $e) { + $this->logger->error('Error assigning role: ' . $e->getMessage()); + return new DataResponse([ + 'success' => false, + 'message' => 'Failed to assign role: ' . $e->getMessage(), + ], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + /** + * Remove a role from a user + * + * @param string $userId The user ID + * @param int $roleId The role ID to remove + * @return DataResponse + * + * 200: Role removed successfully + */ + #[NoAdminRequired] + #[RequirePermission('canAccessAdminTools')] + #[ApiRoute(verb: 'DELETE', url: '/api/admin/users/{userId}/roles/{roleId}')] + public function removeRole(string $userId, int $roleId): DataResponse { + try { + // Check if user exists + $user = $this->userManager->get($userId); + if ($user === null) { + return new DataResponse([ + 'success' => false, + 'message' => "User '$userId' does not exist.", + ], Http::STATUS_NOT_FOUND); + } + + // Check if role exists + try { + $role = $this->roleMapper->find($roleId); + } catch (\OCP\AppFramework\Db\DoesNotExistException $e) { + return new DataResponse([ + 'success' => false, + 'message' => "Role with ID '$roleId' does not exist.", + ], Http::STATUS_NOT_FOUND); + } + + // Remove the role + $removed = $this->userRoleService->removeRole($userId, $roleId); + if (!$removed) { + return new DataResponse([ + 'success' => true, + 'message' => "User '$userId' does not have the role '{$role->getName()}'.", + ]); + } + + $this->logger->info("Removed role '{$role->getName()}' from user '$userId'"); + return new DataResponse([ + 'success' => true, + 'message' => "Successfully removed role '{$role->getName()}' from user '$userId'.", + ]); + } catch (\Exception $e) { + $this->logger->error('Error removing role: ' . $e->getMessage()); + return new DataResponse([ + 'success' => false, + 'message' => 'Failed to remove role: ' . $e->getMessage(), + ], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } } diff --git a/lib/Sections/AdminSection.php b/lib/Sections/AdminSection.php new file mode 100644 index 0000000..11e69a3 --- /dev/null +++ b/lib/Sections/AdminSection.php @@ -0,0 +1,32 @@ +urlGenerator->imagePath(AppInfo\Application::APP_ID, 'app-dark.svg'); + } + + public function getID(): string { + return AppInfo\Application::APP_ID; + } + + public function getName(): string { + return $this->l->t('Forum'); + } + + public function getPriority(): int { + return 80; + } +} diff --git a/lib/Settings/AdminSettings.php b/lib/Settings/AdminSettings.php new file mode 100644 index 0000000..003edc7 --- /dev/null +++ b/lib/Settings/AdminSettings.php @@ -0,0 +1,42 @@ + Application::getViteEntryScript('admin.ts'), + 'style' => Application::getViteEntryScript('style.css'), + ], ''); + } + + public function getSection(): string { + return Application::APP_ID; + } + + /** + * @return int whether the form should be rather on the top or bottom of + * the admin section. The forms are arranged in ascending order of the + * priority values. It is required to return a value between 0 and 100. + * + * E.g.: 70 + */ + public function getPriority(): int { + return 10; + } +} diff --git a/openapi.json b/openapi.json index 1406c7c..780022c 100644 --- a/openapi.json +++ b/openapi.json @@ -454,6 +454,460 @@ } } }, + "/ocs/v2.php/apps/forum/api/admin/repair-seeds": { + "post": { + "operationId": "admin-repair-seeds", + "summary": "Run the repair seeds command to restore default forum data", + "tags": [ + "admin" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Seeds repaired successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "success", + "message" + ], + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/forum/api/admin/roles": { + "get": { + "operationId": "admin-get-roles", + "summary": "Get all available roles", + "tags": [ + "admin" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Roles list returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "roles" + ], + "properties": { + "roles": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "type": "object" + } + } + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/forum/api/admin/users/{userId}/roles": { + "post": { + "operationId": "admin-assign-role", + "summary": "Assign a role to a user", + "tags": [ + "admin" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "roleId" + ], + "properties": { + "roleId": { + "type": "integer", + "format": "int64", + "description": "The role ID to assign" + } + } + } + } + } + }, + "parameters": [ + { + "name": "userId", + "in": "path", + "description": "The user ID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Role assigned successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "success", + "message" + ], + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/apps/forum/api/admin/users/{userId}/roles/{roleId}": { + "delete": { + "operationId": "admin-remove-role", + "summary": "Remove a role from a user", + "tags": [ + "admin" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "userId", + "in": "path", + "description": "The user ID", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "roleId", + "in": "path", + "description": "The role ID to remove", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Role removed successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "success", + "message" + ], + "properties": { + "success": { + "type": "boolean" + }, + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "401": { + "description": "Current user is not logged in", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": {} + } + } + } + } + } + } + } + } + } + }, "/ocs/v2.php/apps/forum/api/bbcodes": { "get": { "operationId": "bb_code-index", diff --git a/src/AdminSettings.vue b/src/AdminSettings.vue new file mode 100644 index 0000000..8acd19a --- /dev/null +++ b/src/AdminSettings.vue @@ -0,0 +1,277 @@ + + + + + diff --git a/src/admin.ts b/src/admin.ts new file mode 100644 index 0000000..420460b --- /dev/null +++ b/src/admin.ts @@ -0,0 +1,5 @@ +import AdminSettings from './AdminSettings.vue' +import './style.scss' +import { createApp } from 'vue' + +createApp(AdminSettings).mount('#forum-settings') diff --git a/templates/settings.php b/templates/settings.php new file mode 100644 index 0000000..3bcd88d --- /dev/null +++ b/templates/settings.php @@ -0,0 +1,12 @@ + +
diff --git a/vite.config.ts b/vite.config.ts index 93e1ea5..927fab2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -34,6 +34,7 @@ const nextcloudSharedList = [ export default createAppConfig( { app: path.resolve(path.join('src', 'app.ts')), + admin: path.resolve(path.join('src', 'admin.ts')), }, { config: {