[ 'tid' => $tenant, 'identity' => $identity ] ], [ '$lookup' => [ 'from' => 'user_roles', 'localField' => 'roles', // Array field in `users` 'foreignField' => 'rid', // Scalar field in `user_roles` 'as' => 'role_details' ] ], // Add flattened, deduplicated permissions while preserving all original user fields [ '$addFields' => [ 'permissions' => [ '$reduce' => [ 'input' => [ '$map' => [ 'input' => '$role_details', 'as' => 'r', 'in' => [ '$ifNull' => ['$$r.permissions', []] ] ] ], 'initialValue' => [], 'in' => [ '$setUnion' => ['$$value', '$$this'] ] ] ] ] ], // Optionally remove expanded role documents from output [ '$unset' => 'role_details' ] ]; $entry = $this->store->selectCollection('users')->aggregate($pipeline)->toArray()[0] ?? null; if (!$entry) { return null; } return (array)$entry; } public function fetchByIdentifier(string $tenant, string $identifier): array | null { $entry = $this->store->selectCollection('users')->findOne(['tid' => $tenant, 'uid' => $identifier]); if (!$entry) { return null; } 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 { $entry = $this->store->selectCollection('users')->findOne([ 'tid' => $tenant, 'provider' => $provider, 'external_subject' => $externalSubject ]); if (!$entry) { return null; } return (array)$entry; } }