authentication provider provisioning
This commit is contained in:
@@ -10,7 +10,6 @@ use KTXC\Security\Authentication\AuthenticationRequest;
|
|||||||
use KTXC\Security\Authentication\AuthenticationResponse;
|
use KTXC\Security\Authentication\AuthenticationResponse;
|
||||||
use KTXC\Service\TokenService;
|
use KTXC\Service\TokenService;
|
||||||
use KTXC\Service\UserService;
|
use KTXC\Service\UserService;
|
||||||
use KTXC\Service\UserProvisioningService;
|
|
||||||
use KTXC\SessionTenant;
|
use KTXC\SessionTenant;
|
||||||
use KTXF\Cache\CacheScope;
|
use KTXF\Cache\CacheScope;
|
||||||
use KTXF\Cache\EphemeralCacheInterface;
|
use KTXF\Cache\EphemeralCacheInterface;
|
||||||
@@ -32,7 +31,6 @@ class AuthenticationManager
|
|||||||
private readonly ProviderManager $providerManager,
|
private readonly ProviderManager $providerManager,
|
||||||
private readonly TokenService $tokenService,
|
private readonly TokenService $tokenService,
|
||||||
private readonly UserService $userService,
|
private readonly UserService $userService,
|
||||||
private readonly UserProvisioningService $provisioningService,
|
|
||||||
) {
|
) {
|
||||||
$this->securityCode = $this->tenant->configuration()->security()->code();
|
$this->securityCode = $this->tenant->configuration()->security()->code();
|
||||||
}
|
}
|
||||||
@@ -392,19 +390,32 @@ class AuthenticationManager
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find or provision user from external identity
|
// Provider has already provisioned the user - just get user identifier
|
||||||
$providerConfig = $this->getProviderConfig($method);
|
$userIdentifier = $result->identity['user_identifier'] ?? null;
|
||||||
$user = $this->findOrProvisionUser($method, $result->identity, $providerConfig);
|
|
||||||
|
if (!$userIdentifier) {
|
||||||
|
$this->deleteSession($session->id);
|
||||||
|
return AuthenticationResponse::failed(
|
||||||
|
AuthenticationResponse::ERROR_INTERNAL,
|
||||||
|
'User provisioning failed',
|
||||||
|
500
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if ($user === null) {
|
// Load user
|
||||||
|
$userData = $this->userService->fetchByIdentifier($userIdentifier);
|
||||||
|
if (!$userData) {
|
||||||
$this->deleteSession($session->id);
|
$this->deleteSession($session->id);
|
||||||
return AuthenticationResponse::failed(
|
return AuthenticationResponse::failed(
|
||||||
AuthenticationResponse::ERROR_USER_NOT_FOUND,
|
AuthenticationResponse::ERROR_USER_NOT_FOUND,
|
||||||
'User not found and auto-provisioning is disabled',
|
'User not found after provisioning',
|
||||||
401
|
401
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$user = new User();
|
||||||
|
$user->populate($userData, 'users');
|
||||||
|
|
||||||
// Set user in session
|
// Set user in session
|
||||||
$session->userIdentifier = $user->getId();
|
$session->userIdentifier = $user->getId();
|
||||||
$session->userIdentity = $user->getIdentity();
|
$session->userIdentity = $user->getIdentity();
|
||||||
@@ -688,6 +699,7 @@ class AuthenticationManager
|
|||||||
$attributes['identity'] = $userIdentity;
|
$attributes['identity'] = $userIdentity;
|
||||||
$attributes['external_subject'] = $externalSubject;
|
$attributes['external_subject'] = $externalSubject;
|
||||||
|
|
||||||
|
/*
|
||||||
// Try to find by external subject first
|
// Try to find by external subject first
|
||||||
if ($externalSubject) {
|
if ($externalSubject) {
|
||||||
$user = $this->provisioningService->findByExternalIdentity($providerId, $externalSubject);
|
$user = $this->provisioningService->findByExternalIdentity($providerId, $externalSubject);
|
||||||
@@ -733,6 +745,7 @@ class AuthenticationManager
|
|||||||
$providerConfig
|
$providerConfig
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,137 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace KTXC\Service;
|
|
||||||
|
|
||||||
use KTXC\Identity\Provider\DefaultIdentityProvider;
|
|
||||||
use KTXC\Models\Identity\User;
|
|
||||||
use Psr\Container\ContainerInterface;
|
|
||||||
use KTXC\SessionTenant;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User Manager Service
|
|
||||||
* Manages authentication providers and user operations across domains
|
|
||||||
*/
|
|
||||||
class UserManagerService
|
|
||||||
{
|
|
||||||
private array $availableIdentityProviders = [];
|
|
||||||
private array $cachedIdentityProviders = [];
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
private readonly SessionTenant $tenant,
|
|
||||||
private readonly UserService $userService,
|
|
||||||
private readonly ContainerInterface $container
|
|
||||||
) {
|
|
||||||
// Register the default identity provider
|
|
||||||
$this->providerRegister('default', DefaultIdentityProvider::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register an authentication provider
|
|
||||||
*/
|
|
||||||
public function providerRegister(string $identifier, string $class): void
|
|
||||||
{
|
|
||||||
$this->availableIdentityProviders[$identifier] = $class;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function providerList(?array $filter = null): array
|
|
||||||
{
|
|
||||||
$requestedProviders = $filter ? $filter : array_keys($this->availableIdentityProviders);
|
|
||||||
$result = [];
|
|
||||||
|
|
||||||
foreach ($requestedProviders as $identifier) {
|
|
||||||
// Check if provider is available
|
|
||||||
if (!isset($this->availableIdentityProviders[$identifier])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check cache first
|
|
||||||
if (isset($this->cachedIdentityProviders[$identifier])) {
|
|
||||||
$result[$identifier] = $this->cachedIdentityProviders[$identifier];
|
|
||||||
} else {
|
|
||||||
// Instantiate the provider and cache it
|
|
||||||
$providerClass = $this->availableIdentityProviders[$identifier];
|
|
||||||
|
|
||||||
try {
|
|
||||||
$providerInstance = $this->container->get($providerClass);
|
|
||||||
|
|
||||||
// Cache the instance
|
|
||||||
$this->cachedIdentityProviders[$identifier] = $providerInstance;
|
|
||||||
$result[$identifier] = $providerInstance;
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
// Skip providers that can't be resolved
|
|
||||||
error_log("Failed to resolve identity provider {$providerClass}: " . $e->getMessage());
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Authenticate user against enabled providers
|
|
||||||
*/
|
|
||||||
public function authenticate(string $identity, string $credential): User | null
|
|
||||||
{
|
|
||||||
// validate identity and credential
|
|
||||||
if (empty($identity) || empty($credential)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// retrieve user by identity
|
|
||||||
$user = $this->userService->fetchByIdentity($identity);
|
|
||||||
|
|
||||||
// determine if user has logged in before
|
|
||||||
if (!$user) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// determine if user has a identity provider assigned
|
|
||||||
if ($user->getProvider() === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$authenticated = $this->authenticateExtant($user->getProvider(), $identity, $credential);
|
|
||||||
|
|
||||||
if ($authenticated) {
|
|
||||||
return $user;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public function authenticateExtant(string $provider, string $identity, string $credential): bool {
|
|
||||||
// determine if provider is enabled
|
|
||||||
$providers = $this->providerList([$provider]);
|
|
||||||
if (empty($providers)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the first (and should be only) provider
|
|
||||||
$provider = reset($providers);
|
|
||||||
|
|
||||||
// authenticate user against provider
|
|
||||||
$user = $provider->authenticate($identity, $credential);
|
|
||||||
|
|
||||||
return $user;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function validate(string $identifier): Bool
|
|
||||||
{
|
|
||||||
$data = $this->userService->fetchByIdentifier($identifier);
|
|
||||||
if (!$data) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($data['enabled'] !== true) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($data['tid'] !== $this->tenant->identifier()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,278 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace KTXC\Service;
|
|
||||||
|
|
||||||
use KTXC\Models\Identity\User;
|
|
||||||
use KTXC\SessionTenant;
|
|
||||||
use KTXC\Stores\UserStore;
|
|
||||||
use KTXC\Stores\ExternalIdentityStore;
|
|
||||||
use KTXF\Utile\UUID;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User Provisioning Service
|
|
||||||
* Handles JIT (Just-In-Time) user provisioning from external identity providers
|
|
||||||
* and profile synchronization on login
|
|
||||||
*/
|
|
||||||
class UserProvisioningService
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
private readonly SessionTenant $tenant,
|
|
||||||
private readonly UserStore $userStore,
|
|
||||||
private readonly ExternalIdentityStore $externalIdentityStore
|
|
||||||
) { }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provision a new user from external provider attributes
|
|
||||||
*
|
|
||||||
* @param string $providerId Provider identifier
|
|
||||||
* @param array $attributes User attributes from provider
|
|
||||||
* @param array $providerConfig Provider configuration including attribute_map and default_roles
|
|
||||||
* @return User|null The provisioned user or null on failure
|
|
||||||
*/
|
|
||||||
public function provisionUser(string $providerId, array $attributes, array $providerConfig): ?User
|
|
||||||
{
|
|
||||||
$tenantId = $this->tenant->identifier();
|
|
||||||
if (!$tenantId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map attributes to user fields
|
|
||||||
$mappedData = $this->mapAttributes($attributes, $providerConfig['attribute_map'] ?? []);
|
|
||||||
|
|
||||||
// Validate required fields
|
|
||||||
$identity = $mappedData['identity'] ?? $attributes['identity'] ?? $attributes['email'] ?? null;
|
|
||||||
if (!$identity) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate user ID
|
|
||||||
$userId = UUID::v4();
|
|
||||||
|
|
||||||
// Build user data
|
|
||||||
$userData = [
|
|
||||||
'tid' => $tenantId,
|
|
||||||
'uid' => $userId,
|
|
||||||
'identity' => $identity,
|
|
||||||
'label' => $mappedData['label'] ?? $attributes['label'] ?? $attributes['name'] ?? $identity,
|
|
||||||
'enabled' => true,
|
|
||||||
'provider' => $providerId,
|
|
||||||
'external_subject' => $attributes['external_subject'] ?? null,
|
|
||||||
'roles' => $providerConfig['default_roles'] ?? [],
|
|
||||||
'profile' => $mappedData['profile'] ?? [],
|
|
||||||
'settings' => [],
|
|
||||||
'initial_login' => time(),
|
|
||||||
'recent_login' => time(),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Create the user
|
|
||||||
$createdUserId = $this->userStore->create($userData);
|
|
||||||
if (!$createdUserId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Link external identity if we have an external subject
|
|
||||||
if (!empty($attributes['external_subject'])) {
|
|
||||||
$this->externalIdentityStore->linkIdentity(
|
|
||||||
$tenantId,
|
|
||||||
$userId,
|
|
||||||
$providerId,
|
|
||||||
$attributes['external_subject'],
|
|
||||||
$attributes['raw'] ?? $attributes
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build and return User object
|
|
||||||
$user = new User();
|
|
||||||
$user->populate($userData, 'users');
|
|
||||||
|
|
||||||
return $user;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Synchronize user profile with attributes from provider
|
|
||||||
* Called on each login to keep profile data up to date
|
|
||||||
*
|
|
||||||
* @param User $user The existing user
|
|
||||||
* @param array $attributes Attributes from provider
|
|
||||||
* @param array $attributeMap Attribute mapping configuration
|
|
||||||
* @return bool Whether sync was successful
|
|
||||||
*/
|
|
||||||
public function syncProfile(User $user, array $attributes, array $attributeMap = []): bool
|
|
||||||
{
|
|
||||||
$tenantId = $this->tenant->identifier();
|
|
||||||
$userId = $user->getId();
|
|
||||||
|
|
||||||
if (!$tenantId || !$userId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map attributes
|
|
||||||
$mappedData = $this->mapAttributes($attributes, $attributeMap);
|
|
||||||
|
|
||||||
// Update profile fields if we have mapped profile data
|
|
||||||
if (!empty($mappedData['profile'])) {
|
|
||||||
$this->userStore->updateProfile($tenantId, $userId, $mappedData['profile']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update label if provided and different
|
|
||||||
if (!empty($mappedData['label']) && $mappedData['label'] !== $user->getLabel()) {
|
|
||||||
$this->userStore->updateLabel($tenantId, $userId, $mappedData['label']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always update last login
|
|
||||||
$this->userStore->updateLastLogin($tenantId, $userId);
|
|
||||||
|
|
||||||
// Update external identity attributes if applicable
|
|
||||||
if (!empty($attributes['external_subject'])) {
|
|
||||||
$this->externalIdentityStore->updateLastLogin(
|
|
||||||
$tenantId,
|
|
||||||
$user->getProvider() ?? '',
|
|
||||||
$attributes['external_subject']
|
|
||||||
);
|
|
||||||
$this->externalIdentityStore->updateAttributes(
|
|
||||||
$tenantId,
|
|
||||||
$user->getProvider() ?? '',
|
|
||||||
$attributes['external_subject'],
|
|
||||||
$attributes['raw'] ?? $attributes
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Link an external identity to an existing user
|
|
||||||
*
|
|
||||||
* @param User $user The user to link
|
|
||||||
* @param string $providerId Provider identifier
|
|
||||||
* @param string $externalSubject External subject identifier
|
|
||||||
* @param array $attributes Optional attributes from provider
|
|
||||||
* @return bool Whether linking was successful
|
|
||||||
*/
|
|
||||||
public function linkExternalIdentity(User $user, string $providerId, string $externalSubject, array $attributes = []): bool
|
|
||||||
{
|
|
||||||
$tenantId = $this->tenant->identifier();
|
|
||||||
$userId = $user->getId();
|
|
||||||
|
|
||||||
if (!$tenantId || !$userId) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->externalIdentityStore->linkIdentity(
|
|
||||||
$tenantId,
|
|
||||||
$userId,
|
|
||||||
$providerId,
|
|
||||||
$externalSubject,
|
|
||||||
$attributes
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find user by external identity
|
|
||||||
*
|
|
||||||
* @param string $providerId Provider identifier
|
|
||||||
* @param string $externalSubject External subject identifier
|
|
||||||
* @return User|null The user or null if not found
|
|
||||||
*/
|
|
||||||
public function findByExternalIdentity(string $providerId, string $externalSubject): ?User
|
|
||||||
{
|
|
||||||
$tenantId = $this->tenant->identifier();
|
|
||||||
if (!$tenantId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look up in external identities table
|
|
||||||
$externalIdentity = $this->externalIdentityStore->findByExternalSubject(
|
|
||||||
$tenantId,
|
|
||||||
$providerId,
|
|
||||||
$externalSubject
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!$externalIdentity) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch the linked user
|
|
||||||
$userData = $this->userStore->fetchByIdentifier($tenantId, $externalIdentity['uid']);
|
|
||||||
if (!$userData) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = new User();
|
|
||||||
$user->populate($userData, 'users');
|
|
||||||
|
|
||||||
return $user;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map provider attributes to user fields using attribute map
|
|
||||||
*
|
|
||||||
* @param array $attributes Raw attributes from provider
|
|
||||||
* @param array $attributeMap Mapping configuration {source_attr: target_field}
|
|
||||||
* @return array Mapped data with 'identity', 'label', 'profile' keys
|
|
||||||
*/
|
|
||||||
protected function mapAttributes(array $attributes, array $attributeMap): array
|
|
||||||
{
|
|
||||||
$result = [
|
|
||||||
'identity' => null,
|
|
||||||
'label' => null,
|
|
||||||
'profile' => [],
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($attributeMap as $sourceAttr => $targetField) {
|
|
||||||
// Get source value (supports nested attributes with dot notation)
|
|
||||||
$value = $this->getNestedValue($attributes, $sourceAttr);
|
|
||||||
if ($value === null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set target value (supports nested targets with dot notation)
|
|
||||||
if ($targetField === 'identity') {
|
|
||||||
$result['identity'] = $value;
|
|
||||||
} elseif ($targetField === 'label') {
|
|
||||||
$result['label'] = $value;
|
|
||||||
} elseif (str_starts_with($targetField, 'profile.')) {
|
|
||||||
$profileField = substr($targetField, 8);
|
|
||||||
$result['profile'][$profileField] = $value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get nested value from array using dot notation
|
|
||||||
*
|
|
||||||
* @param array $array Source array
|
|
||||||
* @param string $key Key with optional dot notation (e.g., 'user.email')
|
|
||||||
* @return mixed|null Value or null if not found
|
|
||||||
*/
|
|
||||||
protected function getNestedValue(array $array, string $key): mixed
|
|
||||||
{
|
|
||||||
$keys = explode('.', $key);
|
|
||||||
$value = $array;
|
|
||||||
|
|
||||||
foreach ($keys as $k) {
|
|
||||||
if (!is_array($value) || !array_key_exists($k, $value)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
$value = $value[$k];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if auto-provisioning is enabled for a provider
|
|
||||||
*
|
|
||||||
* @param string $providerId Provider identifier
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function isAutoProvisioningEnabled(string $providerId): bool
|
|
||||||
{
|
|
||||||
$config = $this->tenant->identityProviderConfig($providerId);
|
|
||||||
return ($config['provisioning'] ?? 'manual') === 'auto';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -17,6 +17,10 @@ class UserService
|
|||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// User Operations
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
public function fetchByIdentity(string $identifier): User | null
|
public function fetchByIdentity(string $identifier): User | null
|
||||||
{
|
{
|
||||||
$data = $this->userStore->fetchByIdentity($this->tenantIdentity->identifier(), $identifier);
|
$data = $this->userStore->fetchByIdentity($this->tenantIdentity->identifier(), $identifier);
|
||||||
@@ -34,6 +38,69 @@ class UserService
|
|||||||
return $this->userStore->fetchByIdentifier($this->tenantIdentity->identifier(), $identifier);
|
return $this->userStore->fetchByIdentifier($this->tenantIdentity->identifier(), $identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function fetchByIdentityRaw(string $identifier): array | null
|
||||||
|
{
|
||||||
|
return $this->userStore->fetchByIdentity($this->tenantIdentity->identifier(), $identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fetchByProviderSubject(string $provider, string $subject): ?array
|
||||||
|
{
|
||||||
|
return $this->userStore->fetchByProviderSubject($this->tenantIdentity->identifier(), $provider, $subject);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createUser(array $userData): array
|
||||||
|
{
|
||||||
|
return $this->userStore->createUser($this->tenantIdentity->identifier(), $userData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateUser(string $uid, array $updates): bool
|
||||||
|
{
|
||||||
|
return $this->userStore->updateUser($this->tenantIdentity->identifier(), $uid, $updates);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteUser(string $uid): bool
|
||||||
|
{
|
||||||
|
return $this->userStore->deleteUser($this->tenantIdentity->identifier(), $uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Profile Operations
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function fetchProfile(string $uid): ?array
|
||||||
|
{
|
||||||
|
return $this->userStore->fetchProfile($this->tenantIdentity->identifier(), $uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeProfile(string $uid, array $profileFields): bool
|
||||||
|
{
|
||||||
|
// Get managed fields to filter out read-only fields
|
||||||
|
$user = $this->fetchByIdentifier($uid);
|
||||||
|
if (!$user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$managedFields = $user['provider_managed_fields'] ?? [];
|
||||||
|
$editableFields = [];
|
||||||
|
|
||||||
|
// Only include fields that are not managed by provider
|
||||||
|
foreach ($profileFields as $field => $value) {
|
||||||
|
if (!in_array($field, $managedFields)) {
|
||||||
|
$editableFields[$field] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($editableFields)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->userStore->storeProfile($this->tenantIdentity->identifier(), $uid, $editableFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Settings Operations
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
public function fetchSettings(array $settings = []): array | null
|
public function fetchSettings(array $settings = []): array | null
|
||||||
{
|
{
|
||||||
return $this->userStore->fetchSettings($this->tenantIdentity->identifier(), $this->userIdentity->identifier(), $settings);
|
return $this->userStore->fetchSettings($this->tenantIdentity->identifier(), $this->userIdentity->identifier(), $settings);
|
||||||
@@ -42,6 +109,56 @@ class UserService
|
|||||||
public function storeSettings(array $settings): bool
|
public function storeSettings(array $settings): bool
|
||||||
{
|
{
|
||||||
return $this->userStore->storeSettings($this->tenantIdentity->identifier(), $this->userIdentity->identifier(), $settings);
|
return $this->userStore->storeSettings($this->tenantIdentity->identifier(), $this->userIdentity->identifier(), $settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Helper Methods
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a profile field is editable by the user
|
||||||
|
*
|
||||||
|
* @param string $uid User identifier
|
||||||
|
* @param string $field Profile field name
|
||||||
|
* @return bool True if field is editable, false if managed by provider
|
||||||
|
*/
|
||||||
|
public function isFieldEditable(string $uid, string $field): bool
|
||||||
|
{
|
||||||
|
$user = $this->fetchByIdentifier($uid);
|
||||||
|
if (!$user) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$managedFields = $user['provider_managed_fields'] ?? [];
|
||||||
|
return !in_array($field, $managedFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get editable fields for a user
|
||||||
|
*
|
||||||
|
* @param string $uid User identifier
|
||||||
|
* @return array Array with field => ['value' => ..., 'editable' => bool, 'provider' => ...]
|
||||||
|
*/
|
||||||
|
public function getEditableFields(string $uid): array
|
||||||
|
{
|
||||||
|
$user = $this->fetchByIdentifier($uid);
|
||||||
|
if (!$user || !isset($user['profile'])) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$managedFields = $user['provider_managed_fields'] ?? [];
|
||||||
|
$provider = $user['provider'] ?? null;
|
||||||
|
$editable = [];
|
||||||
|
|
||||||
|
foreach ($user['profile'] as $field => $value) {
|
||||||
|
$editable[$field] = [
|
||||||
|
'value' => $value,
|
||||||
|
'editable' => !in_array($field, $managedFields),
|
||||||
|
'provider' => in_array($field, $managedFields) ? $provider : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $editable;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,224 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace KTXC\Stores;
|
|
||||||
|
|
||||||
use KTXC\Db\DataStore;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* External Identity Store
|
|
||||||
* Maps external identity provider subjects to local users
|
|
||||||
*
|
|
||||||
* Collection: external_identities
|
|
||||||
* Schema: {
|
|
||||||
* tid: string, // Tenant identifier
|
|
||||||
* uid: string, // Local user identifier
|
|
||||||
* provider: string, // Provider identifier (e.g., 'oidc', 'saml')
|
|
||||||
* external_subject: string, // External subject identifier (e.g., OIDC sub, SAML NameID)
|
|
||||||
* attributes: object, // Cached attributes from provider
|
|
||||||
* linked_at: int, // Timestamp when identity was linked
|
|
||||||
* last_login: int // Last login via this external identity
|
|
||||||
* }
|
|
||||||
*/
|
|
||||||
class ExternalIdentityStore
|
|
||||||
{
|
|
||||||
protected const COLLECTION_NAME = 'external_identities';
|
|
||||||
|
|
||||||
public function __construct(protected DataStore $store)
|
|
||||||
{ }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find external identity by provider and external subject
|
|
||||||
*
|
|
||||||
* @param string $tenant Tenant identifier
|
|
||||||
* @param string $provider Provider identifier
|
|
||||||
* @param string $externalSubject External subject identifier
|
|
||||||
* @return array|null External identity record or null
|
|
||||||
*/
|
|
||||||
public function findByExternalSubject(string $tenant, string $provider, string $externalSubject): ?array
|
|
||||||
{
|
|
||||||
$entry = $this->store->selectCollection(self::COLLECTION_NAME)->findOne([
|
|
||||||
'tid' => $tenant,
|
|
||||||
'provider' => $provider,
|
|
||||||
'external_subject' => $externalSubject
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!$entry) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (array)$entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find all external identities for a user
|
|
||||||
*
|
|
||||||
* @param string $tenant Tenant identifier
|
|
||||||
* @param string $userId Local user identifier
|
|
||||||
* @return array<array> List of external identity records
|
|
||||||
*/
|
|
||||||
public function findByUser(string $tenant, string $userId): array
|
|
||||||
{
|
|
||||||
$cursor = $this->store->selectCollection(self::COLLECTION_NAME)->find([
|
|
||||||
'tid' => $tenant,
|
|
||||||
'uid' => $userId
|
|
||||||
]);
|
|
||||||
|
|
||||||
$result = [];
|
|
||||||
foreach ($cursor as $entry) {
|
|
||||||
$result[] = (array)$entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find external identity for a user from a specific provider
|
|
||||||
*
|
|
||||||
* @param string $tenant Tenant identifier
|
|
||||||
* @param string $userId Local user identifier
|
|
||||||
* @param string $provider Provider identifier
|
|
||||||
* @return array|null External identity record or null
|
|
||||||
*/
|
|
||||||
public function findByUserAndProvider(string $tenant, string $userId, string $provider): ?array
|
|
||||||
{
|
|
||||||
$entry = $this->store->selectCollection(self::COLLECTION_NAME)->findOne([
|
|
||||||
'tid' => $tenant,
|
|
||||||
'uid' => $userId,
|
|
||||||
'provider' => $provider
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!$entry) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (array)$entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Link an external identity to a local user
|
|
||||||
*
|
|
||||||
* @param string $tenant Tenant identifier
|
|
||||||
* @param string $userId Local user identifier
|
|
||||||
* @param string $provider Provider identifier
|
|
||||||
* @param string $externalSubject External subject identifier
|
|
||||||
* @param array $attributes Optional attributes from provider
|
|
||||||
* @return bool Whether the operation was successful
|
|
||||||
*/
|
|
||||||
public function linkIdentity(
|
|
||||||
string $tenant,
|
|
||||||
string $userId,
|
|
||||||
string $provider,
|
|
||||||
string $externalSubject,
|
|
||||||
array $attributes = []
|
|
||||||
): bool {
|
|
||||||
$now = time();
|
|
||||||
|
|
||||||
// Use upsert to handle both create and update
|
|
||||||
$result = $this->store->selectCollection(self::COLLECTION_NAME)->updateOne(
|
|
||||||
[
|
|
||||||
'tid' => $tenant,
|
|
||||||
'provider' => $provider,
|
|
||||||
'external_subject' => $externalSubject
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'$set' => [
|
|
||||||
'uid' => $userId,
|
|
||||||
'attributes' => $attributes,
|
|
||||||
'last_login' => $now
|
|
||||||
],
|
|
||||||
'$setOnInsert' => [
|
|
||||||
'tid' => $tenant,
|
|
||||||
'provider' => $provider,
|
|
||||||
'external_subject' => $externalSubject,
|
|
||||||
'linked_at' => $now
|
|
||||||
]
|
|
||||||
],
|
|
||||||
['upsert' => true]
|
|
||||||
);
|
|
||||||
|
|
||||||
return $result->isAcknowledged();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unlink an external identity from a user
|
|
||||||
*
|
|
||||||
* @param string $tenant Tenant identifier
|
|
||||||
* @param string $userId Local user identifier
|
|
||||||
* @param string $provider Provider identifier
|
|
||||||
* @return bool Whether the operation was successful
|
|
||||||
*/
|
|
||||||
public function unlinkIdentity(string $tenant, string $userId, string $provider): bool
|
|
||||||
{
|
|
||||||
$result = $this->store->selectCollection(self::COLLECTION_NAME)->deleteOne([
|
|
||||||
'tid' => $tenant,
|
|
||||||
'uid' => $userId,
|
|
||||||
'provider' => $provider
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $result->isAcknowledged();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update last login timestamp for an external identity
|
|
||||||
*
|
|
||||||
* @param string $tenant Tenant identifier
|
|
||||||
* @param string $provider Provider identifier
|
|
||||||
* @param string $externalSubject External subject identifier
|
|
||||||
* @return bool Whether the operation was successful
|
|
||||||
*/
|
|
||||||
public function updateLastLogin(string $tenant, string $provider, string $externalSubject): bool
|
|
||||||
{
|
|
||||||
$result = $this->store->selectCollection(self::COLLECTION_NAME)->updateOne(
|
|
||||||
[
|
|
||||||
'tid' => $tenant,
|
|
||||||
'provider' => $provider,
|
|
||||||
'external_subject' => $externalSubject
|
|
||||||
],
|
|
||||||
['$set' => ['last_login' => time()]]
|
|
||||||
);
|
|
||||||
|
|
||||||
return $result->isAcknowledged();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update cached attributes for an external identity
|
|
||||||
*
|
|
||||||
* @param string $tenant Tenant identifier
|
|
||||||
* @param string $provider Provider identifier
|
|
||||||
* @param string $externalSubject External subject identifier
|
|
||||||
* @param array $attributes New attributes to store
|
|
||||||
* @return bool Whether the operation was successful
|
|
||||||
*/
|
|
||||||
public function updateAttributes(string $tenant, string $provider, string $externalSubject, array $attributes): bool
|
|
||||||
{
|
|
||||||
$result = $this->store->selectCollection(self::COLLECTION_NAME)->updateOne(
|
|
||||||
[
|
|
||||||
'tid' => $tenant,
|
|
||||||
'provider' => $provider,
|
|
||||||
'external_subject' => $externalSubject
|
|
||||||
],
|
|
||||||
['$set' => ['attributes' => $attributes]]
|
|
||||||
);
|
|
||||||
|
|
||||||
return $result->isAcknowledged();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete all external identities for a user (used when deleting user)
|
|
||||||
*
|
|
||||||
* @param string $tenant Tenant identifier
|
|
||||||
* @param string $userId Local user identifier
|
|
||||||
* @return int Number of deleted records
|
|
||||||
*/
|
|
||||||
public function deleteAllForUser(string $tenant, string $userId): int
|
|
||||||
{
|
|
||||||
$result = $this->store->selectCollection(self::COLLECTION_NAME)->deleteMany([
|
|
||||||
'tid' => $tenant,
|
|
||||||
'uid' => $userId
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $result->getDeletedCount();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace KTXC\Stores;
|
namespace KTXC\Stores;
|
||||||
|
|
||||||
use KTXC\Db\DataStore;
|
use KTXC\Db\DataStore;
|
||||||
use KTXC\Db\Collection;
|
use KTXF\Utile\UUID;
|
||||||
|
|
||||||
class UserStore
|
class UserStore
|
||||||
{
|
{
|
||||||
@@ -11,6 +11,10 @@ class UserStore
|
|||||||
public function __construct(protected DataStore $store)
|
public function __construct(protected DataStore $store)
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// User Operations (Full User Object)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
public function fetchByIdentity(string $tenant, string $identity): array | null
|
public function fetchByIdentity(string $tenant, string $identity): array | null
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -63,195 +67,137 @@ class UserStore
|
|||||||
return (array)$entry;
|
return (array)$entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public function fetchByProviderSubject(string $tenant, string $provider, string $subject): array | null
|
||||||
* Fetch user settings from the embedded settings field in the user document
|
|
||||||
*
|
|
||||||
* @param string $tenant Tenant identifier
|
|
||||||
* @param string $identifier User identifier
|
|
||||||
* @param array $settings Optional array of specific setting keys to retrieve
|
|
||||||
* @return array|null Settings array or null if user not found
|
|
||||||
*/
|
|
||||||
public function fetchSettings(string $tenant, string $identifier, array $keys = []): array | null
|
|
||||||
{
|
|
||||||
$entry = $this->store->selectCollection('users')->findOne(
|
|
||||||
['tid' => $tenant, 'uid' => $identifier],
|
|
||||||
['projection' => ['settings' => 1]]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!$entry) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$settings = (array)($entry['settings'] ?? []);
|
|
||||||
|
|
||||||
if (empty($keys)) {
|
|
||||||
return $settings;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter to only requested keys
|
|
||||||
return array_filter(
|
|
||||||
$settings,
|
|
||||||
fn($key) => in_array($key, $keys),
|
|
||||||
ARRAY_FILTER_USE_KEY
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store/update user settings in the embedded settings field
|
|
||||||
*
|
|
||||||
* @param string $tenant Tenant identifier
|
|
||||||
* @param string $identifier User identifier
|
|
||||||
* @param array $settings Key-value pairs to set/update
|
|
||||||
* @return bool Whether the update was acknowledged
|
|
||||||
*/
|
|
||||||
public function storeSettings(string $tenant, string $identifier, array $settings): bool
|
|
||||||
{
|
|
||||||
// Build dot-notation update for each setting key
|
|
||||||
$setFields = [];
|
|
||||||
foreach ($settings as $key => $value) {
|
|
||||||
$setFields["settings.$key"] = $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $this->store->selectCollection('users')->updateOne(
|
|
||||||
['tid' => $tenant, 'uid' => $identifier],
|
|
||||||
['$set' => $setFields]
|
|
||||||
);
|
|
||||||
|
|
||||||
return $result->isAcknowledged();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove specific settings from a user
|
|
||||||
*
|
|
||||||
* @param string $tenant Tenant identifier
|
|
||||||
* @param string $identifier User identifier
|
|
||||||
* @param array $keys Setting keys to remove
|
|
||||||
* @return bool Whether the update was acknowledged
|
|
||||||
*/
|
|
||||||
public function removeSettings(string $tenant, string $identifier, array $keys): bool
|
|
||||||
{
|
|
||||||
$unsetFields = [];
|
|
||||||
foreach ($keys as $key) {
|
|
||||||
$unsetFields["settings.$key"] = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $this->store->selectCollection('users')->updateOne(
|
|
||||||
['tid' => $tenant, 'uid' => $identifier],
|
|
||||||
['$unset' => $unsetFields]
|
|
||||||
);
|
|
||||||
|
|
||||||
return $result->isAcknowledged();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new user
|
|
||||||
*
|
|
||||||
* @param array $data User data including tid, uid, identity, label, provider, etc.
|
|
||||||
* @return string|null The created user's UID or null on failure
|
|
||||||
*/
|
|
||||||
public function create(array $data): ?string
|
|
||||||
{
|
|
||||||
// Ensure required fields
|
|
||||||
if (empty($data['tid']) || empty($data['uid']) || empty($data['identity'])) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set defaults
|
|
||||||
$data['enabled'] = $data['enabled'] ?? true;
|
|
||||||
$data['roles'] = $data['roles'] ?? [];
|
|
||||||
$data['profile'] = $data['profile'] ?? [];
|
|
||||||
$data['settings'] = $data['settings'] ?? [];
|
|
||||||
$data['initial_login'] = $data['initial_login'] ?? time();
|
|
||||||
$data['recent_login'] = $data['recent_login'] ?? time();
|
|
||||||
|
|
||||||
$result = $this->store->selectCollection('users')->insertOne($data);
|
|
||||||
|
|
||||||
if ($result->isAcknowledged()) {
|
|
||||||
return $data['uid'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update user profile fields
|
|
||||||
*
|
|
||||||
* @param string $tenant Tenant identifier
|
|
||||||
* @param string $identifier User identifier
|
|
||||||
* @param array $profile Profile data to update
|
|
||||||
* @return bool Whether the update was acknowledged
|
|
||||||
*/
|
|
||||||
public function updateProfile(string $tenant, string $identifier, array $profile): bool
|
|
||||||
{
|
|
||||||
$setFields = [];
|
|
||||||
foreach ($profile as $key => $value) {
|
|
||||||
$setFields["profile.$key"] = $value;
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $this->store->selectCollection('users')->updateOne(
|
|
||||||
['tid' => $tenant, 'uid' => $identifier],
|
|
||||||
['$set' => $setFields]
|
|
||||||
);
|
|
||||||
|
|
||||||
return $result->isAcknowledged();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update user's last login timestamp
|
|
||||||
*
|
|
||||||
* @param string $tenant Tenant identifier
|
|
||||||
* @param string $identifier User identifier
|
|
||||||
* @return bool Whether the update was acknowledged
|
|
||||||
*/
|
|
||||||
public function updateLastLogin(string $tenant, string $identifier): bool
|
|
||||||
{
|
|
||||||
$result = $this->store->selectCollection('users')->updateOne(
|
|
||||||
['tid' => $tenant, 'uid' => $identifier],
|
|
||||||
['$set' => ['recent_login' => time()]]
|
|
||||||
);
|
|
||||||
|
|
||||||
return $result->isAcknowledged();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update user's label
|
|
||||||
*
|
|
||||||
* @param string $tenant Tenant identifier
|
|
||||||
* @param string $identifier User identifier
|
|
||||||
* @param string $label New label
|
|
||||||
* @return bool Whether the update was acknowledged
|
|
||||||
*/
|
|
||||||
public function updateLabel(string $tenant, string $identifier, string $label): bool
|
|
||||||
{
|
|
||||||
$result = $this->store->selectCollection('users')->updateOne(
|
|
||||||
['tid' => $tenant, 'uid' => $identifier],
|
|
||||||
['$set' => ['label' => $label]]
|
|
||||||
);
|
|
||||||
|
|
||||||
return $result->isAcknowledged();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find user by external subject (for external identity providers)
|
|
||||||
*
|
|
||||||
* @param string $tenant Tenant identifier
|
|
||||||
* @param string $provider Provider identifier
|
|
||||||
* @param string $externalSubject External subject identifier
|
|
||||||
* @return array|null User data or null if not found
|
|
||||||
*/
|
|
||||||
public function fetchByExternalSubject(string $tenant, string $provider, string $externalSubject): ?array
|
|
||||||
{
|
{
|
||||||
$entry = $this->store->selectCollection('users')->findOne([
|
$entry = $this->store->selectCollection('users')->findOne([
|
||||||
'tid' => $tenant,
|
'tid' => $tenant,
|
||||||
'provider' => $provider,
|
'provider' => $provider,
|
||||||
'external_subject' => $externalSubject
|
'provider_subject' => $subject
|
||||||
]);
|
]);
|
||||||
|
if (!$entry) { return null; }
|
||||||
if (!$entry) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (array)$entry;
|
return (array)$entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function createUser(string $tenant, array $userData): array
|
||||||
|
{
|
||||||
|
$userData['tid'] = $tenant;
|
||||||
|
$userData['uid'] = $userData['uid'] ?? UUID::v4();
|
||||||
|
$userData['enabled'] = $userData['enabled'] ?? true;
|
||||||
|
$userData['roles'] = $userData['roles'] ?? [];
|
||||||
|
$userData['profile'] = $userData['profile'] ?? [];
|
||||||
|
$userData['settings'] = $userData['settings'] ?? [];
|
||||||
|
|
||||||
|
$this->store->selectCollection('users')->insertOne($userData);
|
||||||
|
|
||||||
|
return $this->fetchByIdentifier($tenant, $userData['uid']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateUser(string $tenant, string $uid, array $updates): bool
|
||||||
|
{
|
||||||
|
$result = $this->store->selectCollection('users')->updateOne(
|
||||||
|
['tid' => $tenant, 'uid' => $uid],
|
||||||
|
['$set' => $updates]
|
||||||
|
);
|
||||||
|
|
||||||
|
return $result->getModifiedCount() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deleteUser(string $tenant, string $uid): bool
|
||||||
|
{
|
||||||
|
$result = $this->store->selectCollection('users')->deleteOne([
|
||||||
|
'tid' => $tenant,
|
||||||
|
'uid' => $uid
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $result->getDeletedCount() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Profile Operations
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function fetchProfile(string $tenant, string $uid): ?array
|
||||||
|
{
|
||||||
|
$user = $this->store->selectCollection('users')->findOne(
|
||||||
|
['tid' => $tenant, 'uid' => $uid],
|
||||||
|
['projection' => ['profile' => 1, 'provider_managed_fields' => 1]]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'profile' => $user['profile'] ?? [],
|
||||||
|
'provider_managed_fields' => $user['provider_managed_fields'] ?? [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeProfile(string $tenant, string $uid, array $profileFields): bool
|
||||||
|
{
|
||||||
|
if (empty($profileFields)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$updates = [];
|
||||||
|
foreach ($profileFields as $key => $value) {
|
||||||
|
$updates["profile.{$key}"] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->store->selectCollection('users')->updateOne(
|
||||||
|
['tid' => $tenant, 'uid' => $uid],
|
||||||
|
['$set' => $updates]
|
||||||
|
);
|
||||||
|
|
||||||
|
return $result->getModifiedCount() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Settings Operations
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function fetchSettings(string $tenant, string $uid, array $settings = []): ?array
|
||||||
|
{
|
||||||
|
// Only fetch the settings field from the database
|
||||||
|
$user = $this->store->selectCollection('users')->findOne(
|
||||||
|
['tid' => $tenant, 'uid' => $uid],
|
||||||
|
['projection' => ['settings' => 1]]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userSettings = $user['settings'] ?? [];
|
||||||
|
|
||||||
|
if (empty($settings)) {
|
||||||
|
return $userSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($settings as $key) {
|
||||||
|
$result[$key] = $userSettings[$key] ?? null;
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeSettings(string $tenant, string $uid, array $settings): bool
|
||||||
|
{
|
||||||
|
if (empty($settings)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$updates = [];
|
||||||
|
foreach ($settings as $key => $value) {
|
||||||
|
$updates["settings.{$key}"] = $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->store->selectCollection('users')->updateOne(
|
||||||
|
['tid' => $tenant, 'uid' => $uid],
|
||||||
|
['$set' => $updates]
|
||||||
|
);
|
||||||
|
|
||||||
|
return $result->getModifiedCount() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user