File size: 3,677 Bytes
b2dcf0f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
<?php

namespace App\Domain\Config;

use App\Domain\Audit\AuditService;
use App\Infrastructure\Ids\UuidGenerator;
use App\Shared\Exceptions\ApiException;

class ConfigService
{
    public function __construct(
        private readonly ConfigRepository $settings,
        private readonly UuidGenerator $ids,
        private readonly AuditService $audit,
    ) {
    }

    public function getNamespace(string $namespace): array
    {
        return [
            'namespace' => $namespace,
            'settings' => array_map(fn (array $row): array => $this->payload($row), $this->settings->listNamespace($namespace)),
        ];
    }

    public function update(string $namespace, string $key, mixed $value, string $actorUserId): array
    {
        $this->validateValue($namespace, $key, $value);

        $id = $this->settings->createVersion($this->ids->generate(), $namespace, $key, $value);
        $this->audit->record('CONFIG_SETTING_UPDATED', $actorUserId, null, 'configuration_settings', $id, 'SUCCESS', [
            'namespace' => $namespace,
            'key' => $key,
        ]);

        return ['setting' => $this->payload((array) $this->settings->latest($namespace, $key))];
    }

    public function publish(string $namespace, string $key, string $actorUserId): array
    {
        $setting = $this->settings->latest($namespace, $key);

        if ($setting === null) {
            throw new ApiException('CONFIG_NOT_FOUND', 'Configuration setting was not found.', [], 404);
        }

        $this->settings->publish($setting->id, $actorUserId);
        $this->audit->record('CONFIG_SETTING_PUBLISHED', $actorUserId, null, 'configuration_settings', $setting->id, 'SUCCESS', [
            'namespace' => $namespace,
            'key' => $key,
        ]);

        return ['setting' => $this->payload((array) $this->settings->latest($namespace, $key))];
    }

    public function versions(string $namespace, string $key): array
    {
        return [
            'namespace' => $namespace,
            'key' => $key,
            'versions' => array_map(fn (array $row): array => $this->payload($row), $this->settings->versions($namespace, $key)),
        ];
    }

    private function payload(array $row): array
    {
        return [
            'id' => $row['id'],
            'namespace' => $row['namespace'],
            'key' => $row['setting_key'],
            'value' => json_decode($row['setting_value'], true, flags: JSON_THROW_ON_ERROR),
            'status' => $row['status'],
            'version_number' => (int) $row['version_number'],
            'published_at' => $row['published_at'],
            'updated_at' => $row['updated_at'],
        ];
    }

    private function validateValue(string $namespace, string $key, mixed $value): void
    {
        if (str_contains(strtolower($key), 'secret') || str_contains(strtolower($key), 'password')) {
            throw new ApiException('SECRET_VALUE_NOT_ALLOWED', 'Raw secrets must not be stored in configuration settings.', [], 422);
        }

        if (str_starts_with($namespace.'.'.$key, 'fees.') && (! is_numeric($value) || (float) $value < 0)) {
            throw new ApiException('INVALID_CONFIG_VALUE', 'Fee values must be non-negative numbers.', [], 422);
        }

        if (str_contains($key, 'days') && (! is_int($value) || $value < 0)) {
            throw new ApiException('INVALID_CONFIG_VALUE', 'Day values must be non-negative integers.', [], 422);
        }

        if (str_contains($key, 'minutes') && (! is_int($value) || $value < 1)) {
            throw new ApiException('INVALID_CONFIG_VALUE', 'Minute values must be positive integers.', [], 422);
        }
    }
}