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;