mirror of
https://github.com/chenasraf/nextcloud-forum.git
synced 2026-05-17 17:28:02 +00:00
feat: admin section with repair seeds+add role helpers
This commit is contained in:
@@ -67,6 +67,10 @@ The forum integrates seamlessly with your Nextcloud instance, using your existin
|
|||||||
<command>OCA\Forum\Command\SetRole</command>
|
<command>OCA\Forum\Command\SetRole</command>
|
||||||
<command>OCA\Forum\Command\TestNotifier</command>
|
<command>OCA\Forum\Command\TestNotifier</command>
|
||||||
</commands>
|
</commands>
|
||||||
|
<settings>
|
||||||
|
<admin>OCA\Forum\Settings\AdminSettings</admin>
|
||||||
|
<admin-section>OCA\Forum\Sections\AdminSection</admin-section>
|
||||||
|
</settings>
|
||||||
<navigations>
|
<navigations>
|
||||||
<navigation role="all">
|
<navigation role="all">
|
||||||
<name>Forum</name>
|
<name>Forum</name>
|
||||||
|
|||||||
@@ -11,9 +11,12 @@ use OCA\Forum\Attribute\RequirePermission;
|
|||||||
use OCA\Forum\Db\CategoryMapper;
|
use OCA\Forum\Db\CategoryMapper;
|
||||||
use OCA\Forum\Db\ForumUserMapper;
|
use OCA\Forum\Db\ForumUserMapper;
|
||||||
use OCA\Forum\Db\PostMapper;
|
use OCA\Forum\Db\PostMapper;
|
||||||
|
use OCA\Forum\Db\RoleMapper;
|
||||||
use OCA\Forum\Db\ThreadMapper;
|
use OCA\Forum\Db\ThreadMapper;
|
||||||
use OCA\Forum\Db\UserRoleMapper;
|
use OCA\Forum\Db\UserRoleMapper;
|
||||||
|
use OCA\Forum\Migration\SeedHelper;
|
||||||
use OCA\Forum\Service\AdminSettingsService;
|
use OCA\Forum\Service\AdminSettingsService;
|
||||||
|
use OCA\Forum\Service\UserRoleService;
|
||||||
use OCA\Forum\Service\UserService;
|
use OCA\Forum\Service\UserService;
|
||||||
use OCP\AppFramework\Http;
|
use OCP\AppFramework\Http;
|
||||||
use OCP\AppFramework\Http\Attribute\ApiRoute;
|
use OCP\AppFramework\Http\Attribute\ApiRoute;
|
||||||
@@ -23,6 +26,7 @@ use OCP\AppFramework\OCSController;
|
|||||||
use OCP\IRequest;
|
use OCP\IRequest;
|
||||||
use OCP\IUserManager;
|
use OCP\IUserManager;
|
||||||
use OCP\IUserSession;
|
use OCP\IUserSession;
|
||||||
|
use OCP\Migration\IOutput;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
class AdminController extends OCSController {
|
class AdminController extends OCSController {
|
||||||
@@ -36,6 +40,8 @@ class AdminController extends OCSController {
|
|||||||
private PostMapper $postMapper,
|
private PostMapper $postMapper,
|
||||||
private CategoryMapper $categoryMapper,
|
private CategoryMapper $categoryMapper,
|
||||||
private UserRoleMapper $userRoleMapper,
|
private UserRoleMapper $userRoleMapper,
|
||||||
|
private RoleMapper $roleMapper,
|
||||||
|
private UserRoleService $userRoleService,
|
||||||
private IUserManager $userManager,
|
private IUserManager $userManager,
|
||||||
private IUserSession $userSession,
|
private IUserSession $userSession,
|
||||||
private AdminSettingsService $settingsService,
|
private AdminSettingsService $settingsService,
|
||||||
@@ -228,4 +234,202 @@ class AdminController extends OCSController {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the repair seeds command to restore default forum data
|
||||||
|
*
|
||||||
|
* @return DataResponse<Http::STATUS_OK, array{success: bool, message: string}, array{}>
|
||||||
|
*
|
||||||
|
* 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<string> */
|
||||||
|
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<Http::STATUS_OK, array{roles: list<array<string, mixed>>}, 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<Http::STATUS_OK, array{success: bool, message: string}, array{}>
|
||||||
|
*
|
||||||
|
* 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<Http::STATUS_OK, array{success: bool, message: string}, array{}>
|
||||||
|
*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
32
lib/Sections/AdminSection.php
Normal file
32
lib/Sections/AdminSection.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\Forum\Sections;
|
||||||
|
|
||||||
|
use OCA\Forum\AppInfo;
|
||||||
|
use OCP\IL10N;
|
||||||
|
use OCP\IURLGenerator;
|
||||||
|
use OCP\Settings\IIconSection;
|
||||||
|
|
||||||
|
class AdminSection implements IIconSection {
|
||||||
|
public function __construct(
|
||||||
|
private IL10N $l,
|
||||||
|
private IURLGenerator $urlGenerator,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIcon(): string {
|
||||||
|
return $this->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
42
lib/Settings/AdminSettings.php
Normal file
42
lib/Settings/AdminSettings.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace OCA\Forum\Settings;
|
||||||
|
|
||||||
|
use OCA\Forum\AppInfo\Application;
|
||||||
|
use OCP\AppFramework\Http\TemplateResponse;
|
||||||
|
use OCP\IAppConfig;
|
||||||
|
use OCP\IL10N;
|
||||||
|
use OCP\Settings\ISettings;
|
||||||
|
|
||||||
|
class AdminSettings implements ISettings {
|
||||||
|
public function __construct(
|
||||||
|
private IAppConfig $config,
|
||||||
|
private IL10N $l,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return TemplateResponse
|
||||||
|
*/
|
||||||
|
public function getForm(): TemplateResponse {
|
||||||
|
return new TemplateResponse(Application::APP_ID, 'settings', [
|
||||||
|
'script' => 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
454
openapi.json
454
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": {
|
"/ocs/v2.php/apps/forum/api/bbcodes": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "bb_code-index",
|
"operationId": "bb_code-index",
|
||||||
|
|||||||
277
src/AdminSettings.vue
Normal file
277
src/AdminSettings.vue
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
<template>
|
||||||
|
<div id="forum-settings" class="section">
|
||||||
|
<h2>{{ strings.title }}</h2>
|
||||||
|
|
||||||
|
<NcSettingsSection :name="strings.repairSeedsHeader">
|
||||||
|
<NcNoteCard type="info">
|
||||||
|
{{ strings.repairSeedsHelp }}
|
||||||
|
</NcNoteCard>
|
||||||
|
|
||||||
|
<div class="settings-section-content">
|
||||||
|
<div class="repair-seeds-container">
|
||||||
|
<NcButton :disabled="repairSeedsLoading" @click="runRepairSeeds">
|
||||||
|
<template #icon>
|
||||||
|
<WrenchIcon v-if="!repairSeedsLoading" :size="20" />
|
||||||
|
<NcLoadingIcon v-else :size="20" />
|
||||||
|
</template>
|
||||||
|
{{ strings.runRepairSeeds }}
|
||||||
|
</NcButton>
|
||||||
|
|
||||||
|
<NcNoteCard v-if="repairSeedsResult" :type="repairSeedsSuccess ? 'success' : 'error'">
|
||||||
|
<pre class="repair-seeds-output">{{ repairSeedsResult }}</pre>
|
||||||
|
</NcNoteCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NcSettingsSection>
|
||||||
|
|
||||||
|
<NcSettingsSection :name="strings.userRolesHeader">
|
||||||
|
<NcNoteCard type="info">
|
||||||
|
{{ strings.userRolesHelp }}
|
||||||
|
</NcNoteCard>
|
||||||
|
|
||||||
|
<div class="settings-section-content">
|
||||||
|
<div class="user-role-form">
|
||||||
|
<div class="field-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="user-id">{{ strings.userIdLabel }}</label>
|
||||||
|
<NcTextField
|
||||||
|
id="user-id"
|
||||||
|
v-model="userId"
|
||||||
|
:placeholder="strings.userIdPlaceholder"
|
||||||
|
:disabled="assignRoleLoading"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="role-select">{{ strings.roleLabel }}</label>
|
||||||
|
<NcSelect
|
||||||
|
input-id="role-select"
|
||||||
|
v-model="selectedRole"
|
||||||
|
:options="roleOptions"
|
||||||
|
:placeholder="strings.rolePlaceholder"
|
||||||
|
:disabled="assignRoleLoading || rolesLoading"
|
||||||
|
:loading="rolesLoading"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="button-row">
|
||||||
|
<NcButton
|
||||||
|
variant="primary"
|
||||||
|
:disabled="!canAssignRole || assignRoleLoading"
|
||||||
|
@click="assignRole"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<PlusIcon v-if="!assignRoleLoading" :size="20" />
|
||||||
|
<NcLoadingIcon v-else :size="20" />
|
||||||
|
</template>
|
||||||
|
{{ strings.assignRole }}
|
||||||
|
</NcButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NcNoteCard v-if="assignRoleResult" :type="assignRoleSuccess ? 'success' : 'error'">
|
||||||
|
<p>{{ assignRoleResult }}</p>
|
||||||
|
</NcNoteCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NcSettingsSection>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
|
||||||
|
import NcSelect from '@nextcloud/vue/components/NcSelect'
|
||||||
|
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||||
|
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
|
||||||
|
import NcTextField from '@nextcloud/vue/components/NcTextField'
|
||||||
|
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
||||||
|
import WrenchIcon from '@icons/Wrench.vue'
|
||||||
|
import PlusIcon from '@icons/Plus.vue'
|
||||||
|
|
||||||
|
import { ocs } from '@/axios'
|
||||||
|
import { t } from '@nextcloud/l10n'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AdminSettings',
|
||||||
|
components: {
|
||||||
|
NcSettingsSection,
|
||||||
|
NcButton,
|
||||||
|
NcSelect,
|
||||||
|
NcNoteCard,
|
||||||
|
NcTextField,
|
||||||
|
NcLoadingIcon,
|
||||||
|
WrenchIcon,
|
||||||
|
PlusIcon,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// Repair seeds
|
||||||
|
repairSeedsLoading: false,
|
||||||
|
repairSeedsResult: null,
|
||||||
|
repairSeedsSuccess: false,
|
||||||
|
|
||||||
|
// User roles
|
||||||
|
rolesLoading: true,
|
||||||
|
roles: [],
|
||||||
|
userId: '',
|
||||||
|
selectedRole: null,
|
||||||
|
assignRoleLoading: false,
|
||||||
|
assignRoleResult: null,
|
||||||
|
assignRoleSuccess: false,
|
||||||
|
|
||||||
|
strings: {
|
||||||
|
title: t('forum', 'Forum'),
|
||||||
|
repairSeedsHeader: t('forum', 'Repair Seeds'),
|
||||||
|
repairSeedsHelp: t(
|
||||||
|
'forum',
|
||||||
|
'Run the repair seeds command to restore default forum data (roles, categories, permissions, BBCodes). This is safe to run multiple times as it will skip data that already exists.',
|
||||||
|
),
|
||||||
|
runRepairSeeds: t('forum', 'Run Repair Seeds'),
|
||||||
|
userRolesHeader: t('forum', 'User Roles'),
|
||||||
|
userRolesHelp: t(
|
||||||
|
'forum',
|
||||||
|
'Assign forum roles to users. This allows you to grant administrative or moderator privileges to specific users.',
|
||||||
|
),
|
||||||
|
userIdLabel: t('forum', 'User ID'),
|
||||||
|
userIdPlaceholder: t('forum', 'Enter user ID'),
|
||||||
|
roleLabel: t('forum', 'Role'),
|
||||||
|
rolePlaceholder: t('forum', 'Select a role'),
|
||||||
|
assignRole: t('forum', 'Assign Role'),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
roleOptions() {
|
||||||
|
return this.roles.map((role) => ({
|
||||||
|
id: role.id,
|
||||||
|
label: role.name,
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
canAssignRole() {
|
||||||
|
return this.userId.trim() !== '' && this.selectedRole !== null
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchRoles()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async fetchRoles() {
|
||||||
|
try {
|
||||||
|
this.rolesLoading = true
|
||||||
|
const resp = await ocs.get('/admin/roles')
|
||||||
|
this.roles = resp.data.roles
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to fetch roles', e)
|
||||||
|
} finally {
|
||||||
|
this.rolesLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async runRepairSeeds() {
|
||||||
|
try {
|
||||||
|
this.repairSeedsLoading = true
|
||||||
|
this.repairSeedsResult = null
|
||||||
|
const resp = await ocs.post('/admin/repair-seeds')
|
||||||
|
this.repairSeedsSuccess = resp.data.success
|
||||||
|
this.repairSeedsResult = resp.data.message
|
||||||
|
if (resp.data.success) {
|
||||||
|
await this.fetchRoles()
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to run repair seeds', e)
|
||||||
|
this.repairSeedsSuccess = false
|
||||||
|
this.repairSeedsResult =
|
||||||
|
e.response?.data?.message || t('forum', 'Failed to run repair seeds')
|
||||||
|
} finally {
|
||||||
|
this.repairSeedsLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async assignRole() {
|
||||||
|
if (!this.canAssignRole) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.assignRoleLoading = true
|
||||||
|
this.assignRoleResult = null
|
||||||
|
const resp = await ocs.post(
|
||||||
|
`/admin/users/${encodeURIComponent(this.userId.trim())}/roles`,
|
||||||
|
{
|
||||||
|
roleId: this.selectedRole.id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
this.assignRoleSuccess = resp.data.success
|
||||||
|
this.assignRoleResult = resp.data.message
|
||||||
|
if (resp.data.success) {
|
||||||
|
// Clear form on success
|
||||||
|
this.userId = ''
|
||||||
|
this.selectedRole = null
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to assign role', e)
|
||||||
|
this.assignRoleSuccess = false
|
||||||
|
this.assignRoleResult = e.response?.data?.message || t('forum', 'Failed to assign role')
|
||||||
|
} finally {
|
||||||
|
this.assignRoleLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
#forum-settings {
|
||||||
|
h2:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repair-seeds-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repair-seeds-output {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
margin: 0;
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-role-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
max-width: 600px;
|
||||||
|
|
||||||
|
.field-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
label {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
5
src/admin.ts
Normal file
5
src/admin.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import AdminSettings from './AdminSettings.vue'
|
||||||
|
import './style.scss'
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
|
||||||
|
createApp(AdminSettings).mount('#forum-settings')
|
||||||
12
templates/settings.php
Normal file
12
templates/settings.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use OCA\Forum\AppInfo\Application;
|
||||||
|
use OCP\Util;
|
||||||
|
|
||||||
|
/* @var array $_ */
|
||||||
|
$script = $_['script'];
|
||||||
|
$style = $_['style'];
|
||||||
|
Util::addScript(Application::APP_ID, Application::JS_DIR . "/$script");
|
||||||
|
Util::addStyle(Application::APP_ID, Application::CSS_DIR . "/$style");
|
||||||
|
?>
|
||||||
|
<div id="forum-settings"></div>
|
||||||
@@ -34,6 +34,7 @@ const nextcloudSharedList = [
|
|||||||
export default createAppConfig(
|
export default createAppConfig(
|
||||||
{
|
{
|
||||||
app: path.resolve(path.join('src', 'app.ts')),
|
app: path.resolve(path.join('src', 'app.ts')),
|
||||||
|
admin: path.resolve(path.join('src', 'admin.ts')),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
config: {
|
config: {
|
||||||
|
|||||||
Reference in New Issue
Block a user