Compare commits
3 Commits
60dc35961a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb416dff55 | ||
|
|
62c13ff0b1 | ||
|
|
d85406f0c7 |
292
DEPLOYMENT.md
Normal file
292
DEPLOYMENT.md
Normal file
@@ -0,0 +1,292 @@
|
||||
# Deployment Setup für SKAMP API
|
||||
|
||||
Diese Anleitung beschreibt das vollständige Setup für das automatische Deployment der Spring Boot Applikation auf einen Remote Server.
|
||||
|
||||
## Übersicht
|
||||
|
||||
Bei einem Push auf den `main` Branch wird automatisch:
|
||||
1. Die Spring Boot Applikation gebaut
|
||||
2. Ein Docker Image erstellt
|
||||
3. Das Image in die Gitea Container Registry gepusht
|
||||
4. Das Image auf dem Remote Server deployed
|
||||
|
||||
## Voraussetzungen
|
||||
|
||||
### Auf dem Deployment-Server
|
||||
|
||||
1. **Docker installiert** (Version 20.10+)
|
||||
```bash
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
```
|
||||
|
||||
2. **Keine weiteren Dateien notwendig**
|
||||
- Der Workflow deployed direkt via `docker run`
|
||||
- Keine compose.yml oder .env Dateien auf dem Server erforderlich
|
||||
- Alle Konfiguration erfolgt über Gitea Secrets
|
||||
|
||||
## Gitea Configuration
|
||||
|
||||
Im Gitea Repository müssen folgende Variables und Secrets angelegt werden:
|
||||
|
||||
### Repository Settings > Actions > Variables
|
||||
|
||||
| Variable Name | Beschreibung | Beispiel |
|
||||
|--------------|--------------|----------|
|
||||
| `REGISTRY_URL` | Gitea Container Registry URL | `gitea.moz-tech.de` |
|
||||
| `NAMESPACE` | Organisation/Benutzer | `murat` |
|
||||
| `REPO_NAME` | Repository Name | `skamp` |
|
||||
|
||||
### Repository Settings > Actions > Secrets
|
||||
|
||||
| Secret Name | Beschreibung | Beispiel |
|
||||
|------------|--------------|----------|
|
||||
| `CI_GITEA_USER` | Gitea Benutzername | `murat` |
|
||||
| `CI_GITEA_TOKEN` | Gitea Access Token | `ghp_xxxxxxxxxxxx` |
|
||||
| `SSH_HOST` | Server IP/Hostname | `123.456.789.0` |
|
||||
| `SSH_USERNAME` | SSH Benutzername | `deploy` |
|
||||
| `SSH_PRIVATE_KEY` | SSH Private Key | `-----BEGIN RSA PRIVATE KEY-----...` |
|
||||
| `SSH_PORT` | SSH Port | `22` |
|
||||
| `APP_PORT` | Anwendungs-Port | `8080` |
|
||||
| `SPRING_PROFILES_ACTIVE` | Spring Profil | `prod` |
|
||||
| `POSTGRES_HOST` | PostgreSQL Host | `db.example.com` |
|
||||
| `POSTGRES_PORT` | PostgreSQL Port | `5432` |
|
||||
| `POSTGRES_DB` | Datenbank Name | `skamp` |
|
||||
| `POSTGRES_USER` | Datenbank User | `skamp` |
|
||||
| `POSTGRES_PASSWORD` | Datenbank Passwort | `IhrSicheresPasswort123!` |
|
||||
| `HIBERNATE_DDL_AUTO` | Hibernate DDL | `update` |
|
||||
| `KEYCLOAK_ISSUER_URI` | Keycloak Issuer URI | `https://keycloak.example.com/realms/skamp` |
|
||||
| `CORS_ALLOWED_ORIGINS` | Erlaubte CORS Origins | `https://frontend.example.com` |
|
||||
| `MINIO_ENDPOINT` | MinIO/S3 Endpoint | `https://s3.example.com` |
|
||||
| `MINIO_ACCESS_KEY` | MinIO Access Key | `your_access_key` |
|
||||
| `MINIO_SECRET_KEY` | MinIO Secret Key | `your_secret_key` |
|
||||
| `JAVA_OPTS` | Java Optionen | `-Xmx1024m -Xms512m` |
|
||||
|
||||
### Gitea Token erstellen
|
||||
|
||||
1. Gitea → Einstellungen → Anwendungen → Access Tokens
|
||||
2. Token Name: `deployment-token`
|
||||
3. Berechtigungen: `read:packages`, `write:packages`
|
||||
4. Token generieren und als `CI_GITEA_TOKEN` Secret speichern
|
||||
|
||||
### SSH Key erstellen
|
||||
|
||||
```bash
|
||||
# Auf deinem lokalen Rechner
|
||||
ssh-keygen -t ed25519 -C "deployment-key" -f ~/.ssh/skamp_deploy
|
||||
|
||||
# Public Key auf den Server kopieren
|
||||
ssh-copy-id -i ~/.ssh/skamp_deploy.pub user@server
|
||||
|
||||
# Private Key als Secret speichern
|
||||
cat ~/.ssh/skamp_deploy
|
||||
```
|
||||
|
||||
## Erstes Deployment
|
||||
|
||||
Das erste Deployment erfolgt automatisch beim Push auf `main`. Der Workflow:
|
||||
|
||||
1. Baut die Spring Boot 3 Applikation mit Maven
|
||||
2. Erstellt ein Docker Image
|
||||
3. Pusht das Image zur Gitea Registry
|
||||
4. Loggt sich per SSH auf dem Server ein
|
||||
5. Pulled das Image
|
||||
6. Startet den Container mit allen Environment-Variablen
|
||||
|
||||
Alternativ kannst du das erste Deployment manuell testen:
|
||||
|
||||
```bash
|
||||
# Auf dem Server
|
||||
# Login zur Container Registry
|
||||
docker login gitea.moz-tech.de -u murat
|
||||
|
||||
# Pull Image (ersetze IMAGE_TAG mit einem Commit-SHA oder 'latest')
|
||||
docker pull gitea.moz-tech.de/murat/skamp:latest
|
||||
|
||||
# Starte Container
|
||||
docker run -d \
|
||||
--name skamp-app \
|
||||
--restart unless-stopped \
|
||||
-p 8080:8080 \
|
||||
-e SPRING_PROFILES_ACTIVE=prod \
|
||||
-e SPRING_DATASOURCE_URL=jdbc:postgresql://db.example.com:5432/skamp \
|
||||
-e SPRING_DATASOURCE_USERNAME=skamp \
|
||||
-e SPRING_DATASOURCE_PASSWORD=IhrPasswort \
|
||||
-e SPRING_JPA_HIBERNATE_DDL_AUTO=update \
|
||||
-e KEYCLOAK_ISSUER_URI=https://keycloak.example.com/realms/skamp \
|
||||
-e CORS_ALLOWED_ORIGINS=https://frontend.example.com \
|
||||
-e S3_ACCESS_KEY=your_access_key \
|
||||
-e S3_SECRET_KEY=your_secret_key \
|
||||
-e S3_ENDPOINT=https://s3.example.com \
|
||||
-e JAVA_OPTS="-Xmx1024m -Xms512m" \
|
||||
gitea.moz-tech.de/murat/skamp:latest
|
||||
|
||||
# Prüfe Status
|
||||
docker ps --filter name=skamp-app
|
||||
docker logs -f skamp-app
|
||||
```
|
||||
|
||||
## Workflow Trigger
|
||||
|
||||
Das Deployment wird automatisch ausgelöst durch:
|
||||
|
||||
```bash
|
||||
git add .
|
||||
git commit -m "Deploy: Update application"
|
||||
git push origin main
|
||||
```
|
||||
|
||||
## Monitoring
|
||||
|
||||
### Logs anzeigen
|
||||
|
||||
```bash
|
||||
# Auf dem Server
|
||||
|
||||
# Container Logs live anzeigen
|
||||
docker logs -f skamp-app
|
||||
|
||||
# Letzte 100 Zeilen
|
||||
docker logs --tail 100 skamp-app
|
||||
|
||||
# Mit Timestamps
|
||||
docker logs -f --timestamps skamp-app
|
||||
```
|
||||
|
||||
### Container Status
|
||||
|
||||
```bash
|
||||
# Status prüfen
|
||||
docker ps --filter name=skamp-app
|
||||
|
||||
# Detaillierte Informationen
|
||||
docker inspect skamp-app
|
||||
|
||||
# Resource-Nutzung
|
||||
docker stats skamp-app
|
||||
```
|
||||
|
||||
### Health Check
|
||||
|
||||
```bash
|
||||
# HTTP Health Check
|
||||
curl http://localhost:8080/actuator/health
|
||||
|
||||
# Container Health Status
|
||||
docker inspect --format='{{.State.Health.Status}}' skamp-app
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Workflow schlägt fehl
|
||||
|
||||
1. **Build Error**: Prüfe die Java Version und Maven Dependencies in `pom.xml`
|
||||
2. **Registry Login Error**: Prüfe `CI_GITEA_TOKEN` Secret
|
||||
3. **SSH Error**: Prüfe SSH Secrets (`SSH_HOST`, `SSH_USERNAME`, `SSH_PRIVATE_KEY`, `SSH_PORT`)
|
||||
4. **Deployment Error**: Prüfe Server-Logs mit `docker logs skamp-app`
|
||||
|
||||
### Container startet nicht
|
||||
|
||||
```bash
|
||||
# Prüfe Logs
|
||||
docker logs skamp-app
|
||||
|
||||
# Prüfe Container-Details
|
||||
docker inspect skamp-app
|
||||
|
||||
# Container neu starten
|
||||
docker restart skamp-app
|
||||
|
||||
# Container mit neuen Einstellungen starten
|
||||
docker stop skamp-app
|
||||
docker rm skamp-app
|
||||
# Dann docker run erneut ausführen (siehe Deployment-Sektion)
|
||||
```
|
||||
|
||||
### Datenbank-Verbindungsfehler
|
||||
|
||||
```bash
|
||||
# 1. Prüfe ob PostgreSQL erreichbar ist
|
||||
docker exec skamp-app ping -c 3 $POSTGRES_HOST
|
||||
|
||||
# 2. Prüfe Verbindung zur Datenbank
|
||||
docker exec -it skamp-app sh -c "apt update && apt install -y postgresql-client"
|
||||
docker exec -it skamp-app psql -h $POSTGRES_HOST -U $POSTGRES_USER -d $POSTGRES_DB
|
||||
|
||||
# 3. Prüfe Environment-Variablen im Container
|
||||
docker exec skamp-app env | grep SPRING_DATASOURCE
|
||||
```
|
||||
|
||||
### Port-Konflikte
|
||||
|
||||
```bash
|
||||
# Prüfe ob Port bereits belegt ist
|
||||
sudo netstat -tlnp | grep :8080
|
||||
# oder
|
||||
sudo lsof -i :8080
|
||||
|
||||
# Anderen Port verwenden (z.B. 8081)
|
||||
docker stop skamp-app
|
||||
docker rm skamp-app
|
||||
# Dann docker run mit -p 8081:8080 statt -p 8080:8080
|
||||
```
|
||||
|
||||
## Updates
|
||||
|
||||
### Automatisches Update
|
||||
|
||||
Jeder Push auf `main` löst ein automatisches Update aus.
|
||||
|
||||
### Manuelles Update auf spezifische Version
|
||||
|
||||
```bash
|
||||
# Auf dem Server
|
||||
|
||||
# Stoppe aktuellen Container
|
||||
docker stop skamp-app
|
||||
docker rm skamp-app
|
||||
|
||||
# Pull spezifische Version (ersetze COMMIT_SHA)
|
||||
docker pull gitea.moz-tech.de/murat/skamp:COMMIT_SHA
|
||||
|
||||
# Starte Container mit neuer Version
|
||||
docker run -d \
|
||||
--name skamp-app \
|
||||
--restart unless-stopped \
|
||||
-p 8080:8080 \
|
||||
-e ... (alle Environment-Variablen) \
|
||||
gitea.moz-tech.de/murat/skamp:COMMIT_SHA
|
||||
```
|
||||
|
||||
### Rollback zu vorheriger Version
|
||||
|
||||
```bash
|
||||
# Prüfe verfügbare Images
|
||||
docker images | grep skamp
|
||||
|
||||
# Stoppe aktuellen Container
|
||||
docker stop skamp-app
|
||||
docker rm skamp-app
|
||||
|
||||
# Starte mit älterer Version
|
||||
docker run -d \
|
||||
--name skamp-app \
|
||||
--restart unless-stopped \
|
||||
-p 8080:8080 \
|
||||
-e ... (alle Environment-Variablen) \
|
||||
gitea.moz-tech.de/murat/skamp:OLD_COMMIT_SHA
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
1. **Secrets niemals committen** - Nutze `.gitignore` für `.env`
|
||||
2. **Starke Passwörter** - Generiere sichere Passwörter für DB und S3
|
||||
3. **SSH Key Rotation** - Rotiere SSH Keys regelmäßig
|
||||
4. **Firewall** - Beschränke Zugriff auf notwendige Ports
|
||||
5. **Updates** - Halte Docker und Images aktuell
|
||||
6. **Monitoring** - Implementiere Monitoring und Alerting
|
||||
|
||||
## Weitere Informationen
|
||||
|
||||
- Gitea Actions: https://docs.gitea.com/usage/actions/overview
|
||||
- Docker Compose: https://docs.docker.com/compose/
|
||||
- Spring Boot: https://spring.io/projects/spring-boot
|
||||
181
DEPLOYMENT_SECRETS.md
Normal file
181
DEPLOYMENT_SECRETS.md
Normal file
@@ -0,0 +1,181 @@
|
||||
# Deployment Secrets Configuration
|
||||
|
||||
Diese Dokumentation beschreibt die erforderlichen Secrets für den automatischen Deployment-Workflow in Gitea.
|
||||
|
||||
## Erforderliche Secrets und Variablen in Gitea
|
||||
|
||||
### Gitea Variables (nicht-sensitive Daten)
|
||||
|
||||
Navigiere zu: **Repository Settings → Variables → Actions**
|
||||
|
||||
Diese Werte sind nicht sensitiv und können als Variables gespeichert werden:
|
||||
|
||||
| Variable Name | Beschreibung | Beispiel |
|
||||
|------------|--------------|----------|
|
||||
| `REGISTRY_URL` | URL der Container Registry | `gitea.moz-tech.de` |
|
||||
| `NAMESPACE` | Namespace/Organisation in der Registry | `murat` |
|
||||
| `REPO_NAME` | Repository Name für Docker Image | `skamp` |
|
||||
|
||||
### Gitea Secrets (sensitive Daten)
|
||||
|
||||
Navigiere zu: **Repository Settings → Secrets → Actions**
|
||||
|
||||
Diese Werte sind sensitiv und müssen als Secrets gespeichert werden:
|
||||
|
||||
#### Container Registry Secrets
|
||||
|
||||
| Secret Name | Beschreibung | Beispiel |
|
||||
|------------|--------------|----------|
|
||||
| `CI_GITEA_USER` | Gitea Benutzername für Registry Login | `murat` |
|
||||
| `CI_GITEA_TOKEN` | Gitea Access Token mit Registry-Rechten | `74a7738116bfb99497a7781291efc5766901f497` |
|
||||
|
||||
#### SSH Server Secrets
|
||||
|
||||
| Secret Name | Beschreibung | Beispiel |
|
||||
|------------|--------------|----------|
|
||||
| `SSH_HOST` | Hostname oder IP des Deployment-Servers | `server.example.com` |
|
||||
| `SSH_USERNAME` | SSH Benutzername | `deploy` |
|
||||
| `SSH_PRIVATE_KEY` | SSH Private Key (kompletter Inhalt) | `-----BEGIN OPENSSH PRIVATE KEY-----...` |
|
||||
| `SSH_PORT` | SSH Port (optional, Standard: 22) | `22` |
|
||||
| `DEPLOY_PATH` | Pfad zum Deployment-Verzeichnis auf dem Server | `/opt/skamp/docker` |
|
||||
|
||||
### Application Secrets
|
||||
|
||||
| Secret Name | Beschreibung | Beispiel |
|
||||
|------------|--------------|----------|
|
||||
| `APP_PORT` | Port für die Anwendung | `8082` |
|
||||
| `SPRING_PROFILES_ACTIVE` | Spring Boot Profil | `prod` |
|
||||
| `APPLICATION_NAME` | Name der Anwendung | `skamp-api` |
|
||||
| `CORS_ALLOWED_ORIGINS` | Erlaubte CORS Origins | `https://app.example.com` |
|
||||
|
||||
### PostgreSQL Secrets
|
||||
|
||||
| Secret Name | Beschreibung | Beispiel |
|
||||
|------------|--------------|----------|
|
||||
| `POSTGRES_HOST` | PostgreSQL Hostname | `postgres` oder `db.example.com` |
|
||||
| `POSTGRES_PORT` | PostgreSQL Port | `5432` |
|
||||
| `POSTGRES_DB` | Datenbank Name | `skamp` |
|
||||
| `POSTGRES_USER` | Datenbank Benutzername | `skamp_user` |
|
||||
| `POSTGRES_PASSWORD` | Datenbank Passwort | `secure_password_here` |
|
||||
| `HIBERNATE_DDL_AUTO` | Hibernate DDL Modus | `update` oder `validate` |
|
||||
|
||||
### Keycloak Secrets
|
||||
|
||||
| Secret Name | Beschreibung | Beispiel |
|
||||
|------------|--------------|----------|
|
||||
| `KEYCLOAK_URL` | Keycloak Server URL | `https://auth.moz-tech.de/` |
|
||||
| `KEYCLOAK_REALM` | Keycloak Realm | `enerport` |
|
||||
| `KEYCLOAK_ISSUER_URI` | Keycloak Issuer URI | `https://auth.moz-tech.de/realms/enerport` |
|
||||
|
||||
### MinIO (S3) Secrets
|
||||
|
||||
| Secret Name | Beschreibung | Beispiel |
|
||||
|------------|--------------|----------|
|
||||
| `MINIO_ENDPOINT` | MinIO Endpoint URL | `http://minio:9000` |
|
||||
| `MINIO_ACCESS_KEY` | MinIO Access Key | `minioadmin` |
|
||||
| `MINIO_SECRET_KEY` | MinIO Secret Key | `minioadmin123` |
|
||||
|
||||
### AWS S3 Secrets (Optional)
|
||||
|
||||
| Secret Name | Beschreibung | Beispiel |
|
||||
|------------|--------------|----------|
|
||||
| `AWS_ACCESS_KEY_ID` | AWS Access Key ID | `AKIAIOSFODNN7EXAMPLE` |
|
||||
| `AWS_SECRET_ACCESS_KEY` | AWS Secret Access Key | `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY` |
|
||||
| `AWS_S3_BUCKET_NAME` | AWS S3 Bucket Name | `my-bucket` |
|
||||
| `AWS_S3_REGION` | AWS S3 Region | `eu-central-1` |
|
||||
| `AWS_S3_ENDPOINT` | AWS S3 Endpoint (optional) | `https://s3.eu-central-1.amazonaws.com` |
|
||||
|
||||
### Additional Configuration Secrets
|
||||
|
||||
| Secret Name | Beschreibung | Beispiel |
|
||||
|------------|--------------|----------|
|
||||
| `JAVA_OPTS` | Java JVM Optionen | `-Xmx512m -Xms256m` |
|
||||
|
||||
## Workflow Ablauf
|
||||
|
||||
Der Workflow wird bei jedem Push auf den `main` Branch ausgelöst:
|
||||
|
||||
1. **Checkout Code** - Lädt den Repository-Code
|
||||
2. **Login to Container Registry** - Authentifiziert sich bei der Gitea Registry
|
||||
3. **Extract metadata** - Erstellt Image-Tags (commit SHA + latest)
|
||||
4. **Build and Push Docker Image** - Baut das Docker Image mit Multi-Stage Build (Maven Build + Runtime Image) und pusht es zur Registry
|
||||
5. **Deploy to Remote Server** - Verbindet per SSH zum Server, erstellt .env Datei aus Secrets und deployed die Anwendung
|
||||
|
||||
**Hinweis:** Der Maven Build erfolgt im Docker Multi-Stage Build (siehe Dockerfile), daher ist kein separater Maven-Build-Step im Workflow erforderlich.
|
||||
|
||||
## Server-Vorbereitung
|
||||
|
||||
Auf dem Deployment-Server müssen folgende Voraussetzungen erfüllt sein:
|
||||
|
||||
1. **Docker und Docker Compose installiert**
|
||||
```bash
|
||||
docker --version
|
||||
docker compose version
|
||||
```
|
||||
|
||||
2. **Deployment-Verzeichnis eingerichtet**
|
||||
```bash
|
||||
mkdir -p /opt/skamp/docker
|
||||
cd /opt/skamp/docker
|
||||
```
|
||||
|
||||
3. **Docker Compose Konfiguration**
|
||||
- `compose.yml` - Die Docker Compose Konfiguration
|
||||
- `.env` - Umgebungsvariablen für das Deployment
|
||||
|
||||
4. **SSH-Zugriff konfiguriert**
|
||||
- SSH Public Key des CI/CD Servers muss auf dem Deployment-Server hinterlegt sein
|
||||
- SSH Private Key muss als Secret `SSH_PRIVATE_KEY` in Gitea hinterlegt sein
|
||||
|
||||
## Image Tagging
|
||||
|
||||
Der Workflow erstellt zwei Image-Tags:
|
||||
|
||||
- `${REGISTRY_URL}/${NAMESPACE}/${REPO_NAME}:${COMMIT_SHA}` - Spezifischer Commit
|
||||
- `${REGISTRY_URL}/${NAMESPACE}/${REPO_NAME}:latest` - Immer die neueste Version
|
||||
|
||||
## Deployment auf dem Server
|
||||
|
||||
Das Deployment-Script auf dem Server:
|
||||
|
||||
1. Navigiert zum Deployment-Verzeichnis
|
||||
2. Authentifiziert sich bei der Container Registry
|
||||
3. Setzt die IMAGE_TAG Umgebungsvariable auf den Commit SHA
|
||||
4. Pullt das neueste Image
|
||||
5. Startet die Services neu mit `docker compose up -d`
|
||||
6. Räumt alte Images auf (älter als 7 Tage)
|
||||
7. Zeigt laufende Container an
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Problem: Login zur Registry schlägt fehl
|
||||
- Überprüfe CI_GITEA_TOKEN hat die richtigen Berechtigungen
|
||||
- Stelle sicher, dass REGISTRY_URL korrekt ist (ohne `https://`)
|
||||
|
||||
### Problem: SSH-Verbindung schlägt fehl
|
||||
- Überprüfe SSH_HOST, SSH_USERNAME und SSH_PORT
|
||||
- Stelle sicher, dass SSH_PRIVATE_KEY korrekt formatiert ist (mit Zeilenumbrüchen)
|
||||
- Prüfe, ob der Public Key auf dem Server hinterlegt ist
|
||||
|
||||
### Problem: Docker Compose startet nicht
|
||||
- Überprüfe, ob die .env Datei auf dem Server existiert und korrekt ist
|
||||
- Stelle sicher, dass compose.yml im DEPLOY_PATH vorhanden ist
|
||||
- Prüfe die Logs mit `docker compose logs`
|
||||
|
||||
## Manuelles Deployment
|
||||
|
||||
Falls ein manuelles Deployment erforderlich ist:
|
||||
|
||||
```bash
|
||||
# Auf dem Server
|
||||
cd /opt/skamp/docker
|
||||
|
||||
# Login zur Registry
|
||||
echo "YOUR_TOKEN" | docker login gitea.moz-tech.de -u murat --password-stdin
|
||||
|
||||
# Image pullen und starten
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
|
||||
# Logs prüfen
|
||||
docker compose logs -f app
|
||||
@@ -1,7 +1,7 @@
|
||||
meta {
|
||||
name: Auth
|
||||
name: Auth Bruno
|
||||
type: http
|
||||
seq: 2
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
@@ -15,10 +15,9 @@ headers {
|
||||
}
|
||||
|
||||
body:form-urlencoded {
|
||||
grant_type: password
|
||||
client_id: {{KEYCLOAK_CLIENT_ID}}
|
||||
username: {{ADMIN_USERNAME}}
|
||||
password: {{ADMIN_PASSWORD}}
|
||||
grant_type: client_credentials
|
||||
client_id: {{KEYCLOAK_BRUNO_CLIENT_ID}}
|
||||
client_secret: {{KEYCLOAK_BRUNO_CLIENT_SECRET}}
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
@@ -34,6 +33,7 @@ script:post-response {
|
||||
// (z.B. für eine bestimmte Umgebung wie "Development")
|
||||
bru.setEnvVar("BEARER_TOKEN", jsonResponse.access_token);
|
||||
|
||||
console.log("Updated access token.");
|
||||
// Option 2: In eine Collection-Variable schreiben
|
||||
// (Diese Variable ist global für alle Anfragen in deiner Collection)
|
||||
// bru.setVar("bearerToken", "Bearer " + jsonResponse.access_token);
|
||||
39
bruno/propify/DataImport/Upload Organizations.bru
Normal file
39
bruno/propify/DataImport/Upload Organizations.bru
Normal file
@@ -0,0 +1,39 @@
|
||||
meta {
|
||||
name: Upload Organizations
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{API_BASE_URL}}/api/{{API_VERSION}}/data-import/organizations
|
||||
body: multipartForm
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{BEARER_TOKEN}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"name": "Bungalow",
|
||||
"street": "Hebbelstraße",
|
||||
"houseNumber": "30",
|
||||
"zipCode": "55127",
|
||||
"city": "Mainz",
|
||||
"country": "DE",
|
||||
"notes": "Lorem ipsum"
|
||||
}
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
file: @file(/Users/muratoezkorkmaz/projects/misc/skamp/init_data/organizations-17911945-23.xlsx)
|
||||
}
|
||||
|
||||
body:file {
|
||||
file: @file(/Users/murat/Pictures/IMG_0229.jpeg) @contentType(image/jpeg)
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
39
bruno/propify/DataImport/Upload Persons.bru
Normal file
39
bruno/propify/DataImport/Upload Persons.bru
Normal file
@@ -0,0 +1,39 @@
|
||||
meta {
|
||||
name: Upload Persons
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{API_BASE_URL}}/api/{{API_VERSION}}/data-import/persons
|
||||
body: multipartForm
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{BEARER_TOKEN}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"name": "Bungalow",
|
||||
"street": "Hebbelstraße",
|
||||
"houseNumber": "30",
|
||||
"zipCode": "55127",
|
||||
"city": "Mainz",
|
||||
"country": "DE",
|
||||
"notes": "Lorem ipsum"
|
||||
}
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
file: @file(/Users/muratoezkorkmaz/projects/misc/skamp/init_data/people-17911945-22.xlsx)
|
||||
}
|
||||
|
||||
body:file {
|
||||
file: @file(/Users/murat/Pictures/IMG_0229.jpeg) @contentType(image/jpeg)
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
8
bruno/propify/DataImport/folder.bru
Normal file
8
bruno/propify/DataImport/folder.bru
Normal file
@@ -0,0 +1,8 @@
|
||||
meta {
|
||||
name: DataImport
|
||||
seq: 6
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
||||
31
bruno/propify/Industries/Create new.bru
Normal file
31
bruno/propify/Industries/Create new.bru
Normal file
@@ -0,0 +1,31 @@
|
||||
meta {
|
||||
name: Create new
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{API_BASE_URL}}/api/{{API_VERSION}}/industries
|
||||
body: json
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{BEARER_TOKEN}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"name": "Bungalow",
|
||||
"street": "Hebbelstraße",
|
||||
"houseNumber": "30",
|
||||
"zipCode": "55127",
|
||||
"city": "Mainz",
|
||||
"country": "DE",
|
||||
"notes": "Lorem ipsum"
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
31
bruno/propify/Industries/Delete one by ID.bru
Normal file
31
bruno/propify/Industries/Delete one by ID.bru
Normal file
@@ -0,0 +1,31 @@
|
||||
meta {
|
||||
name: Delete one by ID
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
delete {
|
||||
url: API_BASE_URL}}/api/{{API_VERSION}}/industries/e64d5e53-cd45-45fb-9237-46078077bf22
|
||||
body: json
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{BEARER_TOKEN}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"name": "Mustername 1",
|
||||
"street": "Musterstraße",
|
||||
"houseNumber": "1",
|
||||
"zipCode": "55123",
|
||||
"city": "Musterstadt",
|
||||
"country": "de",
|
||||
"notes": "Lorem ipsum"
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
19
bruno/propify/Industries/Get all.bru
Normal file
19
bruno/propify/Industries/Get all.bru
Normal file
@@ -0,0 +1,19 @@
|
||||
meta {
|
||||
name: Get all
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{API_BASE_URL}}/api/{{API_VERSION}}/industries
|
||||
body: none
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{BEARER_TOKEN}}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
19
bruno/propify/Industries/Get one by ID.bru
Normal file
19
bruno/propify/Industries/Get one by ID.bru
Normal file
@@ -0,0 +1,19 @@
|
||||
meta {
|
||||
name: Get one by ID
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{API_BASE_URL}}/api/{{API_VERSION}}/industries/da002464-33e9-4bfd-9ae2-836b26955502
|
||||
body: none
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{BEARER_TOKEN}}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
39
bruno/propify/Industries/Upload files.bru
Normal file
39
bruno/propify/Industries/Upload files.bru
Normal file
@@ -0,0 +1,39 @@
|
||||
meta {
|
||||
name: Upload files
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{API_BASE_URL}}/api/{{API_VERSION}}/industries/f4eb0c54-7c8f-4e60-b71f-1d08b8b6e2d4/upload
|
||||
body: multipartForm
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{BEARER_TOKEN}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"name": "Bungalow",
|
||||
"street": "Hebbelstraße",
|
||||
"houseNumber": "30",
|
||||
"zipCode": "55127",
|
||||
"city": "Mainz",
|
||||
"country": "DE",
|
||||
"notes": "Lorem ipsum"
|
||||
}
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
attachments: @file(/Users/murat/Pictures/IMG_0229.jpeg|/Users/murat/Pictures/schnürsenkel technik.gif)
|
||||
}
|
||||
|
||||
body:file {
|
||||
file: @file(/Users/murat/Pictures/IMG_0229.jpeg) @contentType(image/jpeg)
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
8
bruno/propify/Industries/folder.bru
Normal file
8
bruno/propify/Industries/folder.bru
Normal file
@@ -0,0 +1,8 @@
|
||||
meta {
|
||||
name: Industries
|
||||
seq: 9
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
||||
31
bruno/propify/Organizations/Create new.bru
Normal file
31
bruno/propify/Organizations/Create new.bru
Normal file
@@ -0,0 +1,31 @@
|
||||
meta {
|
||||
name: Create new
|
||||
type: http
|
||||
seq: 3
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{API_BASE_URL}}/api/{{API_VERSION}}/organizations
|
||||
body: json
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{BEARER_TOKEN}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"name": "Bungalow",
|
||||
"street": "Hebbelstraße",
|
||||
"houseNumber": "30",
|
||||
"zipCode": "55127",
|
||||
"city": "Mainz",
|
||||
"country": "DE",
|
||||
"notes": "Lorem ipsum"
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
31
bruno/propify/Organizations/Delete one by ID.bru
Normal file
31
bruno/propify/Organizations/Delete one by ID.bru
Normal file
@@ -0,0 +1,31 @@
|
||||
meta {
|
||||
name: Delete one by ID
|
||||
type: http
|
||||
seq: 4
|
||||
}
|
||||
|
||||
delete {
|
||||
url: API_BASE_URL}}/api/{{API_VERSION}}/organizations/e64d5e53-cd45-45fb-9237-46078077bf22
|
||||
body: json
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{BEARER_TOKEN}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"name": "Mustername 1",
|
||||
"street": "Musterstraße",
|
||||
"houseNumber": "1",
|
||||
"zipCode": "55123",
|
||||
"city": "Musterstadt",
|
||||
"country": "de",
|
||||
"notes": "Lorem ipsum"
|
||||
}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
19
bruno/propify/Organizations/Get all.bru
Normal file
19
bruno/propify/Organizations/Get all.bru
Normal file
@@ -0,0 +1,19 @@
|
||||
meta {
|
||||
name: Get all
|
||||
type: http
|
||||
seq: 1
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{API_BASE_URL}}/api/{{API_VERSION}}/organizations
|
||||
body: none
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{BEARER_TOKEN}}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
19
bruno/propify/Organizations/Get one by ID.bru
Normal file
19
bruno/propify/Organizations/Get one by ID.bru
Normal file
@@ -0,0 +1,19 @@
|
||||
meta {
|
||||
name: Get one by ID
|
||||
type: http
|
||||
seq: 2
|
||||
}
|
||||
|
||||
get {
|
||||
url: {{API_BASE_URL}}/api/{{API_VERSION}}/organizations/5dba067e-d7fd-4d79-a08a-ec379834938a
|
||||
body: none
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{BEARER_TOKEN}}
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
39
bruno/propify/Organizations/Upload files.bru
Normal file
39
bruno/propify/Organizations/Upload files.bru
Normal file
@@ -0,0 +1,39 @@
|
||||
meta {
|
||||
name: Upload files
|
||||
type: http
|
||||
seq: 5
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{API_BASE_URL}}/api/{{API_VERSION}}/organizations/f4eb0c54-7c8f-4e60-b71f-1d08b8b6e2d4/upload
|
||||
body: multipartForm
|
||||
auth: bearer
|
||||
}
|
||||
|
||||
auth:bearer {
|
||||
token: {{BEARER_TOKEN}}
|
||||
}
|
||||
|
||||
body:json {
|
||||
{
|
||||
"name": "Bungalow",
|
||||
"street": "Hebbelstraße",
|
||||
"houseNumber": "30",
|
||||
"zipCode": "55127",
|
||||
"city": "Mainz",
|
||||
"country": "DE",
|
||||
"notes": "Lorem ipsum"
|
||||
}
|
||||
}
|
||||
|
||||
body:multipart-form {
|
||||
attachments: @file(/Users/murat/Pictures/IMG_0229.jpeg|/Users/murat/Pictures/schnürsenkel technik.gif)
|
||||
}
|
||||
|
||||
body:file {
|
||||
file: @file(/Users/murat/Pictures/IMG_0229.jpeg) @contentType(image/jpeg)
|
||||
}
|
||||
|
||||
settings {
|
||||
encodeUrl: true
|
||||
}
|
||||
8
bruno/propify/Organizations/folder.bru
Normal file
8
bruno/propify/Organizations/folder.bru
Normal file
@@ -0,0 +1,8 @@
|
||||
meta {
|
||||
name: Organizations
|
||||
seq: 8
|
||||
}
|
||||
|
||||
auth {
|
||||
mode: inherit
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
vars {
|
||||
API_BASE_URL: http://localhost:8080
|
||||
API_BASE_URL: http://localhost:8180
|
||||
API_VERSION: v1
|
||||
KEYCLOAK_BASE_URL: http://localhost:8280
|
||||
KEYCLOAK_BASE_URL: https://kc.dev.localhost
|
||||
DEV_USERNAME: dev@example.com
|
||||
DEV_PASSWORD: dev
|
||||
ADMIN_USERNAME: admin@example.com
|
||||
ADMIN_PASSWORD: admin
|
||||
KEYCLOAK_CLIENT_ID: skamp-app
|
||||
KEYCLOAK_REALM: skamp
|
||||
KEYCLOAK_BRUNO_CLIENT_ID: skamp-bruno
|
||||
}
|
||||
vars:secret [
|
||||
BEARER_TOKEN
|
||||
BEARER_TOKEN,
|
||||
KEYCLOAK_BRUNO_CLIENT_SECRET
|
||||
]
|
||||
|
||||
BIN
init_data/organizations-17911945-23.xlsx
Normal file
BIN
init_data/organizations-17911945-23.xlsx
Normal file
Binary file not shown.
BIN
init_data/people-17911945-22.xlsx
Normal file
BIN
init_data/people-17911945-22.xlsx
Normal file
Binary file not shown.
7
pom.xml
7
pom.xml
@@ -110,6 +110,13 @@
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- EasyExcel for Excel processing -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>easyexcel</artifactId>
|
||||
<version>3.3.4</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package de.iwomm.propify_api.controller;
|
||||
|
||||
import de.iwomm.propify_api.service.OrganizationImportService;
|
||||
import de.iwomm.propify_api.service.PersonImportService;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/data-import")
|
||||
public class DataImportController {
|
||||
|
||||
private final PersonImportService personImportService;
|
||||
private final OrganizationImportService organizationImportService;
|
||||
|
||||
public DataImportController(PersonImportService personImportService,
|
||||
OrganizationImportService organizationImportService) {
|
||||
this.personImportService = personImportService;
|
||||
this.organizationImportService = organizationImportService;
|
||||
}
|
||||
|
||||
@PostMapping("/persons")
|
||||
public ResponseEntity<?> importPersons(@RequestParam("file") MultipartFile file) {
|
||||
if (file.isEmpty()) {
|
||||
return ResponseEntity.badRequest().body("Please upload a file");
|
||||
}
|
||||
|
||||
if (!file.getOriginalFilename().endsWith(".xlsx")) {
|
||||
return ResponseEntity.badRequest().body("Only .xlsx files are supported");
|
||||
}
|
||||
|
||||
try {
|
||||
PersonImportService.ImportResult result = personImportService.importPersonsFromExcel(file);
|
||||
return ResponseEntity.ok(result);
|
||||
} catch (IOException e) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body("Error processing file: " + e.getMessage());
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body("Unexpected error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/organizations")
|
||||
public ResponseEntity<?> importOrganizations(@RequestParam("file") MultipartFile file) {
|
||||
if (file.isEmpty()) {
|
||||
return ResponseEntity.badRequest().body("Please upload a file");
|
||||
}
|
||||
|
||||
if (!file.getOriginalFilename().endsWith(".xlsx")) {
|
||||
return ResponseEntity.badRequest().body("Only .xlsx files are supported");
|
||||
}
|
||||
|
||||
try {
|
||||
OrganizationImportService.ImportResult result = organizationImportService.importOrganizationsFromExcel(file);
|
||||
return ResponseEntity.ok(result);
|
||||
} catch (IOException e) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body("Error processing file: " + e.getMessage());
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body("Unexpected error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package de.iwomm.propify_api.controller;
|
||||
|
||||
import de.iwomm.propify_api.dto.*;
|
||||
import de.iwomm.propify_api.entity.Industry;
|
||||
import de.iwomm.propify_api.mapper.IndustryMapper;
|
||||
import de.iwomm.propify_api.service.IndustryService;
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/industries")
|
||||
public class IndustryController {
|
||||
private final IndustryService industryService;
|
||||
private final IndustryMapper industryMapper;
|
||||
|
||||
public IndustryController(IndustryService industryService, IndustryMapper industryMapper) {
|
||||
this.industryService = industryService;
|
||||
this.industryMapper = industryMapper;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER', 'ROLE_DEV')")
|
||||
public ResponseEntity<?> getAll() {
|
||||
List<IndustryDTO> industryDTOs = industryService.toDTOs(industryService.findAll());
|
||||
|
||||
return ResponseEntity
|
||||
.ok(industryDTOs);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER', 'ROLE_DEV')")
|
||||
public ResponseEntity<?> getById(@PathVariable UUID id) {
|
||||
try {
|
||||
Industry industry = industryService.findById(id).orElseThrow(EntityNotFoundException::new);
|
||||
IndustryDTO projectDTO = industryMapper.toDto(industry);
|
||||
|
||||
return ResponseEntity
|
||||
.ok(projectDTO);
|
||||
} catch (EntityNotFoundException e) {
|
||||
return ResponseEntity
|
||||
.notFound()
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity
|
||||
.internalServerError()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER', 'ROLE_DEV')")
|
||||
public ResponseEntity<?> create(@RequestBody IndustryDTO newIndustryDTO) {
|
||||
try {
|
||||
Industry newItem = industryService.save(newIndustryDTO);
|
||||
|
||||
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
|
||||
.path("/{id}")
|
||||
.buildAndExpand(newItem.getId())
|
||||
.toUri();
|
||||
|
||||
return ResponseEntity
|
||||
.created(location)
|
||||
.body(newItem);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity
|
||||
.internalServerError()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@PatchMapping("/{id}")
|
||||
@PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER', 'ROLE_DEV')")
|
||||
public ResponseEntity<?> update(@PathVariable UUID id, @RequestBody IndustryDTO industryDTO) {
|
||||
try {
|
||||
return ResponseEntity
|
||||
.ok(industryService.update(id, industryDTO));
|
||||
} catch (EntityNotFoundException e) {
|
||||
return ResponseEntity
|
||||
.notFound()
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity
|
||||
.internalServerError()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER', 'ROLE_DEV')")
|
||||
public ResponseEntity<?> delete(@PathVariable UUID id) {
|
||||
try {
|
||||
industryService.deleteById(id);
|
||||
|
||||
return ResponseEntity
|
||||
.noContent()
|
||||
.build();
|
||||
} catch (EntityNotFoundException e) {
|
||||
return ResponseEntity
|
||||
.notFound()
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity
|
||||
.internalServerError()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/bulk-delete")
|
||||
@PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER', 'ROLE_DEV')")
|
||||
public ResponseEntity<?> deleteMany(@RequestBody BulkDeleteIdsDTO bulkDeleteIdsDTO) {
|
||||
try {
|
||||
industryService.deleteByIds(bulkDeleteIdsDTO);
|
||||
|
||||
return ResponseEntity
|
||||
.noContent()
|
||||
.build();
|
||||
} catch (EntityNotFoundException e) {
|
||||
return ResponseEntity
|
||||
.notFound()
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity
|
||||
.internalServerError()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package de.iwomm.propify_api.controller;
|
||||
|
||||
import de.iwomm.propify_api.dto.OrganizationDTO;
|
||||
import de.iwomm.propify_api.dto.ProjectDetailsDTO;
|
||||
import de.iwomm.propify_api.entity.Organization;
|
||||
import de.iwomm.propify_api.entity.Project;
|
||||
import de.iwomm.propify_api.mapper.OrganizationMapper;
|
||||
import de.iwomm.propify_api.service.OrganisationService;
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/organizations")
|
||||
public class OrganizationController {
|
||||
|
||||
private final OrganizationMapper organizationMapper;
|
||||
private OrganisationService organisationService;
|
||||
|
||||
public OrganizationController(OrganisationService organisationService, OrganizationMapper organizationMapper) {
|
||||
this.organisationService = organisationService;
|
||||
this.organizationMapper = organizationMapper;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER')")
|
||||
public ResponseEntity<?> getAll() {
|
||||
List<OrganizationDTO> organizationDTOs = organisationService.toDTOs(organisationService.findAll());
|
||||
|
||||
return ResponseEntity
|
||||
.ok(organizationDTOs);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER')")
|
||||
public ResponseEntity<?> getById(@PathVariable UUID id) {
|
||||
try {
|
||||
Organization organization = organisationService.findById(id).orElseThrow(EntityNotFoundException::new);
|
||||
OrganizationDTO organizationDTO = organizationMapper.toDto(organization);
|
||||
|
||||
return ResponseEntity
|
||||
.ok(organizationDTO);
|
||||
} catch (EntityNotFoundException e) {
|
||||
return ResponseEntity
|
||||
.notFound()
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity
|
||||
.internalServerError()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_USER')")
|
||||
public ResponseEntity<?> deleteById(@PathVariable UUID id) {
|
||||
try {
|
||||
Organization organization = organisationService.findById(id).orElseThrow(EntityNotFoundException::new);
|
||||
|
||||
organisationService.delete(organization);
|
||||
|
||||
return ResponseEntity
|
||||
.ok()
|
||||
.build();
|
||||
} catch (EntityNotFoundException e) {
|
||||
return ResponseEntity
|
||||
.notFound()
|
||||
.build();
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity
|
||||
.internalServerError()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAnyRole('ROLE_ADMIN', 'ROLE_DEV')")
|
||||
public ResponseEntity<?> create(@RequestBody OrganizationDTO organizationDTO) {
|
||||
try {
|
||||
Organization newItem = organisationService.save(organizationDTO);
|
||||
|
||||
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
|
||||
.path("/{id}")
|
||||
.buildAndExpand(newItem.getId())
|
||||
.toUri();
|
||||
|
||||
return ResponseEntity
|
||||
.created(location)
|
||||
.body(newItem);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity
|
||||
.internalServerError()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.iwomm.propify_api.dto;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public record BulkDeleteIdsDTO(
|
||||
List<UUID> ids
|
||||
) {
|
||||
}
|
||||
9
src/main/java/de/iwomm/propify_api/dto/IndustryDTO.java
Normal file
9
src/main/java/de/iwomm/propify_api/dto/IndustryDTO.java
Normal file
@@ -0,0 +1,9 @@
|
||||
package de.iwomm.propify_api.dto;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record IndustryDTO(
|
||||
UUID id,
|
||||
String name
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package de.iwomm.propify_api.dto;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record NewOrganizationDTO(
|
||||
String name,
|
||||
String owner,
|
||||
UUID industryId
|
||||
) {
|
||||
}
|
||||
14
src/main/java/de/iwomm/propify_api/dto/OrganizationDTO.java
Normal file
14
src/main/java/de/iwomm/propify_api/dto/OrganizationDTO.java
Normal file
@@ -0,0 +1,14 @@
|
||||
package de.iwomm.propify_api.dto;
|
||||
|
||||
import de.iwomm.propify_api.entity.Industry;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public record OrganizationDTO(
|
||||
UUID id,
|
||||
String name,
|
||||
IndustryDTO industry,
|
||||
String owner,
|
||||
Set<OrganizationLabelDTO> labels
|
||||
) { }
|
||||
@@ -0,0 +1,84 @@
|
||||
package de.iwomm.propify_api.dto;
|
||||
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
|
||||
public class OrganizationImportDTO {
|
||||
|
||||
@ExcelProperty(index = 0, value = "Organisation - Name")
|
||||
private String name;
|
||||
|
||||
@ExcelProperty(index = 1, value = "Organisation - Branche Intern")
|
||||
private String industry;
|
||||
|
||||
@ExcelProperty(index = 2, value = "Organisation - Label")
|
||||
private String labels;
|
||||
|
||||
@ExcelProperty(index = 3, value = "Organisation - Abgeschlossene Deals")
|
||||
private String completedDeals;
|
||||
|
||||
@ExcelProperty(index = 4, value = "Organisation - Offene Deals")
|
||||
private String openDeals;
|
||||
|
||||
@ExcelProperty(index = 5, value = "Organisation - Datum nächste Aktivität")
|
||||
private String nextActivityDate;
|
||||
|
||||
@ExcelProperty(index = 6, value = "Organisation - Besitzer")
|
||||
private String owner;
|
||||
|
||||
// Getters and Setters
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getIndustry() {
|
||||
return industry;
|
||||
}
|
||||
|
||||
public void setIndustry(String industry) {
|
||||
this.industry = industry;
|
||||
}
|
||||
|
||||
public String getLabels() {
|
||||
return labels;
|
||||
}
|
||||
|
||||
public void setLabels(String labels) {
|
||||
this.labels = labels;
|
||||
}
|
||||
|
||||
public String getCompletedDeals() {
|
||||
return completedDeals;
|
||||
}
|
||||
|
||||
public void setCompletedDeals(String completedDeals) {
|
||||
this.completedDeals = completedDeals;
|
||||
}
|
||||
|
||||
public String getOpenDeals() {
|
||||
return openDeals;
|
||||
}
|
||||
|
||||
public void setOpenDeals(String openDeals) {
|
||||
this.openDeals = openDeals;
|
||||
}
|
||||
|
||||
public String getNextActivityDate() {
|
||||
return nextActivityDate;
|
||||
}
|
||||
|
||||
public void setNextActivityDate(String nextActivityDate) {
|
||||
this.nextActivityDate = nextActivityDate;
|
||||
}
|
||||
|
||||
public String getOwner() {
|
||||
return owner;
|
||||
}
|
||||
|
||||
public void setOwner(String owner) {
|
||||
this.owner = owner;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.iwomm.propify_api.dto;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public record OrganizationLabelDTO(
|
||||
UUID id,
|
||||
String name
|
||||
) {
|
||||
}
|
||||
117
src/main/java/de/iwomm/propify_api/dto/PersonImportDTO.java
Normal file
117
src/main/java/de/iwomm/propify_api/dto/PersonImportDTO.java
Normal file
@@ -0,0 +1,117 @@
|
||||
package de.iwomm.propify_api.dto;
|
||||
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
|
||||
public class PersonImportDTO {
|
||||
|
||||
@ExcelProperty(index = 0, value = "Person - Name")
|
||||
private String name;
|
||||
|
||||
@ExcelProperty(index = 1, value = "Person - Label")
|
||||
private String labels;
|
||||
|
||||
@ExcelProperty(index = 2, value = "Person - Organisation")
|
||||
private String organization;
|
||||
|
||||
@ExcelProperty(index = 3, value = "Person - E-Mail-Adresse - Büro")
|
||||
private String emailOffice;
|
||||
|
||||
@ExcelProperty(index = 4, value = "Person - E-Mail-Adresse - Privat")
|
||||
private String emailPrivate;
|
||||
|
||||
@ExcelProperty(index = 5, value = "Person - E-Mail-Adresse - Sonstiger")
|
||||
private String emailOther;
|
||||
|
||||
@ExcelProperty(index = 6, value = "Person - Telefon - Büro")
|
||||
private String phoneOffice;
|
||||
|
||||
@ExcelProperty(index = 7, value = "Person - Telefon - Privat")
|
||||
private String phonePrivate;
|
||||
|
||||
@ExcelProperty(index = 8, value = "Person - Telefon - Mobil")
|
||||
private String phoneMobile;
|
||||
|
||||
@ExcelProperty(index = 9, value = "Person - Telefon - Sonstiger")
|
||||
private String phoneOther;
|
||||
|
||||
// Getters and Setters
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getLabels() {
|
||||
return labels;
|
||||
}
|
||||
|
||||
public void setLabels(String labels) {
|
||||
this.labels = labels;
|
||||
}
|
||||
|
||||
public String getOrganization() {
|
||||
return organization;
|
||||
}
|
||||
|
||||
public void setOrganization(String organization) {
|
||||
this.organization = organization;
|
||||
}
|
||||
|
||||
public String getEmailOffice() {
|
||||
return emailOffice;
|
||||
}
|
||||
|
||||
public void setEmailOffice(String emailOffice) {
|
||||
this.emailOffice = emailOffice;
|
||||
}
|
||||
|
||||
public String getEmailPrivate() {
|
||||
return emailPrivate;
|
||||
}
|
||||
|
||||
public void setEmailPrivate(String emailPrivate) {
|
||||
this.emailPrivate = emailPrivate;
|
||||
}
|
||||
|
||||
public String getEmailOther() {
|
||||
return emailOther;
|
||||
}
|
||||
|
||||
public void setEmailOther(String emailOther) {
|
||||
this.emailOther = emailOther;
|
||||
}
|
||||
|
||||
public String getPhoneOffice() {
|
||||
return phoneOffice;
|
||||
}
|
||||
|
||||
public void setPhoneOffice(String phoneOffice) {
|
||||
this.phoneOffice = phoneOffice;
|
||||
}
|
||||
|
||||
public String getPhonePrivate() {
|
||||
return phonePrivate;
|
||||
}
|
||||
|
||||
public void setPhonePrivate(String phonePrivate) {
|
||||
this.phonePrivate = phonePrivate;
|
||||
}
|
||||
|
||||
public String getPhoneMobile() {
|
||||
return phoneMobile;
|
||||
}
|
||||
|
||||
public void setPhoneMobile(String phoneMobile) {
|
||||
this.phoneMobile = phoneMobile;
|
||||
}
|
||||
|
||||
public String getPhoneOther() {
|
||||
return phoneOther;
|
||||
}
|
||||
|
||||
public void setPhoneOther(String phoneOther) {
|
||||
this.phoneOther = phoneOther;
|
||||
}
|
||||
}
|
||||
31
src/main/java/de/iwomm/propify_api/entity/Industry.java
Normal file
31
src/main/java/de/iwomm/propify_api/entity/Industry.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package de.iwomm.propify_api.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
public class Industry {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column(nullable = false)
|
||||
private UUID id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package de.iwomm.propify_api.entity;
|
||||
|
||||
|
||||
import de.iwomm.propify_api.entitylistener.TenantEntityListener;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.MappedSuperclass;
|
||||
|
||||
@MappedSuperclass
|
||||
@EntityListeners(TenantEntityListener.class)
|
||||
public abstract class MultiTenantEntity {
|
||||
|
||||
@Column(name = "tenant_id")
|
||||
private String tenantId;
|
||||
|
||||
public String getTenantId() {
|
||||
return tenantId;
|
||||
}
|
||||
|
||||
public void setTenantId(String tenantId) {
|
||||
this.tenantId = tenantId;
|
||||
}
|
||||
|
||||
}
|
||||
90
src/main/java/de/iwomm/propify_api/entity/Organization.java
Normal file
90
src/main/java/de/iwomm/propify_api/entity/Organization.java
Normal file
@@ -0,0 +1,90 @@
|
||||
package de.iwomm.propify_api.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
public class Organization {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column(nullable = false)
|
||||
private UUID id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
@ManyToOne()
|
||||
@JoinColumn(name = "industry_id")
|
||||
private Industry industry;
|
||||
|
||||
@Column
|
||||
private String owner;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
|
||||
@JoinTable(
|
||||
name = "organization_labels",
|
||||
joinColumns = @JoinColumn(name = "organization_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "label_id"),
|
||||
foreignKey = @ForeignKey(name = "fk_organization_labels_organization", foreignKeyDefinition = "FOREIGN KEY (organization_id) REFERENCES organization(id) ON DELETE CASCADE")
|
||||
)
|
||||
private Set<OrganizationLabel> labels = new HashSet<>();
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Industry getIndustry() {
|
||||
return industry;
|
||||
}
|
||||
|
||||
public void setIndustry(Industry industry) {
|
||||
this.industry = industry;
|
||||
}
|
||||
|
||||
public String getOwner() {
|
||||
return owner;
|
||||
}
|
||||
|
||||
public void setOwner(String owner) {
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Set<OrganizationLabel> getLabels() {
|
||||
return labels;
|
||||
}
|
||||
|
||||
public void setLabels(Set<OrganizationLabel> labels) {
|
||||
this.labels = labels;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package de.iwomm.propify_api.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
public class OrganizationLabel {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column(nullable = false)
|
||||
private UUID id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,199 @@
|
||||
package de.iwomm.propify_api.entity;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
public class Person {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column(nullable = false)
|
||||
private UUID id;
|
||||
|
||||
@Column
|
||||
private String firstName;
|
||||
|
||||
@Column
|
||||
private String lastName;
|
||||
|
||||
@Column
|
||||
private String emailOffice;
|
||||
|
||||
@Column
|
||||
private String emailPrivate;
|
||||
|
||||
@Column
|
||||
private String emailOther;
|
||||
|
||||
@Column
|
||||
private String phoneOfficeCountryCode;
|
||||
|
||||
@Column
|
||||
private String phoneOfficeAreaCode;
|
||||
|
||||
@Column
|
||||
private String phoneOfficeNumber;
|
||||
|
||||
@Column
|
||||
private String phonePrivateCountryCode;
|
||||
|
||||
@Column
|
||||
private String phonePrivateAreaCode;
|
||||
|
||||
@Column
|
||||
private String phonePrivateNumber;
|
||||
|
||||
@Column
|
||||
private String phoneMobile;
|
||||
|
||||
@Column(unique = true)
|
||||
private String keycloakId;
|
||||
|
||||
@Column(nullable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
this.createdAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})
|
||||
@JoinTable(
|
||||
name = "person_labels",
|
||||
joinColumns = @JoinColumn(name = "person_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "label_id"),
|
||||
foreignKey = @ForeignKey(name = "fk_person_labels_person", foreignKeyDefinition = "FOREIGN KEY (person_id) REFERENCES person(id) ON DELETE CASCADE")
|
||||
)
|
||||
private Set<PersonLabel> labels = new HashSet<>();
|
||||
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getFirstName() {
|
||||
return firstName;
|
||||
}
|
||||
|
||||
public void setFirstName(String firstName) {
|
||||
this.firstName = firstName;
|
||||
}
|
||||
|
||||
public String getLastName() {
|
||||
return lastName;
|
||||
}
|
||||
|
||||
public void setLastName(String lastName) {
|
||||
this.lastName = lastName;
|
||||
}
|
||||
|
||||
public String getEmailOffice() {
|
||||
return emailOffice;
|
||||
}
|
||||
|
||||
public void setEmailOffice(String emailOffice) {
|
||||
this.emailOffice = emailOffice;
|
||||
}
|
||||
|
||||
public String getEmailPrivate() {
|
||||
return emailPrivate;
|
||||
}
|
||||
|
||||
public void setEmailPrivate(String emailPrivate) {
|
||||
this.emailPrivate = emailPrivate;
|
||||
}
|
||||
|
||||
public String getEmailOther() {
|
||||
return emailOther;
|
||||
}
|
||||
|
||||
public void setEmailOther(String emailOther) {
|
||||
this.emailOther = emailOther;
|
||||
}
|
||||
|
||||
public String getPhoneOfficeCountryCode() {
|
||||
return phoneOfficeCountryCode;
|
||||
}
|
||||
|
||||
public void setPhoneOfficeCountryCode(String phoneOfficeCountryCode) {
|
||||
this.phoneOfficeCountryCode = phoneOfficeCountryCode;
|
||||
}
|
||||
|
||||
public String getPhoneOfficeAreaCode() {
|
||||
return phoneOfficeAreaCode;
|
||||
}
|
||||
|
||||
public void setPhoneOfficeAreaCode(String phoneOfficeAreaCode) {
|
||||
this.phoneOfficeAreaCode = phoneOfficeAreaCode;
|
||||
}
|
||||
|
||||
public String getPhoneOfficeNumber() {
|
||||
return phoneOfficeNumber;
|
||||
}
|
||||
|
||||
public void setPhoneOfficeNumber(String phoneOfficeNumber) {
|
||||
this.phoneOfficeNumber = phoneOfficeNumber;
|
||||
}
|
||||
|
||||
public String getPhonePrivateCountryCode() {
|
||||
return phonePrivateCountryCode;
|
||||
}
|
||||
|
||||
public void setPhonePrivateCountryCode(String phonePrivateCountryCode) {
|
||||
this.phonePrivateCountryCode = phonePrivateCountryCode;
|
||||
}
|
||||
|
||||
public String getPhonePrivateAreaCode() {
|
||||
return phonePrivateAreaCode;
|
||||
}
|
||||
|
||||
public void setPhonePrivateAreaCode(String phonePrivateAreaCode) {
|
||||
this.phonePrivateAreaCode = phonePrivateAreaCode;
|
||||
}
|
||||
|
||||
public String getPhonePrivateNumber() {
|
||||
return phonePrivateNumber;
|
||||
}
|
||||
|
||||
public void setPhonePrivateNumber(String phonePrivateNumber) {
|
||||
this.phonePrivateNumber = phonePrivateNumber;
|
||||
}
|
||||
|
||||
public String getPhoneMobile() {
|
||||
return phoneMobile;
|
||||
}
|
||||
|
||||
public void setPhoneMobile(String phoneMobile) {
|
||||
this.phoneMobile = phoneMobile;
|
||||
}
|
||||
|
||||
public String getKeycloakId() {
|
||||
return keycloakId;
|
||||
}
|
||||
|
||||
public void setKeycloakId(String keycloakId) {
|
||||
this.keycloakId = keycloakId;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public void setCreatedAt(LocalDateTime createdAt) {
|
||||
this.createdAt = createdAt;
|
||||
}
|
||||
|
||||
public Set<PersonLabel> getLabels() {
|
||||
return labels;
|
||||
}
|
||||
|
||||
public void setLabels(Set<PersonLabel> labels) {
|
||||
this.labels = labels;
|
||||
}
|
||||
}
|
||||
|
||||
31
src/main/java/de/iwomm/propify_api/entity/PersonLabel.java
Normal file
31
src/main/java/de/iwomm/propify_api/entity/PersonLabel.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package de.iwomm.propify_api.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
public class PersonLabel {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column(nullable = false)
|
||||
private UUID id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(UUID id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,9 @@ package de.iwomm.propify_api.entity;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonManagedReference;
|
||||
import jakarta.persistence.*;
|
||||
import org.hibernate.annotations.Filter;
|
||||
import org.hibernate.annotations.FilterDef;
|
||||
import org.hibernate.annotations.ParamDef;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
@@ -10,7 +13,9 @@ import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
public class Project {
|
||||
@FilterDef(name = "tenantFilter", parameters = @ParamDef(name = "tenantId", type = String.class))
|
||||
@Filter(name = "tenantFilter", condition = "tenant_id = :tenantId")
|
||||
public class Project extends MultiTenantEntity {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column(nullable = false)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package de.iwomm.propify_api.entitylistener;
|
||||
|
||||
import de.iwomm.propify_api.entity.MultiTenantEntity;
|
||||
import de.iwomm.propify_api.security.TenantContext;
|
||||
import jakarta.persistence.PrePersist;
|
||||
|
||||
public class TenantEntityListener {
|
||||
|
||||
@PrePersist
|
||||
public void setTenant(Object entity) {
|
||||
if (entity instanceof MultiTenantEntity mte) {
|
||||
String tenantId = TenantContext.getTenantId();
|
||||
if (tenantId != null && mte.getTenantId() == null) {
|
||||
mte.setTenantId(tenantId);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.iwomm.propify_api.mapper;
|
||||
|
||||
import de.iwomm.propify_api.dto.IndustryDTO;
|
||||
import de.iwomm.propify_api.entity.Industry;
|
||||
import org.mapstruct.Mapper;
|
||||
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface IndustryMapper {
|
||||
IndustryDTO toDto(Industry industry);
|
||||
Industry fromDto(IndustryDTO industryDTO);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.iwomm.propify_api.mapper;
|
||||
|
||||
import de.iwomm.propify_api.dto.OrganizationLabelDTO;
|
||||
import de.iwomm.propify_api.entity.OrganizationLabel;
|
||||
import org.mapstruct.Mapper;
|
||||
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface OrganizationLabelMapper {
|
||||
OrganizationLabelDTO toDto(OrganizationLabel organizationLabel);
|
||||
OrganizationLabel fromDto(OrganizationLabelDTO organizationLabelDTO);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package de.iwomm.propify_api.mapper;
|
||||
|
||||
import de.iwomm.propify_api.dto.NewOrganizationDTO;
|
||||
import de.iwomm.propify_api.dto.OrganizationDTO;
|
||||
import de.iwomm.propify_api.entity.Organization;
|
||||
import org.mapstruct.Mapper;
|
||||
|
||||
@Mapper(componentModel = "spring", uses = { OrganizationLabelMapper.class, IndustryMapper.class })
|
||||
public interface OrganizationMapper {
|
||||
OrganizationDTO toDto(Organization organization);
|
||||
Organization fromDto(OrganizationDTO organizationDTO);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.iwomm.propify_api.repository;
|
||||
|
||||
import de.iwomm.propify_api.entity.Industry;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface IndustryRepository extends JpaRepository<Industry, UUID> {
|
||||
Optional<Industry> findByName(String name);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.iwomm.propify_api.repository;
|
||||
|
||||
import de.iwomm.propify_api.entity.OrganizationLabel;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface OrganizationLabelRepository extends JpaRepository<OrganizationLabel, UUID> {
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.iwomm.propify_api.repository;
|
||||
|
||||
import de.iwomm.propify_api.entity.Organization;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface OrganizationRepository extends JpaRepository<Organization, UUID> {
|
||||
Optional<Organization> findByName(String name);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.iwomm.propify_api.repository;
|
||||
|
||||
import de.iwomm.propify_api.entity.PersonLabel;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface PersonLabelRepository extends JpaRepository<PersonLabel, UUID> {
|
||||
Optional<PersonLabel> findByName(String name);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package de.iwomm.propify_api.repository;
|
||||
|
||||
import de.iwomm.propify_api.entity.Person;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface PersonRepository extends JpaRepository<Person, UUID> {
|
||||
Optional<Person> findByKeycloakId(String keycloakId);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.iwomm.propify_api.security;
|
||||
|
||||
public class TenantContext {
|
||||
private static final ThreadLocal<String> CURRENT_TENANT = new ThreadLocal<>();
|
||||
|
||||
public static void setTenantId(String tenantId) {
|
||||
CURRENT_TENANT.set(tenantId);
|
||||
}
|
||||
|
||||
public static String getTenantId() {
|
||||
return CURRENT_TENANT.get();
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
CURRENT_TENANT.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package de.iwomm.propify_api.security;
|
||||
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.hibernate.Session;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
@Order(1)
|
||||
public class TenantFilter extends OncePerRequestFilter {
|
||||
|
||||
private final EntityManager entityManager;
|
||||
|
||||
public TenantFilter(EntityManager entityManager) {
|
||||
this.entityManager = entityManager;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
String tenantId = null;
|
||||
|
||||
// 1️⃣ Tenant-ID aus JWT lesen
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (auth instanceof JwtAuthenticationToken jwtAuth) {
|
||||
Jwt jwt = jwtAuth.getToken();
|
||||
|
||||
// tenant_id Claim
|
||||
tenantId = jwt.getClaimAsString("tenant_id");
|
||||
|
||||
// Rollen prüfen, Superadmin darf alles sehen
|
||||
List<String> roles = jwt.getClaimAsStringList("roles");
|
||||
if (roles != null && roles.contains("ROLE_SUPERADMIN")) {
|
||||
tenantId = null; // kein Filter für Superadmin
|
||||
}
|
||||
}
|
||||
|
||||
// 2️⃣ Fallback: Header (optional, falls JWT fehlt)
|
||||
if (tenantId == null) {
|
||||
tenantId = request.getHeader("X-Tenant-ID");
|
||||
}
|
||||
|
||||
// 3️⃣ TenantContext für diesen Thread setzen
|
||||
TenantContext.setTenantId(tenantId);
|
||||
|
||||
// 4️⃣ Hibernate Filter aktivieren (falls Tenant-ID vorhanden)
|
||||
if (tenantId != null) {
|
||||
Session session = entityManager.unwrap(Session.class);
|
||||
session.enableFilter("tenantFilter")
|
||||
.setParameter("tenantId", tenantId);
|
||||
}
|
||||
|
||||
try {
|
||||
filterChain.doFilter(request, response);
|
||||
} finally {
|
||||
// 5️⃣ Aufräumen nach Request
|
||||
TenantContext.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package de.iwomm.propify_api.service;
|
||||
|
||||
import de.iwomm.propify_api.dto.*;
|
||||
import de.iwomm.propify_api.entity.Industry;
|
||||
import de.iwomm.propify_api.entity.Organization;
|
||||
import de.iwomm.propify_api.entity.Project;
|
||||
import de.iwomm.propify_api.entity.Property;
|
||||
import de.iwomm.propify_api.mapper.IndustryMapper;
|
||||
import de.iwomm.propify_api.repository.IndustryRepository;
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class IndustryService {
|
||||
private IndustryRepository industryRepository;
|
||||
private IndustryMapper industryMapper;
|
||||
|
||||
|
||||
public IndustryService(IndustryRepository industryRepository, IndustryMapper industryMapper) {
|
||||
this.industryRepository = industryRepository;
|
||||
this.industryMapper = industryMapper;
|
||||
}
|
||||
|
||||
public List<Industry> findAll() {
|
||||
return industryRepository.findAll();
|
||||
}
|
||||
|
||||
public Optional<Industry> findById(UUID id) {
|
||||
return industryRepository.findById(id);
|
||||
}
|
||||
|
||||
public Industry save(IndustryDTO dto) {
|
||||
return industryRepository.save(industryMapper.fromDto(dto));
|
||||
}
|
||||
|
||||
public void deleteByIds(BulkDeleteIdsDTO bulkDeleteIdsDTO) {
|
||||
industryRepository.deleteAllById(bulkDeleteIdsDTO.ids());
|
||||
}
|
||||
|
||||
public List<IndustryDTO> toDTOs(List<Industry> industries) {
|
||||
List<IndustryDTO> dtos = new ArrayList<>();
|
||||
|
||||
industries.forEach(industry -> {
|
||||
dtos.add(this.industryMapper.toDto(industry));
|
||||
});
|
||||
|
||||
return dtos;
|
||||
}
|
||||
|
||||
public Industry update(UUID id, IndustryDTO industryDTO) {
|
||||
Industry updated = industryRepository.findById(id).orElseThrow(EntityNotFoundException::new);
|
||||
|
||||
updated.setName(industryDTO.name());
|
||||
|
||||
industryRepository.save(updated);
|
||||
|
||||
return updated;
|
||||
}
|
||||
|
||||
public void deleteById(UUID id) {
|
||||
Industry industry = industryRepository.findById(id).orElseThrow(EntityNotFoundException::new);
|
||||
industryRepository.delete(industry);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package de.iwomm.propify_api.service;
|
||||
|
||||
import de.iwomm.propify_api.dto.NewOrganizationDTO;
|
||||
import de.iwomm.propify_api.dto.OrganizationDTO;
|
||||
import de.iwomm.propify_api.dto.ProjectDTO;
|
||||
import de.iwomm.propify_api.entity.Organization;
|
||||
import de.iwomm.propify_api.entity.Project;
|
||||
import de.iwomm.propify_api.mapper.OrganizationMapper;
|
||||
import de.iwomm.propify_api.repository.OrganizationRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class OrganisationService {
|
||||
private final OrganizationRepository organizationRepository;
|
||||
private final OrganizationMapper organizationMapper;
|
||||
|
||||
public OrganisationService(
|
||||
OrganizationRepository organizationRepository,
|
||||
OrganizationMapper organizationMapper
|
||||
) {
|
||||
this.organizationRepository = organizationRepository;
|
||||
this.organizationMapper = organizationMapper;
|
||||
}
|
||||
|
||||
public List<Organization> findAll() {
|
||||
return organizationRepository.findAll();
|
||||
}
|
||||
|
||||
public Optional<Organization> findById(UUID id) {
|
||||
return organizationRepository.findById(id);
|
||||
}
|
||||
|
||||
public void deleteBbyId(UUID id) {
|
||||
organizationRepository.deleteById(id);
|
||||
}
|
||||
|
||||
public void delete(Organization organization) {
|
||||
organizationRepository.delete(organization);
|
||||
}
|
||||
|
||||
public List<OrganizationDTO> toDTOs(List<Organization> organizations) {
|
||||
List<OrganizationDTO> dtos = new ArrayList<>();
|
||||
|
||||
organizations.forEach(organization -> {
|
||||
dtos.add(this.organizationMapper.toDto(organization));
|
||||
});
|
||||
|
||||
return dtos;
|
||||
}
|
||||
|
||||
public Organization save(OrganizationDTO dto) {
|
||||
return organizationRepository.save(organizationMapper.fromDto(dto));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package de.iwomm.propify_api.service;
|
||||
|
||||
import com.alibaba.excel.EasyExcel;
|
||||
import com.alibaba.excel.context.AnalysisContext;
|
||||
import com.alibaba.excel.read.listener.ReadListener;
|
||||
import de.iwomm.propify_api.dto.OrganizationImportDTO;
|
||||
import de.iwomm.propify_api.entity.Industry;
|
||||
import de.iwomm.propify_api.entity.Organization;
|
||||
import de.iwomm.propify_api.repository.IndustryRepository;
|
||||
import de.iwomm.propify_api.repository.OrganizationRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
@Service
|
||||
public class OrganizationImportService {
|
||||
|
||||
private final OrganizationRepository organizationRepository;
|
||||
private final IndustryRepository industryRepository;
|
||||
|
||||
public OrganizationImportService(OrganizationRepository organizationRepository,
|
||||
IndustryRepository industryRepository) {
|
||||
this.organizationRepository = organizationRepository;
|
||||
this.industryRepository = industryRepository;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ImportResult importOrganizationsFromExcel(MultipartFile file) throws IOException {
|
||||
ImportResult result = new ImportResult();
|
||||
|
||||
EasyExcel.read(file.getInputStream(), OrganizationImportDTO.class, new ReadListener<OrganizationImportDTO>() {
|
||||
private int currentRow = 1; // Start at 1 to account for header
|
||||
|
||||
@Override
|
||||
public void invoke(OrganizationImportDTO data, AnalysisContext context) {
|
||||
currentRow++;
|
||||
try {
|
||||
Organization organization = convertToOrganization(data);
|
||||
organizationRepository.save(organization);
|
||||
result.incrementSuccess();
|
||||
} catch (Exception e) {
|
||||
result.addError(currentRow, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doAfterAllAnalysed(AnalysisContext context) {
|
||||
// Called after all data has been analyzed
|
||||
}
|
||||
}).sheet().doRead();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Organization convertToOrganization(OrganizationImportDTO dto) {
|
||||
Organization organization = new Organization();
|
||||
|
||||
// Name
|
||||
if (dto.getName() != null && !dto.getName().trim().isEmpty()) {
|
||||
organization.setName(dto.getName().trim());
|
||||
} else {
|
||||
throw new IllegalArgumentException("Name is required");
|
||||
}
|
||||
|
||||
// Industry (Branche) - create or lookup
|
||||
if (dto.getIndustry() != null && !dto.getIndustry().trim().isEmpty()) {
|
||||
String industryName = dto.getIndustry().trim();
|
||||
Industry industry = industryRepository.findByName(industryName)
|
||||
.orElseGet(() -> {
|
||||
Industry newIndustry = new Industry();
|
||||
newIndustry.setName(industryName);
|
||||
return industryRepository.save(newIndustry);
|
||||
});
|
||||
organization.setIndustry(industry);
|
||||
}
|
||||
|
||||
// Owner
|
||||
organization.setOwner(trimOrNull(dto.getOwner()));
|
||||
|
||||
return organization;
|
||||
}
|
||||
|
||||
private String trimOrNull(String value) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
public static class ImportResult {
|
||||
private int successCount = 0;
|
||||
private int errorCount = 0;
|
||||
private List<ImportError> errors = new ArrayList<>();
|
||||
|
||||
public void incrementSuccess() {
|
||||
successCount++;
|
||||
}
|
||||
|
||||
public void addError(int row, String message) {
|
||||
errorCount++;
|
||||
errors.add(new ImportError(row, message));
|
||||
}
|
||||
|
||||
public int getSuccessCount() {
|
||||
return successCount;
|
||||
}
|
||||
|
||||
public int getErrorCount() {
|
||||
return errorCount;
|
||||
}
|
||||
|
||||
public List<ImportError> getErrors() {
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ImportError {
|
||||
private int row;
|
||||
private String message;
|
||||
|
||||
public ImportError(int row, String message) {
|
||||
this.row = row;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public int getRow() {
|
||||
return row;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
package de.iwomm.propify_api.service;
|
||||
|
||||
import com.alibaba.excel.EasyExcel;
|
||||
import com.alibaba.excel.context.AnalysisContext;
|
||||
import com.alibaba.excel.read.listener.ReadListener;
|
||||
import de.iwomm.propify_api.dto.PersonImportDTO;
|
||||
import de.iwomm.propify_api.entity.Person;
|
||||
import de.iwomm.propify_api.entity.PersonLabel;
|
||||
import de.iwomm.propify_api.repository.OrganizationRepository;
|
||||
import de.iwomm.propify_api.repository.PersonLabelRepository;
|
||||
import de.iwomm.propify_api.repository.PersonRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Service
|
||||
public class PersonImportService {
|
||||
|
||||
private final PersonRepository personRepository;
|
||||
private final PersonLabelRepository personLabelRepository;
|
||||
private final OrganizationRepository organizationRepository;
|
||||
|
||||
public PersonImportService(PersonRepository personRepository,
|
||||
PersonLabelRepository personLabelRepository,
|
||||
OrganizationRepository organizationRepository) {
|
||||
this.personRepository = personRepository;
|
||||
this.personLabelRepository = personLabelRepository;
|
||||
this.organizationRepository = organizationRepository;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public ImportResult importPersonsFromExcel(MultipartFile file) throws IOException {
|
||||
ImportResult result = new ImportResult();
|
||||
|
||||
EasyExcel.read(file.getInputStream(), PersonImportDTO.class, new ReadListener<PersonImportDTO>() {
|
||||
private int currentRow = 1; // Start at 1 to account for header
|
||||
|
||||
@Override
|
||||
public void invoke(PersonImportDTO data, AnalysisContext context) {
|
||||
currentRow++;
|
||||
try {
|
||||
Person person = convertToPerson(data);
|
||||
personRepository.save(person);
|
||||
result.incrementSuccess();
|
||||
} catch (Exception e) {
|
||||
result.addError(currentRow, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doAfterAllAnalysed(AnalysisContext context) {
|
||||
// Called after all data has been analyzed
|
||||
}
|
||||
}).sheet().doRead();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Person convertToPerson(PersonImportDTO dto) {
|
||||
Person person = new Person();
|
||||
|
||||
// Parse name (format: "firstName lastName")
|
||||
if (dto.getName() != null && !dto.getName().trim().isEmpty()) {
|
||||
String[] nameParts = dto.getName().trim().split("\\s+", 2);
|
||||
person.setFirstName(nameParts[0]);
|
||||
person.setLastName(nameParts.length > 1 ? nameParts[1] : null);
|
||||
} else {
|
||||
person.setFirstName(null);
|
||||
person.setLastName(null);
|
||||
}
|
||||
|
||||
// Parse labels (comma-separated)
|
||||
if (dto.getLabels() != null && !dto.getLabels().trim().isEmpty()) {
|
||||
Set<PersonLabel> labels = new HashSet<>();
|
||||
String[] labelNames = dto.getLabels().split(",");
|
||||
for (String labelName : labelNames) {
|
||||
String trimmedName = labelName.trim();
|
||||
if (!trimmedName.isEmpty()) {
|
||||
PersonLabel label = personLabelRepository.findByName(trimmedName)
|
||||
.orElseGet(() -> {
|
||||
PersonLabel newLabel = new PersonLabel();
|
||||
newLabel.setName(trimmedName);
|
||||
return personLabelRepository.save(newLabel);
|
||||
});
|
||||
labels.add(label);
|
||||
}
|
||||
}
|
||||
person.setLabels(labels);
|
||||
}
|
||||
|
||||
// Organization (not yet linked in Person entity)
|
||||
// String organization = dto.getOrganization();
|
||||
|
||||
// Email addresses
|
||||
person.setEmailOffice(trimOrNull(dto.getEmailOffice()));
|
||||
person.setEmailPrivate(trimOrNull(dto.getEmailPrivate()));
|
||||
person.setEmailOther(trimOrNull(dto.getEmailOther()));
|
||||
|
||||
// Office phone
|
||||
if (dto.getPhoneOffice() != null && !dto.getPhoneOffice().trim().isEmpty()) {
|
||||
PhoneNumber parsed = parsePhoneNumber(dto.getPhoneOffice());
|
||||
person.setPhoneOfficeCountryCode(parsed.countryCode);
|
||||
person.setPhoneOfficeAreaCode(parsed.areaCode);
|
||||
person.setPhoneOfficeNumber(parsed.number);
|
||||
}
|
||||
|
||||
// Private phone
|
||||
if (dto.getPhonePrivate() != null && !dto.getPhonePrivate().trim().isEmpty()) {
|
||||
PhoneNumber parsed = parsePhoneNumber(dto.getPhonePrivate());
|
||||
person.setPhonePrivateCountryCode(parsed.countryCode);
|
||||
person.setPhonePrivateAreaCode(parsed.areaCode);
|
||||
person.setPhonePrivateNumber(parsed.number);
|
||||
}
|
||||
|
||||
// Mobile phone (prefer phoneMobile, fallback to phoneOther)
|
||||
String mobile = dto.getPhoneMobile();
|
||||
String other = dto.getPhoneOther();
|
||||
|
||||
if (mobile != null && !mobile.trim().isEmpty()) {
|
||||
person.setPhoneMobile(mobile.trim());
|
||||
} else if (other != null && !other.trim().isEmpty()) {
|
||||
person.setPhoneMobile(other.trim());
|
||||
}
|
||||
|
||||
return person;
|
||||
}
|
||||
|
||||
private String trimOrNull(String value) {
|
||||
if (value == null || value.trim().isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private PhoneNumber parsePhoneNumber(String phone) {
|
||||
PhoneNumber result = new PhoneNumber();
|
||||
|
||||
// Remove all whitespace and special characters except + and digits
|
||||
String cleaned = phone.replaceAll("[^+\\d]", "");
|
||||
|
||||
// Pattern to match: +CountryCode AreaCode Number
|
||||
// Examples: +493012345678, +4930123456
|
||||
Pattern pattern = Pattern.compile("^(\\+\\d{1,3})(\\d{2,5})(\\d+)$");
|
||||
Matcher matcher = pattern.matcher(cleaned);
|
||||
|
||||
if (matcher.matches()) {
|
||||
result.countryCode = matcher.group(1);
|
||||
result.areaCode = matcher.group(2);
|
||||
result.number = matcher.group(3);
|
||||
} else {
|
||||
// If pattern doesn't match, try simpler parsing
|
||||
if (cleaned.startsWith("+")) {
|
||||
// Assume format: +CC followed by rest
|
||||
int ccEnd = Math.min(4, cleaned.length()); // Max 3 digits + '+'
|
||||
result.countryCode = cleaned.substring(0, ccEnd);
|
||||
|
||||
String rest = cleaned.substring(ccEnd);
|
||||
if (rest.length() > 4) {
|
||||
// Split remaining into area code (2-3 digits) and number
|
||||
result.areaCode = rest.substring(0, Math.min(3, rest.length() - 4));
|
||||
result.number = rest.substring(Math.min(3, rest.length() - 4));
|
||||
} else {
|
||||
result.number = rest;
|
||||
}
|
||||
} else {
|
||||
// No country code, store as-is in number field
|
||||
result.number = cleaned;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static class PhoneNumber {
|
||||
String countryCode;
|
||||
String areaCode;
|
||||
String number;
|
||||
}
|
||||
|
||||
public static class ImportResult {
|
||||
private int successCount = 0;
|
||||
private int errorCount = 0;
|
||||
private List<ImportError> errors = new ArrayList<>();
|
||||
|
||||
public void incrementSuccess() {
|
||||
successCount++;
|
||||
}
|
||||
|
||||
public void addError(int row, String message) {
|
||||
errorCount++;
|
||||
errors.add(new ImportError(row, message));
|
||||
}
|
||||
|
||||
public int getSuccessCount() {
|
||||
return successCount;
|
||||
}
|
||||
|
||||
public int getErrorCount() {
|
||||
return errorCount;
|
||||
}
|
||||
|
||||
public List<ImportError> getErrors() {
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ImportError {
|
||||
private int row;
|
||||
private String message;
|
||||
|
||||
public ImportError(int row, String message) {
|
||||
this.row = row;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public int getRow() {
|
||||
return row;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,7 @@ spring:
|
||||
resourceserver:
|
||||
jwt:
|
||||
issuer-uri: ${KEYCLOAK_ISSUER_URI:https://kc.dev.localhost/realms/skamp}
|
||||
#issuer-uri: ${KEYCLOAK_ISSUER_URI:http://localhost:8888/realms/skamp}
|
||||
jwk-set-uri: ${spring.security.oauth2.resourceserver.jwt.issuer-uri}/protocol/openid-connect/certs
|
||||
|
||||
logging:
|
||||
|
||||
Reference in New Issue
Block a user