API updadte
This commit is contained in:
125
README.md
125
README.md
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
33
migrations/Version20251108215623.php
Normal file
33
migrations/Version20251108215623.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
22
migrations/bundeslaender_data.sql
Normal file
22
migrations/bundeslaender_data.sql
Normal 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);
|
||||||
57
src/Doctrine/CurrentUserExtension.php
Normal file
57
src/Doctrine/CurrentUserExtension.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
62
src/Security/ApiKeyAuthenticator.php
Normal file
62
src/Security/ApiKeyAuthenticator.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
67
src/Security/ImmobilieVoter.php
Normal file
67
src/Security/ImmobilieVoter.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user