diff --git a/README.md b/README.md
index 6d5eb8e..bd858db 100644
--- a/README.md
+++ b/README.md
@@ -133,6 +133,8 @@ everything is reachable on plain HTTP:
| | Web client |
| | Swagger UI |
| | APIs (organization, members, events, feedback, finance, letters, helper) |
+| | Keycloak (via Traefik) |
+| | Keycloak (direct, for admin console) |
| | Traefik dashboard |
> **Do not run** `docker compose -f infra/docker-compose.yml up` locally — that
@@ -231,6 +233,27 @@ ghcr (tagged with the commit SHA), then `deploy-k8s` runs `helm upgrade
See [`infra/helm/README.md`](infra/helm/README.md) for the chart layout, required
one-time secrets (`genai-env`, `ghcr-pull`), and manual deploy instructions.
+## Database
+
+All five Spring services share a single **PostgreSQL 15** instance (`app_db`) but each owns a dedicated schema and a least-privilege user:
+
+| Service | Schema | User |
+|---|---|---|
+| Organization | `organization` | `organization_user` |
+| Member | `member` | `member_user` |
+| Event | `event` | `event_user` |
+| Feedback | `feedback` | `feedback_user` |
+| Finance | `finance` | `finance_user` |
+
+Schemas and users are created at DB init time by [`infra/postgres/init-db.sh`](infra/postgres/init-db.sh). Each service runs its own **Flyway** migrations on startup:
+
+- `V1__create_tables.sql` — creates all tables for that schema
+- `V2__add_foreign_keys.sql` — adds cross-schema foreign keys (e.g. `event.events.creator_id → member.members.id`)
+
+Cross-schema `REFERENCES` privileges are granted via `ALTER DEFAULT PRIVILEGES` in `init-db.sh`, so foreign-key constraints across schemas work on fresh deploys without manual intervention.
+
+The letter service has no database (`spring.flyway.enabled=false`). The GenAI service uses file-based storage for RAG documents.
+
## Authentication (Keycloak)
All services are protected by [Keycloak 26](https://www.keycloak.org) via OIDC/JWT. Keycloak is included in both the Docker Compose stack and the Helm chart — no separate installation is needed.
diff --git a/api/openapi.yaml b/api/openapi.yaml
index eace1e7..92163ec 100644
--- a/api/openapi.yaml
+++ b/api/openapi.yaml
@@ -1296,6 +1296,7 @@ components:
required:
- first_name
- last_name
+ - email
description: Data transfer object for creating a new Member.
Event:
type: object
diff --git a/infra/docker-compose.override.yml b/infra/docker-compose.override.yml
index a592764..79bf897 100644
--- a/infra/docker-compose.override.yml
+++ b/infra/docker-compose.override.yml
@@ -11,12 +11,14 @@
# * Removes TLS (no Let's Encrypt, no HTTPS, no 443) -- local has no public DNS
# * Strips the Host(...) requirement from every router so localhost works
# * Disables the HTTP -> HTTPS redirect so plain http://localhost works
+# * Exposes the app database on localhost:5432 for direct DB client access
#
# Access locally:
# http://localhost/ web client
# http://localhost/docs Swagger UI
# http://localhost/api/v1//... APIs
# http://localhost:8080 Traefik dashboard
+# localhost:5432 (app_admin/app_admin_password, db: app_db) PostgreSQL
services:
traefik:
@@ -126,15 +128,20 @@ services:
- "traefik.http.services.web-client.loadbalancer.server.port=8080"
keycloak:
+ environment:
+ KC_HOSTNAME: "http://localhost:8081/auth"
labels: !override
- "traefik.enable=true"
- "traefik.http.routers.keycloak.entrypoints=web"
- "traefik.http.routers.keycloak.rule=PathPrefix(`/auth`)"
- "traefik.http.services.keycloak.loadbalancer.server.port=8080"
- environment:
- KC_HOSTNAME: "http://localhost:8081/auth"
+
+ app-database:
+ ports:
+ - "5432:5432"
traefik-forward-auth:
+ restart: "no"
extra_hosts:
- "localhost:host-gateway"
labels: !override
diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml
index 99c83db..4bd96d2 100644
--- a/infra/docker-compose.yml
+++ b/infra/docker-compose.yml
@@ -26,13 +26,12 @@ services:
expose:
- 8080
depends_on:
- member-database:
+ app-database:
condition: service_healthy
environment:
- - SPRING_DATASOURCE_URL=jdbc:postgresql://member-database:5432/member_db
- - SPRING_DATASOURCE_USERNAME=member_user
- - SPRING_DATASOURCE_PASSWORD=member_password
- - SPRING_JPA_HIBERNATE_DDL_AUTO=update
+ - SPRING_DATASOURCE_URL=jdbc:postgresql://app-database:5432/app_db
+ - SPRING_DATASOURCE_USERNAME=organization_user
+ - SPRING_DATASOURCE_PASSWORD=organization_password
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=https://team-devoops.uaenorth.cloudapp.azure.com/auth/realms/devops
labels:
- "traefik.enable=true"
@@ -53,13 +52,12 @@ services:
expose:
- 8080
depends_on:
- member-database:
+ app-database:
condition: service_healthy
environment:
- - SPRING_DATASOURCE_URL=jdbc:postgresql://member-database:5432/member_db
+ - SPRING_DATASOURCE_URL=jdbc:postgresql://app-database:5432/app_db
- SPRING_DATASOURCE_USERNAME=member_user
- SPRING_DATASOURCE_PASSWORD=member_password
- - SPRING_JPA_HIBERNATE_DDL_AUTO=update
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=https://team-devoops.uaenorth.cloudapp.azure.com/auth/realms/devops
labels:
- "traefik.enable=true"
@@ -80,13 +78,12 @@ services:
expose:
- 8080
depends_on:
- member-database:
+ app-database:
condition: service_healthy
environment:
- - SPRING_DATASOURCE_URL=jdbc:postgresql://member-database:5432/member_db
- - SPRING_DATASOURCE_USERNAME=member_user
- - SPRING_DATASOURCE_PASSWORD=member_password
- - SPRING_JPA_HIBERNATE_DDL_AUTO=update
+ - SPRING_DATASOURCE_URL=jdbc:postgresql://app-database:5432/app_db
+ - SPRING_DATASOURCE_USERNAME=event_user
+ - SPRING_DATASOURCE_PASSWORD=event_password
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=https://team-devoops.uaenorth.cloudapp.azure.com/auth/realms/devops
labels:
- "traefik.enable=true"
@@ -107,13 +104,12 @@ services:
expose:
- 8080
depends_on:
- member-database:
+ app-database:
condition: service_healthy
environment:
- - SPRING_DATASOURCE_URL=jdbc:postgresql://member-database:5432/member_db
- - SPRING_DATASOURCE_USERNAME=member_user
- - SPRING_DATASOURCE_PASSWORD=member_password
- - SPRING_JPA_HIBERNATE_DDL_AUTO=update
+ - SPRING_DATASOURCE_URL=jdbc:postgresql://app-database:5432/app_db
+ - SPRING_DATASOURCE_USERNAME=feedback_user
+ - SPRING_DATASOURCE_PASSWORD=feedback_password
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=https://team-devoops.uaenorth.cloudapp.azure.com/auth/realms/devops
labels:
- "traefik.enable=true"
@@ -134,13 +130,12 @@ services:
expose:
- 8080
depends_on:
- member-database:
+ app-database:
condition: service_healthy
environment:
- - SPRING_DATASOURCE_URL=jdbc:postgresql://member-database:5432/member_db
- - SPRING_DATASOURCE_USERNAME=member_user
- - SPRING_DATASOURCE_PASSWORD=member_password
- - SPRING_JPA_HIBERNATE_DDL_AUTO=update
+ - SPRING_DATASOURCE_URL=jdbc:postgresql://app-database:5432/app_db
+ - SPRING_DATASOURCE_USERNAME=finance_user
+ - SPRING_DATASOURCE_PASSWORD=finance_password
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=https://team-devoops.uaenorth.cloudapp.azure.com/auth/realms/devops
labels:
- "traefik.enable=true"
@@ -161,13 +156,12 @@ services:
expose:
- 8080
depends_on:
- member-database:
+ app-database:
condition: service_healthy
environment:
- - SPRING_DATASOURCE_URL=jdbc:postgresql://member-database:5432/member_db
- - SPRING_DATASOURCE_USERNAME=member_user
- - SPRING_DATASOURCE_PASSWORD=member_password
- - SPRING_JPA_HIBERNATE_DDL_AUTO=update
+ - SPRING_DATASOURCE_URL=jdbc:postgresql://app-database:5432/app_db
+ - SPRING_DATASOURCE_USERNAME=letter_user
+ - SPRING_DATASOURCE_PASSWORD=letter_password
- SPRING_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=https://team-devoops.uaenorth.cloudapp.azure.com/auth/realms/devops
labels:
- "traefik.enable=true"
@@ -345,19 +339,26 @@ services:
networks:
- keycloak
- member-database:
+ app-database:
image: postgres:15.6-alpine
- container_name: member-db
+ container_name: app-db
expose:
- 5432
environment:
- POSTGRES_USER: member_user
- POSTGRES_PASSWORD: member_password
- POSTGRES_DB: member_db
+ POSTGRES_USER: app_admin
+ POSTGRES_PASSWORD: app_admin_password
+ POSTGRES_DB: app_db
+ ORGANIZATION_USER_PASSWORD: organization_password
+ MEMBER_USER_PASSWORD: member_password
+ EVENT_USER_PASSWORD: event_password
+ FEEDBACK_USER_PASSWORD: feedback_password
+ FINANCE_USER_PASSWORD: finance_password
+ LETTER_USER_PASSWORD: letter_password
volumes:
- - member_db_data:/var/lib/postgresql/data
+ - app_db_data:/var/lib/postgresql/data
+ - ./postgres/init-db.sh:/docker-entrypoint-initdb.d/init-db.sh:ro
healthcheck:
- test: ["CMD-SHELL", "pg_isready -U member_user -d member_db"]
+ test: ["CMD-SHELL", "pg_isready -U app_admin -d app_db"]
interval: 10s
timeout: 5s
retries: 5
@@ -366,7 +367,7 @@ services:
- data
volumes:
- member_db_data:
+ app_db_data:
keycloak_db_data:
letsencrypt:
diff --git a/infra/helm/team-devoops/templates/configmap-db.yaml b/infra/helm/team-devoops/templates/configmap-db.yaml
index 4aa676b..593eeae 100644
--- a/infra/helm/team-devoops/templates/configmap-db.yaml
+++ b/infra/helm/team-devoops/templates/configmap-db.yaml
@@ -1,12 +1,18 @@
{{- if .Values.database.enabled }}
+{{- range $name, $svc := .Values.services }}
+{{- if $svc.db }}
+{{- $dbKey := $svc.dbUser }}
+{{- $dbUser := index $.Values.database.users $dbKey }}
+---
apiVersion: v1
kind: ConfigMap
metadata:
- name: db-config
+ name: db-config-{{ $name }}
labels:
- {{- include "team-devoops.labels" (dict "name" "db-config" "root" $) | nindent 4 }}
+ {{- include "team-devoops.labels" (dict "name" (printf "db-config-%s" $name) "root" $) | nindent 4 }}
data:
- SPRING_DATASOURCE_URL: jdbc:postgresql://{{ .Values.database.host }}:{{ .Values.database.port }}/{{ .Values.database.name }}
- SPRING_DATASOURCE_USERNAME: {{ .Values.database.user | quote }}
- SPRING_JPA_HIBERNATE_DDL_AUTO: update
+ SPRING_DATASOURCE_URL: jdbc:postgresql://{{ $.Values.database.host }}:{{ $.Values.database.port }}/{{ $.Values.database.name }}
+ SPRING_DATASOURCE_USERNAME: {{ $dbUser.username | quote }}
+{{- end }}
+{{- end }}
{{- end }}
diff --git a/infra/helm/team-devoops/templates/deployment.yaml b/infra/helm/team-devoops/templates/deployment.yaml
index 978269c..65214fc 100644
--- a/infra/helm/team-devoops/templates/deployment.yaml
+++ b/infra/helm/team-devoops/templates/deployment.yaml
@@ -40,9 +40,9 @@ spec:
envFrom:
{{- if $svc.db }}
- configMapRef:
- name: db-config
+ name: db-config-{{ $name }}
- secretRef:
- name: db-credentials
+ name: db-credentials-{{ $svc.dbUser }}-service
{{- end }}
{{- if $svc.envFromSecret }}
- secretRef:
diff --git a/infra/helm/team-devoops/templates/postgres-init-configmap.yaml b/infra/helm/team-devoops/templates/postgres-init-configmap.yaml
new file mode 100644
index 0000000..a7b6a96
--- /dev/null
+++ b/infra/helm/team-devoops/templates/postgres-init-configmap.yaml
@@ -0,0 +1,115 @@
+{{- if .Values.database.enabled }}
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: db-init-scripts
+ labels:
+ {{- include "team-devoops.labels" (dict "name" "db-init-scripts" "root" $) | nindent 4 }}
+data:
+ init-db.sh: |
+ #!/usr/bin/env bash
+ # init-db.sh — runs once when the Postgres container is first initialised.
+ # Creates per-service users, all application schemas, and grants:
+ # - each service user: WRITE on its own schemas, READ (via reader role) on all
+ # - letter_user: READ only (no write schemas)
+ #
+ # Passwords are injected via environment variables so nothing is hardcoded.
+ set -euo pipefail
+
+ DB="${POSTGRES_DB}"
+ ADMIN="${POSTGRES_USER}"
+
+ psql -v ON_ERROR_STOP=1 --username "$ADMIN" --dbname "$DB" <<-EOSQL
+
+ -- Reader role: granted to every service user so each can SELECT anywhere
+ CREATE ROLE reader NOLOGIN;
+
+ -- Per-service users
+ CREATE USER organization_user WITH PASSWORD '${ORGANIZATION_USER_PASSWORD}';
+ CREATE USER member_user WITH PASSWORD '${MEMBER_USER_PASSWORD}';
+ CREATE USER event_user WITH PASSWORD '${EVENT_USER_PASSWORD}';
+ CREATE USER feedback_user WITH PASSWORD '${FEEDBACK_USER_PASSWORD}';
+ CREATE USER finance_user WITH PASSWORD '${FINANCE_USER_PASSWORD}';
+ CREATE USER letter_user WITH PASSWORD '${LETTER_USER_PASSWORD}';
+
+ -- Allow all users to connect to the application database
+ GRANT CONNECT ON DATABASE ${DB} TO
+ organization_user, member_user, event_user,
+ feedback_user, finance_user, letter_user;
+
+ -- All users inherit the reader role
+ GRANT reader TO
+ organization_user, member_user, event_user,
+ feedback_user, finance_user, letter_user;
+
+ -- Schemas: organization
+ CREATE SCHEMA organization;
+
+ -- Schemas: member
+ CREATE SCHEMA member;
+
+ -- Schemas: event
+ CREATE SCHEMA event;
+
+ -- Schemas: feedback
+ CREATE SCHEMA feedback;
+
+ -- Schemas: finance
+ CREATE SCHEMA finance;
+
+ -- Ownership
+ ALTER SCHEMA organization OWNER TO organization_user;
+ ALTER SCHEMA member OWNER TO member_user;
+ ALTER SCHEMA event OWNER TO event_user;
+ ALTER SCHEMA feedback OWNER TO feedback_user;
+ ALTER SCHEMA finance OWNER TO finance_user;
+
+ -- Reader USAGE on all schemas
+ GRANT USAGE ON SCHEMA
+ organization, member, event, feedback, finance
+ TO reader;
+
+ -- Reader SELECT on existing tables (defensive)
+ GRANT SELECT ON ALL TABLES IN SCHEMA
+ organization, member, event, feedback, finance
+ TO reader;
+
+ -- Default privileges for future tables
+ ALTER DEFAULT PRIVILEGES FOR ROLE organization_user
+ IN SCHEMA organization
+ GRANT SELECT ON TABLES TO reader;
+
+ ALTER DEFAULT PRIVILEGES FOR ROLE member_user
+ IN SCHEMA member
+ GRANT SELECT ON TABLES TO reader;
+
+ ALTER DEFAULT PRIVILEGES FOR ROLE event_user
+ IN SCHEMA event
+ GRANT SELECT ON TABLES TO reader;
+
+ ALTER DEFAULT PRIVILEGES FOR ROLE feedback_user
+ IN SCHEMA feedback
+ GRANT SELECT ON TABLES TO reader;
+
+ ALTER DEFAULT PRIVILEGES FOR ROLE finance_user
+ IN SCHEMA finance
+ GRANT SELECT ON TABLES TO reader;
+
+ -- -----------------------------------------------------------------------
+ -- Cross-schema REFERENCES: required for FK constraints across schemas.
+ -- Granted per-user on the schemas they reference.
+ -- -----------------------------------------------------------------------
+ ALTER DEFAULT PRIVILEGES FOR ROLE member_user
+ IN SCHEMA member
+ GRANT REFERENCES ON TABLES TO organization_user, event_user, feedback_user, finance_user;
+
+ ALTER DEFAULT PRIVILEGES FOR ROLE organization_user
+ IN SCHEMA organization
+ GRANT REFERENCES ON TABLES TO event_user;
+
+ ALTER DEFAULT PRIVILEGES FOR ROLE event_user
+ IN SCHEMA event
+ GRANT REFERENCES ON TABLES TO feedback_user;
+
+ EOSQL
+{{- end }}
diff --git a/infra/helm/team-devoops/templates/postgres-statefulset.yaml b/infra/helm/team-devoops/templates/postgres-statefulset.yaml
index ec9fb96..e8b8203 100644
--- a/infra/helm/team-devoops/templates/postgres-statefulset.yaml
+++ b/infra/helm/team-devoops/templates/postgres-statefulset.yaml
@@ -37,9 +37,14 @@ spec:
secretKeyRef:
name: db-credentials
key: POSTGRES_PASSWORD
+ envFrom:
+ - secretRef:
+ name: db-init-credentials
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
+ - name: init-scripts
+ mountPath: /docker-entrypoint-initdb.d
readinessProbe:
exec:
command: ["pg_isready", "-U", "{{ .Values.database.user }}", "-d", "{{ .Values.database.name }}"]
@@ -52,6 +57,11 @@ spec:
periodSeconds: 20
resources:
{{- toYaml .Values.database.resources | nindent 12 }}
+ volumes:
+ - name: init-scripts
+ configMap:
+ name: db-init-scripts
+ defaultMode: 0755
volumeClaimTemplates:
- metadata:
name: data
diff --git a/infra/helm/team-devoops/templates/secret-db.yaml b/infra/helm/team-devoops/templates/secret-db.yaml
index e5e8598..9eb50cc 100644
--- a/infra/helm/team-devoops/templates/secret-db.yaml
+++ b/infra/helm/team-devoops/templates/secret-db.yaml
@@ -1,4 +1,6 @@
{{- if .Values.database.enabled }}
+---
+# Admin credentials used by the Postgres StatefulSet itself
apiVersion: v1
kind: Secret
metadata:
@@ -7,7 +9,36 @@ metadata:
{{- include "team-devoops.labels" (dict "name" "db-credentials" "root" $) | nindent 4 }}
type: Opaque
stringData:
- # Single source of truth shared by Postgres and the Spring services.
- SPRING_DATASOURCE_PASSWORD: {{ .Values.database.password | quote }}
POSTGRES_PASSWORD: {{ .Values.database.password | quote }}
+---
+# Init-script credentials: all service passwords injected as env vars into the
+# Postgres init container so init-db.sh can create per-service users.
+apiVersion: v1
+kind: Secret
+metadata:
+ name: db-init-credentials
+ labels:
+ {{- include "team-devoops.labels" (dict "name" "db-init-credentials" "root" $) | nindent 4 }}
+type: Opaque
+stringData:
+ ORGANIZATION_USER_PASSWORD: {{ .Values.database.users.organization.password | quote }}
+ MEMBER_USER_PASSWORD: {{ .Values.database.users.member.password | quote }}
+ EVENT_USER_PASSWORD: {{ .Values.database.users.event.password | quote }}
+ FEEDBACK_USER_PASSWORD: {{ .Values.database.users.feedback.password | quote }}
+ FINANCE_USER_PASSWORD: {{ .Values.database.users.finance.password | quote }}
+ LETTER_USER_PASSWORD: {{ .Values.database.users.letter.password | quote }}
+---
+# Per-service datasource password secrets
+{{- range $key, $user := .Values.database.users }}
+apiVersion: v1
+kind: Secret
+metadata:
+ name: db-credentials-{{ $key }}-service
+ labels:
+ {{- include "team-devoops.labels" (dict "name" (printf "db-credentials-%s-service" $key) "root" $) | nindent 4 }}
+type: Opaque
+stringData:
+ SPRING_DATASOURCE_PASSWORD: {{ $user.password | quote }}
+---
+{{- end }}
{{- end }}
diff --git a/infra/helm/team-devoops/values.yaml b/infra/helm/team-devoops/values.yaml
index 521f4d3..dc771c3 100644
--- a/infra/helm/team-devoops/values.yaml
+++ b/infra/helm/team-devoops/values.yaml
@@ -16,15 +16,35 @@ global:
imagePullSecrets:
- name: ghcr-pull
-# Shared in-cluster Postgres database (mirrors the single compose member_db).
+# Shared in-cluster Postgres database — single DB, multiple schemas.
database:
enabled: true
- name: member_db
- user: member_user
+ name: app_db
+ user: app_admin
# Rendered into a Secret (db-credentials). Override in CI/prod via --set.
- password: member_password
- host: member-database
+ password: app_admin_password
+ host: app-database
port: 5432
+ # Per-service users. Override passwords via --set in CI/prod.
+ users:
+ organization:
+ username: organization_user
+ password: organization_password
+ member:
+ username: member_user
+ password: member_password
+ event:
+ username: event_user
+ password: event_password
+ feedback:
+ username: feedback_user
+ password: feedback_password
+ finance:
+ username: finance_user
+ password: finance_password
+ letter:
+ username: letter_user
+ password: letter_password
image: postgres:15.6-alpine
storageSize: 5Gi
resources:
@@ -143,6 +163,7 @@ services:
path: /api/v1/organization
port: 8080
db: true
+ dbUser: organization
health: /actuator/health
stripPrefix: true
env:
@@ -153,6 +174,7 @@ services:
path: /api/v1/members
port: 8080
db: true
+ dbUser: member
health: /actuator/health
stripPrefix: true
env:
@@ -163,6 +185,7 @@ services:
path: /api/v1/events
port: 8080
db: true
+ dbUser: event
health: /actuator/health
stripPrefix: true
env:
@@ -173,6 +196,7 @@ services:
path: /api/v1/feedback
port: 8080
db: true
+ dbUser: feedback
health: /actuator/health
stripPrefix: true
env:
@@ -183,6 +207,7 @@ services:
path: /api/v1/finance
port: 8080
db: true
+ dbUser: finance
health: /actuator/health
stripPrefix: true
env:
@@ -193,6 +218,7 @@ services:
path: /api/v1/letters
port: 8080
db: true
+ dbUser: letter
health: /actuator/health
stripPrefix: true
env:
diff --git a/infra/postgres/init-db.sh b/infra/postgres/init-db.sh
new file mode 100755
index 0000000..3043874
--- /dev/null
+++ b/infra/postgres/init-db.sh
@@ -0,0 +1,117 @@
+#!/usr/bin/env bash
+# init-db.sh — runs once when the Postgres container is first initialised.
+# Creates per-service users, all application schemas, and grants:
+# - each service user: WRITE on its own schemas, READ (via reader role) on all
+# - letter_user: READ only (no write schemas)
+#
+# Passwords are injected via environment variables so nothing is hardcoded.
+# Required env vars (all must be set):
+# ORGANIZATION_USER_PASSWORD
+# MEMBER_USER_PASSWORD
+# EVENT_USER_PASSWORD
+# FEEDBACK_USER_PASSWORD
+# FINANCE_USER_PASSWORD
+# LETTER_USER_PASSWORD
+set -euo pipefail
+
+DB="${POSTGRES_DB}"
+ADMIN="${POSTGRES_USER}"
+
+psql -v ON_ERROR_STOP=1 --username "$ADMIN" --dbname "$DB" <<-EOSQL
+
+-- -------------------------------------------------------------------------
+-- Reader role: granted to every service user so each can SELECT anywhere
+-- -------------------------------------------------------------------------
+CREATE ROLE reader NOLOGIN;
+
+-- -------------------------------------------------------------------------
+-- Per-service users
+-- -------------------------------------------------------------------------
+CREATE USER organization_user WITH PASSWORD '${ORGANIZATION_USER_PASSWORD}';
+CREATE USER member_user WITH PASSWORD '${MEMBER_USER_PASSWORD}';
+CREATE USER event_user WITH PASSWORD '${EVENT_USER_PASSWORD}';
+CREATE USER feedback_user WITH PASSWORD '${FEEDBACK_USER_PASSWORD}';
+CREATE USER finance_user WITH PASSWORD '${FINANCE_USER_PASSWORD}';
+CREATE USER letter_user WITH PASSWORD '${LETTER_USER_PASSWORD}';
+
+-- Allow all users to connect to the application database
+GRANT CONNECT ON DATABASE ${DB} TO
+ organization_user, member_user, event_user,
+ feedback_user, finance_user, letter_user;
+
+-- All users inherit the reader role
+GRANT reader TO
+ organization_user, member_user, event_user,
+ feedback_user, finance_user, letter_user;
+
+-- -------------------------------------------------------------------------
+-- Schemas (one per service)
+-- -------------------------------------------------------------------------
+CREATE SCHEMA organization;
+CREATE SCHEMA member;
+CREATE SCHEMA event;
+CREATE SCHEMA feedback;
+CREATE SCHEMA finance;
+
+-- -------------------------------------------------------------------------
+-- Ownership: each service user owns its schema
+-- -------------------------------------------------------------------------
+ALTER SCHEMA organization OWNER TO organization_user;
+ALTER SCHEMA member OWNER TO member_user;
+ALTER SCHEMA event OWNER TO event_user;
+ALTER SCHEMA feedback OWNER TO feedback_user;
+ALTER SCHEMA finance OWNER TO finance_user;
+
+-- -------------------------------------------------------------------------
+-- Reader role: USAGE on all schemas + SELECT on all current tables
+-- -------------------------------------------------------------------------
+GRANT USAGE ON SCHEMA
+ organization, member, event, feedback, finance
+TO reader;
+
+-- SELECT on any tables that already exist (none yet, but defensive)
+GRANT SELECT ON ALL TABLES IN SCHEMA
+ organization, member, event, feedback, finance
+TO reader;
+
+-- -------------------------------------------------------------------------
+-- Default privileges: future tables created by each service user are
+-- automatically SELECT-able by the reader role
+-- -------------------------------------------------------------------------
+ALTER DEFAULT PRIVILEGES FOR ROLE organization_user
+ IN SCHEMA organization
+ GRANT SELECT ON TABLES TO reader;
+
+ALTER DEFAULT PRIVILEGES FOR ROLE member_user
+ IN SCHEMA member
+ GRANT SELECT ON TABLES TO reader;
+
+ALTER DEFAULT PRIVILEGES FOR ROLE event_user
+ IN SCHEMA event
+ GRANT SELECT ON TABLES TO reader;
+
+ALTER DEFAULT PRIVILEGES FOR ROLE feedback_user
+ IN SCHEMA feedback
+ GRANT SELECT ON TABLES TO reader;
+
+ALTER DEFAULT PRIVILEGES FOR ROLE finance_user
+ IN SCHEMA finance
+ GRANT SELECT ON TABLES TO reader;
+
+-- -------------------------------------------------------------------------
+-- Cross-schema REFERENCES: required for FK constraints across schemas.
+-- Granted per-user on the schemas they reference.
+-- -------------------------------------------------------------------------
+ALTER DEFAULT PRIVILEGES FOR ROLE member_user
+ IN SCHEMA member
+ GRANT REFERENCES ON TABLES TO organization_user, event_user, feedback_user, finance_user;
+
+ALTER DEFAULT PRIVILEGES FOR ROLE organization_user
+ IN SCHEMA organization
+ GRANT REFERENCES ON TABLES TO event_user;
+
+ALTER DEFAULT PRIVILEGES FOR ROLE event_user
+ IN SCHEMA event
+ GRANT REFERENCES ON TABLES TO feedback_user;
+
+EOSQL
diff --git a/lombok-1.18.46.jar b/lombok-1.18.46.jar
new file mode 100644
index 0000000..96d09c0
Binary files /dev/null and b/lombok-1.18.46.jar differ
diff --git a/services/py-genai-helper/generated/models.py b/services/py-genai-helper/generated/models.py
index 2fb651e..5b07de0 100644
--- a/services/py-genai-helper/generated/models.py
+++ b/services/py-genai-helper/generated/models.py
@@ -1,6 +1,6 @@
# generated by datamodel-codegen:
# filename: openapi.yaml
-# timestamp: 2026-06-03T15:08:51+00:00
+# timestamp: 2026-06-12T12:58:15+00:00
from __future__ import annotations
from pydantic import AwareDatetime, BaseModel, Field
@@ -98,7 +98,7 @@ class MemberPartialUpdate(BaseModel):
class MemberCreate(BaseModel):
first_name: str
last_name: str
- email: str | None = None
+ email: str
birthday: date | None = None
phone_number: str | None = None
address: str | None = None
diff --git a/services/spring-event/build.gradle b/services/spring-event/build.gradle
index 3cb821e..36ed041 100644
--- a/services/spring-event/build.gradle
+++ b/services/spring-event/build.gradle
@@ -42,7 +42,11 @@ repositories {
}
dependencies {
+ compileOnly 'org.projectlombok:lombok'
+ annotationProcessor 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.flywaydb:flyway-core'
+ implementation 'org.flywaydb:flyway-database-postgresql'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
diff --git a/services/spring-event/config/checkstyle/checkstyle.xml b/services/spring-event/config/checkstyle/checkstyle.xml
index 580dda9..c99593a 100644
--- a/services/spring-event/config/checkstyle/checkstyle.xml
+++ b/services/spring-event/config/checkstyle/checkstyle.xml
@@ -15,6 +15,13 @@
+
+
+
+
+
+
diff --git a/services/spring-event/src/main/java/tum/devoops/eventservice/entity/Attendance.java b/services/spring-event/src/main/java/tum/devoops/eventservice/entity/Attendance.java
new file mode 100644
index 0000000..5f864dc
--- /dev/null
+++ b/services/spring-event/src/main/java/tum/devoops/eventservice/entity/Attendance.java
@@ -0,0 +1,37 @@
+package tum.devoops.eventservice.entity;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Embeddable;
+import jakarta.persistence.EmbeddedId;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Table(schema = "event", name = "attendances")
+@Getter @Setter @NoArgsConstructor @AllArgsConstructor
+public class Attendance {
+
+ // Composite PK: (event_id, member_id).
+ // event_id references event.event(id).
+ // member_id references member.member(id) — FK added in V3 migration.
+ @EmbeddedId
+ private Id id;
+
+ @Embeddable
+ @Data @NoArgsConstructor @AllArgsConstructor
+ public static class Id implements Serializable {
+ @Column(name = "event_id", nullable = false)
+ private UUID eventId;
+
+ @Column(name = "member_id", nullable = false)
+ private UUID memberId;
+ }
+}
diff --git a/services/spring-event/src/main/java/tum/devoops/eventservice/entity/Event.java b/services/spring-event/src/main/java/tum/devoops/eventservice/entity/Event.java
new file mode 100644
index 0000000..2e277ff
--- /dev/null
+++ b/services/spring-event/src/main/java/tum/devoops/eventservice/entity/Event.java
@@ -0,0 +1,57 @@
+package tum.devoops.eventservice.entity;
+
+import java.time.Instant;
+import java.util.List;
+import java.util.UUID;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.Table;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Table(schema = "event", name = "events")
+@Getter @Setter @NoArgsConstructor
+public class Event {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.UUID)
+ @Column(name = "id", nullable = false, updatable = false)
+ private UUID id;
+
+ @Column(name = "name", nullable = false)
+ private String name;
+
+ @Column(name = "description", columnDefinition = "TEXT")
+ private String description;
+
+ @Column(name = "start_time", nullable = false)
+ private Instant startTime;
+
+ @Column(name = "end_time", nullable = false)
+ private Instant endTime;
+
+ // UUID of the member who created this event.
+ // FK to member.member(id) added in V3 migration.
+ @Column(name = "creator_id", nullable = false)
+ private UUID creatorId;
+
+ @OneToMany
+ @JoinColumn(name = "event_id", referencedColumnName = "id", insertable = false, updatable = false)
+ private List attendees;
+
+ @OneToMany
+ @JoinColumn(name = "event_id", referencedColumnName = "id", insertable = false, updatable = false)
+ private List sportsLinked;
+
+ @OneToMany
+ @JoinColumn(name = "event_id", referencedColumnName = "id", insertable = false, updatable = false)
+ private List teamsLinked;
+}
diff --git a/services/spring-event/src/main/java/tum/devoops/eventservice/entity/SportEvent.java b/services/spring-event/src/main/java/tum/devoops/eventservice/entity/SportEvent.java
new file mode 100644
index 0000000..71971ba
--- /dev/null
+++ b/services/spring-event/src/main/java/tum/devoops/eventservice/entity/SportEvent.java
@@ -0,0 +1,37 @@
+package tum.devoops.eventservice.entity;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Embeddable;
+import jakarta.persistence.EmbeddedId;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Table(schema = "event", name = "sport_events")
+@Getter @Setter @NoArgsConstructor @AllArgsConstructor
+public class SportEvent {
+
+ // Composite PK: (event_id, sport_name).
+ // event_id references event.event(id).
+ // sport_name references organization.sport(name) — FK added in V3 migration.
+ @EmbeddedId
+ private Id id;
+
+ @Embeddable
+ @Data @NoArgsConstructor @AllArgsConstructor
+ public static class Id implements Serializable {
+ @Column(name = "event_id", nullable = false)
+ private UUID eventId;
+
+ @Column(name = "sport_name", nullable = false)
+ private String sportName;
+ }
+}
diff --git a/services/spring-event/src/main/java/tum/devoops/eventservice/entity/TeamEvent.java b/services/spring-event/src/main/java/tum/devoops/eventservice/entity/TeamEvent.java
new file mode 100644
index 0000000..fe385f3
--- /dev/null
+++ b/services/spring-event/src/main/java/tum/devoops/eventservice/entity/TeamEvent.java
@@ -0,0 +1,37 @@
+package tum.devoops.eventservice.entity;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Embeddable;
+import jakarta.persistence.EmbeddedId;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Table(schema = "event", name = "team_events")
+@Getter @Setter @NoArgsConstructor @AllArgsConstructor
+public class TeamEvent {
+
+ // Composite PK: (event_id, team_id).
+ // event_id references event.event(id).
+ // team_id references organization.team(id) — FK added in V3 migration.
+ @EmbeddedId
+ private Id id;
+
+ @Embeddable
+ @Data @NoArgsConstructor @AllArgsConstructor
+ public static class Id implements Serializable {
+ @Column(name = "event_id", nullable = false)
+ private UUID eventId;
+
+ @Column(name = "team_id", nullable = false)
+ private UUID teamId;
+ }
+}
diff --git a/services/spring-event/src/main/java/tum/devoops/eventservice/repository/AttendanceRepository.java b/services/spring-event/src/main/java/tum/devoops/eventservice/repository/AttendanceRepository.java
new file mode 100644
index 0000000..c74263c
--- /dev/null
+++ b/services/spring-event/src/main/java/tum/devoops/eventservice/repository/AttendanceRepository.java
@@ -0,0 +1,20 @@
+package tum.devoops.eventservice.repository;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import tum.devoops.eventservice.entity.Attendance;
+
+public interface AttendanceRepository extends JpaRepository {
+
+ // SELECT * FROM event.attendances WHERE event_id = ?
+ List findAllById_EventId(UUID eventId);
+
+ // SELECT * FROM event.attendances WHERE member_id = ?
+ List findAllById_MemberId(UUID memberId);
+
+ // DELETE FROM event.attendances WHERE event_id = ?
+ void deleteAllById_EventId(UUID eventId);
+}
diff --git a/services/spring-event/src/main/java/tum/devoops/eventservice/repository/EventRepository.java b/services/spring-event/src/main/java/tum/devoops/eventservice/repository/EventRepository.java
new file mode 100644
index 0000000..36a64df
--- /dev/null
+++ b/services/spring-event/src/main/java/tum/devoops/eventservice/repository/EventRepository.java
@@ -0,0 +1,14 @@
+package tum.devoops.eventservice.repository;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import tum.devoops.eventservice.entity.Event;
+
+public interface EventRepository extends JpaRepository {
+
+ // SELECT * FROM event.events WHERE creator_id = ?
+ List findAllByCreatorId(UUID creatorId);
+}
diff --git a/services/spring-event/src/main/java/tum/devoops/eventservice/repository/SportEventRepository.java b/services/spring-event/src/main/java/tum/devoops/eventservice/repository/SportEventRepository.java
new file mode 100644
index 0000000..08be72f
--- /dev/null
+++ b/services/spring-event/src/main/java/tum/devoops/eventservice/repository/SportEventRepository.java
@@ -0,0 +1,20 @@
+package tum.devoops.eventservice.repository;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import tum.devoops.eventservice.entity.SportEvent;
+
+public interface SportEventRepository extends JpaRepository {
+
+ // SELECT * FROM event.sport_events WHERE event_id = ?
+ List findAllById_EventId(UUID eventId);
+
+ // SELECT * FROM event.sport_events WHERE sport_name = ?
+ List findAllById_SportName(String sportName);
+
+ // DELETE FROM event.sport_events WHERE event_id = ?
+ void deleteAllById_EventId(UUID eventId);
+}
diff --git a/services/spring-event/src/main/java/tum/devoops/eventservice/repository/TeamEventRepository.java b/services/spring-event/src/main/java/tum/devoops/eventservice/repository/TeamEventRepository.java
new file mode 100644
index 0000000..9ff9e29
--- /dev/null
+++ b/services/spring-event/src/main/java/tum/devoops/eventservice/repository/TeamEventRepository.java
@@ -0,0 +1,20 @@
+package tum.devoops.eventservice.repository;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import tum.devoops.eventservice.entity.TeamEvent;
+
+public interface TeamEventRepository extends JpaRepository {
+
+ // SELECT * FROM event.team_events WHERE event_id = ?
+ List findAllById_EventId(UUID eventId);
+
+ // SELECT * FROM event.team_events WHERE team_id = ?
+ List findAllById_TeamId(UUID teamId);
+
+ // DELETE FROM event.team_events WHERE event_id = ?
+ void deleteAllById_EventId(UUID eventId);
+}
diff --git a/services/spring-event/src/main/resources/application.properties b/services/spring-event/src/main/resources/application.properties
index 670e53c..fdb1784 100644
--- a/services/spring-event/src/main/resources/application.properties
+++ b/services/spring-event/src/main/resources/application.properties
@@ -2,3 +2,10 @@ spring.application.name=event-service
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8081/auth/realms/devops
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://keycloak:8080/auth/realms/devops/protocol/openid-connect/certs
+
+spring.jpa.hibernate.ddl-auto=validate
+spring.jpa.properties.hibernate.default_schema=event
+
+spring.flyway.default-schema=event
+spring.flyway.schemas=event
+spring.flyway.create-schemas=true
diff --git a/services/spring-event/src/main/resources/db/migration/V1__create_tables.sql b/services/spring-event/src/main/resources/db/migration/V1__create_tables.sql
new file mode 100644
index 0000000..b335158
--- /dev/null
+++ b/services/spring-event/src/main/resources/db/migration/V1__create_tables.sql
@@ -0,0 +1,30 @@
+CREATE TABLE event.events (
+ id UUID NOT NULL DEFAULT gen_random_uuid(),
+ name VARCHAR(255) NOT NULL,
+ description TEXT,
+ start_time TIMESTAMPTZ NOT NULL,
+ end_time TIMESTAMPTZ NOT NULL,
+ creator_id UUID NOT NULL,
+ CONSTRAINT pk_events PRIMARY KEY (id)
+);
+
+CREATE TABLE event.attendances (
+ event_id UUID NOT NULL,
+ member_id UUID NOT NULL,
+ CONSTRAINT pk_attendances PRIMARY KEY (event_id, member_id),
+ CONSTRAINT fk_attendances_event FOREIGN KEY (event_id) REFERENCES event.events (id)
+);
+
+CREATE TABLE event.sport_events (
+ event_id UUID NOT NULL,
+ sport_name VARCHAR(255) NOT NULL,
+ CONSTRAINT pk_sport_events PRIMARY KEY (event_id, sport_name),
+ CONSTRAINT fk_sport_events_event FOREIGN KEY (event_id) REFERENCES event.events (id)
+);
+
+CREATE TABLE event.team_events (
+ event_id UUID NOT NULL,
+ team_id UUID NOT NULL,
+ CONSTRAINT pk_team_events PRIMARY KEY (event_id, team_id),
+ CONSTRAINT fk_team_events_event FOREIGN KEY (event_id) REFERENCES event.events (id)
+);
diff --git a/services/spring-event/src/main/resources/db/migration/V2__add_foreign_keys.sql b/services/spring-event/src/main/resources/db/migration/V2__add_foreign_keys.sql
new file mode 100644
index 0000000..1325e3e
--- /dev/null
+++ b/services/spring-event/src/main/resources/db/migration/V2__add_foreign_keys.sql
@@ -0,0 +1,15 @@
+-- creator_id and attendances.member_id reference member.members(id).
+-- sport_events.sport_name references organization.sports(name).
+-- team_events.team_id references organization.teams(id).
+-- All added after member and organization services have bootstrapped.
+ALTER TABLE event.events
+ ADD CONSTRAINT fk_events_creator FOREIGN KEY (creator_id) REFERENCES member.members (id);
+
+ALTER TABLE event.attendances
+ ADD CONSTRAINT fk_attendances_member FOREIGN KEY (member_id) REFERENCES member.members (id);
+
+ALTER TABLE event.sport_events
+ ADD CONSTRAINT fk_sport_events_sport FOREIGN KEY (sport_name) REFERENCES organization.sports (name);
+
+ALTER TABLE event.team_events
+ ADD CONSTRAINT fk_team_events_team FOREIGN KEY (team_id) REFERENCES organization.teams (id);
diff --git a/services/spring-feedback/build.gradle b/services/spring-feedback/build.gradle
index 2737002..293992f 100644
--- a/services/spring-feedback/build.gradle
+++ b/services/spring-feedback/build.gradle
@@ -42,7 +42,11 @@ repositories {
}
dependencies {
+ compileOnly 'org.projectlombok:lombok'
+ annotationProcessor 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.flywaydb:flyway-core'
+ implementation 'org.flywaydb:flyway-database-postgresql'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
diff --git a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/entity/Feedback.java b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/entity/Feedback.java
new file mode 100644
index 0000000..17d3022
--- /dev/null
+++ b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/entity/Feedback.java
@@ -0,0 +1,45 @@
+package tum.devoops.feedbackservice.entity;
+
+import java.time.Instant;
+import java.util.UUID;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Table(schema = "feedback", name = "feedback")
+@Getter @Setter @NoArgsConstructor
+public class Feedback {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.UUID)
+ @Column(name = "id", nullable = false, updatable = false)
+ private UUID id;
+
+ // FK to event.event(id) added in V3 migration.
+ @Column(name = "event_id", nullable = false)
+ private UUID eventId;
+
+ // UUID of the member this feedback is about.
+ // FK to member.member(id) added in V3 migration.
+ @Column(name = "member_id", nullable = false)
+ private UUID memberId;
+
+ // UUID of the member who wrote this feedback.
+ // FK to member.member(id) added in V3 migration.
+ @Column(name = "creator_id", nullable = false)
+ private UUID creatorId;
+
+ @Column(name = "created_at", nullable = false)
+ private Instant createdAt;
+
+ @Column(name = "feedback", nullable = false, columnDefinition = "TEXT")
+ private String feedback;
+}
diff --git a/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/repository/FeedbackRepository.java b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/repository/FeedbackRepository.java
new file mode 100644
index 0000000..3d34851
--- /dev/null
+++ b/services/spring-feedback/src/main/java/tum/devoops/feedbackservice/repository/FeedbackRepository.java
@@ -0,0 +1,20 @@
+package tum.devoops.feedbackservice.repository;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import tum.devoops.feedbackservice.entity.Feedback;
+
+public interface FeedbackRepository extends JpaRepository {
+
+ // SELECT * FROM feedback.feedback WHERE event_id = ?
+ List findAllByEventId(UUID eventId);
+
+ // SELECT * FROM feedback.feedback WHERE member_id = ?
+ List findAllByMemberId(UUID memberId);
+
+ // SELECT * FROM feedback.feedback WHERE creator_id = ?
+ List findAllByCreatorId(UUID creatorId);
+}
diff --git a/services/spring-feedback/src/main/resources/application.properties b/services/spring-feedback/src/main/resources/application.properties
index d0381b2..3017a6e 100644
--- a/services/spring-feedback/src/main/resources/application.properties
+++ b/services/spring-feedback/src/main/resources/application.properties
@@ -2,3 +2,10 @@ spring.application.name=feedback-service
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8081/auth/realms/devops
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://keycloak:8080/auth/realms/devops/protocol/openid-connect/certs
+
+spring.jpa.hibernate.ddl-auto=validate
+spring.jpa.properties.hibernate.default_schema=feedback
+
+spring.flyway.default-schema=feedback
+spring.flyway.schemas=feedback
+spring.flyway.create-schemas=true
diff --git a/services/spring-feedback/src/main/resources/db/migration/V1__create_tables.sql b/services/spring-feedback/src/main/resources/db/migration/V1__create_tables.sql
new file mode 100644
index 0000000..ef8f430
--- /dev/null
+++ b/services/spring-feedback/src/main/resources/db/migration/V1__create_tables.sql
@@ -0,0 +1,9 @@
+CREATE TABLE feedback.feedback (
+ id UUID NOT NULL DEFAULT gen_random_uuid(),
+ event_id UUID NOT NULL,
+ member_id UUID NOT NULL,
+ creator_id UUID NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL,
+ feedback TEXT NOT NULL,
+ CONSTRAINT pk_feedback PRIMARY KEY (id)
+);
diff --git a/services/spring-feedback/src/main/resources/db/migration/V2__add_foreign_keys.sql b/services/spring-feedback/src/main/resources/db/migration/V2__add_foreign_keys.sql
new file mode 100644
index 0000000..755b44b
--- /dev/null
+++ b/services/spring-feedback/src/main/resources/db/migration/V2__add_foreign_keys.sql
@@ -0,0 +1,11 @@
+-- event_id references event.events(id).
+-- member_id and creator_id reference member.members(id).
+-- Added after event and member services have bootstrapped.
+ALTER TABLE feedback.feedback
+ ADD CONSTRAINT fk_feedback_event FOREIGN KEY (event_id) REFERENCES event.events (id);
+
+ALTER TABLE feedback.feedback
+ ADD CONSTRAINT fk_feedback_member FOREIGN KEY (member_id) REFERENCES member.members (id);
+
+ALTER TABLE feedback.feedback
+ ADD CONSTRAINT fk_feedback_creator FOREIGN KEY (creator_id) REFERENCES member.members (id);
diff --git a/services/spring-finance/build.gradle b/services/spring-finance/build.gradle
index 828b58a..4e959c6 100644
--- a/services/spring-finance/build.gradle
+++ b/services/spring-finance/build.gradle
@@ -42,7 +42,11 @@ repositories {
}
dependencies {
+ compileOnly 'org.projectlombok:lombok'
+ annotationProcessor 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.flywaydb:flyway-core'
+ implementation 'org.flywaydb:flyway-database-postgresql'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
diff --git a/services/spring-finance/src/main/java/tum/devoops/financeservice/entity/Transaction.java b/services/spring-finance/src/main/java/tum/devoops/financeservice/entity/Transaction.java
new file mode 100644
index 0000000..5e96766
--- /dev/null
+++ b/services/spring-finance/src/main/java/tum/devoops/financeservice/entity/Transaction.java
@@ -0,0 +1,47 @@
+package tum.devoops.financeservice.entity;
+
+import java.time.Instant;
+import java.util.UUID;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Table(schema = "finance", name = "transactions")
+@Getter @Setter @NoArgsConstructor
+public class Transaction {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.UUID)
+ @Column(name = "id", nullable = false, updatable = false)
+ private UUID id;
+
+ // FK to member.member(id) added in V3 migration.
+ @Column(name = "member_id", nullable = false)
+ private UUID memberId;
+
+ // UUID of the member who created this transaction.
+ // FK to member.member(id) added in V3 migration.
+ @Column(name = "creator_id", nullable = false)
+ private UUID creatorId;
+
+ // Amount in cents (e.g. 1000 = €10.00). Positive = credit, negative = debit.
+ @Column(name = "amount_cents", nullable = false)
+ private int amountCents;
+
+ @Column(name = "created_at", nullable = false)
+ private Instant createdAt;
+
+ @Column(name = "title", nullable = false)
+ private String title;
+
+ @Column(name = "description", nullable = false, columnDefinition = "TEXT")
+ private String description;
+}
diff --git a/services/spring-finance/src/main/java/tum/devoops/financeservice/repository/TransactionRepository.java b/services/spring-finance/src/main/java/tum/devoops/financeservice/repository/TransactionRepository.java
new file mode 100644
index 0000000..4610576
--- /dev/null
+++ b/services/spring-finance/src/main/java/tum/devoops/financeservice/repository/TransactionRepository.java
@@ -0,0 +1,17 @@
+package tum.devoops.financeservice.repository;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import tum.devoops.financeservice.entity.Transaction;
+
+public interface TransactionRepository extends JpaRepository {
+
+ // SELECT * FROM finance.transactions WHERE member_id = ?
+ List findAllByMemberId(UUID memberId);
+
+ // SELECT * FROM finance.transactions WHERE creator_id = ?
+ List findAllByCreatorId(UUID creatorId);
+}
diff --git a/services/spring-finance/src/main/resources/application.properties b/services/spring-finance/src/main/resources/application.properties
index e8af8a3..dcdce07 100644
--- a/services/spring-finance/src/main/resources/application.properties
+++ b/services/spring-finance/src/main/resources/application.properties
@@ -2,3 +2,10 @@ spring.application.name=finance-service
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8081/auth/realms/devops
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://keycloak:8080/auth/realms/devops/protocol/openid-connect/certs
+
+spring.jpa.hibernate.ddl-auto=validate
+spring.jpa.properties.hibernate.default_schema=finance
+
+spring.flyway.default-schema=finance
+spring.flyway.schemas=finance
+spring.flyway.create-schemas=true
diff --git a/services/spring-finance/src/main/resources/db/migration/V1__create_tables.sql b/services/spring-finance/src/main/resources/db/migration/V1__create_tables.sql
new file mode 100644
index 0000000..0d98100
--- /dev/null
+++ b/services/spring-finance/src/main/resources/db/migration/V1__create_tables.sql
@@ -0,0 +1,10 @@
+CREATE TABLE finance.transactions (
+ id UUID NOT NULL DEFAULT gen_random_uuid(),
+ member_id UUID NOT NULL,
+ creator_id UUID NOT NULL,
+ amount_cents INTEGER NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL,
+ title VARCHAR(255) NOT NULL,
+ description TEXT NOT NULL,
+ CONSTRAINT pk_transactions PRIMARY KEY (id)
+);
diff --git a/services/spring-finance/src/main/resources/db/migration/V2__add_foreign_keys.sql b/services/spring-finance/src/main/resources/db/migration/V2__add_foreign_keys.sql
new file mode 100644
index 0000000..d3bfef1
--- /dev/null
+++ b/services/spring-finance/src/main/resources/db/migration/V2__add_foreign_keys.sql
@@ -0,0 +1,7 @@
+-- member_id and creator_id reference member.members(id).
+-- Added after member service has bootstrapped.
+ALTER TABLE finance.transactions
+ ADD CONSTRAINT fk_transactions_member FOREIGN KEY (member_id) REFERENCES member.members (id);
+
+ALTER TABLE finance.transactions
+ ADD CONSTRAINT fk_transactions_creator FOREIGN KEY (creator_id) REFERENCES member.members (id);
diff --git a/services/spring-letter/build.gradle b/services/spring-letter/build.gradle
index b057c66..5726a04 100644
--- a/services/spring-letter/build.gradle
+++ b/services/spring-letter/build.gradle
@@ -42,6 +42,8 @@ repositories {
}
dependencies {
+ compileOnly 'org.projectlombok:lombok'
+ annotationProcessor 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
diff --git a/services/spring-letter/src/main/resources/application.properties b/services/spring-letter/src/main/resources/application.properties
index d550a7f..a374c20 100644
--- a/services/spring-letter/src/main/resources/application.properties
+++ b/services/spring-letter/src/main/resources/application.properties
@@ -2,3 +2,6 @@ spring.application.name=letter-service
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8081/auth/realms/devops
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://keycloak:8080/auth/realms/devops/protocol/openid-connect/certs
+
+spring.flyway.enabled=false
+spring.jpa.hibernate.ddl-auto=none
diff --git a/services/spring-member/build.gradle b/services/spring-member/build.gradle
index b5d0952..70965ad 100644
--- a/services/spring-member/build.gradle
+++ b/services/spring-member/build.gradle
@@ -42,7 +42,11 @@ repositories {
}
dependencies {
+ compileOnly 'org.projectlombok:lombok'
+ annotationProcessor 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.flywaydb:flyway-core'
+ implementation 'org.flywaydb:flyway-database-postgresql'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
diff --git a/services/spring-member/gradle.properties b/services/spring-member/gradle.properties
new file mode 100644
index 0000000..e69de29
diff --git a/services/spring-member/src/generated/java/tum/devoops/memberservice/model/MemberCreate.java b/services/spring-member/src/generated/java/tum/devoops/memberservice/model/MemberCreate.java
index 3504d27..c4c2cac 100644
--- a/services/spring-member/src/generated/java/tum/devoops/memberservice/model/MemberCreate.java
+++ b/services/spring-member/src/generated/java/tum/devoops/memberservice/model/MemberCreate.java
@@ -28,7 +28,7 @@ public class MemberCreate {
private String lastName;
- private @Nullable String email;
+ private String email;
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
private @Nullable LocalDate birthday;
@@ -46,9 +46,10 @@ public MemberCreate() {
/**
* Constructor with only required parameters
*/
- public MemberCreate(String firstName, String lastName) {
+ public MemberCreate(String firstName, String lastName, String email) {
this.firstName = firstName;
this.lastName = lastName;
+ this.email = email;
}
public MemberCreate firstName(String firstName) {
@@ -91,7 +92,7 @@ public void setLastName(String lastName) {
this.lastName = lastName;
}
- public MemberCreate email(@Nullable String email) {
+ public MemberCreate email(String email) {
this.email = email;
return this;
}
@@ -100,14 +101,14 @@ public MemberCreate email(@Nullable String email) {
* Get email
* @return email
*/
-
- @Schema(name = "email", requiredMode = Schema.RequiredMode.NOT_REQUIRED)
+ @NotNull
+ @Schema(name = "email", requiredMode = Schema.RequiredMode.REQUIRED)
@JsonProperty("email")
- public @Nullable String getEmail() {
+ public String getEmail() {
return email;
}
- public void setEmail(@Nullable String email) {
+ public void setEmail(String email) {
this.email = email;
}
diff --git a/services/spring-member/src/main/java/tum/devoops/memberservice/entity/Member.java b/services/spring-member/src/main/java/tum/devoops/memberservice/entity/Member.java
new file mode 100644
index 0000000..0b801c7
--- /dev/null
+++ b/services/spring-member/src/main/java/tum/devoops/memberservice/entity/Member.java
@@ -0,0 +1,49 @@
+package tum.devoops.memberservice.entity;
+
+import java.time.LocalDate;
+import java.util.UUID;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Table(schema = "member", name = "members")
+@Getter @Setter @NoArgsConstructor
+public class Member {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.UUID)
+ @Column(name = "id", nullable = false, updatable = false)
+ private UUID id;
+
+ @Column(name = "first_name", nullable = false)
+ private String firstName;
+
+ @Column(name = "last_name", nullable = false)
+ private String lastName;
+
+ @Column(name = "email", nullable = false, unique = true)
+ private String email;
+
+ @Column(name = "birthday", nullable = true)
+ private LocalDate birthday;
+
+ @Column(name = "phone_number", nullable = true)
+ private String phoneNumber;
+
+ @Column(name = "address", nullable = true)
+ private String address;
+
+ @Column(name = "joining_date", nullable = false)
+ private LocalDate joiningDate;
+
+ @Column(name = "information", nullable = true, columnDefinition = "TEXT")
+ private String information;
+}
diff --git a/services/spring-member/src/main/java/tum/devoops/memberservice/repository/MemberRepository.java b/services/spring-member/src/main/java/tum/devoops/memberservice/repository/MemberRepository.java
new file mode 100644
index 0000000..04d32f6
--- /dev/null
+++ b/services/spring-member/src/main/java/tum/devoops/memberservice/repository/MemberRepository.java
@@ -0,0 +1,14 @@
+package tum.devoops.memberservice.repository;
+
+import java.util.Optional;
+import java.util.UUID;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import tum.devoops.memberservice.entity.Member;
+
+public interface MemberRepository extends JpaRepository {
+
+ // SELECT * FROM member.members WHERE email = ? LIMIT 1
+ Optional findByEmail(String email);
+}
diff --git a/services/spring-member/src/main/resources/application.properties b/services/spring-member/src/main/resources/application.properties
index acd890e..68f33fc 100644
--- a/services/spring-member/src/main/resources/application.properties
+++ b/services/spring-member/src/main/resources/application.properties
@@ -2,3 +2,10 @@ spring.application.name=member-service
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8081/auth/realms/devops
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://keycloak:8080/auth/realms/devops/protocol/openid-connect/certs
+
+spring.jpa.hibernate.ddl-auto=validate
+spring.jpa.properties.hibernate.default_schema=member
+
+spring.flyway.default-schema=member
+spring.flyway.schemas=member
+spring.flyway.create-schemas=true
diff --git a/services/spring-member/src/main/resources/db/migration/V1__create_tables.sql b/services/spring-member/src/main/resources/db/migration/V1__create_tables.sql
new file mode 100644
index 0000000..0ba3174
--- /dev/null
+++ b/services/spring-member/src/main/resources/db/migration/V1__create_tables.sql
@@ -0,0 +1,13 @@
+CREATE TABLE member.members (
+ id UUID NOT NULL DEFAULT gen_random_uuid(),
+ first_name VARCHAR(255) NOT NULL,
+ last_name VARCHAR(255) NOT NULL,
+ email VARCHAR(255) NOT NULL,
+ birthday DATE,
+ phone_number VARCHAR(255),
+ address VARCHAR(255),
+ joining_date DATE NOT NULL,
+ information TEXT,
+ CONSTRAINT pk_members PRIMARY KEY (id),
+ CONSTRAINT uq_members_email UNIQUE (email)
+);
diff --git a/services/spring-organization/build.gradle b/services/spring-organization/build.gradle
index cd032ae..b5aeca9 100644
--- a/services/spring-organization/build.gradle
+++ b/services/spring-organization/build.gradle
@@ -42,7 +42,11 @@ repositories {
}
dependencies {
+ compileOnly 'org.projectlombok:lombok'
+ annotationProcessor 'org.projectlombok:lombok'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.flywaydb:flyway-core'
+ implementation 'org.flywaydb:flyway-database-postgresql'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
diff --git a/services/spring-organization/config/checkstyle/checkstyle.xml b/services/spring-organization/config/checkstyle/checkstyle.xml
index 580dda9..7e1b5e0 100644
--- a/services/spring-organization/config/checkstyle/checkstyle.xml
+++ b/services/spring-organization/config/checkstyle/checkstyle.xml
@@ -15,6 +15,13 @@
+
+
+
+
+
+
diff --git a/services/spring-organization/src/main/java/tum/devoops/organizationservice/entity/Director.java b/services/spring-organization/src/main/java/tum/devoops/organizationservice/entity/Director.java
new file mode 100644
index 0000000..f0a6844
--- /dev/null
+++ b/services/spring-organization/src/main/java/tum/devoops/organizationservice/entity/Director.java
@@ -0,0 +1,37 @@
+package tum.devoops.organizationservice.entity;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Embeddable;
+import jakarta.persistence.EmbeddedId;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Table(schema = "organization", name = "directors")
+@Getter @Setter @NoArgsConstructor @AllArgsConstructor
+public class Director {
+
+ // Composite PK: (sport_name, member_id).
+ // sport_name references organization.sport(name).
+ // member_id references member.member(id) — FK added in V3 migration.
+ @EmbeddedId
+ private Id id;
+
+ @Embeddable
+ @Data @NoArgsConstructor @AllArgsConstructor
+ public static class Id implements Serializable {
+ @Column(name = "sport_name", nullable = false)
+ private String sportName;
+
+ @Column(name = "member_id", nullable = false)
+ private UUID memberId;
+ }
+}
diff --git a/services/spring-organization/src/main/java/tum/devoops/organizationservice/entity/Sport.java b/services/spring-organization/src/main/java/tum/devoops/organizationservice/entity/Sport.java
new file mode 100644
index 0000000..b38f5cf
--- /dev/null
+++ b/services/spring-organization/src/main/java/tum/devoops/organizationservice/entity/Sport.java
@@ -0,0 +1,35 @@
+package tum.devoops.organizationservice.entity;
+
+import java.time.LocalDate;
+import java.util.List;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.Table;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Table(schema = "organization", name = "sports")
+@Getter @Setter @NoArgsConstructor
+public class Sport {
+
+ @Id
+ @Column(name = "name", nullable = false)
+ private String name;
+
+ @Column(name = "description", columnDefinition = "TEXT")
+ private String description;
+
+ @Column(name = "created_at", nullable = false)
+ private LocalDate createdAt;
+
+ // Each Director row links this sport to a member (director role).
+ @OneToMany
+ @JoinColumn(name = "sport_name", referencedColumnName = "name", insertable = false, updatable = false)
+ private List directors;
+}
diff --git a/services/spring-organization/src/main/java/tum/devoops/organizationservice/entity/Team.java b/services/spring-organization/src/main/java/tum/devoops/organizationservice/entity/Team.java
new file mode 100644
index 0000000..8ca78ad
--- /dev/null
+++ b/services/spring-organization/src/main/java/tum/devoops/organizationservice/entity/Team.java
@@ -0,0 +1,52 @@
+package tum.devoops.organizationservice.entity;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.UUID;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.Table;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Table(schema = "organization", name = "teams")
+@Getter @Setter @NoArgsConstructor
+public class Team {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.UUID)
+ @Column(name = "id", nullable = false, updatable = false)
+ private UUID id;
+
+ @Column(name = "name", nullable = false)
+ private String name;
+
+ @Column(name = "description", nullable = true, columnDefinition = "TEXT")
+ private String description;
+
+ @Column(name = "created_at", nullable = false)
+ private LocalDate createdAt;
+
+ @Column(name = "address")
+ private String address;
+
+ // FK to organization.sport(name). REFERENCES constraint added in V3 migration.
+ @Column(name = "sport_name", nullable = false)
+ private String sportName;
+
+ @OneToMany
+ @JoinColumn(name = "team_id", referencedColumnName = "id", insertable = false, updatable = false)
+ private List trainers;
+
+ @OneToMany
+ @JoinColumn(name = "team_id", referencedColumnName = "id", insertable = false, updatable = false)
+ private List trainees;
+}
diff --git a/services/spring-organization/src/main/java/tum/devoops/organizationservice/entity/Trainee.java b/services/spring-organization/src/main/java/tum/devoops/organizationservice/entity/Trainee.java
new file mode 100644
index 0000000..634e6dc
--- /dev/null
+++ b/services/spring-organization/src/main/java/tum/devoops/organizationservice/entity/Trainee.java
@@ -0,0 +1,37 @@
+package tum.devoops.organizationservice.entity;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Embeddable;
+import jakarta.persistence.EmbeddedId;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Table(schema = "organization", name = "trainees")
+@Getter @Setter @NoArgsConstructor @AllArgsConstructor
+public class Trainee {
+
+ // Composite PK: (team_id, member_id).
+ // team_id references organization.team(id).
+ // member_id references member.member(id) — FK added in V3 migration.
+ @EmbeddedId
+ private Id id;
+
+ @Embeddable
+ @Data @NoArgsConstructor @AllArgsConstructor
+ public static class Id implements Serializable {
+ @Column(name = "team_id", nullable = false)
+ private UUID teamId;
+
+ @Column(name = "member_id", nullable = false)
+ private UUID memberId;
+ }
+}
diff --git a/services/spring-organization/src/main/java/tum/devoops/organizationservice/entity/Trainer.java b/services/spring-organization/src/main/java/tum/devoops/organizationservice/entity/Trainer.java
new file mode 100644
index 0000000..ce5d052
--- /dev/null
+++ b/services/spring-organization/src/main/java/tum/devoops/organizationservice/entity/Trainer.java
@@ -0,0 +1,37 @@
+package tum.devoops.organizationservice.entity;
+
+import java.io.Serializable;
+import java.util.UUID;
+
+import jakarta.persistence.Column;
+import jakarta.persistence.Embeddable;
+import jakarta.persistence.EmbeddedId;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Table;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+@Entity
+@Table(schema = "organization", name = "trainers")
+@Getter @Setter @NoArgsConstructor @AllArgsConstructor
+public class Trainer {
+
+ // Composite PK: (team_id, member_id).
+ // team_id references organization.team(id).
+ // member_id references member.member(id) — FK added in V3 migration.
+ @EmbeddedId
+ private Id id;
+
+ @Embeddable
+ @Data @NoArgsConstructor @AllArgsConstructor
+ public static class Id implements Serializable {
+ @Column(name = "team_id", nullable = false)
+ private UUID teamId;
+
+ @Column(name = "member_id", nullable = false)
+ private UUID memberId;
+ }
+}
diff --git a/services/spring-organization/src/main/java/tum/devoops/organizationservice/repository/DirectorRepository.java b/services/spring-organization/src/main/java/tum/devoops/organizationservice/repository/DirectorRepository.java
new file mode 100644
index 0000000..cc1a1ec
--- /dev/null
+++ b/services/spring-organization/src/main/java/tum/devoops/organizationservice/repository/DirectorRepository.java
@@ -0,0 +1,20 @@
+package tum.devoops.organizationservice.repository;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import tum.devoops.organizationservice.entity.Director;
+
+public interface DirectorRepository extends JpaRepository {
+
+ // SELECT * FROM organization.directors WHERE sport_name = ?
+ List findAllById_SportName(String sportName);
+
+ // SELECT * FROM organization.directors WHERE member_id = ?
+ List findAllById_MemberId(UUID memberId);
+
+ // DELETE FROM organization.directors WHERE sport_name = ?
+ void deleteAllById_SportName(String sportName);
+}
diff --git a/services/spring-organization/src/main/java/tum/devoops/organizationservice/repository/SportRepository.java b/services/spring-organization/src/main/java/tum/devoops/organizationservice/repository/SportRepository.java
new file mode 100644
index 0000000..22809cd
--- /dev/null
+++ b/services/spring-organization/src/main/java/tum/devoops/organizationservice/repository/SportRepository.java
@@ -0,0 +1,15 @@
+package tum.devoops.organizationservice.repository;
+
+import java.util.List;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import tum.devoops.organizationservice.entity.Sport;
+
+public interface SportRepository extends JpaRepository {
+
+ // SELECT s.* FROM organization.sports s
+ // JOIN organization.directors d ON d.sport_name = s.name
+ // WHERE d.member_id = ?
+ List findAllByDirectors_Id_MemberId(java.util.UUID memberId);
+}
diff --git a/services/spring-organization/src/main/java/tum/devoops/organizationservice/repository/TeamRepository.java b/services/spring-organization/src/main/java/tum/devoops/organizationservice/repository/TeamRepository.java
new file mode 100644
index 0000000..181b5cc
--- /dev/null
+++ b/services/spring-organization/src/main/java/tum/devoops/organizationservice/repository/TeamRepository.java
@@ -0,0 +1,14 @@
+package tum.devoops.organizationservice.repository;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import tum.devoops.organizationservice.entity.Team;
+
+public interface TeamRepository extends JpaRepository {
+
+ // SELECT * FROM organization.teams WHERE sport_name = ?
+ List findAllBySportName(String sportName);
+}
diff --git a/services/spring-organization/src/main/java/tum/devoops/organizationservice/repository/TraineeRepository.java b/services/spring-organization/src/main/java/tum/devoops/organizationservice/repository/TraineeRepository.java
new file mode 100644
index 0000000..6092226
--- /dev/null
+++ b/services/spring-organization/src/main/java/tum/devoops/organizationservice/repository/TraineeRepository.java
@@ -0,0 +1,20 @@
+package tum.devoops.organizationservice.repository;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import tum.devoops.organizationservice.entity.Trainee;
+
+public interface TraineeRepository extends JpaRepository {
+
+ // SELECT * FROM organization.trainees WHERE team_id = ?
+ List findAllById_TeamId(UUID teamId);
+
+ // SELECT * FROM organization.trainees WHERE member_id = ?
+ List findAllById_MemberId(UUID memberId);
+
+ // DELETE FROM organization.trainees WHERE team_id = ?
+ void deleteAllById_TeamId(UUID teamId);
+}
diff --git a/services/spring-organization/src/main/java/tum/devoops/organizationservice/repository/TrainerRepository.java b/services/spring-organization/src/main/java/tum/devoops/organizationservice/repository/TrainerRepository.java
new file mode 100644
index 0000000..47424b6
--- /dev/null
+++ b/services/spring-organization/src/main/java/tum/devoops/organizationservice/repository/TrainerRepository.java
@@ -0,0 +1,20 @@
+package tum.devoops.organizationservice.repository;
+
+import java.util.List;
+import java.util.UUID;
+
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import tum.devoops.organizationservice.entity.Trainer;
+
+public interface TrainerRepository extends JpaRepository {
+
+ // SELECT * FROM organization.trainers WHERE team_id = ?
+ List findAllById_TeamId(UUID teamId);
+
+ // SELECT * FROM organization.trainers WHERE member_id = ?
+ List findAllById_MemberId(UUID memberId);
+
+ // DELETE FROM organization.trainers WHERE team_id = ?
+ void deleteAllById_TeamId(UUID teamId);
+}
diff --git a/services/spring-organization/src/main/resources/application.properties b/services/spring-organization/src/main/resources/application.properties
index 8f1688e..0f5a352 100644
--- a/services/spring-organization/src/main/resources/application.properties
+++ b/services/spring-organization/src/main/resources/application.properties
@@ -2,3 +2,10 @@ spring.application.name=organization-service
spring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8081/auth/realms/devops
spring.security.oauth2.resourceserver.jwt.jwk-set-uri=http://keycloak:8080/auth/realms/devops/protocol/openid-connect/certs
+
+spring.jpa.hibernate.ddl-auto=validate
+spring.jpa.properties.hibernate.default_schema=organization
+
+spring.flyway.default-schema=organization
+spring.flyway.schemas=organization
+spring.flyway.create-schemas=true
diff --git a/services/spring-organization/src/main/resources/db/migration/V1__create_tables.sql b/services/spring-organization/src/main/resources/db/migration/V1__create_tables.sql
new file mode 100644
index 0000000..034b7e2
--- /dev/null
+++ b/services/spring-organization/src/main/resources/db/migration/V1__create_tables.sql
@@ -0,0 +1,38 @@
+CREATE TABLE organization.sports (
+ name VARCHAR(255) NOT NULL,
+ description TEXT,
+ created_at DATE NOT NULL,
+ CONSTRAINT pk_sports PRIMARY KEY (name)
+);
+
+CREATE TABLE organization.teams (
+ id UUID NOT NULL DEFAULT gen_random_uuid(),
+ name VARCHAR(255) NOT NULL,
+ description TEXT,
+ created_at DATE NOT NULL,
+ address VARCHAR(255),
+ sport_name VARCHAR(255) NOT NULL,
+ CONSTRAINT pk_teams PRIMARY KEY (id),
+ CONSTRAINT fk_teams_sport FOREIGN KEY (sport_name) REFERENCES organization.sports (name)
+);
+
+CREATE TABLE organization.directors (
+ sport_name VARCHAR(255) NOT NULL,
+ member_id UUID NOT NULL,
+ CONSTRAINT pk_directors PRIMARY KEY (sport_name, member_id),
+ CONSTRAINT fk_directors_sport FOREIGN KEY (sport_name) REFERENCES organization.sports (name)
+);
+
+CREATE TABLE organization.trainers (
+ team_id UUID NOT NULL,
+ member_id UUID NOT NULL,
+ CONSTRAINT pk_trainers PRIMARY KEY (team_id, member_id),
+ CONSTRAINT fk_trainers_team FOREIGN KEY (team_id) REFERENCES organization.teams (id)
+);
+
+CREATE TABLE organization.trainees (
+ team_id UUID NOT NULL,
+ member_id UUID NOT NULL,
+ CONSTRAINT pk_trainees PRIMARY KEY (team_id, member_id),
+ CONSTRAINT fk_trainees_team FOREIGN KEY (team_id) REFERENCES organization.teams (id)
+);
diff --git a/services/spring-organization/src/main/resources/db/migration/V2__add_foreign_keys.sql b/services/spring-organization/src/main/resources/db/migration/V2__add_foreign_keys.sql
new file mode 100644
index 0000000..2847b6e
--- /dev/null
+++ b/services/spring-organization/src/main/resources/db/migration/V2__add_foreign_keys.sql
@@ -0,0 +1,9 @@
+-- member_id columns reference member.members(id), added after member service bootstraps.
+ALTER TABLE organization.directors
+ ADD CONSTRAINT fk_directors_member FOREIGN KEY (member_id) REFERENCES member.members (id);
+
+ALTER TABLE organization.trainers
+ ADD CONSTRAINT fk_trainers_member FOREIGN KEY (member_id) REFERENCES member.members (id);
+
+ALTER TABLE organization.trainees
+ ADD CONSTRAINT fk_trainees_member FOREIGN KEY (member_id) REFERENCES member.members (id);
diff --git a/web-client/src/api.ts b/web-client/src/api.ts
index 878ff91..b4a6582 100644
--- a/web-client/src/api.ts
+++ b/web-client/src/api.ts
@@ -417,7 +417,7 @@ export interface components {
MemberCreate: {
first_name: string;
last_name: string;
- email?: string;
+ email: string;
/** Format: date */
birthday?: string;
phone_number?: string;