diff --git a/core/lib/Security/AuthenticationManager.php b/core/lib/Security/AuthenticationManager.php index 9e5cea1..67b2549 100644 --- a/core/lib/Security/AuthenticationManager.php +++ b/core/lib/Security/AuthenticationManager.php @@ -10,7 +10,6 @@ use KTXC\Security\Authentication\AuthenticationRequest; use KTXC\Security\Authentication\AuthenticationResponse; use KTXC\Service\TokenService; use KTXC\Service\UserService; -use KTXC\Service\UserProvisioningService; use KTXC\SessionTenant; use KTXF\Cache\CacheScope; use KTXF\Cache\EphemeralCacheInterface; @@ -32,7 +31,6 @@ class AuthenticationManager private readonly ProviderManager $providerManager, private readonly TokenService $tokenService, private readonly UserService $userService, - private readonly UserProvisioningService $provisioningService, ) { $this->securityCode = $this->tenant->configuration()->security()->code(); } @@ -392,19 +390,32 @@ class AuthenticationManager ); } - // Find or provision user from external identity - $providerConfig = $this->getProviderConfig($method); - $user = $this->findOrProvisionUser($method, $result->identity, $providerConfig); + // Provider has already provisioned the user - just get user identifier + $userIdentifier = $result->identity['user_identifier'] ?? null; + + 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); return AuthenticationResponse::failed( AuthenticationResponse::ERROR_USER_NOT_FOUND, - 'User not found and auto-provisioning is disabled', + 'User not found after provisioning', 401 ); } + $user = new User(); + $user->populate($userData, 'users'); + // Set user in session $session->userIdentifier = $user->getId(); $session->userIdentity = $user->getIdentity(); @@ -688,6 +699,7 @@ class AuthenticationManager $attributes['identity'] = $userIdentity; $attributes['external_subject'] = $externalSubject; + /* // Try to find by external subject first if ($externalSubject) { $user = $this->provisioningService->findByExternalIdentity($providerId, $externalSubject); @@ -733,6 +745,7 @@ class AuthenticationManager $providerConfig ); } + */ return null; } diff --git a/core/lib/Service/UserManagerService.php b/core/lib/Service/UserManagerService.php deleted file mode 100644 index d8dfb01..0000000 --- a/core/lib/Service/UserManagerService.php +++ /dev/null @@ -1,137 +0,0 @@ -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; - } - -} diff --git a/core/lib/Service/UserProvisioningService.php b/core/lib/Service/UserProvisioningService.php deleted file mode 100644 index bafe57a..0000000 --- a/core/lib/Service/UserProvisioningService.php +++ /dev/null @@ -1,278 +0,0 @@ -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'; - } -} diff --git a/core/lib/Service/UserService.php b/core/lib/Service/UserService.php index 43eccbf..35d18a9 100644 --- a/core/lib/Service/UserService.php +++ b/core/lib/Service/UserService.php @@ -17,6 +17,10 @@ class UserService ) { } + // ========================================================================= + // User Operations + // ========================================================================= + public function fetchByIdentity(string $identifier): User | null { $data = $this->userStore->fetchByIdentity($this->tenantIdentity->identifier(), $identifier); @@ -34,6 +38,69 @@ class UserService 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 { return $this->userStore->fetchSettings($this->tenantIdentity->identifier(), $this->userIdentity->identifier(), $settings); @@ -42,6 +109,56 @@ class UserService public function storeSettings(array $settings): bool { 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; + } } diff --git a/core/lib/Stores/ExternalIdentityStore.php b/core/lib/Stores/ExternalIdentityStore.php deleted file mode 100644 index bffcde5..0000000 --- a/core/lib/Stores/ExternalIdentityStore.php +++ /dev/null @@ -1,224 +0,0 @@ -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 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(); - } -} diff --git a/core/lib/Stores/UserStore.php b/core/lib/Stores/UserStore.php index 1037b71..81a2f3f 100644 --- a/core/lib/Stores/UserStore.php +++ b/core/lib/Stores/UserStore.php @@ -3,7 +3,7 @@ namespace KTXC\Stores; use KTXC\Db\DataStore; -use KTXC\Db\Collection; +use KTXF\Utile\UUID; class UserStore { @@ -11,6 +11,10 @@ class UserStore public function __construct(protected DataStore $store) { } + // ========================================================================= + // User Operations (Full User Object) + // ========================================================================= + public function fetchByIdentity(string $tenant, string $identity): array | null { @@ -63,195 +67,137 @@ class UserStore return (array)$entry; } - /** - * 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 + public function fetchByProviderSubject(string $tenant, string $provider, string $subject): array | null { $entry = $this->store->selectCollection('users')->findOne([ 'tid' => $tenant, 'provider' => $provider, - 'external_subject' => $externalSubject + 'provider_subject' => $subject ]); - - if (!$entry) { - return null; - } - + if (!$entry) { return null; } 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; + } + } \ No newline at end of file