refactor(frontend) : make page headers and filters sticky across all pages
Wrap title + filters in a sticky container (top-8 sm:top-12, z-20, bg-white) on all pages for consistent scroll behavior. Also fix SidebarTimer icon visibility when sidebar is collapsed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ use Symfony\Component\String\Slugger\AsciiSlugger;
|
||||
use Symfony\Component\String\Slugger\SluggerInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
use Throwable;
|
||||
|
||||
final readonly class GiteaApiService
|
||||
{
|
||||
@@ -203,7 +204,11 @@ final readonly class GiteaApiService
|
||||
throw new GiteaApiException('Gitea token is not set.');
|
||||
}
|
||||
|
||||
return $this->tokenEncryptor->decrypt($encrypted);
|
||||
try {
|
||||
return $this->tokenEncryptor->decrypt($encrypted);
|
||||
} catch (Throwable $e) {
|
||||
throw new GiteaApiException('Failed to decrypt Gitea token: '.$e->getMessage(), 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
private function assertProjectHasRepo(Project $project): void
|
||||
|
||||
@@ -4,31 +4,51 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use App\Exception\GiteaApiException;
|
||||
use RuntimeException;
|
||||
use SodiumException;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
|
||||
final readonly class TokenEncryptor
|
||||
final class TokenEncryptor
|
||||
{
|
||||
private string $key;
|
||||
private readonly string $key;
|
||||
private readonly bool $configured;
|
||||
|
||||
public function __construct(
|
||||
#[Autowire('%env(GITEA_ENCRYPTION_KEY)%')]
|
||||
string $encryptionKey,
|
||||
) {
|
||||
if ('' === $encryptionKey) {
|
||||
throw new InvalidArgumentException('GITEA_ENCRYPTION_KEY environment variable must be set.');
|
||||
$this->key = '';
|
||||
$this->configured = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->key = sodium_hex2bin($encryptionKey);
|
||||
try {
|
||||
$key = sodium_hex2bin($encryptionKey);
|
||||
} catch (SodiumException) {
|
||||
$this->key = '';
|
||||
$this->configured = false;
|
||||
|
||||
if (SODIUM_CRYPTO_SECRETBOX_KEYBYTES !== mb_strlen($this->key, '8bit')) {
|
||||
throw new InvalidArgumentException('GITEA_ENCRYPTION_KEY must be a valid sodium secret box key.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (SODIUM_CRYPTO_SECRETBOX_KEYBYTES !== mb_strlen($key, '8bit')) {
|
||||
$this->key = '';
|
||||
$this->configured = false;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->key = $key;
|
||||
$this->configured = true;
|
||||
}
|
||||
|
||||
public function encrypt(string $plaintext): string
|
||||
{
|
||||
$this->assertConfigured();
|
||||
|
||||
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
|
||||
$ciphertext = sodium_crypto_secretbox($plaintext, $nonce, $this->key);
|
||||
|
||||
@@ -37,6 +57,8 @@ final readonly class TokenEncryptor
|
||||
|
||||
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');
|
||||
@@ -49,4 +71,11 @@ final readonly class TokenEncryptor
|
||||
|
||||
return $plaintext;
|
||||
}
|
||||
|
||||
private function assertConfigured(): void
|
||||
{
|
||||
if (!$this->configured) {
|
||||
throw new GiteaApiException('Gitea encryption is not configured. Please set a valid GITEA_ENCRYPTION_KEY.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user