API updadte

This commit is contained in:
2025-11-08 23:21:26 +01:00
parent c0c346a9ed
commit 81374cc659
12 changed files with 477 additions and 37 deletions

125
README.md
View File

@@ -5,10 +5,13 @@ Eine moderne Webanwendung zur Immobilienberechnung, entwickelt mit Symfony 7.3,
## Features ## Features
- **REST-API**: Vollständige REST-API mit API Platform 4.2 - **REST-API**: Vollständige REST-API mit API Platform 4.2
- **API-Key-Authentifizierung**: Sichere Authentifizierung über eindeutige API-Keys
- **Rollenbasierte Zugriffskontrolle**: USER, ADMIN, MODERATOR mit feingranularen Berechtigungen
- **Swagger/OpenAPI**: Automatische interaktive API-Dokumentation (Swagger UI) - **Swagger/OpenAPI**: Automatische interaktive API-Dokumentation (Swagger UI)
- **Web-Interface**: Benutzerfreundliche Weboberfläche mit Twig-Templates - **Web-Interface**: Benutzerfreundliche Weboberfläche mit Twig-Templates
- **Immobilien-Management**: Vollständige CRUD-Operationen für Immobilien mit automatischen Berechnungen - **Immobilien-Management**: Vollständige CRUD-Operationen mit automatischen Berechnungen
- **User Management**: Benutzerverwaltung mit Rollenbasierter Zugriffskontrolle (USER, ADMIN, MODERATOR) - **Mandantenfähigkeit**: Jeder User sieht nur seine eigenen Immobilien (außer Admins)
- **Öffentliche Ressourcen**: Bundesländer und Heizungstypen ohne Authentifizierung abrufbar
- **Docker-Setup**: Vollständig containerisierte Entwicklungsumgebung - **Docker-Setup**: Vollständig containerisierte Entwicklungsumgebung
- **Datenbank-Migrationen**: Versionskontrollierte Datenbankschema-Verwaltung mit Doctrine - **Datenbank-Migrationen**: Versionskontrollierte Datenbankschema-Verwaltung mit Doctrine
- **CORS-Unterstützung**: Konfigurierbare CORS-Einstellungen für API-Zugriffe - **CORS-Unterstützung**: Konfigurierbare CORS-Einstellungen für API-Zugriffe
@@ -110,9 +113,22 @@ Wichtigste Endpunkte:
| id | INT | Primärschlüssel (Auto-Increment) | | id | INT | Primärschlüssel (Auto-Increment) |
| name | VARCHAR(255) | Benutzername (min. 2 Zeichen) | | name | VARCHAR(255) | Benutzername (min. 2 Zeichen) |
| email | VARCHAR(255) | E-Mail-Adresse (Unique) | | email | VARCHAR(255) | E-Mail-Adresse (Unique) |
| role | ENUM | Benutzerrolle (user, admin, moderator) | | role | ENUM | Benutzerrolle (user, admin, moderator, technical) |
| api_key | VARCHAR(64) | API-Key für Authentifizierung (Unique, automatisch generiert) |
| created_at | DATETIME | Erstellungsdatum | | created_at | DATETIME | Erstellungsdatum |
**Benutzerrollen:**
- `user` - Normaler Benutzer (sieht nur eigene Immobilien)
- `admin` - Administrator (uneingeschränkter Zugriff auf alle Ressourcen)
- `moderator` - Moderator (erweiterte Rechte)
- `technical` - Technischer User (kann Bundesländer und Heizungstypen verwalten)
**API-Key:**
- Wird automatisch beim Erstellen eines Users generiert (SHA256-Hash)
- Ist eindeutig für jeden User
- Kann über die Methode `regenerateApiKey()` neu generiert werden
- Wird für die API-Authentifizierung verwendet
### Bundesland-Tabelle ### Bundesland-Tabelle
| Feld | Typ | Beschreibung | | Feld | Typ | Beschreibung |
@@ -393,6 +409,67 @@ services:
Die Anwendung nutzt **API Platform** zur automatischen Generierung von OpenAPI/Swagger-Dokumentation. Die interaktive Dokumentation ermöglicht es, alle API-Endpunkte direkt im Browser zu testen. Die Anwendung nutzt **API Platform** zur automatischen Generierung von OpenAPI/Swagger-Dokumentation. Die interaktive Dokumentation ermöglicht es, alle API-Endpunkte direkt im Browser zu testen.
### API-Authentifizierung
Die API verwendet **API-Key-basierte Authentifizierung**. Jeder User erhält automatisch beim Erstellen einen eindeutigen API-Key.
#### API-Key verwenden
Fügen Sie den API-Key im Header `X-API-KEY` zu allen API-Anfragen hinzu:
```bash
curl -H "X-API-KEY: your-api-key-here" http://localhost:8080/api/immobilies
```
#### API-Key erhalten
Der API-Key wird automatisch generiert, wenn ein neuer User erstellt wird. Sie können ihn abrufen über:
```bash
# User erstellen
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{
"name": "Max Mustermann",
"email": "max@example.com",
"role": "user"
}'
# Response enthält den API-Key
{
"@context": "/api/contexts/User",
"@id": "/api/users/1",
"@type": "User",
"id": 1,
"name": "Max Mustermann",
"email": "max@example.com",
"role": "user",
"apiKey": "a1b2c3d4e5f6...", // Verwenden Sie diesen Key für die Authentifizierung
"createdAt": "2025-11-08T22:00:00+00:00"
}
```
### Zugriffsrechte
Die API implementiert folgende Zugriffsrechte:
| Ressource | Aktion | Berechtigung |
|-----------|--------|--------------|
| **Bundesländer** | GET (Lesen) | Öffentlich (keine Authentifizierung) |
| **Bundesländer** | POST, PUT, DELETE | ADMIN oder TECHNICAL |
| **Heizungstypen** | GET (Lesen) | Öffentlich (keine Authentifizierung) |
| **Heizungstypen** | POST, PUT, DELETE | ADMIN oder TECHNICAL |
| **Immobilien** | GET, POST, PATCH, DELETE | Authentifiziert |
| **Immobilien** | Sichtbarkeit | User sehen nur eigene Immobilien |
| **Immobilien** | Admin-Zugriff | ADMIN sieht alle Immobilien |
| **Users** | Alle Aktionen | Authentifiziert |
**Wichtig:**
- Normale User sehen und verwalten nur ihre eigenen Immobilien
- Admins haben uneingeschränkten Zugriff auf alle Ressourcen
- Technische User können Bundesländer und Heizungstypen anlegen, ändern und löschen
- Bundesländer und Heizungstypen können ohne API-Key gelesen werden
### Swagger UI (Interaktive Dokumentation) ### Swagger UI (Interaktive Dokumentation)
Die **Swagger UI** bietet eine vollständige, interaktive Dokumentation aller API-Endpunkte: Die **Swagger UI** bietet eine vollständige, interaktive Dokumentation aller API-Endpunkte:
@@ -494,32 +571,53 @@ curl -H "Accept: application/ld+json" http://localhost:8080/api/users
### Beispiele ### Beispiele
#### Bundesland erstellen #### Bundesländer abrufen (Öffentlich, kein API-Key benötigt)
```bash
curl http://localhost:8080/api/bundeslands
```
#### Bundesland erstellen (Admin oder Technical User)
```bash ```bash
curl -X POST http://localhost:8080/api/bundeslands \ curl -X POST http://localhost:8080/api/bundeslands \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "X-API-KEY: admin-or-technical-api-key" \
-d '{ -d '{
"name": "Bayern", "name": "Bayern",
"grunderwerbsteuer": 3.5 "grunderwerbsteuer": 3.5
}' }'
``` ```
#### Heizungstyp erstellen #### Heizungstyp erstellen (Admin oder Technical User)
```bash ```bash
curl -X POST http://localhost:8080/api/heizungstyps \ curl -X POST http://localhost:8080/api/heizungstyps \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "X-API-KEY: admin-or-technical-api-key" \
-d '{ -d '{
"name": "Wärmepumpe" "name": "Wärmepumpe"
}' }'
``` ```
#### Immobilie erstellen #### Technischen User erstellen
```bash
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{
"name": "Technical User",
"email": "technical@example.com",
"role": "technical"
}'
```
#### Immobilie erstellen (Mit API-Key)
```bash ```bash
curl -X POST http://localhost:8080/api/immobilies \ curl -X POST http://localhost:8080/api/immobilies \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "X-API-KEY: your-api-key-here" \
-d '{ -d '{
"verwalter": "/api/users/1", "verwalter": "/api/users/1",
"adresse": "Hauptstraße 123, 12345 Musterstadt", "adresse": "Hauptstraße 123, 12345 Musterstadt",
@@ -549,21 +647,25 @@ curl -X POST http://localhost:8080/api/users \
}' }'
``` ```
#### Immobilien mit Filter abrufen (Pagination) #### Eigene Immobilien abrufen (Mit API-Key)
```bash ```bash
# Erste Seite (Standard) # Erste Seite (Standard) - User sieht nur eigene Immobilien
curl http://localhost:8080/api/immobilies curl -H "X-API-KEY: your-api-key-here" http://localhost:8080/api/immobilies
# Zweite Seite # Zweite Seite
curl http://localhost:8080/api/immobilies?page=2 curl -H "X-API-KEY: your-api-key-here" http://localhost:8080/api/immobilies?page=2
# Als Admin: Sieht alle Immobilien
curl -H "X-API-KEY: admin-api-key-here" http://localhost:8080/api/immobilies
``` ```
#### Einzelne Immobilie aktualisieren (PATCH) #### Einzelne Immobilie aktualisieren (PATCH, Mit API-Key)
```bash ```bash
curl -X PATCH http://localhost:8080/api/immobilies/1 \ curl -X PATCH http://localhost:8080/api/immobilies/1 \
-H "Content-Type: application/merge-patch+json" \ -H "Content-Type: application/merge-patch+json" \
-H "X-API-KEY: your-api-key-here" \
-d '{ -d '{
"wohnflaeche": 90, "wohnflaeche": 90,
"kaufpreis": 360000, "kaufpreis": 360000,
@@ -596,6 +698,7 @@ Die API verwendet folgende Enums:
- `user` - Benutzer - `user` - Benutzer
- `admin` - Administrator - `admin` - Administrator
- `moderator` - Moderator - `moderator` - Moderator
- `technical` - Technischer User
### Relationen ### Relationen

View File

@@ -2,28 +2,53 @@ security:
# https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords # https://symfony.com/doc/current/security.html#registering-the-user-hashing-passwords
password_hashers: password_hashers:
Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto'
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider # https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers: providers:
users_in_memory: { memory: null } app_user_provider:
entity:
class: App\Entity\User
property: email
firewalls: firewalls:
dev: dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/ pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false security: false
# API-Dokumentation (öffentlich zugänglich)
api_docs:
pattern: ^/api/docs
stateless: true
security: false
# Öffentliche Endpunkte (Bundesländer, Heizungstypen)
public_api:
pattern: ^/api/(bundeslands|heizungstyps)
stateless: true
security: false
# API mit API-Key-Authentifizierung
api:
pattern: ^/api
stateless: true
custom_authenticators:
- App\Security\ApiKeyAuthenticator
main: main:
lazy: true lazy: true
provider: users_in_memory provider: app_user_provider
# activate different ways to authenticate
# https://symfony.com/doc/current/security.html#the-firewall
# https://symfony.com/doc/current/security/impersonating_user.html
# switch_user: true
# Easy way to control access for large sections of your site # Easy way to control access for large sections of your site
# Note: Only the *first* access control that matches will be used # Note: Only the *first* access control that matches will be used
access_control: access_control:
# - { path: ^/admin, roles: ROLE_ADMIN } # API-Dokumentation ist öffentlich
# - { path: ^/profile, roles: ROLE_USER } - { path: ^/api/docs, roles: PUBLIC_ACCESS }
# Öffentlicher Zugriff auf Bundesländer und Heizungstypen (GET)
- { path: ^/api/(bundeslands|heizungstyps), methods: [GET], roles: PUBLIC_ACCESS }
# Admin und Technical User Zugriff für Änderungen an Bundesländern und Heizungstypen
- { path: ^/api/(bundeslands|heizungstyps), methods: [POST, PUT, DELETE, PATCH], roles: [ROLE_ADMIN, ROLE_TECHNICAL] }
# Alle anderen API-Endpunkte erfordern Authentifizierung
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
when@test: when@test:
security: security:

View File

@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20251108215623 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE users ADD api_key VARCHAR(64) NOT NULL');
$this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E9C912ED9D ON users (api_key)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP INDEX UNIQ_1483A5E9C912ED9D ON users');
$this->addSql('ALTER TABLE users DROP api_key');
}
}

View File

@@ -0,0 +1,22 @@
-- Bundesländer mit Grunderwerbsteuer-Sätzen (Stand 2025)
-- Dieses Script wird nach der Migration automatisch ausgeführt
INSERT INTO bundeslaender (name, grunderwerbsteuer) VALUES
('Baden-Württemberg', 5.00),
('Bayern', 3.50),
('Berlin', 6.00),
('Brandenburg', 6.50),
('Bremen', 5.00),
('Hamburg', 5.50),
('Hessen', 6.00),
('Mecklenburg-Vorpommern', 6.00),
('Niedersachsen', 5.00),
('Nordrhein-Westfalen', 6.50),
('Rheinland-Pfalz', 5.00),
('Saarland', 6.50),
('Sachsen', 5.50),
('Sachsen-Anhalt', 5.00),
('Schleswig-Holstein', 6.50),
('Thüringen', 5.00)
ON DUPLICATE KEY UPDATE
grunderwerbsteuer = VALUES(grunderwerbsteuer);

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Doctrine;
use ApiPlatform\Doctrine\Orm\Extension\QueryCollectionExtensionInterface;
use ApiPlatform\Doctrine\Orm\Extension\QueryItemExtensionInterface;
use ApiPlatform\Doctrine\Orm\Util\QueryNameGeneratorInterface;
use ApiPlatform\Metadata\Operation;
use App\Entity\Immobilie;
use App\Entity\User;
use App\Enum\UserRole;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bundle\SecurityBundle\Security;
class CurrentUserExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface
{
public function __construct(
private Security $security
) {
}
public function applyToCollection(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, Operation $operation = null, array $context = []): void
{
$this->addWhere($queryBuilder, $resourceClass);
}
public function applyToItem(QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, Operation $operation = null, array $context = []): void
{
$this->addWhere($queryBuilder, $resourceClass);
}
private function addWhere(QueryBuilder $queryBuilder, string $resourceClass): void
{
// Nur für Immobilie-Entity
if (Immobilie::class !== $resourceClass) {
return;
}
$user = $this->security->getUser();
// Wenn nicht eingeloggt, keine Ergebnisse
if (!$user instanceof User) {
$queryBuilder->andWhere('1 = 0');
return;
}
// Admin sieht alles
if ($user->getRole() === UserRole::ADMIN) {
return;
}
// Normale User sehen nur eigene Immobilien
$rootAlias = $queryBuilder->getRootAliases()[0];
$queryBuilder->andWhere(sprintf('%s.verwalter = :current_user', $rootAlias))
->setParameter('current_user', $user);
}
}

View File

@@ -16,11 +16,11 @@ use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Table(name: 'bundeslaender')] #[ORM\Table(name: 'bundeslaender')]
#[ApiResource( #[ApiResource(
operations: [ operations: [
new Get(), new Get(security: 'is_granted("PUBLIC_ACCESS")'),
new GetCollection(), new GetCollection(security: 'is_granted("PUBLIC_ACCESS")'),
new Post(), new Post(security: 'is_granted("ROLE_ADMIN") or is_granted("ROLE_TECHNICAL")'),
new Put(), new Put(security: 'is_granted("ROLE_ADMIN") or is_granted("ROLE_TECHNICAL")'),
new Delete() new Delete(security: 'is_granted("ROLE_ADMIN") or is_granted("ROLE_TECHNICAL")')
] ]
)] )]
class Bundesland class Bundesland

View File

@@ -16,11 +16,11 @@ use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Table(name: 'heizungstypen')] #[ORM\Table(name: 'heizungstypen')]
#[ApiResource( #[ApiResource(
operations: [ operations: [
new Get(), new Get(security: 'is_granted("PUBLIC_ACCESS")'),
new GetCollection(), new GetCollection(security: 'is_granted("PUBLIC_ACCESS")'),
new Post(), new Post(security: 'is_granted("ROLE_ADMIN") or is_granted("ROLE_TECHNICAL")'),
new Put(), new Put(security: 'is_granted("ROLE_ADMIN") or is_granted("ROLE_TECHNICAL")'),
new Delete() new Delete(security: 'is_granted("ROLE_ADMIN") or is_granted("ROLE_TECHNICAL")')
] ]
)] )]
class Heizungstyp class Heizungstyp

View File

@@ -3,6 +3,11 @@
namespace App\Entity; namespace App\Entity;
use ApiPlatform\Metadata\ApiResource; use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Metadata\Post;
use ApiPlatform\Metadata\Patch;
use ApiPlatform\Metadata\Delete;
use App\Enum\ImmobilienTyp; use App\Enum\ImmobilienTyp;
use App\Repository\ImmobilieRepository; use App\Repository\ImmobilieRepository;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
@@ -11,7 +16,16 @@ use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: ImmobilieRepository::class)] #[ORM\Entity(repositoryClass: ImmobilieRepository::class)]
#[ORM\Table(name: 'immobilien')] #[ORM\Table(name: 'immobilien')]
#[ORM\HasLifecycleCallbacks] #[ORM\HasLifecycleCallbacks]
#[ApiResource] #[ApiResource(
operations: [
new Get(security: 'is_granted("view", object)'),
new GetCollection(),
new Post(),
new Patch(security: 'is_granted("edit", object)'),
new Delete(security: 'is_granted("delete", object)')
],
security: 'is_granted("ROLE_USER")'
)]
class Immobilie class Immobilie
{ {
#[ORM\Id] #[ORM\Id]
@@ -298,7 +312,7 @@ class Immobilie
*/ */
public function getKaufnebenkosten(): array public function getKaufnebenkosten(): array
{ {
if (!$this->kaufpreis || !$this->bundesland) { if (!$this->getKaufpreis() || !$this->bundesland) {
return [ return [
'notar' => 0, 'notar' => 0,
'grundbuch' => 0, 'grundbuch' => 0,
@@ -308,14 +322,14 @@ class Immobilie
} }
// Notarkosten: ca. 1,5% des Kaufpreises // Notarkosten: ca. 1,5% des Kaufpreises
$notar = (int) round($this->kaufpreis * 0.015); $notar = $this->getKaufpreis() * 0.015;
// Grundbuchkosten: ca. 0,5% des Kaufpreises // Grundbuchkosten: ca. 0,5% des Kaufpreises
$grundbuch = (int) round($this->kaufpreis * 0.005); $grundbuch = $this->getKaufpreis() * 0.005;
// Grunderwerbsteuer: abhängig vom Bundesland // Grunderwerbsteuer: abhängig vom Bundesland
$grunderwerbsteuerSatz = $this->bundesland->getGrunderwerbsteuer() / 100; $grunderwerbsteuerSatz = $this->bundesland->getGrunderwerbsteuer() / 100;
$grunderwerbsteuer = (int) round($this->kaufpreis * $grunderwerbsteuerSatz); $grunderwerbsteuer = $this->getKaufpreis() * $grunderwerbsteuerSatz;
$gesamt = $notar + $grundbuch + $grunderwerbsteuer; $gesamt = $notar + $grundbuch + $grunderwerbsteuer;

View File

@@ -13,6 +13,7 @@ use App\Repository\UserRepository;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
#[ORM\Entity(repositoryClass: UserRepository::class)] #[ORM\Entity(repositoryClass: UserRepository::class)]
@@ -26,7 +27,7 @@ use Symfony\Component\Validator\Constraints as Assert;
new Delete() new Delete()
] ]
)] )]
class User class User implements UserInterface
{ {
#[ORM\Id] #[ORM\Id]
#[ORM\GeneratedValue] #[ORM\GeneratedValue]
@@ -46,6 +47,9 @@ class User
#[ORM\Column(type: 'string', enumType: UserRole::class)] #[ORM\Column(type: 'string', enumType: UserRole::class)]
private UserRole $role; private UserRole $role;
#[ORM\Column(type: 'string', length: 64, unique: true)]
private string $apiKey;
#[ORM\Column(type: 'datetime')] #[ORM\Column(type: 'datetime')]
private \DateTimeInterface $createdAt; private \DateTimeInterface $createdAt;
@@ -57,6 +61,15 @@ class User
$this->createdAt = new \DateTime(); $this->createdAt = new \DateTime();
$this->role = UserRole::USER; $this->role = UserRole::USER;
$this->immobilien = new ArrayCollection(); $this->immobilien = new ArrayCollection();
$this->apiKey = $this->generateApiKey();
}
/**
* Generiert einen eindeutigen API-Key
*/
private function generateApiKey(): string
{
return hash('sha256', random_bytes(32) . microtime(true));
} }
public function getId(): ?int public function getId(): ?int
@@ -131,4 +144,46 @@ class User
$this->immobilien->removeElement($immobilie); $this->immobilien->removeElement($immobilie);
return $this; return $this;
} }
public function getApiKey(): string
{
return $this->apiKey;
}
/**
* Generiert einen neuen API-Key
*/
public function regenerateApiKey(): self
{
$this->apiKey = $this->generateApiKey();
return $this;
}
/**
* UserInterface Methods
*/
public function getUserIdentifier(): string
{
return $this->email;
}
public function getRoles(): array
{
$roles = ['ROLE_USER'];
if ($this->role === UserRole::ADMIN) {
$roles[] = 'ROLE_ADMIN';
} elseif ($this->role === UserRole::MODERATOR) {
$roles[] = 'ROLE_MODERATOR';
} elseif ($this->role === UserRole::TECHNICAL) {
$roles[] = 'ROLE_TECHNICAL';
}
return array_unique($roles);
}
public function eraseCredentials(): void
{
// Nothing to erase as we use API keys
}
} }

View File

@@ -7,6 +7,7 @@ enum UserRole: string
case USER = 'user'; case USER = 'user';
case ADMIN = 'admin'; case ADMIN = 'admin';
case MODERATOR = 'moderator'; case MODERATOR = 'moderator';
case TECHNICAL = 'technical';
public function getLabel(): string public function getLabel(): string
{ {
@@ -14,6 +15,7 @@ enum UserRole: string
self::USER => 'Benutzer', self::USER => 'Benutzer',
self::ADMIN => 'Administrator', self::ADMIN => 'Administrator',
self::MODERATOR => 'Moderator', self::MODERATOR => 'Moderator',
self::TECHNICAL => 'Technischer User',
}; };
} }
} }

View File

@@ -0,0 +1,62 @@
<?php
namespace App\Security;
use App\Repository\UserRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
class ApiKeyAuthenticator extends AbstractAuthenticator
{
public function __construct(
private UserRepository $userRepository
) {
}
public function supports(Request $request): ?bool
{
return $request->headers->has('X-API-KEY');
}
public function authenticate(Request $request): Passport
{
$apiKey = $request->headers->get('X-API-KEY');
if (null === $apiKey) {
throw new CustomUserMessageAuthenticationException('No API key provided');
}
return new SelfValidatingPassport(
new UserBadge($apiKey, function($apiKey) {
$user = $this->userRepository->findOneBy(['apiKey' => $apiKey]);
if (!$user) {
throw new CustomUserMessageAuthenticationException('Invalid API key');
}
return $user;
})
);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
// On success, let the request continue
return null;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
return new JsonResponse([
'message' => strtr($exception->getMessageKey(), $exception->getMessageData())
], Response::HTTP_UNAUTHORIZED);
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace App\Security;
use App\Entity\Immobilie;
use App\Entity\User;
use App\Enum\UserRole;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class ImmobilieVoter extends Voter
{
const VIEW = 'view';
const EDIT = 'edit';
const DELETE = 'delete';
protected function supports(string $attribute, mixed $subject): bool
{
// Voter unterstützt nur diese Attribute und nur Immobilie-Objekte
return in_array($attribute, [self::VIEW, self::EDIT, self::DELETE])
&& $subject instanceof Immobilie;
}
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
{
$user = $token->getUser();
// User muss eingeloggt sein
if (!$user instanceof User) {
return false;
}
/** @var Immobilie $immobilie */
$immobilie = $subject;
// Admin hat uneingeschränkten Zugriff
if ($user->getRole() === UserRole::ADMIN) {
return true;
}
// Prüfe je nach Attribut
return match($attribute) {
self::VIEW => $this->canView($immobilie, $user),
self::EDIT => $this->canEdit($immobilie, $user),
self::DELETE => $this->canDelete($immobilie, $user),
default => false
};
}
private function canView(Immobilie $immobilie, User $user): bool
{
// User kann nur eigene Immobilien sehen
return $immobilie->getVerwalter() === $user;
}
private function canEdit(Immobilie $immobilie, User $user): bool
{
// User kann nur eigene Immobilien bearbeiten
return $immobilie->getVerwalter() === $user;
}
private function canDelete(Immobilie $immobilie, User $user): bool
{
// User kann nur eigene Immobilien löschen
return $immobilie->getVerwalter() === $user;
}
}