324 lines
10 KiB
PHP
324 lines
10 KiB
PHP
<?php
|
|
|
|
namespace KTXC\Stores;
|
|
|
|
use KTXC\Db\DataStore;
|
|
use KTXF\Utile\UUID;
|
|
|
|
class UserAccountsStore
|
|
{
|
|
|
|
public function __construct(protected DataStore $store)
|
|
{ }
|
|
|
|
// =========================================================================
|
|
// User Operations (Full User Object)
|
|
// =========================================================================
|
|
|
|
/**
|
|
* List all users for a tenant with optional filters
|
|
*/
|
|
public function listUsers(string $tenant, array $filters = []): array
|
|
{
|
|
// Build filter
|
|
$filter = ['tid' => $tenant];
|
|
|
|
if (isset($filters['enabled'])) {
|
|
$filter['enabled'] = (bool)$filters['enabled'];
|
|
}
|
|
|
|
if (isset($filters['role'])) {
|
|
$filter['roles'] = $filters['role'];
|
|
}
|
|
|
|
// Fetch users with aggregated role data
|
|
$pipeline = [
|
|
['$match' => $filter],
|
|
[
|
|
'$lookup' => [
|
|
'from' => 'user_roles',
|
|
'localField' => 'roles',
|
|
'foreignField' => 'rid',
|
|
'as' => 'role_details'
|
|
]
|
|
],
|
|
[
|
|
'$addFields' => [
|
|
'permissions' => [
|
|
'$reduce' => [
|
|
'input' => [
|
|
'$map' => [
|
|
'input' => '$role_details',
|
|
'as' => 'r',
|
|
'in' => ['$ifNull' => ['$$r.permissions', []]]
|
|
]
|
|
],
|
|
'initialValue' => [],
|
|
'in' => ['$setUnion' => ['$$value', '$$this']]
|
|
]
|
|
]
|
|
]
|
|
],
|
|
['$unset' => 'role_details'],
|
|
['$sort' => ['label' => 1]]
|
|
];
|
|
|
|
$cursor = $this->store->selectCollection('user_accounts')->aggregate($pipeline);
|
|
|
|
$users = [];
|
|
foreach ($cursor as $entry) {
|
|
$users[] = (array)$entry;
|
|
}
|
|
|
|
return $users;
|
|
}
|
|
|
|
public function fetchByIdentity(string $tenant, string $identity): array | null
|
|
{
|
|
|
|
$pipeline = [
|
|
[
|
|
'$match' => [
|
|
'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('user_accounts')->aggregate($pipeline)->toArray()[0] ?? null;
|
|
if (!$entry) { return null; }
|
|
return (array)$entry;
|
|
}
|
|
|
|
public function fetchByIdentifier(string $tenant, string $identifier): array | null
|
|
{
|
|
$pipeline = [
|
|
[
|
|
'$match' => [
|
|
'tid' => $tenant,
|
|
'uid' => $identifier
|
|
]
|
|
],
|
|
[
|
|
'$lookup' => [
|
|
'from' => 'user_roles',
|
|
'localField' => 'roles',
|
|
'foreignField' => 'rid',
|
|
'as' => 'role_details'
|
|
]
|
|
],
|
|
[
|
|
'$addFields' => [
|
|
'permissions' => [
|
|
'$reduce' => [
|
|
'input' => [
|
|
'$map' => [
|
|
'input' => '$role_details',
|
|
'as' => 'r',
|
|
'in' => [ '$ifNull' => ['$$r.permissions', []] ]
|
|
]
|
|
],
|
|
'initialValue' => [],
|
|
'in' => [ '$setUnion' => ['$$value', '$$this'] ]
|
|
]
|
|
]
|
|
]
|
|
],
|
|
[ '$unset' => 'role_details' ]
|
|
];
|
|
|
|
$entry = $this->store->selectCollection('user_accounts')->aggregate($pipeline)->toArray()[0] ?? null;
|
|
if (!$entry) { return null; }
|
|
return (array)$entry;
|
|
}
|
|
|
|
public function fetchByProviderSubject(string $tenant, string $provider, string $subject): array | null
|
|
{
|
|
$entry = $this->store->selectCollection('user_accounts')->findOne([
|
|
'tid' => $tenant,
|
|
'provider' => $provider,
|
|
'provider_subject' => $subject
|
|
]);
|
|
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('user_accounts')->insertOne($userData);
|
|
|
|
return $this->fetchByIdentifier($tenant, $userData['uid']);
|
|
}
|
|
|
|
public function updateUser(string $tenant, string $uid, array $updates): bool
|
|
{
|
|
$result = $this->store->selectCollection('user_accounts')->updateOne(
|
|
['tid' => $tenant, 'uid' => $uid],
|
|
['$set' => $updates]
|
|
);
|
|
|
|
return $result->getModifiedCount() > 0;
|
|
}
|
|
|
|
public function deleteUser(string $tenant, string $uid): bool
|
|
{
|
|
$result = $this->store->selectCollection('user_accounts')->deleteOne([
|
|
'tid' => $tenant,
|
|
'uid' => $uid
|
|
]);
|
|
|
|
return $result->getDeletedCount() > 0;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Profile Operations
|
|
// =========================================================================
|
|
|
|
public function fetchProfile(string $tenant, string $uid): ?array
|
|
{
|
|
$user = $this->store->selectCollection('user_accounts')->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('user_accounts')->updateOne(
|
|
['tid' => $tenant, 'uid' => $uid],
|
|
['$set' => $updates]
|
|
);
|
|
|
|
return $result->getModifiedCount() > 0;
|
|
}
|
|
|
|
// =========================================================================
|
|
// Settings Operations
|
|
// =========================================================================
|
|
|
|
public function fetchSettings(string $tenant, string $uid, array $settings = [], bool $flatten = false): ?array
|
|
{
|
|
// Only fetch the settings field from the database
|
|
$user = $this->store->selectCollection('user_accounts')->findOne(
|
|
['tid' => $tenant, 'uid' => $uid],
|
|
['projection' => ['settings' => 1]]
|
|
);
|
|
|
|
if (!$user) {
|
|
return null;
|
|
}
|
|
|
|
$userSettings = $user['settings'] ?? [];
|
|
|
|
if (empty($settings)) {
|
|
return $flatten ? $this->flattenSettings($userSettings) : $userSettings;
|
|
}
|
|
|
|
// Specific keys are always returned in dot-notation (already flat)
|
|
$result = [];
|
|
foreach ($settings as $key) {
|
|
$parts = explode('.', $key);
|
|
$value = $userSettings;
|
|
foreach ($parts as $part) {
|
|
if (!is_array($value) || !array_key_exists($part, $value)) {
|
|
$value = null;
|
|
break;
|
|
}
|
|
$value = $value[$part];
|
|
}
|
|
$result[$key] = $value;
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Recursively flatten a nested settings array into dot-notation keys.
|
|
*/
|
|
private function flattenSettings(array $settings, string $prefix = ''): array
|
|
{
|
|
$result = [];
|
|
foreach ($settings as $key => $value) {
|
|
$fullKey = $prefix !== '' ? "{$prefix}.{$key}" : (string) $key;
|
|
if (is_array($value)) {
|
|
$result = array_merge($result, $this->flattenSettings($value, $fullKey));
|
|
} else {
|
|
$result[$fullKey] = $value;
|
|
}
|
|
}
|
|
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('user_accounts')->updateOne(
|
|
['tid' => $tenant, 'uid' => $uid],
|
|
['$set' => $updates]
|
|
);
|
|
|
|
// Return true if document was matched (exists), even if not modified
|
|
return $result->getMatchedCount() > 0;
|
|
}
|
|
|
|
} |