From a25513575819637d4e0d63af3467b9919a34913b Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Thu, 16 Apr 2026 10:10:19 +0200 Subject: [PATCH 1/3] Removed all user, group and access grants logic --- CHANGELOG.md | 4 +- README.md | 12 +- assets/controllers/tabs_controller.js | 35 --- migrations/Version20260306143859.php | 35 --- migrations/Version20260309094222.php | 49 --- migrations/Version20260316140458.php | 41 --- ...16134624.php => Version20260416071816.php} | 8 +- src/Command/SyncOpenWebUiCommand.php | 2 +- src/Controller/DashboardController.php | 49 +-- src/Entity/AccessGrant.php | 114 ------- src/Entity/Group.php | 107 ------- src/Entity/Model.php | 46 --- src/Entity/User.php | 154 --------- src/Repository/AccessGrantRepository.php | 18 -- src/Repository/GroupRepository.php | 18 -- src/Repository/UserRepository.php | 18 -- src/Service/OpenWebUiClient.php | 10 - src/Service/OpenWebUiSyncService.php | 145 +-------- templates/dashboard/index.html.twig | 295 +++--------------- templates/icons/groups.html.twig | 4 - templates/icons/models.html.twig | 4 - templates/icons/users.html.twig | 3 - 22 files changed, 57 insertions(+), 1114 deletions(-) delete mode 100644 assets/controllers/tabs_controller.js delete mode 100644 migrations/Version20260306143859.php delete mode 100644 migrations/Version20260309094222.php delete mode 100644 migrations/Version20260316140458.php rename migrations/{Version20260316134624.php => Version20260416071816.php} (52%) delete mode 100644 src/Entity/AccessGrant.php delete mode 100644 src/Entity/Group.php delete mode 100644 src/Entity/User.php delete mode 100644 src/Repository/AccessGrantRepository.php delete mode 100644 src/Repository/GroupRepository.php delete mode 100644 src/Repository/UserRepository.php delete mode 100644 templates/icons/groups.html.twig delete mode 100644 templates/icons/models.html.twig delete mode 100644 templates/icons/users.html.twig diff --git a/CHANGELOG.md b/CHANGELOG.md index ab33e95..40efbfa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Symfony 7.4 project skeleton with Docker Compose setup - OpenWebUI API sync service supporting multiple sites (production/test) -- CLI command `app:sync-openwebui` for syncing models, users, groups, and access grants -- Dashboard with tabbed views for models, users, and groups +- CLI command `app:sync-openwebui` for syncing models +- Dashboard with models overview - Site selector pills for filtering by site - Health check indicators for configured OpenWebUI instances - Sortable table columns and expandable detail rows via Stimulus controllers diff --git a/README.md b/README.md index 2093781..ac363eb 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # AarhusAI Overview A dashboard application for monitoring and managing -[OpenWebUI](https://github.com/open-webui/open-webui) instances. Syncs models, -users, groups, and access grants from multiple OpenWebUI sites into a local -database and presents them in a unified overview. +[OpenWebUI](https://github.com/open-webui/open-webui) instances. Syncs models +from multiple OpenWebUI sites into a local database and presents them in a +unified overview. ## Requirements @@ -68,8 +68,7 @@ database and presents them in a unified overview. ### Syncing data -Pull models, users, groups, and access grants from all configured OpenWebUI -sites: +Pull models from all configured OpenWebUI sites: ```bash docker compose exec phpfpm bin/console app:sync-openwebui @@ -83,11 +82,10 @@ docker compose exec phpfpm bin/console app:sync-openwebui --site=production ### Dashboard -The web dashboard shows tabbed views for models, users, and groups with: +The web dashboard shows a models overview with: - Site selector pills for filtering by OpenWebUI instance - Sortable table columns -- Expandable detail rows - Health check indicators for configured instances ## Development diff --git a/assets/controllers/tabs_controller.js b/assets/controllers/tabs_controller.js deleted file mode 100644 index 5c82185..0000000 --- a/assets/controllers/tabs_controller.js +++ /dev/null @@ -1,35 +0,0 @@ -import { Controller } from "@hotwired/stimulus"; - -const ACTIVE = [ - "bg-white", - "shadow", - "text-indigo-600", - "border-b-2", - "border-indigo-500", -]; -const INACTIVE = ["text-slate-500", "border-b-2", "border-transparent"]; - -export default class extends Controller { - static targets = ["btn", "panel"]; - - select(event) { - const index = this.btnTargets.indexOf(event.currentTarget); - - this.btnTargets.forEach((btn, i) => { - const isActive = i === index; - btn.setAttribute("aria-selected", isActive); - - if (isActive) { - btn.classList.add(...ACTIVE); - btn.classList.remove(...INACTIVE); - } else { - btn.classList.remove(...ACTIVE); - btn.classList.add(...INACTIVE); - } - }); - - this.panelTargets.forEach((panel, i) => { - panel.style.display = i === index ? "" : "none"; - }); - } -} diff --git a/migrations/Version20260306143859.php b/migrations/Version20260306143859.php deleted file mode 100644 index 9c301aa..0000000 --- a/migrations/Version20260306143859.php +++ /dev/null @@ -1,35 +0,0 @@ -addSql('CREATE TABLE `group` (external_id VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, description LONGTEXT DEFAULT NULL, user_count INT NOT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (external_id)) DEFAULT CHARACTER SET utf8mb4'); - $this->addSql('CREATE TABLE model (external_id VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, owned_by VARCHAR(255) DEFAULT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (external_id)) DEFAULT CHARACTER SET utf8mb4'); - $this->addSql('CREATE TABLE user (external_id VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, role VARCHAR(50) NOT NULL, last_active_at DATETIME DEFAULT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (external_id)) DEFAULT CHARACTER SET utf8mb4'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('DROP TABLE `group`'); - $this->addSql('DROP TABLE model'); - $this->addSql('DROP TABLE user'); - } -} diff --git a/migrations/Version20260309094222.php b/migrations/Version20260309094222.php deleted file mode 100644 index 37fc022..0000000 --- a/migrations/Version20260309094222.php +++ /dev/null @@ -1,49 +0,0 @@ -addSql('CREATE TABLE access_grant (external_id VARCHAR(255) NOT NULL, resource_type VARCHAR(50) NOT NULL, resource_id VARCHAR(255) NOT NULL, principal_type VARCHAR(50) NOT NULL, principal_id VARCHAR(255) NOT NULL, permission VARCHAR(50) NOT NULL, created_at DATETIME DEFAULT NULL, model_external_id VARCHAR(255) NOT NULL, INDEX IDX_20901A1FF177EE5C (model_external_id), PRIMARY KEY (external_id)) DEFAULT CHARACTER SET utf8mb4'); - $this->addSql('CREATE TABLE user_group (user_external_id VARCHAR(255) NOT NULL, group_external_id VARCHAR(255) NOT NULL, INDEX IDX_8F02BF9D366E1E25 (user_external_id), INDEX IDX_8F02BF9DBACDC871 (group_external_id), PRIMARY KEY (user_external_id, group_external_id)) DEFAULT CHARACTER SET utf8mb4'); - $this->addSql('ALTER TABLE access_grant ADD CONSTRAINT FK_20901A1FF177EE5C FOREIGN KEY (model_external_id) REFERENCES model (external_id)'); - $this->addSql('ALTER TABLE user_group ADD CONSTRAINT FK_8F02BF9D366E1E25 FOREIGN KEY (user_external_id) REFERENCES user (external_id)'); - $this->addSql('ALTER TABLE user_group ADD CONSTRAINT FK_8F02BF9DBACDC871 FOREIGN KEY (group_external_id) REFERENCES `group` (external_id)'); - $this->addSql('ALTER TABLE `group` CHANGE user_count member_count INT NOT NULL'); - $this->addSql('ALTER TABLE model ADD base_model_id VARCHAR(255) DEFAULT NULL, ADD description LONGTEXT DEFAULT NULL, ADD system_prompt LONGTEXT DEFAULT NULL, ADD is_active TINYINT NOT NULL, ADD created_at DATETIME DEFAULT NULL, ADD owner_id VARCHAR(255) DEFAULT NULL, DROP owned_by'); - $this->addSql('ALTER TABLE model ADD CONSTRAINT FK_D79572D97E3C61F9 FOREIGN KEY (owner_id) REFERENCES user (external_id)'); - $this->addSql('CREATE INDEX IDX_D79572D97E3C61F9 ON model (owner_id)'); - $this->addSql('ALTER TABLE user ADD username VARCHAR(255) DEFAULT NULL'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE access_grant DROP FOREIGN KEY FK_20901A1FF177EE5C'); - $this->addSql('ALTER TABLE user_group DROP FOREIGN KEY FK_8F02BF9D366E1E25'); - $this->addSql('ALTER TABLE user_group DROP FOREIGN KEY FK_8F02BF9DBACDC871'); - $this->addSql('DROP TABLE access_grant'); - $this->addSql('DROP TABLE user_group'); - $this->addSql('ALTER TABLE `group` CHANGE member_count user_count INT NOT NULL'); - $this->addSql('ALTER TABLE model DROP FOREIGN KEY FK_D79572D97E3C61F9'); - $this->addSql('DROP INDEX IDX_D79572D97E3C61F9 ON model'); - $this->addSql('ALTER TABLE model ADD owned_by VARCHAR(255) DEFAULT NULL, DROP base_model_id, DROP description, DROP system_prompt, DROP is_active, DROP created_at, DROP owner_id'); - $this->addSql('ALTER TABLE user DROP username'); - } -} diff --git a/migrations/Version20260316140458.php b/migrations/Version20260316140458.php deleted file mode 100644 index e7a607a..0000000 --- a/migrations/Version20260316140458.php +++ /dev/null @@ -1,41 +0,0 @@ -addSql("ALTER TABLE access_grant ADD site VARCHAR(50) NOT NULL DEFAULT 'test'"); - $this->addSql("ALTER TABLE `group` ADD site VARCHAR(50) NOT NULL DEFAULT 'test'"); - $this->addSql("ALTER TABLE model ADD site VARCHAR(50) NOT NULL DEFAULT 'test'"); - $this->addSql("ALTER TABLE user ADD site VARCHAR(50) NOT NULL DEFAULT 'test'"); - $this->addSql('ALTER TABLE access_grant ALTER site DROP DEFAULT'); - $this->addSql('ALTER TABLE `group` ALTER site DROP DEFAULT'); - $this->addSql('ALTER TABLE model ALTER site DROP DEFAULT'); - $this->addSql('ALTER TABLE user ALTER site DROP DEFAULT'); - } - - public function down(Schema $schema): void - { - // this down() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE access_grant DROP site'); - $this->addSql('ALTER TABLE `group` DROP site'); - $this->addSql('ALTER TABLE model DROP site'); - $this->addSql('ALTER TABLE user DROP site'); - } -} diff --git a/migrations/Version20260316134624.php b/migrations/Version20260416071816.php similarity index 52% rename from migrations/Version20260316134624.php rename to migrations/Version20260416071816.php index ebf7c9f..31d367e 100644 --- a/migrations/Version20260316134624.php +++ b/migrations/Version20260416071816.php @@ -10,7 +10,7 @@ /** * Auto-generated Migration: Please modify to your needs! */ -final class Version20260316134624 extends AbstractMigration +final class Version20260416071816 extends AbstractMigration { public function getDescription(): string { @@ -20,14 +20,12 @@ public function getDescription(): string public function up(Schema $schema): void { // this up() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE model DROP FOREIGN KEY `FK_D79572D97E3C61F9`'); - $this->addSql('ALTER TABLE model ADD CONSTRAINT FK_D79572D97E3C61F9 FOREIGN KEY (owner_id) REFERENCES user (external_id) ON DELETE SET NULL'); + $this->addSql('CREATE TABLE model (external_id VARCHAR(255) NOT NULL, site VARCHAR(50) NOT NULL, name VARCHAR(255) NOT NULL, base_model_id VARCHAR(255) DEFAULT NULL, description LONGTEXT DEFAULT NULL, system_prompt LONGTEXT DEFAULT NULL, is_active TINYINT NOT NULL, created_at DATETIME DEFAULT NULL, updated_at DATETIME NOT NULL, PRIMARY KEY (external_id)) DEFAULT CHARACTER SET utf8mb4'); } public function down(Schema $schema): void { // this down() migration is auto-generated, please modify it to your needs - $this->addSql('ALTER TABLE model DROP FOREIGN KEY FK_D79572D97E3C61F9'); - $this->addSql('ALTER TABLE model ADD CONSTRAINT `FK_D79572D97E3C61F9` FOREIGN KEY (owner_id) REFERENCES user (external_id)'); + $this->addSql('DROP TABLE model'); } } diff --git a/src/Command/SyncOpenWebUiCommand.php b/src/Command/SyncOpenWebUiCommand.php index e7bbb66..32e9949 100644 --- a/src/Command/SyncOpenWebUiCommand.php +++ b/src/Command/SyncOpenWebUiCommand.php @@ -48,7 +48,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $io->warning(sprintf('[%s] Skipped: %s', $site, $counts['error'])); continue; } - $io->success(sprintf('[%s] Synced %d models, %d users, and %d groups.', $site, $counts['models'], $counts['users'], $counts['groups'])); + $io->success(sprintf('[%s] Synced %d models.', $site, $counts['models'])); } return Command::SUCCESS; diff --git a/src/Controller/DashboardController.php b/src/Controller/DashboardController.php index 4004db0..a349a07 100644 --- a/src/Controller/DashboardController.php +++ b/src/Controller/DashboardController.php @@ -2,9 +2,7 @@ namespace App\Controller; -use App\Entity\Group; use App\Entity\Model; -use App\Entity\User; use App\Service\OpenWebUiClientFactory; use App\Service\OpenWebUiSyncService; use Doctrine\ORM\EntityManagerInterface; @@ -41,57 +39,12 @@ public function index(Request $request, EntityManagerInterface $em, OpenWebUiCli $criteria = null !== $activeSite ? ['site' => $activeSite] : []; $models = $em->getRepository(Model::class)->findBy($criteria); - $users = $em->getRepository(User::class)->findBy($criteria); - $groups = $em->getRepository(Group::class)->findBy($criteria); - - $userMap = []; - foreach ($users as $user) { - $userMap[$user->getExternalId()] = $user->getName(); - } - $groupMap = []; - foreach ($groups as $group) { - $groupMap[$group->getExternalId()] = $group->getName(); - } - - $modelAccessGrants = []; - foreach ($models as $model) { - $grouped = []; - foreach ($model->getAccessGrants() as $grant) { - $key = $grant->getPrincipalType().':'.$grant->getPrincipalId(); - if (!isset($grouped[$key])) { - $grouped[$key] = [ - 'principalType' => $grant->getPrincipalType(), - 'principalId' => $grant->getPrincipalId(), - 'principalName' => match ($grant->getPrincipalType()) { - 'group' => $groupMap[$grant->getPrincipalId()] ?? $grant->getPrincipalId(), - 'user' => $userMap[$grant->getPrincipalId()] ?? $grant->getPrincipalId(), - default => $grant->getPrincipalId(), - }, - 'permissions' => [], - ]; - } - $grouped[$key]['permissions'][] = $grant->getPermission(); - } - - // Sort: groups first, then users - usort($grouped, fn ($a, $b) => ('group' === $a['principalType'] ? 0 : 1) <=> ('group' === $b['principalType'] ? 0 : 1)); - - // Sort permissions within each entry - foreach ($grouped as &$entry) { - sort($entry['permissions']); - } - - $modelAccessGrants[$model->getExternalId()] = $grouped; - } return $this->render('dashboard/index.html.twig', [ 'models' => $models, - 'users' => $users, - 'groups' => $groups, 'siteKeys' => $siteKeys, 'siteHealth' => $siteHealth, 'activeSite' => $activeSite, - 'modelAccessGrants' => $modelAccessGrants, ]); } @@ -111,7 +64,7 @@ public function sync(Request $request, OpenWebUiSyncService $syncService): Respo $this->addFlash('warning', sprintf('[%s] Skipped: %s', $site, $counts['error'])); continue; } - $this->addFlash('success', sprintf('[%s] Synced %d models, %d users, %d groups.', $site, $counts['models'], $counts['users'], $counts['groups'])); + $this->addFlash('success', sprintf('[%s] Synced %d models.', $site, $counts['models'])); } return $this->redirectToRoute('dashboard'); diff --git a/src/Entity/AccessGrant.php b/src/Entity/AccessGrant.php deleted file mode 100644 index c983550..0000000 --- a/src/Entity/AccessGrant.php +++ /dev/null @@ -1,114 +0,0 @@ -externalId = $externalId; - $this->site = $site; - $this->model = $model; - $this->resourceType = $resourceType; - $this->resourceId = $resourceId; - $this->principalType = $principalType; - $this->principalId = $principalId; - $this->permission = $permission; - } - - public function getExternalId(): string - { - return $this->externalId; - } - - public function getSite(): string - { - return $this->site; - } - - public function getModel(): Model - { - return $this->model; - } - - public function setModel(Model $model): void - { - $this->model = $model; - } - - public function getResourceType(): string - { - return $this->resourceType; - } - - public function getResourceId(): string - { - return $this->resourceId; - } - - public function getPrincipalType(): string - { - return $this->principalType; - } - - public function getPrincipalId(): string - { - return $this->principalId; - } - - public function getPermission(): string - { - return $this->permission; - } - - public function getCreatedAt(): ?\DateTimeImmutable - { - return $this->createdAt; - } - - public function setCreatedAt(?\DateTimeImmutable $createdAt): void - { - $this->createdAt = $createdAt; - } -} diff --git a/src/Entity/Group.php b/src/Entity/Group.php deleted file mode 100644 index f52bfc8..0000000 --- a/src/Entity/Group.php +++ /dev/null @@ -1,107 +0,0 @@ - - */ - #[ORM\ManyToMany(targetEntity: User::class, mappedBy: 'groups')] - private Collection $users; - - public function __construct(string $externalId, string $site, string $name, ?string $description = null, int $memberCount = 0) - { - $this->externalId = $externalId; - $this->site = $site; - $this->name = $name; - $this->description = $description; - $this->memberCount = $memberCount; - $this->updatedAt = new \DateTimeImmutable(); - $this->users = new ArrayCollection(); - } - - public function getExternalId(): string - { - return $this->externalId; - } - - public function getSite(): string - { - return $this->site; - } - - public function getName(): string - { - return $this->name; - } - - public function setName(string $name): void - { - $this->name = $name; - } - - public function getDescription(): ?string - { - return $this->description; - } - - public function setDescription(?string $description): void - { - $this->description = $description; - } - - public function getMemberCount(): int - { - return $this->memberCount; - } - - public function setMemberCount(int $memberCount): void - { - $this->memberCount = $memberCount; - } - - public function getUpdatedAt(): \DateTimeImmutable - { - return $this->updatedAt; - } - - public function setUpdatedAt(\DateTimeImmutable $updatedAt): void - { - $this->updatedAt = $updatedAt; - } - - /** - * @return Collection - */ - public function getUsers(): Collection - { - return $this->users; - } -} diff --git a/src/Entity/Model.php b/src/Entity/Model.php index 7a62547..0f7e6e8 100644 --- a/src/Entity/Model.php +++ b/src/Entity/Model.php @@ -3,8 +3,6 @@ namespace App\Entity; use App\Repository\ModelRepository; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity(repositoryClass: ModelRepository::class)] @@ -32,22 +30,12 @@ class Model #[ORM\Column] private bool $isActive = true; - #[ORM\ManyToOne(targetEntity: User::class)] - #[ORM\JoinColumn(name: 'owner_id', referencedColumnName: 'external_id', nullable: true, onDelete: 'SET NULL')] - private ?User $owner = null; - #[ORM\Column(nullable: true)] private ?\DateTimeImmutable $createdAt = null; #[ORM\Column] private \DateTimeImmutable $updatedAt; - /** - * @var Collection - */ - #[ORM\OneToMany(targetEntity: AccessGrant::class, mappedBy: 'model', cascade: ['persist', 'remove'], orphanRemoval: true)] - private Collection $accessGrants; - public function __construct( string $externalId, string $site, @@ -56,7 +44,6 @@ public function __construct( ?string $description = null, ?string $systemPrompt = null, bool $isActive = true, - ?User $owner = null, ) { $this->externalId = $externalId; $this->site = $site; @@ -65,9 +52,7 @@ public function __construct( $this->description = $description; $this->systemPrompt = $systemPrompt; $this->isActive = $isActive; - $this->owner = $owner; $this->updatedAt = new \DateTimeImmutable(); - $this->accessGrants = new ArrayCollection(); } public function getExternalId(): string @@ -130,16 +115,6 @@ public function setIsActive(bool $isActive): void $this->isActive = $isActive; } - public function getOwner(): ?User - { - return $this->owner; - } - - public function setOwner(?User $owner): void - { - $this->owner = $owner; - } - public function getCreatedAt(): ?\DateTimeImmutable { return $this->createdAt; @@ -159,25 +134,4 @@ public function setUpdatedAt(\DateTimeImmutable $updatedAt): void { $this->updatedAt = $updatedAt; } - - /** - * @return Collection - */ - public function getAccessGrants(): Collection - { - return $this->accessGrants; - } - - public function addAccessGrant(AccessGrant $accessGrant): void - { - if (!$this->accessGrants->contains($accessGrant)) { - $this->accessGrants->add($accessGrant); - $accessGrant->setModel($this); - } - } - - public function clearAccessGrants(): void - { - $this->accessGrants->clear(); - } } diff --git a/src/Entity/User.php b/src/Entity/User.php deleted file mode 100644 index 971dc1d..0000000 --- a/src/Entity/User.php +++ /dev/null @@ -1,154 +0,0 @@ - - */ - #[ORM\ManyToMany(targetEntity: Group::class, inversedBy: 'users')] - #[ORM\JoinTable( - name: 'user_group', - joinColumns: [new ORM\JoinColumn(name: 'user_external_id', referencedColumnName: 'external_id')], - inverseJoinColumns: [new ORM\JoinColumn(name: 'group_external_id', referencedColumnName: 'external_id')], - )] - private Collection $groups; - - public function __construct(string $externalId, string $site, string $name, string $email, string $role) - { - $this->externalId = $externalId; - $this->site = $site; - $this->name = $name; - $this->email = $email; - $this->role = $role; - $this->updatedAt = new \DateTimeImmutable(); - $this->groups = new ArrayCollection(); - } - - public function getExternalId(): string - { - return $this->externalId; - } - - public function getSite(): string - { - return $this->site; - } - - public function getName(): string - { - return $this->name; - } - - public function setName(string $name): void - { - $this->name = $name; - } - - public function getUsername(): ?string - { - return $this->username; - } - - public function setUsername(?string $username): void - { - $this->username = $username; - } - - public function getEmail(): string - { - return $this->email; - } - - public function setEmail(string $email): void - { - $this->email = $email; - } - - public function getRole(): string - { - return $this->role; - } - - public function setRole(string $role): void - { - $this->role = $role; - } - - public function getLastActiveAt(): ?\DateTimeImmutable - { - return $this->lastActiveAt; - } - - public function setLastActiveAt(?\DateTimeImmutable $lastActiveAt): void - { - $this->lastActiveAt = $lastActiveAt; - } - - public function getUpdatedAt(): \DateTimeImmutable - { - return $this->updatedAt; - } - - public function setUpdatedAt(\DateTimeImmutable $updatedAt): void - { - $this->updatedAt = $updatedAt; - } - - /** - * @return Collection - */ - public function getGroups(): Collection - { - return $this->groups; - } - - public function addGroup(Group $group): void - { - if (!$this->groups->contains($group)) { - $this->groups->add($group); - } - } - - public function removeGroup(Group $group): void - { - $this->groups->removeElement($group); - } - - public function clearGroups(): void - { - $this->groups->clear(); - } -} diff --git a/src/Repository/AccessGrantRepository.php b/src/Repository/AccessGrantRepository.php deleted file mode 100644 index 8df1305..0000000 --- a/src/Repository/AccessGrantRepository.php +++ /dev/null @@ -1,18 +0,0 @@ - - */ -class AccessGrantRepository extends ServiceEntityRepository -{ - public function __construct(ManagerRegistry $registry) - { - parent::__construct($registry, AccessGrant::class); - } -} diff --git a/src/Repository/GroupRepository.php b/src/Repository/GroupRepository.php deleted file mode 100644 index 283eebd..0000000 --- a/src/Repository/GroupRepository.php +++ /dev/null @@ -1,18 +0,0 @@ - - */ -class GroupRepository extends ServiceEntityRepository -{ - public function __construct(ManagerRegistry $registry) - { - parent::__construct($registry, Group::class); - } -} diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php deleted file mode 100644 index da4b8be..0000000 --- a/src/Repository/UserRepository.php +++ /dev/null @@ -1,18 +0,0 @@ - - */ -class UserRepository extends ServiceEntityRepository -{ - public function __construct(ManagerRegistry $registry) - { - parent::__construct($registry, User::class); - } -} diff --git a/src/Service/OpenWebUiClient.php b/src/Service/OpenWebUiClient.php index c96bba0..bbda465 100644 --- a/src/Service/OpenWebUiClient.php +++ b/src/Service/OpenWebUiClient.php @@ -18,16 +18,6 @@ public function fetchModels(): array return $this->request('/api/v1/models/list')['items'] ?? []; } - public function fetchUsers(): array - { - return $this->request('/api/v1/users/')['users'] ?? []; - } - - public function fetchGroups(): array - { - return $this->request('/api/v1/groups/'); - } - public function isHealthy(): bool { try { diff --git a/src/Service/OpenWebUiSyncService.php b/src/Service/OpenWebUiSyncService.php index cb78e86..1fd823a 100644 --- a/src/Service/OpenWebUiSyncService.php +++ b/src/Service/OpenWebUiSyncService.php @@ -2,10 +2,7 @@ namespace App\Service; -use App\Entity\AccessGrant; -use App\Entity\Group; use App\Entity\Model; -use App\Entity\User; use Doctrine\ORM\EntityManagerInterface; final class OpenWebUiSyncService @@ -17,7 +14,7 @@ public function __construct( } /** - * @return array + * @return array */ public function syncAll(?string $siteKey = null): array { @@ -33,8 +30,6 @@ public function syncAll(?string $siteKey = null): array } $results[$key] = [ - 'groups' => $this->syncGroups($key, $client), - 'users' => $this->syncUsers($key, $client), 'models' => $this->syncModels($key, $client), ]; } @@ -46,7 +41,6 @@ private function syncModels(string $siteKey, OpenWebUiClient $client): int { $apiModels = $client->fetchModels(); $modelRepository = $this->entityManager->getRepository(Model::class); - $userRepository = $this->entityManager->getRepository(User::class); $seenIds = []; $count = 0; @@ -54,9 +48,6 @@ private function syncModels(string $siteKey, OpenWebUiClient $client): int $id = $item['id']; $seenIds[] = $id; $model = $modelRepository->find($id); - $owner = isset($item['user_id']) - ? $userRepository->findOneBy(['externalId' => $item['user_id'], 'site' => $siteKey]) - : null; if (null === $model) { $model = new Model( @@ -67,7 +58,6 @@ private function syncModels(string $siteKey, OpenWebUiClient $client): int description: $item['meta']['description'] ?? null, systemPrompt: $item['params']['system'] ?? null, isActive: $item['is_active'] ?? true, - owner: $owner, ); $this->entityManager->persist($model); } else { @@ -76,7 +66,6 @@ private function syncModels(string $siteKey, OpenWebUiClient $client): int $model->setDescription($item['meta']['description'] ?? null); $model->setSystemPrompt($item['params']['system'] ?? null); $model->setIsActive($item['is_active'] ?? true); - $model->setOwner($owner); } if (isset($item['created_at'])) { @@ -86,8 +75,6 @@ private function syncModels(string $siteKey, OpenWebUiClient $client): int $model->setUpdatedAt(new \DateTimeImmutable('@'.$item['updated_at'])); } - $this->syncAccessGrants($model, $siteKey, $item['access_grants'] ?? []); - ++$count; } @@ -99,136 +86,6 @@ private function syncModels(string $siteKey, OpenWebUiClient $client): int return $count; } - private function syncUsers(string $siteKey, OpenWebUiClient $client): int - { - $apiUsers = $client->fetchUsers(); - $repository = $this->entityManager->getRepository(User::class); - $groupRepository = $this->entityManager->getRepository(Group::class); - $seenIds = []; - $count = 0; - - foreach ($apiUsers as $item) { - $id = $item['id']; - $seenIds[] = $id; - $user = $repository->find($id); - - if (null === $user) { - $user = new User( - externalId: $id, - site: $siteKey, - name: $item['name'] ?? '', - email: $item['email'] ?? '', - role: $item['role'] ?? 'user', - ); - $this->entityManager->persist($user); - } else { - $user->setName($item['name'] ?? ''); - $user->setEmail($item['email'] ?? ''); - $user->setRole($item['role'] ?? 'user'); - $user->setUpdatedAt(new \DateTimeImmutable()); - } - - $user->setUsername($item['username'] ?? null); - - if (isset($item['last_active_at'])) { - $user->setLastActiveAt(new \DateTimeImmutable('@'.$item['last_active_at'])); - } - - $user->clearGroups(); - foreach ($item['group_ids'] ?? [] as $groupId) { - $group = $groupRepository->findOneBy(['externalId' => $groupId, 'site' => $siteKey]); - if (null !== $group) { - $user->addGroup($group); - } - } - - ++$count; - } - - if ($count > 0) { - $this->removeStaleEntities(User::class, $seenIds, $siteKey); - } - $this->entityManager->flush(); - - return $count; - } - - private function syncGroups(string $siteKey, OpenWebUiClient $client): int - { - $apiGroups = $client->fetchGroups(); - $repository = $this->entityManager->getRepository(Group::class); - $seenIds = []; - $count = 0; - - foreach ($apiGroups as $item) { - $id = $item['id']; - $seenIds[] = $id; - $group = $repository->find($id); - $memberCount = $item['member_count'] ?? 0; - - if (null === $group) { - $group = new Group( - externalId: $id, - site: $siteKey, - name: $item['name'] ?? '', - description: $item['description'] ?? null, - memberCount: $memberCount, - ); - $this->entityManager->persist($group); - } else { - $group->setName($item['name'] ?? ''); - $group->setDescription($item['description'] ?? null); - $group->setMemberCount($memberCount); - $group->setUpdatedAt(new \DateTimeImmutable()); - } - - ++$count; - } - - if ($count > 0) { - $this->removeStaleEntities(Group::class, $seenIds, $siteKey); - } - $this->entityManager->flush(); - - return $count; - } - - /** - * @param array> $grants - */ - private function syncAccessGrants(Model $model, string $siteKey, array $grants): void - { - // Explicitly remove + flush old grants before inserting new ones. - // New grants may reuse the same primary key (externalId), so the old - // entities must be deleted from both the database and Doctrine's - // identity map first to avoid a duplicate-ID conflict. - foreach ($model->getAccessGrants() as $oldGrant) { - $this->entityManager->remove($oldGrant); - } - $model->clearAccessGrants(); - $this->entityManager->flush(); - - foreach ($grants as $grantData) { - $grant = new AccessGrant( - externalId: $grantData['id'], - site: $siteKey, - model: $model, - resourceType: $grantData['resource_type'] ?? '', - resourceId: $grantData['resource_id'] ?? '', - principalType: $grantData['principal_type'] ?? '', - principalId: $grantData['principal_id'] ?? '', - permission: $grantData['permission'] ?? '', - ); - - if (isset($grantData['created_at'])) { - $grant->setCreatedAt(new \DateTimeImmutable('@'.$grantData['created_at'])); - } - - $model->addAccessGrant($grant); - $this->entityManager->persist($grant); - } - } - /** * @param class-string $entityClass * @param list $seenIds diff --git a/templates/dashboard/index.html.twig b/templates/dashboard/index.html.twig index 130770a..a65cc7c 100644 --- a/templates/dashboard/index.html.twig +++ b/templates/dashboard/index.html.twig @@ -44,267 +44,60 @@ {% endfor %} -
- {# Tab navigation #} -
-
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
-
- - {# Models panel #} -
-
- - +
+
+
+ + + + + {% if activeSite is null %} + + {% endif %} + + + + {% for model in models %} + - - - - - {% if activeSite is null %} - - {% endif %} - - - - {% for model in models %} - - - - - - - {% if activeSite is null %} - {% endif %} - - - {% if model.accessGrants|length > 0 %} - - - - {% endif %} - - {% else %} - - - - - - {% endfor %} -
+ Name + Base ModelSiteUpdated
- Name - OwnerBase ModelAccess GrantsSiteUpdated
+ + {% if model.description %} + + {% else %} {{ model.name }} - {{ model.owner ? model.owner.name : '-' }}{{ model.baseModelId ?? '-' }} - {% if model.accessGrants|length > 0 %} - - {% else %} - 0 - {% endif %} - - {{ model.site }} - {{ model.updatedAt|date('Y-m-d H:i') }}
- - - - - - - - - - - - - - - {% for entry in modelAccessGrants[model.externalId] %} - - - - - - {% endfor %} - -
TypeNamePermissions
- - {{ entry.principalType }} - - - {{ entry.principalName }} - - {% for perm in entry.permissions %} - - {{ perm }} - - {% endfor %} -
-
No models synced yet. Run bin/console app:sync-openwebui
-
-
- - {# Users panel #} -
-
- - - - - - - + + {% if activeSite is null %} - - {% endif %} - - - {% for user in users %} - - - - - - {% if activeSite is null %} - - {% endif %} - - {% if user.groups|length > 0 %} - - - - {% endif %} - - {% else %} - - - - - - {% endfor %} -
- Name - EmailRoleGroups{{ model.baseModelId ?? '-' }}Site
{{ user.name }}{{ user.email }} - - {{ user.role }} - - - {% if user.groups|length > 0 %} - - {% else %} - 0 - {% endif %} + {{ model.site }} - {{ user.site }} -
-
- {% for group in user.groups|sort((a, b) => a.name <=> b.name) %} - {{ group.name }} - {% endfor %} -
-
No users synced yet. Run bin/console app:sync-openwebui
-
-
- - {# Groups panel #} -
-
- - - - - - {% if activeSite is null %} - {% endif %} + - - {% set preview_count = activeSite is null ? 3 : 5 %} - {% for group in groups %} - {% set sorted_users = group.users|sort((a, b) => a.name <=> b.name) %} - - - - + - {% if activeSite is null %} - - {% endif %} - - - {% else %} - - - - - {% endfor %} -
- Name - MembersSite{{ model.updatedAt|date('Y-m-d H:i') }}
{{ group.name }} -
-
- {% for user in sorted_users|slice(0, preview_count) %} - {{ user.name }} - {% endfor %} -
- {% if sorted_users|length > preview_count %} - - {% endif %} -
- {% if sorted_users|length > preview_count %} - - {% endif %} + {% if model.description %} +
+ {{ model.description }} - {{ group.site }} -
No groups synced yet. Run bin/console app:sync-openwebui
-
+ {% endif %} + + {% else %} + + + No models synced yet. Run bin/console app:sync-openwebui + + + {% endfor %} +
{% endblock %} diff --git a/templates/icons/groups.html.twig b/templates/icons/groups.html.twig deleted file mode 100644 index f15b1b0..0000000 --- a/templates/icons/groups.html.twig +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/templates/icons/models.html.twig b/templates/icons/models.html.twig deleted file mode 100644 index 0c7fab7..0000000 --- a/templates/icons/models.html.twig +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/templates/icons/users.html.twig b/templates/icons/users.html.twig deleted file mode 100644 index a73740a..0000000 --- a/templates/icons/users.html.twig +++ /dev/null @@ -1,3 +0,0 @@ - - - From 590fec6f6117653df1fca1f787393faf194450cb Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Thu, 16 Apr 2026 10:40:51 +0200 Subject: [PATCH 2/3] Added security --- CHANGELOG.md | 2 + composer.json | 1 + composer.lock | 586 +++++++++++++++++++++++++- config/bundles.php | 1 + config/packages/property_info.yaml | 3 + config/packages/security.yaml | 39 ++ config/reference.php | 321 +++++++++++++- config/routes/security.yaml | 3 + migrations/Version20260416082435.php | 31 ++ src/Command/CreateUserCommand.php | 58 +++ src/Controller/SecurityController.php | 30 ++ src/Entity/User.php | 66 +++ src/Repository/UserRepository.php | 18 + symfony.lock | 25 ++ templates/base.html.twig | 12 +- templates/security/login.html.twig | 38 ++ 16 files changed, 1229 insertions(+), 5 deletions(-) create mode 100644 config/packages/property_info.yaml create mode 100644 config/packages/security.yaml create mode 100644 config/routes/security.yaml create mode 100644 migrations/Version20260416082435.php create mode 100644 src/Command/CreateUserCommand.php create mode 100644 src/Controller/SecurityController.php create mode 100644 src/Entity/User.php create mode 100644 src/Repository/UserRepository.php create mode 100644 templates/security/login.html.twig diff --git a/CHANGELOG.md b/CHANGELOG.md index 40efbfa..f914e02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,3 +18,5 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Sortable table columns and expandable detail rows via Stimulus controllers - Tailwind CSS styling via symfonycasts/tailwind-bundle - CI workflows for PHP, Twig, YAML, Markdown, and Composer checks +- Form-based login with email and password +- CLI command `app:create-user` for creating users with generated passwords diff --git a/composer.json b/composer.json index 8136649..f933e70 100644 --- a/composer.json +++ b/composer.json @@ -17,6 +17,7 @@ "symfony/framework-bundle": "^7.4", "symfony/http-client": "^7.4", "symfony/runtime": "^7.4", + "symfony/security-bundle": "^7.4", "symfony/security-csrf": "^7.4", "symfony/stimulus-bundle": "^2.32", "symfony/twig-bundle": "^7.4", diff --git a/composer.lock b/composer.lock index fc9d33f..6a5dcb9 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0f1729d473cfd9a0874ce8a244778567", + "content-hash": "9b6f97e1cb9adc28c9ba9bba28327c35", "packages": [ { "name": "composer/semver", @@ -1245,6 +1245,54 @@ }, "time": "2021-02-03T23:26:27+00:00" }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, { "name": "psr/container", "version": "2.0.2", @@ -1663,6 +1711,84 @@ ], "time": "2025-03-13T15:25:07+00:00" }, + { + "name": "symfony/clock", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/674fa3b98e21531dd040e613479f5f6fa8f32111", + "reference": "674fa3b98e21531dd040e613479f5f6fa8f32111", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, { "name": "symfony/config", "version": "v7.4.6", @@ -3807,6 +3933,177 @@ ], "time": "2026-01-26T15:07:59+00:00" }, + { + "name": "symfony/property-access", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-access.git", + "reference": "b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-access/zipball/b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc", + "reference": "b7dad9dae8b8a47ef7ecc76c8569e7d8c7d90cfc", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/property-info": "^6.4.32|~7.3.10|^7.4.4|^8.0.4" + }, + "require-dev": { + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/var-exporter": "^6.4.1|^7.0.1|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyAccess\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides functions to read and write from/to an object or array using a simple string notation", + "homepage": "https://symfony.com", + "keywords": [ + "access", + "array", + "extraction", + "index", + "injection", + "object", + "property", + "property-path", + "reflection" + ], + "support": { + "source": "https://github.com/symfony/property-access/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, + { + "name": "symfony/property-info", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/property-info.git", + "reference": "ac5e82528b986c4f7cfccbf7764b5d2e824d6175" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/property-info/zipball/ac5e82528b986c4f7cfccbf7764b5d2e824d6175", + "reference": "ac5e82528b986c4f7cfccbf7764b5d2e824d6175", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/string": "^6.4|^7.0|^8.0", + "symfony/type-info": "^7.4.7|^8.0.7" + }, + "conflict": { + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1", + "symfony/cache": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/serializer": "<6.4" + }, + "require-dev": { + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\PropertyInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Kévin Dunglas", + "email": "dunglas@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts information about PHP class' properties using metadata of popular sources", + "homepage": "https://symfony.com", + "keywords": [ + "doctrine", + "phpdoc", + "property", + "symfony", + "type", + "validator" + ], + "support": { + "source": "https://github.com/symfony/property-info/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, { "name": "symfony/routing", "version": "v7.4.6", @@ -3975,6 +4272,118 @@ ], "time": "2025-12-05T14:04:53+00:00" }, + { + "name": "symfony/security-bundle", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-bundle.git", + "reference": "6f73fdfd9ad23bf24b6f6c8d35be3ea6853d91af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-bundle/zipball/6f73fdfd9ad23bf24b6f6c8d35be3ea6853d91af", + "reference": "6f73fdfd9ad23bf24b6f6c8d35be3ea6853d91af", + "shasum": "" + }, + "require": { + "composer-runtime-api": ">=2.1", + "ext-xml": "*", + "php": ">=8.2", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^6.4.11|^7.1.4|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4.13|^7.1.6|^8.0", + "symfony/password-hasher": "^6.4|^7.0|^8.0", + "symfony/security-core": "^7.4|^8.0", + "symfony/security-csrf": "^6.4|^7.0|^8.0", + "symfony/security-http": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/console": "<6.4", + "symfony/framework-bundle": "<6.4", + "symfony/http-client": "<6.4", + "symfony/ldap": "<6.4", + "symfony/serializer": "<6.4", + "symfony/twig-bundle": "<6.4", + "symfony/validator": "<6.4" + }, + "require-dev": { + "symfony/asset": "^6.4|^7.0|^8.0", + "symfony/browser-kit": "^6.4|^7.0|^8.0", + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/css-selector": "^6.4|^7.0|^8.0", + "symfony/dom-crawler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/form": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4.13|^7.1.6|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/ldap": "^6.4|^7.0|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/runtime": "^6.4.13|^7.1.6|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "symfony/twig-bridge": "^6.4|^7.0|^8.0", + "symfony/twig-bundle": "^6.4|^7.0|^8.0", + "symfony/validator": "^6.4|^7.0|^8.0", + "symfony/yaml": "^6.4|^7.0|^8.0", + "twig/twig": "^3.15", + "web-token/jwt-library": "^3.3.2|^4.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "Symfony\\Bundle\\SecurityBundle\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-bundle/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-30T13:54:39+00:00" + }, { "name": "symfony/security-core", "version": "v7.4.4", @@ -4140,6 +4549,98 @@ ], "time": "2026-02-11T16:03:16+00:00" }, + { + "name": "symfony/security-http", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/security-http.git", + "reference": "1b07d7d472ba967fd66697067e6274084d2d1d7c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/security-http/zipball/1b07d7d472ba967fd66697067e6274084d2d1d7c", + "reference": "1b07d7d472ba967fd66697067e6274084d2d1d7c", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/polyfill-mbstring": "~1.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/security-core": "^7.3|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/clock": "<6.4", + "symfony/http-client-contracts": "<3.0", + "symfony/security-bundle": "<6.4", + "symfony/security-csrf": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/cache": "^6.4|^7.0|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-client": "^6.4|^7.0|^8.0", + "symfony/http-client-contracts": "^3.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.0", + "symfony/routing": "^6.4|^7.0|^8.0", + "symfony/security-csrf": "^6.4|^7.0|^8.0", + "symfony/translation": "^6.4|^7.0|^8.0", + "web-token/jwt-library": "^3.3.2|^4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Security\\Http\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Security Component - HTTP Integration", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/security-http/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, { "name": "symfony/service-contracts", "version": "v3.6.1", @@ -4744,6 +5245,89 @@ ], "time": "2026-01-06T12:34:24+00:00" }, + { + "name": "symfony/type-info", + "version": "v7.4.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/type-info.git", + "reference": "6bf34da885ff5143a3dfd8f1b863bb8ab95f50bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/type-info/zipball/6bf34da885ff5143a3dfd8f1b863bb8ab95f50bd", + "reference": "6bf34da885ff5143a3dfd8f1b863bb8ab95f50bd", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "phpstan/phpdoc-parser": "<1.30" + }, + "require-dev": { + "phpstan/phpdoc-parser": "^1.30|^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\TypeInfo\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mathias Arlaud", + "email": "mathias.arlaud@gmail.com" + }, + { + "name": "Baptiste LEDUC", + "email": "baptiste.leduc@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Extracts PHP types information.", + "homepage": "https://symfony.com", + "keywords": [ + "PHPStan", + "phpdoc", + "symfony", + "type" + ], + "support": { + "source": "https://github.com/symfony/type-info/tree/v7.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-24T13:12:05+00:00" + }, { "name": "symfony/var-dumper", "version": "v7.4.6", diff --git a/config/bundles.php b/config/bundles.php index c17e623..6c1b88b 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -8,4 +8,5 @@ Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true], Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true], Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], + Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true], ]; diff --git a/config/packages/property_info.yaml b/config/packages/property_info.yaml new file mode 100644 index 0000000..dd31b9d --- /dev/null +++ b/config/packages/property_info.yaml @@ -0,0 +1,3 @@ +framework: + property_info: + with_constructor_extractor: true diff --git a/config/packages/security.yaml b/config/packages/security.yaml new file mode 100644 index 0000000..b23543a --- /dev/null +++ b/config/packages/security.yaml @@ -0,0 +1,39 @@ +security: + password_hashers: + Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' + + providers: + app_user_provider: + entity: + class: App\Entity\User + property: email + + firewalls: + dev: + pattern: ^/(_(profiler|wdt)|assets|build)/ + security: false + main: + lazy: true + provider: app_user_provider + form_login: + login_path: app_login + check_path: app_login + default_target_path: dashboard + username_parameter: _email + password_parameter: _password + logout: + path: app_logout + csrf_token_id: logout + + access_control: + - { path: ^/login$, roles: PUBLIC_ACCESS } + - { path: ^/, roles: ROLE_USER } + +when@test: + security: + password_hashers: + Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: + algorithm: auto + cost: 4 + time_cost: 3 + memory_cost: 10 diff --git a/config/reference.php b/config/reference.php index 4862767..631237f 100644 --- a/config/reference.php +++ b/config/reference.php @@ -369,7 +369,7 @@ * }>, * }, * property_access?: bool|array{ // Property access configuration - * enabled?: bool|Param, // Default: false + * enabled?: bool|Param, // Default: true * magic_call?: bool|Param, // Default: false * magic_get?: bool|Param, // Default: true * magic_set?: bool|Param, // Default: true @@ -377,11 +377,11 @@ * throw_exception_on_invalid_property_path?: bool|Param, // Default: true * }, * type_info?: bool|array{ // Type info configuration - * enabled?: bool|Param, // Default: false + * enabled?: bool|Param, // Default: true * aliases?: array, * }, * property_info?: bool|array{ // Property info configuration - * enabled?: bool|Param, // Default: false + * enabled?: bool|Param, // Default: true * with_constructor_extractor?: bool|Param, // Registers the constructor extractor. * }, * cache?: array{ // Cache configuration @@ -949,6 +949,317 @@ * generate_final_classes?: bool|Param, // Default: true * generate_final_entities?: bool|Param, // Default: false * } + * @psalm-type SecurityConfig = array{ + * access_denied_url?: scalar|Param|null, // Default: null + * session_fixation_strategy?: "none"|"migrate"|"invalidate"|Param, // Default: "migrate" + * hide_user_not_found?: bool|Param, // Deprecated: The "hide_user_not_found" option is deprecated and will be removed in 8.0. Use the "expose_security_errors" option instead. + * expose_security_errors?: \Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel::None|\Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel::AccountStatus|\Symfony\Component\Security\Http\Authentication\ExposeSecurityLevel::All|Param, // Default: "none" + * erase_credentials?: bool|Param, // Default: true + * access_decision_manager?: array{ + * strategy?: "affirmative"|"consensus"|"unanimous"|"priority"|Param, + * service?: scalar|Param|null, + * strategy_service?: scalar|Param|null, + * allow_if_all_abstain?: bool|Param, // Default: false + * allow_if_equal_granted_denied?: bool|Param, // Default: true + * }, + * password_hashers?: array, + * hash_algorithm?: scalar|Param|null, // Name of hashing algorithm for PBKDF2 (i.e. sha256, sha512, etc..) See hash_algos() for a list of supported algorithms. // Default: "sha512" + * key_length?: scalar|Param|null, // Default: 40 + * ignore_case?: bool|Param, // Default: false + * encode_as_base64?: bool|Param, // Default: true + * iterations?: scalar|Param|null, // Default: 5000 + * cost?: int|Param, // Default: null + * memory_cost?: scalar|Param|null, // Default: null + * time_cost?: scalar|Param|null, // Default: null + * id?: scalar|Param|null, + * }>, + * providers?: array, + * }, + * entity?: array{ + * class?: scalar|Param|null, // The full entity class name of your user class. + * property?: scalar|Param|null, // Default: null + * manager_name?: scalar|Param|null, // Default: null + * }, + * memory?: array{ + * users?: array, + * }>, + * }, + * ldap?: array{ + * service?: scalar|Param|null, + * base_dn?: scalar|Param|null, + * search_dn?: scalar|Param|null, // Default: null + * search_password?: scalar|Param|null, // Default: null + * extra_fields?: list, + * default_roles?: list, + * role_fetcher?: scalar|Param|null, // Default: null + * uid_key?: scalar|Param|null, // Default: "sAMAccountName" + * filter?: scalar|Param|null, // Default: "({uid_key}={user_identifier})" + * password_attribute?: scalar|Param|null, // Default: null + * }, + * }>, + * firewalls?: array, + * security?: bool|Param, // Default: true + * user_checker?: scalar|Param|null, // The UserChecker to use when authenticating users in this firewall. // Default: "security.user_checker" + * request_matcher?: scalar|Param|null, + * access_denied_url?: scalar|Param|null, + * access_denied_handler?: scalar|Param|null, + * entry_point?: scalar|Param|null, // An enabled authenticator name or a service id that implements "Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface". + * provider?: scalar|Param|null, + * stateless?: bool|Param, // Default: false + * lazy?: bool|Param, // Default: false + * context?: scalar|Param|null, + * logout?: array{ + * enable_csrf?: bool|Param|null, // Default: null + * csrf_token_id?: scalar|Param|null, // Default: "logout" + * csrf_parameter?: scalar|Param|null, // Default: "_csrf_token" + * csrf_token_manager?: scalar|Param|null, + * path?: scalar|Param|null, // Default: "/logout" + * target?: scalar|Param|null, // Default: "/" + * invalidate_session?: bool|Param, // Default: true + * clear_site_data?: list<"*"|"cache"|"cookies"|"storage"|"executionContexts"|Param>, + * delete_cookies?: array, + * }, + * switch_user?: array{ + * provider?: scalar|Param|null, + * parameter?: scalar|Param|null, // Default: "_switch_user" + * role?: scalar|Param|null, // Default: "ROLE_ALLOWED_TO_SWITCH" + * target_route?: scalar|Param|null, // Default: null + * }, + * required_badges?: list, + * custom_authenticators?: list, + * login_throttling?: array{ + * limiter?: scalar|Param|null, // A service id implementing "Symfony\Component\HttpFoundation\RateLimiter\RequestRateLimiterInterface". + * max_attempts?: int|Param, // Default: 5 + * interval?: scalar|Param|null, // Default: "1 minute" + * lock_factory?: scalar|Param|null, // The service ID of the lock factory used by the login rate limiter (or null to disable locking). // Default: null + * cache_pool?: string|Param, // The cache pool to use for storing the limiter state // Default: "cache.rate_limiter" + * storage_service?: string|Param, // The service ID of a custom storage implementation, this precedes any configured "cache_pool" // Default: null + * }, + * x509?: array{ + * provider?: scalar|Param|null, + * user?: scalar|Param|null, // Default: "SSL_CLIENT_S_DN_Email" + * credentials?: scalar|Param|null, // Default: "SSL_CLIENT_S_DN" + * user_identifier?: scalar|Param|null, // Default: "emailAddress" + * }, + * remote_user?: array{ + * provider?: scalar|Param|null, + * user?: scalar|Param|null, // Default: "REMOTE_USER" + * }, + * login_link?: array{ + * check_route?: scalar|Param|null, // Route that will validate the login link - e.g. "app_login_link_verify". + * check_post_only?: scalar|Param|null, // If true, only HTTP POST requests to "check_route" will be handled by the authenticator. // Default: false + * signature_properties?: list, + * lifetime?: int|Param, // The lifetime of the login link in seconds. // Default: 600 + * max_uses?: int|Param, // Max number of times a login link can be used - null means unlimited within lifetime. // Default: null + * used_link_cache?: scalar|Param|null, // Cache service id used to expired links of max_uses is set. + * success_handler?: scalar|Param|null, // A service id that implements Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface. + * failure_handler?: scalar|Param|null, // A service id that implements Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface. + * provider?: scalar|Param|null, // The user provider to load users from. + * secret?: scalar|Param|null, // Default: "%kernel.secret%" + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|Param|null, // Default: "/" + * login_path?: scalar|Param|null, // Default: "/login" + * target_path_parameter?: scalar|Param|null, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|Param|null, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|Param|null, // Default: "_failure_path" + * }, + * form_login?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * username_parameter?: scalar|Param|null, // Default: "_username" + * password_parameter?: scalar|Param|null, // Default: "_password" + * csrf_parameter?: scalar|Param|null, // Default: "_csrf_token" + * csrf_token_id?: scalar|Param|null, // Default: "authenticate" + * enable_csrf?: bool|Param, // Default: false + * post_only?: bool|Param, // Default: true + * form_only?: bool|Param, // Default: false + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|Param|null, // Default: "/" + * target_path_parameter?: scalar|Param|null, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|Param|null, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|Param|null, // Default: "_failure_path" + * }, + * form_login_ldap?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * username_parameter?: scalar|Param|null, // Default: "_username" + * password_parameter?: scalar|Param|null, // Default: "_password" + * csrf_parameter?: scalar|Param|null, // Default: "_csrf_token" + * csrf_token_id?: scalar|Param|null, // Default: "authenticate" + * enable_csrf?: bool|Param, // Default: false + * post_only?: bool|Param, // Default: true + * form_only?: bool|Param, // Default: false + * always_use_default_target_path?: bool|Param, // Default: false + * default_target_path?: scalar|Param|null, // Default: "/" + * target_path_parameter?: scalar|Param|null, // Default: "_target_path" + * use_referer?: bool|Param, // Default: false + * failure_path?: scalar|Param|null, // Default: null + * failure_forward?: bool|Param, // Default: false + * failure_path_parameter?: scalar|Param|null, // Default: "_failure_path" + * service?: scalar|Param|null, // Default: "ldap" + * dn_string?: scalar|Param|null, // Default: "{user_identifier}" + * query_string?: scalar|Param|null, + * search_dn?: scalar|Param|null, // Default: "" + * search_password?: scalar|Param|null, // Default: "" + * }, + * json_login?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * username_path?: scalar|Param|null, // Default: "username" + * password_path?: scalar|Param|null, // Default: "password" + * }, + * json_login_ldap?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * check_path?: scalar|Param|null, // Default: "/login_check" + * use_forward?: bool|Param, // Default: false + * login_path?: scalar|Param|null, // Default: "/login" + * username_path?: scalar|Param|null, // Default: "username" + * password_path?: scalar|Param|null, // Default: "password" + * service?: scalar|Param|null, // Default: "ldap" + * dn_string?: scalar|Param|null, // Default: "{user_identifier}" + * query_string?: scalar|Param|null, + * search_dn?: scalar|Param|null, // Default: "" + * search_password?: scalar|Param|null, // Default: "" + * }, + * access_token?: array{ + * provider?: scalar|Param|null, + * remember_me?: bool|Param, // Default: true + * success_handler?: scalar|Param|null, + * failure_handler?: scalar|Param|null, + * realm?: scalar|Param|null, // Default: null + * token_extractors?: list, + * token_handler?: string|array{ + * id?: scalar|Param|null, + * oidc_user_info?: string|array{ + * base_uri?: scalar|Param|null, // Base URI of the userinfo endpoint on the OIDC server, or the OIDC server URI to use the discovery (require "discovery" to be configured). + * discovery?: array{ // Enable the OIDC discovery. + * cache?: array{ + * id?: scalar|Param|null, // Cache service id to use to cache the OIDC discovery configuration. + * }, + * }, + * claim?: scalar|Param|null, // Claim which contains the user identifier (e.g. sub, email, etc.). // Default: "sub" + * client?: scalar|Param|null, // HttpClient service id to use to call the OIDC server. + * }, + * oidc?: array{ + * discovery?: array{ // Enable the OIDC discovery. + * base_uri?: list, + * cache?: array{ + * id?: scalar|Param|null, // Cache service id to use to cache the OIDC discovery configuration. + * }, + * }, + * claim?: scalar|Param|null, // Claim which contains the user identifier (e.g.: sub, email..). // Default: "sub" + * audience?: scalar|Param|null, // Audience set in the token, for validation purpose. + * issuers?: list, + * algorithm?: array, + * algorithms?: list, + * key?: scalar|Param|null, // Deprecated: The "key" option is deprecated and will be removed in 8.0. Use the "keyset" option instead. // JSON-encoded JWK used to sign the token (must contain a "kty" key). + * keyset?: scalar|Param|null, // JSON-encoded JWKSet used to sign the token (must contain a list of valid public keys). + * encryption?: bool|array{ + * enabled?: bool|Param, // Default: false + * enforce?: bool|Param, // When enabled, the token shall be encrypted. // Default: false + * algorithms?: list, + * keyset?: scalar|Param|null, // JSON-encoded JWKSet used to decrypt the token (must contain a list of valid private keys). + * }, + * }, + * cas?: array{ + * validation_url?: scalar|Param|null, // CAS server validation URL + * prefix?: scalar|Param|null, // CAS prefix // Default: "cas" + * http_client?: scalar|Param|null, // HTTP Client service // Default: null + * }, + * oauth2?: scalar|Param|null, + * }, + * }, + * http_basic?: array{ + * provider?: scalar|Param|null, + * realm?: scalar|Param|null, // Default: "Secured Area" + * }, + * http_basic_ldap?: array{ + * provider?: scalar|Param|null, + * realm?: scalar|Param|null, // Default: "Secured Area" + * service?: scalar|Param|null, // Default: "ldap" + * dn_string?: scalar|Param|null, // Default: "{user_identifier}" + * query_string?: scalar|Param|null, + * search_dn?: scalar|Param|null, // Default: "" + * search_password?: scalar|Param|null, // Default: "" + * }, + * remember_me?: array{ + * secret?: scalar|Param|null, // Default: "%kernel.secret%" + * service?: scalar|Param|null, + * user_providers?: list, + * catch_exceptions?: bool|Param, // Default: true + * signature_properties?: list, + * token_provider?: string|array{ + * service?: scalar|Param|null, // The service ID of a custom remember-me token provider. + * doctrine?: bool|array{ + * enabled?: bool|Param, // Default: false + * connection?: scalar|Param|null, // Default: null + * }, + * }, + * token_verifier?: scalar|Param|null, // The service ID of a custom rememberme token verifier. + * name?: scalar|Param|null, // Default: "REMEMBERME" + * lifetime?: int|Param, // Default: 31536000 + * path?: scalar|Param|null, // Default: "/" + * domain?: scalar|Param|null, // Default: null + * secure?: true|false|"auto"|Param, // Default: false + * httponly?: bool|Param, // Default: true + * samesite?: null|"lax"|"strict"|"none"|Param, // Default: null + * always_remember_me?: bool|Param, // Default: false + * remember_me_parameter?: scalar|Param|null, // Default: "_remember_me" + * }, + * }>, + * access_control?: list, + * attributes?: array, + * route?: scalar|Param|null, // Default: null + * methods?: list, + * allow_if?: scalar|Param|null, // Default: null + * roles?: list, + * }>, + * role_hierarchy?: array>, + * } * @psalm-type ConfigType = array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, @@ -959,6 +1270,7 @@ * doctrine?: DoctrineConfig, * doctrine_migrations?: DoctrineMigrationsConfig, * stimulus?: StimulusConfig, + * security?: SecurityConfig, * "when@dev"?: array{ * imports?: ImportsConfig, * parameters?: ParametersConfig, @@ -970,6 +1282,7 @@ * doctrine_migrations?: DoctrineMigrationsConfig, * stimulus?: StimulusConfig, * maker?: MakerConfig, + * security?: SecurityConfig, * }, * "when@prod"?: array{ * imports?: ImportsConfig, @@ -981,6 +1294,7 @@ * doctrine?: DoctrineConfig, * doctrine_migrations?: DoctrineMigrationsConfig, * stimulus?: StimulusConfig, + * security?: SecurityConfig, * }, * "when@test"?: array{ * imports?: ImportsConfig, @@ -992,6 +1306,7 @@ * doctrine?: DoctrineConfig, * doctrine_migrations?: DoctrineMigrationsConfig, * stimulus?: StimulusConfig, + * security?: SecurityConfig, * }, * ...addSql('CREATE TABLE `user` (id INT AUTO_INCREMENT NOT NULL, email VARCHAR(180) NOT NULL, password VARCHAR(255) NOT NULL, UNIQUE INDEX UNIQ_8D93D649E7927C74 (email), PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP TABLE `user`'); + } +} diff --git a/src/Command/CreateUserCommand.php b/src/Command/CreateUserCommand.php new file mode 100644 index 0000000..9ee1112 --- /dev/null +++ b/src/Command/CreateUserCommand.php @@ -0,0 +1,58 @@ +addArgument('email', InputArgument::REQUIRED, 'The user email address'); + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + + $email = $input->getArgument('email'); + + $existing = $this->em->getRepository(User::class)->findOneBy(['email' => $email]); + if (null !== $existing) { + $io->error(sprintf('A user with email "%s" already exists.', $email)); + + return Command::FAILURE; + } + + $plainPassword = bin2hex(random_bytes(16)); + + $user = new User($email, ''); + $user->setPassword($this->passwordHasher->hashPassword($user, $plainPassword)); + + $this->em->persist($user); + $this->em->flush(); + + $io->success(sprintf('User "%s" created. Password: %s', $email, $plainPassword)); + + return Command::SUCCESS; + } +} diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php new file mode 100644 index 0000000..d9b8e69 --- /dev/null +++ b/src/Controller/SecurityController.php @@ -0,0 +1,30 @@ +getUser()) { + return $this->redirectToRoute('dashboard'); + } + + return $this->render('security/login.html.twig', [ + 'last_email' => $authenticationUtils->getLastUsername(), + 'error' => $authenticationUtils->getLastAuthenticationError(), + ]); + } + + #[Route('/logout', name: 'app_logout')] + public function logout(): never + { + throw new \LogicException('This should never be reached.'); + } +} diff --git a/src/Entity/User.php b/src/Entity/User.php new file mode 100644 index 0000000..1a2b9fa --- /dev/null +++ b/src/Entity/User.php @@ -0,0 +1,66 @@ +email = $email; + $this->password = $password; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getEmail(): string + { + return $this->email; + } + + public function getUserIdentifier(): string + { + return $this->email; + } + + public function getPassword(): string + { + return $this->password; + } + + public function setPassword(string $password): void + { + $this->password = $password; + } + + public function getRoles(): array + { + return ['ROLE_USER']; + } + + public function eraseCredentials(): void + { + } +} diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php new file mode 100644 index 0000000..da4b8be --- /dev/null +++ b/src/Repository/UserRepository.php @@ -0,0 +1,18 @@ + + */ +class UserRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, User::class); + } +} diff --git a/symfony.lock b/symfony.lock index 32fdc33..3f26b9e 100644 --- a/symfony.lock +++ b/symfony.lock @@ -116,6 +116,18 @@ "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" } }, + "symfony/property-info": { + "version": "7.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "7.3", + "ref": "dae70df71978ae9226ae915ffd5fad817f5ca1f7" + }, + "files": [ + "config/packages/property_info.yaml" + ] + }, "symfony/routing": { "version": "7.4", "recipe": { @@ -129,6 +141,19 @@ "config/routes.yaml" ] }, + "symfony/security-bundle": { + "version": "7.4", + "recipe": { + "repo": "github.com/symfony/recipes", + "branch": "main", + "version": "7.4", + "ref": "c42fee7802181cdd50f61b8622715829f5d2335c" + }, + "files": [ + "config/packages/security.yaml", + "config/routes/security.yaml" + ] + }, "symfony/stimulus-bundle": { "version": "2.32", "recipe": { diff --git a/templates/base.html.twig b/templates/base.html.twig index d4b9887..b0688bf 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -16,7 +16,17 @@

AarhusAI Overview

- {% block header_right %}{% endblock %} +
+ {% if app.user %} + {{ app.user.userIdentifier }} +
+ + +
+ {% endif %} + {% block header_right %}{% endblock %} +
diff --git a/templates/security/login.html.twig b/templates/security/login.html.twig new file mode 100644 index 0000000..f0baf10 --- /dev/null +++ b/templates/security/login.html.twig @@ -0,0 +1,38 @@ +{% extends 'base.html.twig' %} + +{% block title %}Login - AarhusAI Overview{% endblock %} + +{% block body %} +
+
+

Sign in

+ + {% if error %} +
+ {{ error.messageKey|trans(error.messageData, 'security') }} +
+ {% endif %} + +
+
+ + +
+ +
+ + +
+ + + + +
+
+
+{% endblock %} From 20d0a02ee667301921aceac321a60ee21d6753e6 Mon Sep 17 00:00:00 2001 From: jekuaitk Date: Thu, 16 Apr 2026 10:44:00 +0200 Subject: [PATCH 3/3] Updated README --- README.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ac363eb..4380ca8 100644 --- a/README.md +++ b/README.md @@ -62,10 +62,28 @@ unified overview. docker compose exec phpfpm bin/console doctrine:migrations:migrate --no-interaction ``` -7. Access the site at `https://`. +7. Create a user account: + + ```bash + docker compose exec phpfpm bin/console app:create-user admin@example.com + ``` + + A random password will be generated and printed in the terminal. + +8. Access the site at `https://` and log in with the created + credentials. ## Usage +### Authentication + +The dashboard requires login. Users are managed via the CLI: + +```bash +# Create a new user (generates and displays a random password) +docker compose exec phpfpm bin/console app:create-user user@example.com +``` + ### Syncing data Pull models from all configured OpenWebUI sites: