refactor: enforce layer discipline — move repositories, fix constructor injection
- Move HelpdeskTenantSettingsRepository and HelpdeskTokenRepository from Service/ to Repository/ directory (GR-SEC-003: SQL only in repositories) - Make AccessControl constructor require IntendedUrlService explicitly instead of falling back to `new IntendedUrlService()` (GR-TEST-002: no service instantiation outside factories) - Update all imports and tests accordingly Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Module\Helpdesk\Repository;
|
||||
|
||||
use MintyPHP\DB;
|
||||
|
||||
/**
|
||||
* Repository for per-tenant helpdesk BC connection overrides (helpdesk_tenant_settings table).
|
||||
*
|
||||
* All queries are scoped by tenant_id. No cross-tenant access is possible.
|
||||
*/
|
||||
class HelpdeskTenantSettingsRepository
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function findByTenantId(int $tenantId): ?array
|
||||
{
|
||||
if ($tenantId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = DB::selectOne(
|
||||
'SELECT * FROM helpdesk_tenant_settings WHERE tenant_id = ? LIMIT 1',
|
||||
(string) $tenantId
|
||||
);
|
||||
|
||||
if (!is_array($result) || $result === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// MintyPHP wraps selectOne results in ['table_name' => [...]]
|
||||
return $result['helpdesk_tenant_settings'] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
public function upsert(int $tenantId, array $data): bool
|
||||
{
|
||||
if ($tenantId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$existing = $this->findByTenantId($tenantId);
|
||||
|
||||
if ($existing !== null) {
|
||||
return $this->update($tenantId, $data);
|
||||
}
|
||||
|
||||
return $this->insert($tenantId, $data);
|
||||
}
|
||||
|
||||
public function deleteByTenantId(int $tenantId): bool
|
||||
{
|
||||
if ($tenantId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool) DB::delete(
|
||||
'DELETE FROM helpdesk_tenant_settings WHERE tenant_id = ?',
|
||||
(string) $tenantId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
private function insert(int $tenantId, array $data): bool
|
||||
{
|
||||
$row = $this->buildRow($data);
|
||||
|
||||
return (bool) DB::insert(
|
||||
'INSERT INTO helpdesk_tenant_settings (tenant_id, override_enabled, auth_mode, odata_base_url, company_name, basic_user, basic_password_enc, oauth_tenant_id, oauth_client_id, oauth_client_secret_enc, oauth_token_endpoint, system_recommendations_config, controlling_risk_config) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
(string) $tenantId,
|
||||
$row['override_enabled'],
|
||||
$row['auth_mode'],
|
||||
$row['odata_base_url'],
|
||||
$row['company_name'],
|
||||
$row['basic_user'],
|
||||
$row['basic_password_enc'],
|
||||
$row['oauth_tenant_id'],
|
||||
$row['oauth_client_id'],
|
||||
$row['oauth_client_secret_enc'],
|
||||
$row['oauth_token_endpoint'],
|
||||
$row['system_recommendations_config'],
|
||||
$row['controlling_risk_config']
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update only the columns present in $data — leaves other columns untouched.
|
||||
* This prevents overwriting secrets (basic_password_enc, oauth_client_secret_enc)
|
||||
* when they are not explicitly included in the save payload.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
*/
|
||||
private function update(int $tenantId, array $data): bool
|
||||
{
|
||||
$allowed = self::allowedColumns();
|
||||
$setClauses = [];
|
||||
$params = [];
|
||||
|
||||
foreach ($allowed as $col) {
|
||||
if (!array_key_exists($col, $data)) {
|
||||
continue;
|
||||
}
|
||||
$setClauses[] = "{$col} = ?";
|
||||
$params[] = (string) ($data[$col] ?? '');
|
||||
}
|
||||
|
||||
if ($setClauses === []) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$params[] = (string) $tenantId;
|
||||
|
||||
return (bool) DB::update(
|
||||
'UPDATE helpdesk_tenant_settings SET ' . implode(', ', $setClauses) . ' WHERE tenant_id = ?',
|
||||
...$params
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a complete row for INSERT, using empty strings for missing columns.
|
||||
*
|
||||
* @param array<string, mixed> $data
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function buildRow(array $data): array
|
||||
{
|
||||
$row = [];
|
||||
foreach (self::allowedColumns() as $col) {
|
||||
$row[$col] = (string) ($data[$col] ?? '');
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private static function allowedColumns(): array
|
||||
{
|
||||
return [
|
||||
'override_enabled',
|
||||
'auth_mode',
|
||||
'odata_base_url',
|
||||
'company_name',
|
||||
'basic_user',
|
||||
'basic_password_enc',
|
||||
'oauth_tenant_id',
|
||||
'oauth_client_id',
|
||||
'oauth_client_secret_enc',
|
||||
'oauth_token_endpoint',
|
||||
'system_recommendations_config',
|
||||
'controlling_risk_config',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Module\Helpdesk\Repository;
|
||||
|
||||
use MintyPHP\DB;
|
||||
use MintyPHP\Service\Settings\SettingsCryptoGatewayInterface;
|
||||
|
||||
/**
|
||||
* Repository for OAuth2 token cache (helpdesk_oauth_token_cache table).
|
||||
*
|
||||
* All tokens are stored encrypted and tenant-scoped.
|
||||
* This is part of the OAuth2 skeleton for V1.
|
||||
*/
|
||||
class HelpdeskTokenRepository
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SettingsCryptoGatewayInterface $settingsCryptoGateway
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a valid (non-expired) cached token for the given tenant.
|
||||
*/
|
||||
public function findValidToken(int $tenantId): ?string
|
||||
{
|
||||
if ($tenantId <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = DB::selectOne(
|
||||
'SELECT access_token_encrypted FROM helpdesk_oauth_token_cache WHERE tenant_id = ? AND expires_at > NOW() ORDER BY created_at DESC LIMIT 1',
|
||||
(string) $tenantId
|
||||
);
|
||||
|
||||
// MintyPHP wraps selectOne results in ['table_name' => [...]]
|
||||
$row = is_array($result) ? ($result['helpdesk_oauth_token_cache'] ?? $result) : null;
|
||||
if (!is_array($row) || !isset($row['access_token_encrypted'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$encrypted = trim((string) $row['access_token_encrypted']);
|
||||
if ($encrypted === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return $this->settingsCryptoGateway->decryptString($encrypted);
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all cached tokens (used when global BC config changes).
|
||||
*/
|
||||
public function deleteAll(): bool
|
||||
{
|
||||
return (bool) DB::delete('DELETE FROM helpdesk_oauth_token_cache WHERE 1=1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete cached tokens for a specific tenant (used when tenant config changes).
|
||||
*/
|
||||
public function deleteByTenantId(int $tenantId): bool
|
||||
{
|
||||
if ($tenantId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (bool) DB::delete(
|
||||
'DELETE FROM helpdesk_oauth_token_cache WHERE tenant_id = ?',
|
||||
(string) $tenantId
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a new token in the cache (encrypted, tenant-scoped).
|
||||
*/
|
||||
public function storeToken(int $tenantId, string $accessToken, \DateTimeInterface $expiresAt): bool
|
||||
{
|
||||
if ($tenantId <= 0 || $accessToken === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$encrypted = $this->settingsCryptoGateway->encryptString($accessToken);
|
||||
} catch (\Throwable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove existing tokens for this tenant first
|
||||
DB::delete(
|
||||
'DELETE FROM helpdesk_oauth_token_cache WHERE tenant_id = ?',
|
||||
(string) $tenantId
|
||||
);
|
||||
|
||||
return (bool) DB::insert(
|
||||
'INSERT INTO helpdesk_oauth_token_cache (tenant_id, access_token_encrypted, expires_at) VALUES (?, ?, ?)',
|
||||
(string) $tenantId,
|
||||
$encrypted,
|
||||
$expiresAt->format('Y-m-d H:i:s')
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user