Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docker/nginx/default.conf
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ server {
client_body_timeout 300s;
client_header_timeout 300s;

# Disallow directory listing for /uploads (avoids "directory index forbidden" in error log)
location = /uploads { return 404; }
location = /uploads/ { return 404; }

location / {
try_files $uri /index.php$is_args$args;
}
Expand Down
36 changes: 36 additions & 0 deletions migrations/Version20250312100000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Add validation_pattern and validation_normalizer to setting table.
* When null/empty, accept-all regex is used so existing settings are unaffected.
*/
final class Version20250312100000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add validation_pattern and validation_normalizer to setting table';
}

public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE setting ADD validation_pattern VARCHAR(500) DEFAULT NULL');
$this->addSql('ALTER TABLE setting ADD validation_normalizer VARCHAR(100) DEFAULT NULL');
$this->addSql(
"UPDATE setting SET validation_pattern = '#^(?i)(light|dark|auto)$#', validation_normalizer = 'strtolower' WHERE name = 'theme_default_mode'"
);
}

public function down(Schema $schema): void
{
$this->addSql("UPDATE setting SET validation_pattern = NULL, validation_normalizer = NULL WHERE name = 'theme_default_mode'");
$this->addSql('ALTER TABLE setting DROP validation_pattern');
$this->addSql('ALTER TABLE setting DROP validation_normalizer');
}
}
25 changes: 25 additions & 0 deletions public/assets/css/dark-mode-bootstrap.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* PteroCA shared dark mode — Bootstrap variable overrides when data-bs-theme="dark".
* Load this when a theme has "forceDarkMode": true so .bg-light, .card, body, etc. stay dark
* without each theme redefining every override.
*/
[data-bs-theme="dark"] {
--bs-body-bg: #0d0d0d;
--bs-body-color: #dee2e6;
--bs-light-rgb: 33, 37, 41;
--bs-card-bg: #212529;
--bs-card-color: #dee2e6;
--bs-border-color: rgba(255, 255, 255, 0.15);
--bs-border-color-translucent: rgba(255, 255, 255, 0.15);
}

[data-bs-theme="dark"] .card,
[data-bs-theme="dark"] .card .card-body {
background-color: var(--bs-card-bg) !important;
color: var(--bs-card-color);
border-color: var(--bs-border-color);
}

[data-bs-theme="dark"] .bg-light {
background-color: rgba(33, 37, 41, 1) !important;
}
23 changes: 20 additions & 3 deletions src/Core/Controller/Panel/DashboardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@
use EasyCorp\Bundle\EasyAdminBundle\Config\Assets;
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;
use EasyCorp\Bundle\EasyAdminBundle\Config\Option\ColorScheme;
use EasyCorp\Bundle\EasyAdminBundle\Config\UserMenu;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
use Symfony\Component\HttpFoundation\RequestStack;
Expand Down Expand Up @@ -157,8 +156,10 @@ public function configureDashboard(): Dashboard
$disableDarkMode = !$currentTemplateOptions->isSupportDarkMode()
|| $this->settingService->getSetting(SettingEnum::THEME_DISABLE_DARK_MODE->value);
$defaultMode = $disableDarkMode
? ColorScheme::LIGHT
: $this->settingService->getSetting(SettingEnum::THEME_DEFAULT_MODE->value);
? 'light'
: $this->normalizeColorSchemeSetting(
$this->settingService->getSetting(SettingEnum::THEME_DEFAULT_MODE->value)
);

return Dashboard::new()
->setTitle($logo)
Expand Down Expand Up @@ -254,4 +255,20 @@ private function generateSettingsUrl(SettingContextEnum $context): string
'crudControllerFqcn' => $crudFqcn,
]);
}

/**
* Normalise theme_default_mode setting to a valid value for setDefaultColorScheme.
* Accepts only "light", "dark", "auto" (case-insensitive); defaults to light when invalid.
*
* @return 'light'|'dark'|'auto'
*/
private function normalizeColorSchemeSetting(?string $value): string
{
$normalized = strtolower(trim((string) $value));
return match ($normalized) {
'dark' => 'dark',
'auto' => 'auto',
default => 'light',
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ abstract class AbstractSettingCrudController extends AbstractPanelController
{
use CrudFlashMessagesTrait;

private const ACCEPT_ALL_PATTERN = '#^.*$#';

protected bool $useConventionBasedPermissions = false;

protected ?Setting $currentEntity = null;
Expand Down Expand Up @@ -173,6 +175,17 @@ public function configureFields(string $pageName): iterable
->hideOnIndex();
}

$fields[] = TextField::new('validationPattern', $this->translator->trans('pteroca.crud.setting.validation_pattern'))
->setHelp($this->translator->trans('pteroca.crud.setting.validation_pattern_help'))
->setRequired(false)
->setColumns(6)
->hideOnIndex();
$fields[] = TextField::new('validationNormalizer', $this->translator->trans('pteroca.crud.setting.validation_normalizer'))
->setHelp($this->translator->trans('pteroca.crud.setting.validation_normalizer_help'))
->setRequired(false)
->setColumns(6)
->hideOnIndex();

$fields[] = ChoiceField::new('context', $this->translator->trans('pteroca.crud.setting.context'))
->setChoices(SettingContextEnum::getValues())
->setRequired(true)
Expand Down Expand Up @@ -280,6 +293,7 @@ public function persistEntity(EntityManagerInterface $entityManager, $entityInst
{
try {
$this->handleSetAsEmpty($entityInstance);
$this->normalizeAndValidateSettingValue($entityInstance);
$this->validateSettingValue($entityInstance);
$this->settingService->saveSettingInCache($entityInstance->getName(), $entityInstance->getValue());
parent::persistEntity($entityManager, $entityInstance);
Expand All @@ -295,6 +309,7 @@ public function updateEntity(EntityManagerInterface $entityManager, $entityInsta
{
try {
$this->handleSetAsEmpty($entityInstance);
$this->normalizeAndValidateSettingValue($entityInstance);
$this->validateSettingValue($entityInstance);
$this->settingService->saveSettingInCache($entityInstance->getName(), $entityInstance->getValue());
parent::updateEntity($entityManager, $entityInstance);
Expand Down Expand Up @@ -375,6 +390,48 @@ private function getSelectOptions(?string $settingName): array
return $this->settingOptionRepository->getOptionsForSetting($settingName);
}

/**
* Apply normaliser from entity (if set) and validate value against entity's validation_pattern.
* When pattern is null/empty, use accept-all pattern so any value passes.
*/
private function normalizeAndValidateSettingValue(Setting $setting): void
{
$value = $setting->getValue();
$normalizerName = $setting->getValidationNormalizer();
if ($normalizerName !== null && $normalizerName !== '') {
$value = $this->applyNormalizer((string) $value, trim($normalizerName));
$setting->setValue($value);
}
$pattern = $setting->getValidationPattern();
if ($pattern === null || trim($pattern) === '') {
$pattern = self::ACCEPT_ALL_PATTERN;
}
$valueToCheck = $setting->getValue() ?? '';
if (!preg_match($pattern, $valueToCheck)) {
throw new \InvalidArgumentException(
$this->translator->trans('pteroca.crud.setting.validation_pattern_mismatch')
);
}
}

/**
* Apply a known normaliser by name (e.g. strtolower, trim). Comma-separated names applied in order.
*/
private function applyNormalizer(string $value, string $normalizerName): string
{
$names = array_map('trim', explode(',', $normalizerName));
$normalizers = [
'strtolower' => fn (string $v): string => strtolower($v),
'trim' => fn (string $v): string => trim($v),
];
foreach ($names as $name) {
if (isset($normalizers[$name])) {
$value = $normalizers[$name]($value);
}
}
return $value;
}

private function validateSettingValue(Setting $setting): void
{
if ($setting->getName() === 'minimum_topup_amount') {
Expand Down
7 changes: 7 additions & 0 deletions src/Core/DTO/TemplateOptionsDTO.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
public function __construct(
private bool $supportDarkMode,
private bool $supportCustomColors,
private bool $forceDarkMode = false,
) {}

public function isSupportDarkMode(): bool
Expand All @@ -18,4 +19,10 @@ public function isSupportCustomColors(): bool
{
return $this->supportCustomColors;
}

/** When true, the panel is forced to Bootstrap dark mode (data-bs-theme="dark") and shared dark variables apply. */
public function isForceDarkMode(): bool
{
return $this->forceDarkMode;
}
}
2 changes: 1 addition & 1 deletion src/Core/Entity/Log.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class Log extends AbstractEntity
#[ORM\Column(type: "datetime")]
private DateTimeInterface $createdAt;

#[ORM\ManyToOne(targetEntity: User::class)]
#[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'logs')]
#[ORM\JoinColumn(nullable: false)]
private UserInterface $user;

Expand Down
28 changes: 28 additions & 0 deletions src/Core/Entity/Setting.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ class Setting extends AbstractEntity
#[ORM\Column(type: "boolean", options: ["default" => false])]
private bool $nullable = false;

#[ORM\Column(length: 500, nullable: true)]
private ?string $validationPattern = null;

#[ORM\Column(length: 100, nullable: true)]
private ?string $validationNormalizer = null;

public function getId(): int
{
return $this->id;
Expand Down Expand Up @@ -102,6 +108,28 @@ public function setNullable(bool $nullable): self
return $this;
}

public function getValidationPattern(): ?string
{
return $this->validationPattern;
}

public function setValidationPattern(?string $validationPattern): self
{
$this->validationPattern = $validationPattern;
return $this;
}

public function getValidationNormalizer(): ?string
{
return $this->validationNormalizer;
}

public function setValidationNormalizer(?string $validationNormalizer): self
{
$this->validationNormalizer = $validationNormalizer;
return $this;
}

public function __toString(): string
{
return $this->name;
Expand Down
6 changes: 3 additions & 3 deletions src/Core/Entity/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class User extends AbstractEntity implements UserInterface
private array $roles = [];

#[ORM\Column(type: "decimal", precision: 10, scale: 2)]
private float $balance = 0;
private string $balance = '0.00';

#[ORM\Column(type: 'string', length: 255)]
private string $name;
Expand Down Expand Up @@ -193,12 +193,12 @@ public function setPassword(string $password): static

public function getBalance(): float
{
return $this->balance;
return (float) $this->balance;
}

public function setBalance(float $balance): static
{
$this->balance = $balance;
$this->balance = (string) $balance;

return $this;
}
Expand Down
5 changes: 5 additions & 0 deletions src/Core/Resources/translations/messages.en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1134,6 +1134,11 @@ pteroca:
hint: 'Description'
set_as_empty: 'Set as empty'
set_as_empty_help: 'Check this to set the value as empty (null).'
validation_pattern: 'Validation pattern (regex)'
validation_pattern_help: 'Optional. PCRE regex the value must match (e.g. #^(light|dark|auto)$#). Leave empty to accept any value.'
validation_normalizer: 'Normalizer'
validation_normalizer_help: 'Optional. Apply before validation: strtolower, trim (comma-separated).'
validation_pattern_mismatch: 'The value does not match the required format for this setting.'
updated_successfully: 'Setting updated successfully.'
created_successfully: 'Setting created successfully.'
create_error: 'An error occurred while creating the setting: %error%'
Expand Down
1 change: 1 addition & 0 deletions src/Core/Service/Template/TemplateManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public function getCurrentTemplateOptions(): TemplateOptionsDTO
return new TemplateOptionsDTO(
$this->currentTemplateMetadata['options']['supportDarkMode'] ?? false,
$this->currentTemplateMetadata['options']['supportCustomColors'] ?? false,
$this->currentTemplateMetadata['options']['forceDarkMode'] ?? false,
);
}

Expand Down
8 changes: 4 additions & 4 deletions src/Core/Trait/ProductPriceEntityTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ trait ProductPriceEntityTrait
private ProductPriceUnitEnum $unit;

#[ORM\Column(type: "decimal", precision: 10, scale: 2)]
private float $price;
private string $price = '0.00';

#[ORM\Column(type: "datetime", nullable: true)]
private ?DateTime $deletedAt = null;
Expand Down Expand Up @@ -69,12 +69,12 @@ public function setUnit(ProductPriceUnitEnum $unit): self

public function getPrice(): float
{
return $this->price;
return (float) $this->price;
}

public function setPrice(float $price): self
{
$this->price = $price;
$this->price = (string) $price;
return $this;
}

Expand All @@ -101,7 +101,7 @@ public function __toString(): string
'%d %s: %.2f',
$this->value,
$unit,
$this->price,
(float) $this->price,
);
}
}
7 changes: 5 additions & 2 deletions themes/default/panel/bundles/EasyAdminBundle/layout.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
{% block head_stylesheets %}
<link rel="stylesheet" href="{{ template_asset('fonts/fonts.css') }}">
<link rel="stylesheet" href="{{ asset('app.css', ea.assets.defaultAssetPackageName) }}">
{% if get_current_template_options().isForceDarkMode() %}
<link rel="stylesheet" href="{{ asset('assets/css/dark-mode-bootstrap.css') }}">
{% endif %}
{% if get_current_template_options().isSupportCustomColors() %}
{% include 'components/theme_styles.html.twig' %}
{% endif %}
Expand Down Expand Up @@ -61,12 +64,12 @@
</head>

{% block body %}
<body {% block body_attr %}{% endblock %}
<body {% block body_attr %}{% if get_current_template_options().isForceDarkMode() %}data-bs-theme="dark" {% endif %}{% endblock %}
id="{% block body_id %}{% endblock %}"
class="ea {% block body_class %}{% endblock %}"
data-ea-content-width="{{ ea.crud.contentWidth ?? ea.dashboardContentWidth ?? 'normal' }}"
data-ea-sidebar-width="{{ ea.crud.sidebarWidth ?? ea.dashboardSidebarWidth ?? 'normal' }}"
data-ea-dark-scheme-is-enabled="{{ ea.dashboardHasDarkModeEnabled ? 'true' : 'false' }}"
data-ea-dark-scheme-is-enabled="{{ get_current_template_options().isForceDarkMode() ? 'true' : (ea.dashboardHasDarkModeEnabled ? 'true' : 'false') }}"
>
{% block javascript_page_layout %}
<script src="{{ asset('page-layout.js', ea.assets.defaultAssetPackageName) }}"></script>
Expand Down
4 changes: 2 additions & 2 deletions themes/default/panel/dashboard/components/motd.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<div class="card-header">
<div class="d-flex align-items-center">
<i class="fas fa-bullhorn me-2"></i>
<h5 class="mb-0 fw-semibold">{{ motdTitle|raw }}</h5>
<h5 class="mb-0 fw-semibold">{{ motdTitle }}</h5>
</div>
</div>
<div class="card-body p-4">
Expand All @@ -12,7 +12,7 @@
<i class="fas fa-info-circle motd-icon" style="color: var(--stripe-blue-600);"></i>
</div>
<div class="flex-grow-1 motd-message" style="color: var(--stripe-gray-700);">
{{ motdMessage|raw }}
{{ motdMessage }}
</div>
</div>
</div>
Expand Down
Loading