Files
Lesstime/src/Service/TokenEncryptor.php
matthieu e4fc34b90f refactor : simplify codebase and fix critical issues
Backend:
- Add MCP Serializer to centralize entity-to-array conversion (~300 lines deduped)
- Fix race condition in task/ticket number generation (SELECT FOR UPDATE + transaction)
- Add unique constraint on task (project_id, number) with migration
- Fix MIME type validation: use server-detected finfo instead of client-supplied type
- Add allowlist of permitted MIME types for uploads
- Fix TaskDocumentDownloadController: allow ROLE_CLIENT access, add priority:1
- Fix notification sent even when ticket status unchanged
- Remove redundant exception constructors
- Simplify services (BookStackApi double fetch, TokenEncryptor, GiteaApi)
- Consolidate duplicate checks in processors

Frontend:
- Fix useApi isHandlingUnauthorized scope (module-level to prevent double 401 redirect)
- Fix client-tickets toast key copy-paste bug
- Merge duplicated tasks service methods (getByProject + getByProjectArchived)
- Extract shared uploadWithRelation helper in task-documents service
- Extract formatFileSize utility from duplicated component code
- Extract status transition logic into useClientTicketHelpers composable
- Remove dead code (unused router, handleLogout, empty script blocks)
- Merge duplicate watchers and onMounted calls
- Normalize arrow functions to function declarations per convention

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 22:09:16 +01:00

79 lines
2.0 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Service;
use RuntimeException;
use SodiumException;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
final class TokenEncryptor
{
private readonly string $key;
private readonly bool $configured;
public function __construct(
#[Autowire('%env(ENCRYPTION_KEY)%')]
string $encryptionKey,
) {
$key = $this->tryDecodeKey($encryptionKey);
$this->key = $key ?? '';
$this->configured = null !== $key;
}
public function encrypt(string $plaintext): string
{
$this->assertConfigured();
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$ciphertext = sodium_crypto_secretbox($plaintext, $nonce, $this->key);
return sodium_bin2hex($nonce.$ciphertext);
}
public function decrypt(string $encrypted): string
{
$this->assertConfigured();
$decoded = sodium_hex2bin($encrypted);
$nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit');
$ciphertext = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit');
$plaintext = sodium_crypto_secretbox_open($ciphertext, $nonce, $this->key);
if (false === $plaintext) {
throw new RuntimeException('Failed to decrypt token.');
}
return $plaintext;
}
private function tryDecodeKey(string $encryptionKey): ?string
{
if ('' === $encryptionKey) {
return null;
}
try {
$key = sodium_hex2bin($encryptionKey);
} catch (SodiumException) {
return null;
}
if (SODIUM_CRYPTO_SECRETBOX_KEYBYTES !== mb_strlen($key, '8bit')) {
return null;
}
return $key;
}
private function assertConfigured(): void
{
if (!$this->configured) {
throw new RuntimeException('Encryption is not configured. Please set a valid ENCRYPTION_KEY.');
}
}
}