Initial Version
This commit is contained in:
361
core/lib/Controllers/AuthenticationController.php
Normal file
361
core/lib/Controllers/AuthenticationController.php
Normal file
@@ -0,0 +1,361 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace KTXC\Controllers;
|
||||
|
||||
use KTXC\Http\Cookie;
|
||||
use KTXC\Http\Request\Request;
|
||||
use KTXC\Http\Response\JsonResponse;
|
||||
use KTXC\Http\Response\RedirectResponse;
|
||||
use KTXC\Security\Authentication\AuthenticationRequest;
|
||||
use KTXC\Security\Authentication\AuthenticationResponse;
|
||||
use KTXC\Security\AuthenticationManager;
|
||||
use KTXF\Controller\ControllerAbstract;
|
||||
use KTXF\Routing\Attributes\AnonymousRoute;
|
||||
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
||||
|
||||
/**
|
||||
* Authentication Controller
|
||||
*/
|
||||
class AuthenticationController extends ControllerAbstract
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AuthenticationManager $authManager
|
||||
) {}
|
||||
|
||||
// =========================================================================
|
||||
// Authentication Operations
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Start authentication session
|
||||
*/
|
||||
#[AnonymousRoute('/auth/start', name: 'auth.start', methods: ['GET'])]
|
||||
public function start(): JsonResponse
|
||||
{
|
||||
$request = AuthenticationRequest::start();
|
||||
$response = $this->authManager->handle($request);
|
||||
|
||||
return $this->buildJsonResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identify user for identity-first login flow
|
||||
*/
|
||||
#[AnonymousRoute('/auth/identify', name: 'auth.identify', methods: ['POST'])]
|
||||
public function identify(string $session, string $identity): JsonResponse
|
||||
{
|
||||
if (empty($session) || empty($identity)) {
|
||||
return new JsonResponse(
|
||||
['error' => 'Session and identity are required', 'error_code' => 'invalid_request'],
|
||||
JsonResponse::HTTP_BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
$request = AuthenticationRequest::identify($session, trim($identity));
|
||||
$response = $this->authManager->handle($request);
|
||||
|
||||
return $this->buildJsonResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a challenge for methods that require it (SMS, email, TOTP)
|
||||
*/
|
||||
#[AnonymousRoute('/auth/challenge', name: 'auth.challenge', methods: ['POST'])]
|
||||
public function challenge(string $session, string $method): JsonResponse
|
||||
{
|
||||
if (empty($session) || empty($method)) {
|
||||
return new JsonResponse(
|
||||
['error' => 'Session ID and method are required', 'error_code' => 'invalid_request'],
|
||||
JsonResponse::HTTP_BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
$request = AuthenticationRequest::challenge($session, $method);
|
||||
$response = $this->authManager->handle($request);
|
||||
|
||||
return $this->buildJsonResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a credential or challenge response
|
||||
*/
|
||||
#[AnonymousRoute('/auth/verify', name: 'auth.verify', methods: ['POST'])]
|
||||
public function verify(string $session, string $method, string $response): JsonResponse
|
||||
{
|
||||
if (empty($session) || empty($method)) {
|
||||
return new JsonResponse(
|
||||
['error' => 'Session ID and method are required', 'error_code' => 'invalid_request'],
|
||||
JsonResponse::HTTP_BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
$request = AuthenticationRequest::verify($session, $method, $response);
|
||||
$authResponse = $this->authManager->handle($request);
|
||||
|
||||
return $this->buildJsonResponse($authResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin redirect-based authentication (OIDC/SAML)
|
||||
*/
|
||||
#[AnonymousRoute('/auth/redirect', name: 'auth.redirect', methods: ['POST'])]
|
||||
public function redirect(Request $request): JsonResponse
|
||||
{
|
||||
$data = $this->getRequestData($request);
|
||||
|
||||
$sessionId = $data['session'] ?? '';
|
||||
$method = $data['method'] ?? '';
|
||||
$returnUrl = $data['return_url'] ?? '/';
|
||||
|
||||
if (empty($sessionId) || empty($method)) {
|
||||
return new JsonResponse(
|
||||
['error' => 'Session ID and method are required', 'error_code' => 'invalid_request'],
|
||||
JsonResponse::HTTP_BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
$scheme = $request->isSecure() ? 'https' : 'http';
|
||||
$host = $request->getHost();
|
||||
$callbackUrl = "{$scheme}://{$host}/auth/callback/{$method}";
|
||||
|
||||
$authRequest = AuthenticationRequest::redirect($sessionId, $method, $callbackUrl, $returnUrl);
|
||||
$response = $this->authManager->handle($authRequest);
|
||||
|
||||
return $this->buildJsonResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle callback from identity provider (OIDC/SAML)
|
||||
*/
|
||||
#[AnonymousRoute('/auth/callback/{provider}', name: 'auth.callback', methods: ['GET', 'POST'])]
|
||||
public function callback(Request $request, string $provider): JsonResponse|RedirectResponse
|
||||
{
|
||||
$params = $request->isMethod('POST')
|
||||
? $request->request->all()
|
||||
: $request->query->all();
|
||||
|
||||
$sessionId = $params['state'] ?? null;
|
||||
|
||||
if (!$sessionId) {
|
||||
return $this->redirectWithError('Missing state parameter');
|
||||
}
|
||||
|
||||
$authRequest = AuthenticationRequest::callback($sessionId, $provider, $params);
|
||||
$response = $this->authManager->handle($authRequest);
|
||||
|
||||
if ($response->isSuccess()) {
|
||||
$returnUrl = $response->returnUrl ?? '/';
|
||||
$httpResponse = new RedirectResponse($returnUrl);
|
||||
|
||||
if ($response->hasTokens()) {
|
||||
return $this->setTokenCookies($httpResponse, $response->tokens, $request->isSecure());
|
||||
}
|
||||
|
||||
return $httpResponse;
|
||||
}
|
||||
|
||||
if ($response->isPending()) {
|
||||
return new RedirectResponse('/login/mfa?session=' . urlencode($response->sessionId));
|
||||
}
|
||||
|
||||
return $this->redirectWithError($response->errorMessage ?? 'Authentication failed');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current session status
|
||||
*/
|
||||
#[AnonymousRoute('/auth/status', name: 'auth.status', methods: ['GET'])]
|
||||
public function status(Request $request): JsonResponse
|
||||
{
|
||||
$sessionId = $request->query->get('session', '');
|
||||
|
||||
if (empty($sessionId)) {
|
||||
return new JsonResponse(
|
||||
['error' => 'Session ID is required', 'error_code' => 'invalid_request'],
|
||||
JsonResponse::HTTP_BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
$authRequest = AuthenticationRequest::status($sessionId);
|
||||
$response = $this->authManager->handle($authRequest);
|
||||
|
||||
return $this->buildJsonResponse($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel authentication session
|
||||
*/
|
||||
#[AnonymousRoute('/auth/session', name: 'auth.session.cancel', methods: ['DELETE'])]
|
||||
public function cancel(Request $request): JsonResponse
|
||||
{
|
||||
$sessionId = $request->query->get('session', '');
|
||||
|
||||
$authRequest = AuthenticationRequest::cancel($sessionId);
|
||||
$this->authManager->handle($authRequest);
|
||||
|
||||
return new JsonResponse(['status' => 'cancelled', 'message' => 'Session cancelled']);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Token Operations
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Refresh access token
|
||||
*/
|
||||
#[AnonymousRoute('/auth/refresh', name: 'auth.refresh', methods: ['POST'])]
|
||||
public function refresh(Request $request): JsonResponse
|
||||
{
|
||||
$refreshToken = $request->cookies->get('refreshToken');
|
||||
|
||||
if (!$refreshToken) {
|
||||
return new JsonResponse(
|
||||
['error' => 'Refresh token required', 'error_code' => 'missing_token'],
|
||||
JsonResponse::HTTP_UNAUTHORIZED
|
||||
);
|
||||
}
|
||||
|
||||
$authRequest = AuthenticationRequest::refresh($refreshToken);
|
||||
$response = $this->authManager->handle($authRequest);
|
||||
|
||||
if ($response->isFailed()) {
|
||||
$httpResponse = new JsonResponse($response->toArray(), $response->httpStatus);
|
||||
return $this->clearTokenCookies($httpResponse);
|
||||
}
|
||||
|
||||
$httpResponse = new JsonResponse(['status' => 'success', 'message' => 'Token refreshed']);
|
||||
|
||||
if ($response->tokens && isset($response->tokens['access'])) {
|
||||
$httpResponse->headers->setCookie(
|
||||
Cookie::create('accessToken')
|
||||
->withValue($response->tokens['access'])
|
||||
->withExpires(time() + 900)
|
||||
->withPath('/')
|
||||
->withSecure($request->isSecure())
|
||||
->withHttpOnly(true)
|
||||
->withSameSite(Cookie::SAMESITE_STRICT)
|
||||
);
|
||||
}
|
||||
|
||||
return $httpResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout current device
|
||||
*/
|
||||
#[AuthenticatedRoute('/auth/logout', name: 'auth.logout', methods: ['POST'])]
|
||||
public function logout(Request $request): JsonResponse
|
||||
{
|
||||
$token = $request->cookies->get('accessToken');
|
||||
|
||||
$authRequest = AuthenticationRequest::logout($token, false);
|
||||
$this->authManager->handle($authRequest);
|
||||
|
||||
$response = new JsonResponse(['status' => 'success', 'message' => 'Logged out successfully']);
|
||||
return $this->clearTokenCookies($response);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout all devices
|
||||
*/
|
||||
#[AuthenticatedRoute('/auth/logout-all', name: 'auth.logout.all', methods: ['POST'])]
|
||||
public function logoutAll(Request $request): JsonResponse
|
||||
{
|
||||
$token = $request->cookies->get('accessToken');
|
||||
|
||||
$authRequest = AuthenticationRequest::logout($token, true);
|
||||
$this->authManager->handle($authRequest);
|
||||
|
||||
$response = new JsonResponse(['status' => 'success', 'message' => 'Logged out from all devices']);
|
||||
return $this->clearTokenCookies($response);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Response Helpers
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Build JSON response from AuthenticationResponse
|
||||
*/
|
||||
private function buildJsonResponse(AuthenticationResponse $response): JsonResponse
|
||||
{
|
||||
$httpResponse = new JsonResponse($response->toArray(), $response->httpStatus);
|
||||
|
||||
// Set token cookies if present
|
||||
if ($response->hasTokens()) {
|
||||
return $this->setTokenCookies($httpResponse, $response->tokens, true);
|
||||
}
|
||||
|
||||
return $httpResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set authentication token cookies
|
||||
*/
|
||||
private function setTokenCookies(JsonResponse|RedirectResponse $response, array $tokens, bool $secure = true): JsonResponse|RedirectResponse
|
||||
{
|
||||
if (isset($tokens['access'])) {
|
||||
$response->headers->setCookie(
|
||||
Cookie::create('accessToken')
|
||||
->withValue($tokens['access'])
|
||||
->withExpires(time() + 900)
|
||||
->withPath('/')
|
||||
->withSecure($secure)
|
||||
->withHttpOnly(true)
|
||||
->withSameSite(Cookie::SAMESITE_STRICT)
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($tokens['refresh'])) {
|
||||
$response->headers->setCookie(
|
||||
Cookie::create('refreshToken')
|
||||
->withValue($tokens['refresh'])
|
||||
->withExpires(time() + 604800)
|
||||
->withPath('/auth/refresh')
|
||||
->withSecure($secure)
|
||||
->withHttpOnly(true)
|
||||
->withSameSite(Cookie::SAMESITE_STRICT)
|
||||
);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear authentication token cookies
|
||||
*/
|
||||
private function clearTokenCookies(JsonResponse $response): JsonResponse
|
||||
{
|
||||
$response->headers->clearCookie('accessToken', '/');
|
||||
$response->headers->clearCookie('refreshToken', '/auth/refresh');
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect with error message
|
||||
*/
|
||||
private function redirectWithError(string $error): RedirectResponse
|
||||
{
|
||||
return new RedirectResponse('/login?error=' . urlencode($error));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get request data from JSON body or form data
|
||||
*/
|
||||
private function getRequestData(Request $request): array
|
||||
{
|
||||
$contentType = $request->headers->get('Content-Type', '');
|
||||
|
||||
if (str_contains($contentType, 'application/json')) {
|
||||
try {
|
||||
return $request->toArray();
|
||||
} catch (\Throwable) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
return $request->request->all();
|
||||
}
|
||||
}
|
||||
144
core/lib/Controllers/DefaultController.php
Normal file
144
core/lib/Controllers/DefaultController.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace KTXC\Controllers;
|
||||
|
||||
use DI\Attribute\Inject;
|
||||
use KTXC\Http\Response\Response;
|
||||
use KTXC\Http\Response\FileResponse;
|
||||
use KTXC\Http\Response\JsonResponse;
|
||||
use KTXC\Http\Response\RedirectResponse;
|
||||
use KTXF\Controller\ControllerAbstract;
|
||||
use KTXF\Routing\Attributes\AnonymousRoute;
|
||||
use KTXC\Service\SecurityService;
|
||||
use KTXC\SessionIdentity;
|
||||
use KTXC\Http\Request\Request;
|
||||
|
||||
class DefaultController extends ControllerAbstract
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SecurityService $securityService,
|
||||
private readonly SessionIdentity $identity,
|
||||
#[Inject('rootDir')] private readonly string $rootDir,
|
||||
) {}
|
||||
|
||||
#[AnonymousRoute('/', name: 'root', methods: ['GET'])]
|
||||
public function home(Request $request): Response
|
||||
{
|
||||
// If an authenticated identity is available, serve the private app
|
||||
if ($this->identity->identifier()) {
|
||||
return new FileResponse(
|
||||
$this->rootDir . '/public/private.html',
|
||||
Response::HTTP_OK,
|
||||
['Content-Type' => 'text/html']
|
||||
);
|
||||
}
|
||||
|
||||
// User is not authenticated - serve the public app
|
||||
// If there's an accessToken cookie present but invalid, clear it
|
||||
$response = new FileResponse(
|
||||
$this->rootDir . '/public/public.html',
|
||||
Response::HTTP_OK,
|
||||
['Content-Type' => 'text/html']
|
||||
);
|
||||
|
||||
// Clear any stale auth cookies since the user is not authenticated
|
||||
if ($request->cookies->has('accessToken')) {
|
||||
$response->headers->clearCookie('accessToken', '/');
|
||||
}
|
||||
if ($request->cookies->has('refreshToken')) {
|
||||
$response->headers->clearCookie('refreshToken', '/security/refresh');
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
#[AnonymousRoute('/login', name: 'login', methods: ['GET'])]
|
||||
public function login(): Response
|
||||
{
|
||||
return new FileResponse(
|
||||
$this->rootDir . '/public/public.html',
|
||||
Response::HTTP_OK,
|
||||
['Content-Type' => 'text/html']
|
||||
);
|
||||
}
|
||||
|
||||
#[AnonymousRoute('/logout', name: 'logout_get', methods: ['GET'])]
|
||||
public function logoutGet(Request $request): Response
|
||||
{
|
||||
// Blacklist the current access token if present
|
||||
$accessToken = $request->cookies->get('accessToken');
|
||||
if ($accessToken) {
|
||||
$claims = $this->securityService->extractTokenClaims($accessToken);
|
||||
if ($claims && isset($claims['jti'])) {
|
||||
$this->securityService->logout($claims['jti'], $claims['exp'] ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
$response = new RedirectResponse(
|
||||
'/login',
|
||||
Response::HTTP_SEE_OTHER
|
||||
);
|
||||
|
||||
// Clear both authentication cookies
|
||||
$response->headers->clearCookie('accessToken', '/');
|
||||
$response->headers->clearCookie('refreshToken', '/security/refresh');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
#[AnonymousRoute('/logout', name: 'logout_post', methods: ['POST'])]
|
||||
public function logoutPost(Request $request): Response
|
||||
{
|
||||
// Blacklist the current access token if present
|
||||
$accessToken = $request->cookies->get('accessToken');
|
||||
if ($accessToken) {
|
||||
$claims = $this->securityService->extractTokenClaims($accessToken);
|
||||
if ($claims && isset($claims['jti'])) {
|
||||
$this->securityService->logout($claims['jti'], $claims['exp'] ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
$response = new JsonResponse(['message' => 'Logged out successfully']);
|
||||
|
||||
// Clear both authentication cookies
|
||||
$response->headers->clearCookie('accessToken', '/');
|
||||
$response->headers->clearCookie('refreshToken', '/security/refresh');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Catch-all route for SPA routing.
|
||||
* Serves the appropriate HTML based on authentication status,
|
||||
* allowing client-side routing to handle the actual path.
|
||||
*/
|
||||
#[AnonymousRoute('/{path}', name: 'spa_catchall', methods: ['GET'])]
|
||||
public function catchAll(Request $request, string $path = ''): Response
|
||||
{
|
||||
// If an authenticated identity is available, serve the private app
|
||||
if ($this->identity->identifier()) {
|
||||
return new FileResponse(
|
||||
$this->rootDir . '/public/private.html',
|
||||
Response::HTTP_OK,
|
||||
['Content-Type' => 'text/html']
|
||||
);
|
||||
}
|
||||
|
||||
// User is not authenticated - serve the public app
|
||||
$response = new FileResponse(
|
||||
$this->rootDir . '/public/public.html',
|
||||
Response::HTTP_OK,
|
||||
['Content-Type' => 'text/html']
|
||||
);
|
||||
|
||||
// Clear any stale auth cookies since the user is not authenticated
|
||||
if ($request->cookies->has('accessToken')) {
|
||||
$response->headers->clearCookie('accessToken', '/');
|
||||
}
|
||||
if ($request->cookies->has('refreshToken')) {
|
||||
$response->headers->clearCookie('refreshToken', '/security/refresh');
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
95
core/lib/Controllers/InitController.php
Normal file
95
core/lib/Controllers/InitController.php
Normal file
@@ -0,0 +1,95 @@
|
||||
<?php
|
||||
|
||||
namespace KTXC\Controllers;
|
||||
|
||||
use KTXC\Http\Response\JsonResponse;
|
||||
use KTXC\Module\ModuleManager;
|
||||
use KTXC\Security\Authorization\PermissionChecker;
|
||||
use KTXC\Service\UserAccountsService;
|
||||
use KTXC\SessionIdentity;
|
||||
use KTXF\Controller\ControllerAbstract;
|
||||
use KTXC\SessionTenant;
|
||||
use KTXF\Module\ModuleBrowserInterface;
|
||||
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
||||
|
||||
class InitController extends ControllerAbstract
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SessionTenant $tenant,
|
||||
private readonly SessionIdentity $userIdentity,
|
||||
private readonly ModuleManager $moduleManager,
|
||||
private readonly UserAccountsService $userService,
|
||||
private readonly PermissionChecker $permissionChecker,
|
||||
) {}
|
||||
|
||||
#[AuthenticatedRoute('/init', name: 'init', methods: ['GET'])]
|
||||
public function index(): JsonResponse {
|
||||
|
||||
$configuration = [];
|
||||
|
||||
// modules - filter by permissions
|
||||
$configuration['modules'] = [];
|
||||
foreach ($this->moduleManager->list() as $module) {
|
||||
// Check if user has permission to view this module
|
||||
// Allow access if user has: {module_handle}, {module_handle}.*, or * permission
|
||||
$handle = $module->handle();
|
||||
if (!$this->hasModuleViewPermission($handle)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$integrations = $module->registerBI();
|
||||
if ($integrations !== null) {
|
||||
$configuration['modules'][$handle] = $integrations;
|
||||
}
|
||||
}
|
||||
|
||||
// tenant
|
||||
$configuration['tenant'] = [
|
||||
'id' => $this->tenant->identifier(),
|
||||
'domain' => $this->tenant->domain(),
|
||||
'label' => $this->tenant->label(),
|
||||
];
|
||||
|
||||
// user
|
||||
$configuration['user'] = [
|
||||
'auth' => [
|
||||
'identifier' => $this->userIdentity->identifier(),
|
||||
'identity' => $this->userIdentity->identity()->getIdentity(),
|
||||
'label' => $this->userIdentity->label(),
|
||||
'roles' => $this->userIdentity->identity()->getRoles(),
|
||||
'permissions' => $this->userIdentity->identity()->getPermissions(),
|
||||
],
|
||||
'profile' => $this->userService->getEditableFields($this->userIdentity->identifier()),
|
||||
'settings' => $this->userService->fetchSettings(),
|
||||
];
|
||||
|
||||
return new JsonResponse($configuration);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user has permission to view a module
|
||||
*
|
||||
* Checks for the following permissions (in order):
|
||||
* 1. {module_handle} - module access permission
|
||||
* 2. {module_handle}.* - wildcard for the module
|
||||
* 3. * - global wildcard
|
||||
*
|
||||
* @param string $moduleHandle The module handle to check
|
||||
* @return bool
|
||||
*/
|
||||
private function hasModuleViewPermission(string $moduleHandle): bool
|
||||
{
|
||||
// Core module is always accessible to authenticated users
|
||||
if ($moduleHandle === 'core') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for specific module permission or wildcard permissions
|
||||
return $this->permissionChecker->canAny([
|
||||
"{$moduleHandle}",
|
||||
"{$moduleHandle}.*",
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
68
core/lib/Controllers/ModuleController.php
Normal file
68
core/lib/Controllers/ModuleController.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace KTXC\Controllers;
|
||||
|
||||
use KTXC\Module\ModuleManager;
|
||||
use KTXF\Controller\ControllerAbstract;
|
||||
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
||||
use KTXC\Http\Response\JsonResponse;
|
||||
|
||||
class ModuleController extends ControllerAbstract
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ModuleManager $moduleManager
|
||||
) { }
|
||||
|
||||
#[AuthenticatedRoute(
|
||||
'/modules/list',
|
||||
name: 'modules.index',
|
||||
methods: ['GET'],
|
||||
permissions: ['module_manager.modules.view']
|
||||
)]
|
||||
public function index(): JsonResponse
|
||||
{
|
||||
$modules = $this->moduleManager->list(false);
|
||||
|
||||
return new JsonResponse(['modules' => $modules]);
|
||||
}
|
||||
|
||||
#[AuthenticatedRoute(
|
||||
'/modules/manage',
|
||||
name: 'modules.manage',
|
||||
methods: ['POST'],
|
||||
permissions: ['module_manager.modules.manage']
|
||||
)]
|
||||
public function manage(string $handle, string $action): JsonResponse
|
||||
{
|
||||
// Verify module exists
|
||||
$moduleInstance = $this->moduleManager->moduleInstance($handle, null);
|
||||
if (!$moduleInstance) {
|
||||
return new JsonResponse(['error' => 'Module "' . $handle . '" not found.'], 404);
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'install':
|
||||
$this->moduleManager->install($handle);
|
||||
return new JsonResponse(['message' => 'Module "' . $handle . '" installed successfully.']);
|
||||
|
||||
case 'uninstall':
|
||||
$this->moduleManager->uninstall($handle);
|
||||
return new JsonResponse(['message' => 'Module "' . $handle . '" uninstalled successfully.']);
|
||||
|
||||
case 'enable':
|
||||
$this->moduleManager->enable($handle);
|
||||
return new JsonResponse(['message' => 'Module "' . $handle . '" enabled successfully.']);
|
||||
|
||||
case 'disable':
|
||||
$this->moduleManager->disable($handle);
|
||||
return new JsonResponse(['message' => 'Module "' . $handle . '" disabled successfully.']);
|
||||
|
||||
case 'upgrade':
|
||||
$this->moduleManager->upgrade($handle);
|
||||
return new JsonResponse(['message' => 'Module "' . $handle . '" upgraded successfully.']);
|
||||
|
||||
default:
|
||||
return new JsonResponse(['error' => 'Invalid action.'], 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
251
core/lib/Controllers/UserAccountsController.php
Normal file
251
core/lib/Controllers/UserAccountsController.php
Normal file
@@ -0,0 +1,251 @@
|
||||
<?php
|
||||
|
||||
namespace KTXC\Controllers;
|
||||
|
||||
use KTXC\Http\Response\JsonResponse;
|
||||
use KTXC\Service\UserAccountsService;
|
||||
use KTXC\SessionIdentity;
|
||||
use KTXC\SessionTenant;
|
||||
use KTXF\Controller\ControllerAbstract;
|
||||
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* User Accounts Controller
|
||||
* Core administrative user management operations
|
||||
*/
|
||||
class UserAccountsController extends ControllerAbstract
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SessionTenant $tenantIdentity,
|
||||
private readonly SessionIdentity $userIdentity,
|
||||
private readonly UserAccountsService $userService,
|
||||
private readonly LoggerInterface $logger
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Main versioned endpoint for user management
|
||||
*/
|
||||
#[AuthenticatedRoute('/user/accounts/v1', name: 'user.accounts.v1', methods: ['POST'])]
|
||||
public function index(int $version, string $transaction, string $operation, array $data = []): JsonResponse
|
||||
{
|
||||
try {
|
||||
// Check admin permission
|
||||
if (!$this->userIdentity->hasPermission('user.admin')) {
|
||||
return new JsonResponse([
|
||||
'status' => 'error',
|
||||
'data' => ['code' => 403, 'message' => 'Insufficient permissions']
|
||||
], JsonResponse::HTTP_FORBIDDEN);
|
||||
}
|
||||
|
||||
$result = $this->process($operation, $data);
|
||||
|
||||
return new JsonResponse([
|
||||
'version' => $version,
|
||||
'transaction' => $transaction,
|
||||
'operation' => $operation,
|
||||
'status' => 'success',
|
||||
'data' => $result,
|
||||
], JsonResponse::HTTP_OK);
|
||||
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return new JsonResponse([
|
||||
'version' => $version,
|
||||
'transaction' => $transaction,
|
||||
'operation' => $operation,
|
||||
'status' => 'error',
|
||||
'data' => ['code' => 400, 'message' => $e->getMessage()]
|
||||
], JsonResponse::HTTP_BAD_REQUEST);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error('User manager operation failed', [
|
||||
'operation' => $operation,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
|
||||
return new JsonResponse([
|
||||
'version' => $version,
|
||||
'transaction' => $transaction,
|
||||
'operation' => $operation,
|
||||
'status' => 'error',
|
||||
'data' => ['code' => $e->getCode(), 'message' => $e->getMessage()]
|
||||
], JsonResponse::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process operation
|
||||
*/
|
||||
private function process(string $operation, array $data): mixed
|
||||
{
|
||||
return match ($operation) {
|
||||
'user.list' => $this->userList($data),
|
||||
'user.fetch' => $this->userFetch($data),
|
||||
'user.create' => $this->userCreate($data),
|
||||
'user.update' => $this->userUpdate($data),
|
||||
'user.delete' => $this->userDelete($data),
|
||||
'user.provider.unlink' => $this->userProviderUnlink($data),
|
||||
default => throw new \InvalidArgumentException("Invalid operation: {$operation}"),
|
||||
};
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// User Operations
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* List all users for tenant
|
||||
*/
|
||||
private function userList(array $data): array
|
||||
{
|
||||
return $this->userService->listUsers($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch single user by UID
|
||||
*/
|
||||
private function userFetch(array $data): array
|
||||
{
|
||||
$uid = $data['uid'] ?? throw new \InvalidArgumentException('User ID required');
|
||||
|
||||
$user = $this->userService->fetchByIdentifier($uid);
|
||||
if (!$user) {
|
||||
throw new \InvalidArgumentException('User not found');
|
||||
}
|
||||
|
||||
// Get editable fields for profile
|
||||
$editableFields = $this->userService->getEditableFields($uid);
|
||||
$user['profile_editable'] = $editableFields;
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new user
|
||||
*/
|
||||
private function userCreate(array $data): array
|
||||
{
|
||||
if (!$this->userIdentity->hasPermission('user.create')) {
|
||||
throw new \InvalidArgumentException('Insufficient permissions to create users');
|
||||
}
|
||||
|
||||
$userData = [
|
||||
'identity' => $data['identity'] ?? throw new \InvalidArgumentException('Identity required'),
|
||||
'label' => $data['label'] ?? $data['identity'],
|
||||
'enabled' => $data['enabled'] ?? true,
|
||||
'roles' => $data['roles'] ?? [],
|
||||
'profile' => $data['profile'] ?? [],
|
||||
'settings' => [],
|
||||
'provider' => null,
|
||||
'provider_subject' => null,
|
||||
'provider_managed_fields' => []
|
||||
];
|
||||
|
||||
$this->logger->info('Creating user', [
|
||||
'tenant' => $this->tenantIdentity->identifier(),
|
||||
'identity' => $userData['identity'],
|
||||
'actor' => $this->userIdentity->identifier()
|
||||
]);
|
||||
|
||||
return $this->userService->createUser($userData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update existing user
|
||||
*/
|
||||
private function userUpdate(array $data): bool
|
||||
{
|
||||
if (!$this->userIdentity->hasPermission('user.update')) {
|
||||
throw new \InvalidArgumentException('Insufficient permissions to update users');
|
||||
}
|
||||
|
||||
$uid = $data['uid'] ?? throw new \InvalidArgumentException('User ID required');
|
||||
|
||||
// Build updates (exclude sensitive fields)
|
||||
$updates = [];
|
||||
$allowedFields = ['label', 'enabled', 'roles', 'profile'];
|
||||
|
||||
foreach ($allowedFields as $field) {
|
||||
if (isset($data[$field])) {
|
||||
$updates[$field] = $data[$field];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($updates)) {
|
||||
throw new \InvalidArgumentException('No valid fields to update');
|
||||
}
|
||||
|
||||
// Special handling for profile updates (respect managed fields)
|
||||
if (isset($updates['profile'])) {
|
||||
$user = $this->userService->fetchByIdentifier($uid);
|
||||
$managedFields = $user['provider_managed_fields'] ?? [];
|
||||
|
||||
foreach ($managedFields as $field) {
|
||||
unset($updates['profile'][$field]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->logger->info('Updating user', [
|
||||
'tenant' => $this->tenantIdentity->identifier(),
|
||||
'uid' => $uid,
|
||||
'actor' => $this->userIdentity->identifier()
|
||||
]);
|
||||
|
||||
return $this->userService->updateUser($uid, $updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete user
|
||||
*/
|
||||
private function userDelete(array $data): bool
|
||||
{
|
||||
if (!$this->userIdentity->hasPermission('user.delete')) {
|
||||
throw new \InvalidArgumentException('Insufficient permissions to delete users');
|
||||
}
|
||||
|
||||
$uid = $data['uid'] ?? throw new \InvalidArgumentException('User ID required');
|
||||
|
||||
// Prevent self-deletion
|
||||
if ($uid === $this->userIdentity->identifier()) {
|
||||
throw new \InvalidArgumentException('Cannot delete your own account');
|
||||
}
|
||||
|
||||
$this->logger->info('Deleting user', [
|
||||
'tenant' => $this->tenantIdentity->identifier(),
|
||||
'uid' => $uid,
|
||||
'actor' => $this->userIdentity->identifier()
|
||||
]);
|
||||
|
||||
return $this->userService->deleteUser($uid);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Security Operations
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* Unlink external provider
|
||||
*/
|
||||
private function userProviderUnlink(array $data): bool
|
||||
{
|
||||
if (!$this->userIdentity->hasPermission('user.admin')) {
|
||||
throw new \InvalidArgumentException('Insufficient permissions');
|
||||
}
|
||||
|
||||
$uid = $data['uid'] ?? throw new \InvalidArgumentException('User ID required');
|
||||
|
||||
$updates = [
|
||||
'provider' => null,
|
||||
'provider_subject' => null,
|
||||
'provider_managed_fields' => []
|
||||
];
|
||||
|
||||
$this->logger->info('Unlinking provider', [
|
||||
'tenant' => $this->tenantIdentity->identifier(),
|
||||
'uid' => $uid,
|
||||
'actor' => $this->userIdentity->identifier()
|
||||
]);
|
||||
|
||||
return $this->userService->updateUser($uid, $updates);
|
||||
}
|
||||
}
|
||||
76
core/lib/Controllers/UserProfileController.php
Normal file
76
core/lib/Controllers/UserProfileController.php
Normal file
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace KTXC\Controllers;
|
||||
|
||||
use KTXC\Http\Response\JsonResponse;
|
||||
use KTXC\Service\UserAccountsService;
|
||||
use KTXC\SessionIdentity;
|
||||
use KTXC\SessionTenant;
|
||||
use KTXF\Controller\ControllerAbstract;
|
||||
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
||||
|
||||
class UserProfileController extends ControllerAbstract
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SessionTenant $tenantIdentity,
|
||||
private readonly SessionIdentity $userIdentity,
|
||||
private readonly UserAccountsService $userService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve user profile
|
||||
*
|
||||
* @return JsonResponse Profile data with editability metadata
|
||||
*/
|
||||
#[AuthenticatedRoute(
|
||||
'/user/profile',
|
||||
name: 'user.profile.read',
|
||||
methods: ['GET'],
|
||||
permissions: ['user.profile.read']
|
||||
)]
|
||||
public function read(): JsonResponse
|
||||
{
|
||||
$userId = $this->userIdentity->identifier();
|
||||
|
||||
// Get profile with editability metadata
|
||||
$profile = $this->userService->getEditableFields($userId);
|
||||
|
||||
return new JsonResponse($profile, JsonResponse::HTTP_OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user profile fields
|
||||
* Only editable fields can be updated. Provider-managed fields are automatically filtered out.
|
||||
*
|
||||
* @param array $data Key-value pairs of profile fields to update
|
||||
*
|
||||
* @example request body:
|
||||
* {
|
||||
* "data": {
|
||||
* "name_given": "John",
|
||||
* "name_family": "Doe",
|
||||
* "phone": "+1234567890"
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @return JsonResponse Updated profile data
|
||||
*/
|
||||
#[AuthenticatedRoute(
|
||||
'/user/profile',
|
||||
name: 'user.profile.update',
|
||||
methods: ['PUT', 'PATCH'],
|
||||
permissions: ['user.profile.update']
|
||||
)]
|
||||
public function update(array $data): JsonResponse
|
||||
{
|
||||
$userId = $this->userIdentity->identifier();
|
||||
|
||||
// storeProfile automatically filters out provider-managed fields
|
||||
$this->userService->storeProfile($userId, $data);
|
||||
|
||||
// Return updated profile with metadata
|
||||
$updatedProfile = $this->userService->getEditableFields($userId);
|
||||
|
||||
return new JsonResponse($updatedProfile, JsonResponse::HTTP_OK);
|
||||
}
|
||||
}
|
||||
201
core/lib/Controllers/UserRolesController.php
Normal file
201
core/lib/Controllers/UserRolesController.php
Normal file
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
namespace KTXC\Controllers;
|
||||
|
||||
use KTXC\Http\Response\JsonResponse;
|
||||
use KTXC\SessionIdentity;
|
||||
use KTXC\SessionTenant;
|
||||
use KTXC\Service\UserRolesService;
|
||||
use KTXF\Controller\ControllerAbstract;
|
||||
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* User Roles Controller
|
||||
* Core administrative role management operations
|
||||
*/
|
||||
class UserRolesController extends ControllerAbstract
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SessionTenant $tenantIdentity,
|
||||
private readonly SessionIdentity $userIdentity,
|
||||
private readonly UserRolesService $roleService,
|
||||
private readonly LoggerInterface $logger
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Main versioned endpoint for role management
|
||||
*/
|
||||
#[AuthenticatedRoute('/user/roles/v1', name: 'user.roles.v1', methods: ['POST'])]
|
||||
public function index(int $version, string $transaction, string $operation, array $data = []): JsonResponse
|
||||
{
|
||||
try {
|
||||
// Check role admin permission
|
||||
if (!$this->userIdentity->hasPermission('role.admin')) {
|
||||
return new JsonResponse([
|
||||
'status' => 'error',
|
||||
'data' => ['code' => 403, 'message' => 'Insufficient permissions']
|
||||
], JsonResponse::HTTP_FORBIDDEN);
|
||||
}
|
||||
|
||||
$result = $this->process($operation, $data);
|
||||
|
||||
return new JsonResponse([
|
||||
'version' => $version,
|
||||
'transaction' => $transaction,
|
||||
'operation' => $operation,
|
||||
'status' => 'success',
|
||||
'data' => $result,
|
||||
], JsonResponse::HTTP_OK);
|
||||
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$this->logger->error('Role manager validation error', [
|
||||
'operation' => $operation,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return new JsonResponse([
|
||||
'version' => $version,
|
||||
'transaction' => $transaction,
|
||||
'operation' => $operation,
|
||||
'status' => 'error',
|
||||
'data' => ['code' => 400, 'message' => $e->getMessage()]
|
||||
], JsonResponse::HTTP_BAD_REQUEST);
|
||||
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error('Role manager operation failed', [
|
||||
'operation' => $operation,
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString()
|
||||
]);
|
||||
|
||||
return new JsonResponse([
|
||||
'version' => $version,
|
||||
'transaction' => $transaction,
|
||||
'operation' => $operation,
|
||||
'status' => 'error',
|
||||
'data' => ['code' => $e->getCode(), 'message' => $e->getMessage()]
|
||||
], JsonResponse::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process operation
|
||||
*/
|
||||
private function process(string $operation, array $data): mixed
|
||||
{
|
||||
return match ($operation) {
|
||||
'role.list' => $this->roleList($data),
|
||||
'role.fetch' => $this->roleFetch($data),
|
||||
'role.create' => $this->roleCreate($data),
|
||||
'role.update' => $this->roleUpdate($data),
|
||||
'role.delete' => $this->roleDelete($data),
|
||||
'permissions.list' => $this->permissionsList($data),
|
||||
default => throw new \InvalidArgumentException("Invalid operation: {$operation}"),
|
||||
};
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Role Operations
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* List all roles
|
||||
*/
|
||||
private function roleList(array $data): array
|
||||
{
|
||||
$roles = $this->roleService->listRoles();
|
||||
|
||||
// Add user count to each role
|
||||
foreach ($roles as &$role) {
|
||||
$role['user_count'] = $this->roleService->getRoleUserCount($role['rid']);
|
||||
}
|
||||
|
||||
return $roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch single role
|
||||
*/
|
||||
private function roleFetch(array $data): array
|
||||
{
|
||||
$rid = $data['rid'] ?? throw new \InvalidArgumentException('Role ID required');
|
||||
|
||||
$role = $this->roleService->getRole($rid);
|
||||
if (!$role) {
|
||||
throw new \InvalidArgumentException('Role not found');
|
||||
}
|
||||
|
||||
$role['user_count'] = $this->roleService->getRoleUserCount($rid);
|
||||
|
||||
return $role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create new role
|
||||
*/
|
||||
private function roleCreate(array $data): array
|
||||
{
|
||||
if (!$this->userIdentity->hasPermission('role.manage')) {
|
||||
throw new \InvalidArgumentException('Insufficient permissions to create roles');
|
||||
}
|
||||
|
||||
$roleData = [
|
||||
'label' => $data['label'] ?? throw new \InvalidArgumentException('Role label required'),
|
||||
'description' => $data['description'] ?? '',
|
||||
'permissions' => $data['permissions'] ?? []
|
||||
];
|
||||
|
||||
return $this->roleService->createRole($roleData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update existing role
|
||||
*/
|
||||
private function roleUpdate(array $data): bool
|
||||
{
|
||||
if (!$this->userIdentity->hasPermission('role.manage')) {
|
||||
throw new \InvalidArgumentException('Insufficient permissions to update roles');
|
||||
}
|
||||
|
||||
$rid = $data['rid'] ?? throw new \InvalidArgumentException('Role ID required');
|
||||
|
||||
$updates = [];
|
||||
$allowedFields = ['label', 'description', 'permissions'];
|
||||
|
||||
foreach ($allowedFields as $field) {
|
||||
if (isset($data[$field])) {
|
||||
$updates[$field] = $data[$field];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($updates)) {
|
||||
throw new \InvalidArgumentException('No valid fields to update');
|
||||
}
|
||||
|
||||
return $this->roleService->updateRole($rid, $updates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete role
|
||||
*/
|
||||
private function roleDelete(array $data): bool
|
||||
{
|
||||
if (!$this->userIdentity->hasPermission('role.manage')) {
|
||||
throw new \InvalidArgumentException('Insufficient permissions to delete roles');
|
||||
}
|
||||
|
||||
$rid = $data['rid'] ?? throw new \InvalidArgumentException('Role ID required');
|
||||
|
||||
return $this->roleService->deleteRole($rid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available permissions
|
||||
*/
|
||||
private function permissionsList(array $data): array
|
||||
{
|
||||
return $this->roleService->availablePermissions();
|
||||
}
|
||||
}
|
||||
71
core/lib/Controllers/UserSettingsController.php
Normal file
71
core/lib/Controllers/UserSettingsController.php
Normal file
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace KTXC\Controllers;
|
||||
|
||||
use KTXC\Http\Response\JsonResponse;
|
||||
use KTXC\Service\UserAccountsService;
|
||||
use KTXC\SessionIdentity;
|
||||
use KTXC\SessionTenant;
|
||||
use KTXF\Controller\ControllerAbstract;
|
||||
use KTXF\Routing\Attributes\AuthenticatedRoute;
|
||||
|
||||
class UserSettingsController extends ControllerAbstract
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SessionTenant $tenantIdentity,
|
||||
private readonly SessionIdentity $userIdentity,
|
||||
private readonly UserAccountsService $userService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Retrieve user settings
|
||||
* If no specific settings are requested, all settings are returned
|
||||
*
|
||||
* @return JsonResponse Settings data as key-value pairs
|
||||
*/
|
||||
#[AuthenticatedRoute(
|
||||
'/user/settings',
|
||||
name: 'user.settings.read',
|
||||
methods: ['GET'],
|
||||
permissions: ['user.settings.read']
|
||||
)]
|
||||
public function read(): JsonResponse
|
||||
{
|
||||
// Fetch all settings (no filter)
|
||||
$settings = $this->userService->fetchSettings();
|
||||
|
||||
return new JsonResponse($settings, JsonResponse::HTTP_OK);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update user settings
|
||||
*
|
||||
* @param array $data Key-value pairs of settings to update
|
||||
*
|
||||
* @example request body:
|
||||
* {
|
||||
* "data": {
|
||||
* "theme": "dark",
|
||||
* "language": "en",
|
||||
* "notifications": true
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @return JsonResponse Updated settings data
|
||||
*/
|
||||
#[AuthenticatedRoute(
|
||||
'/user/settings',
|
||||
name: 'user.settings.update',
|
||||
methods: ['PUT', 'PATCH'],
|
||||
permissions: ['user.settings.update']
|
||||
)]
|
||||
public function update(array $data): JsonResponse
|
||||
{
|
||||
$this->userService->storeSettings($data);
|
||||
|
||||
// Return updated settings
|
||||
$updatedSettings = $this->userService->fetchSettings(array_keys($data));
|
||||
|
||||
return new JsonResponse($updatedSettings, JsonResponse::HTTP_OK);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user