From 93cd985f43960ddf88b56a84fbc3b5c91950f3b7 Mon Sep 17 00:00:00 2001 From: Maksymilian Krywionek Date: Sat, 16 May 2026 16:26:46 +0200 Subject: [PATCH 1/7] (SKY-55) Extract mapAircraftResponse helper in aircraft store --- skyroster-frontend/src/stores/aircraft.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/skyroster-frontend/src/stores/aircraft.js b/skyroster-frontend/src/stores/aircraft.js index 9938c35..d38a0d3 100644 --- a/skyroster-frontend/src/stores/aircraft.js +++ b/skyroster-frontend/src/stores/aircraft.js @@ -14,6 +14,15 @@ async function getAircraftList() { return res.data; } +function mapAircraftResponse(a) { + return { + id: a.id, + rejestracja: a.registrationNumber, + typ: a.aircraftType.icaoCode, + baza: a.operationalBase.icaoCode + }; +} + export const useAircraftStore = defineStore('aircraft', () => { const aircraft = ref([]); const aircraftTypeOptions = AIRCRAFT_TYPES @@ -30,12 +39,7 @@ export const useAircraftStore = defineStore('aircraft', () => { try { const response = await getAircraftList(); - aircraft.value = response.map(aircraft => ({ - id: aircraft.id, - rejestracja: aircraft.registrationNumber, - typ: aircraft.aircraftType.icaoCode, - baza: aircraft.operationalBase.icaoCode - })); + aircraft.value = response.map(mapAircraftResponse); } catch (error) { console.error(error); } From 7209f68ac6e53b40227dd9953fb817f0380c31b3 Mon Sep 17 00:00:00 2001 From: Maksymilian Krywionek Date: Sat, 16 May 2026 16:30:55 +0200 Subject: [PATCH 2/7] (SKY-55) Wire addAircraft to POST /aircraft with error handling Co-Authored-By: Claude Sonnet 4.6 --- skyroster-frontend/src/stores/aircraft.js | 52 +++++++++++++++++------ 1 file changed, 38 insertions(+), 14 deletions(-) diff --git a/skyroster-frontend/src/stores/aircraft.js b/skyroster-frontend/src/stores/aircraft.js index d38a0d3..8e2219f 100644 --- a/skyroster-frontend/src/stores/aircraft.js +++ b/skyroster-frontend/src/stores/aircraft.js @@ -23,6 +23,28 @@ function mapAircraftResponse(a) { }; } +function translateAircraftSaveError(error, registrationNumber) { + const status = error.response?.status; + const backendMessage = error.response?.data?.message ?? ''; + + if (status === 409) { + return `Samolot o rejestracji "${registrationNumber}" już istnieje`; + } + if (status === 404) { + return 'Nieprawidłowa baza operacyjna'; + } + if (status === 400) { + if (backendMessage.startsWith('Aircraft type')) { + return 'Nieprawidłowy typ samolotu'; + } + if (backendMessage.startsWith('Registration number')) { + return 'Rejestracja nie może być pusta'; + } + return 'Nieprawidłowe dane samolotu'; + } + return 'Nie udało się zapisać samolotu'; +} + export const useAircraftStore = defineStore('aircraft', () => { const aircraft = ref([]); const aircraftTypeOptions = AIRCRAFT_TYPES @@ -45,21 +67,23 @@ export const useAircraftStore = defineStore('aircraft', () => { } } - function generateId() { - const maxId = aircraft.value.reduce((max, a) => { - const num = parseInt(a.id.replace('A', '')) - return num > max ? num : max - }, 0) - return `A${String(maxId + 1).padStart(3, '0')}` - } - - function addAircraft(aircraftData) { - const newAircraft = { - ...aircraftData, - id: generateId() + async function addAircraft(aircraftData) { + try { + const { data } = await apiClient.post('/aircraft', { + registrationNumber: aircraftData.rejestracja, + aircraftTypeCode: aircraftData.typ, + operationalBaseCode: aircraftData.baza + }); + const mapped = mapAircraftResponse(data); + aircraft.value.push(mapped); + return { success: true, aircraft: mapped }; + } catch (error) { + console.error('addAircraft failed', error); + return { + success: false, + message: translateAircraftSaveError(error, aircraftData.rejestracja) + }; } - aircraft.value.push(newAircraft) - return newAircraft } function updateAircraft(id, aircraftData) { From 48f1f140e6e28c0ceb06ab6883edea178288a9da Mon Sep 17 00:00:00 2001 From: Maksymilian Krywionek Date: Sat, 16 May 2026 16:36:09 +0200 Subject: [PATCH 3/7] (SKY-55) Document backend exception coupling in translateAircraftSaveError Co-Authored-By: Claude Opus 4.7 (1M context) --- skyroster-frontend/src/stores/aircraft.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/skyroster-frontend/src/stores/aircraft.js b/skyroster-frontend/src/stores/aircraft.js index 8e2219f..0b2e1b0 100644 --- a/skyroster-frontend/src/stores/aircraft.js +++ b/skyroster-frontend/src/stores/aircraft.js @@ -34,10 +34,13 @@ function translateAircraftSaveError(error, registrationNumber) { return 'Nieprawidłowa baza operacyjna'; } if (status === 400) { + // Prefixes pinned to backend exception messages — if those change server-side, frontend silently falls through to the generic fallback below if (backendMessage.startsWith('Aircraft type')) { + // AircraftTypeNotFoundException: "Aircraft type with code '...' not found" return 'Nieprawidłowy typ samolotu'; } if (backendMessage.startsWith('Registration number')) { + // IllegalArgumentException from Aircraft.validateRegistrationNumber: "Registration number must not be blank" return 'Rejestracja nie może być pusta'; } return 'Nieprawidłowe dane samolotu'; From e0123dedc0c32af032b256a08a366cc60fcec672 Mon Sep 17 00:00:00 2001 From: Maksymilian Krywionek Date: Sat, 16 May 2026 16:38:49 +0200 Subject: [PATCH 4/7] (SKY-55) Wire updateAircraft to PUT /aircraft/{id} with error handling Co-Authored-By: Claude Sonnet 4.6 --- skyroster-frontend/src/stores/aircraft.js | 27 ++++++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/skyroster-frontend/src/stores/aircraft.js b/skyroster-frontend/src/stores/aircraft.js index 0b2e1b0..8960b25 100644 --- a/skyroster-frontend/src/stores/aircraft.js +++ b/skyroster-frontend/src/stores/aircraft.js @@ -89,13 +89,28 @@ export const useAircraftStore = defineStore('aircraft', () => { } } - function updateAircraft(id, aircraftData) { - const index = aircraft.value.findIndex(a => a.id === id) - if (index !== -1) { - aircraft.value[index] = { ...aircraft.value[index], ...aircraftData } - return aircraft.value[index] + async function updateAircraft(id, aircraftData) { + try { + const { data } = await apiClient.put(`/aircraft/${id}`, { + registrationNumber: aircraftData.rejestracja, + aircraftTypeCode: aircraftData.typ, + operationalBaseCode: aircraftData.baza + }); + const mapped = mapAircraftResponse(data); + const index = aircraft.value.findIndex(a => a.id === id); + if (index !== -1) { + aircraft.value[index] = mapped; + } else { + console.warn(`updateAircraft: aircraft with id ${id} not in local store, skipping local update`); + } + return { success: true, aircraft: mapped }; + } catch (error) { + console.error('updateAircraft failed', error); + return { + success: false, + message: translateAircraftSaveError(error, aircraftData.rejestracja) + }; } - return null } async function deleteAircraft(id) { From a4a4cb4b5b5205de417b764ab6f5825c96185fda Mon Sep 17 00:00:00 2001 From: Maksymilian Krywionek Date: Sat, 16 May 2026 16:42:50 +0200 Subject: [PATCH 5/7] =?UTF-8?q?(SKY-55)=20Distinguish=20404=20cases=20?= =?UTF-8?q?=E2=80=94=20aircraft=20deleted=20vs=20invalid=20base?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PUT /aircraft/{id} can return 404 for either an unknown operational base code or a concurrently-deleted aircraft. Branch on backend message so the toast tells the user which one happened. Co-Authored-By: Claude Opus 4.7 (1M context) --- skyroster-frontend/src/stores/aircraft.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/skyroster-frontend/src/stores/aircraft.js b/skyroster-frontend/src/stores/aircraft.js index 8960b25..f87446e 100644 --- a/skyroster-frontend/src/stores/aircraft.js +++ b/skyroster-frontend/src/stores/aircraft.js @@ -31,6 +31,12 @@ function translateAircraftSaveError(error, registrationNumber) { return `Samolot o rejestracji "${registrationNumber}" już istnieje`; } if (status === 404) { + // 404 can mean the operational base code is invalid OR (on PUT) the aircraft id no longer exists + if (backendMessage.startsWith('Aircraft not found')) { + // AircraftNotFoundException — concurrent delete on PUT + return 'Samolot nie istnieje (mógł zostać usunięty)'; + } + // OperationalBaseNotFoundException: "Operational base with code '...' not found" return 'Nieprawidłowa baza operacyjna'; } if (status === 400) { From 3b58f17247f99ba8bf9c2defa6700c6eeb808e8e Mon Sep 17 00:00:00 2001 From: Maksymilian Krywionek Date: Sat, 16 May 2026 16:43:32 +0200 Subject: [PATCH 6/7] (SKY-55) AircraftView: await save and show error toast on failure Co-Authored-By: Claude Sonnet 4.6 --- skyroster-frontend/src/views/AircraftView.vue | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/skyroster-frontend/src/views/AircraftView.vue b/skyroster-frontend/src/views/AircraftView.vue index 5ebd602..738abbb 100644 --- a/skyroster-frontend/src/views/AircraftView.vue +++ b/skyroster-frontend/src/views/AircraftView.vue @@ -55,13 +55,25 @@ function openEditDialog(aircraft) { dialogVisible.value = true } -function handleSave(aircraftData) { - if (isEditMode.value && selectedAircraft.value) { - aircraftStore.updateAircraft(selectedAircraft.value.id, aircraftData) - toast.add({ severity: 'success', summary: 'Sukces', detail: 'Samolot został zaktualizowany', life: 3000 }) +async function handleSave(aircraftData) { + const result = isEditMode.value && selectedAircraft.value + ? await aircraftStore.updateAircraft(selectedAircraft.value.id, aircraftData) + : await aircraftStore.addAircraft(aircraftData) + + if (result.success) { + toast.add({ + severity: 'success', + summary: 'Sukces', + detail: isEditMode.value ? 'Samolot został zaktualizowany' : 'Samolot został dodany', + life: 3000 + }) } else { - aircraftStore.addAircraft(aircraftData) - toast.add({ severity: 'success', summary: 'Sukces', detail: 'Samolot został dodany', life: 3000 }) + toast.add({ + severity: 'error', + summary: 'Błąd', + detail: result.message, + life: 5000 + }) } } From 191389f9175271afa9fe1e5c2db055e8c8b5f571 Mon Sep 17 00:00:00 2001 From: Maksymilian Krywionek Date: Fri, 22 May 2026 23:38:22 +0200 Subject: [PATCH 7/7] chore(infra): run containers as non-root for hardening - Frontend image switches to nginxinc/nginx-unprivileged:alpine; nginx listens on 8080 and docker-compose maps host 5173 to that port. - Backend image adds a system `app` user and runs the jar as that user instead of root. Co-Authored-By: Claude Opus 4.7 (1M context) --- infrastructure/local/docker-compose.yaml | 2 +- skyroster-backend/Dockerfile | 6 +++++- skyroster-frontend/Dockerfile | 4 ++-- skyroster-frontend/nginx.conf | 2 +- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/infrastructure/local/docker-compose.yaml b/infrastructure/local/docker-compose.yaml index 42cc84d..431eae5 100644 --- a/infrastructure/local/docker-compose.yaml +++ b/infrastructure/local/docker-compose.yaml @@ -71,7 +71,7 @@ services: - frontend - full ports: - - "5173:80" + - "5173:8080" volumes: - ../../skyroster-frontend:/app - /app/node_modules diff --git a/skyroster-backend/Dockerfile b/skyroster-backend/Dockerfile index ac4ec3e..13a03ac 100644 --- a/skyroster-backend/Dockerfile +++ b/skyroster-backend/Dockerfile @@ -17,7 +17,11 @@ RUN ./mvnw package -DskipTests -B FROM eclipse-temurin:25-jre WORKDIR /app -COPY --from=build /app/target/*.jar app.jar +RUN groupadd --system app && useradd --system --gid app --home /app app + +COPY --from=build --chown=app:app /app/target/*.jar app.jar + +USER app EXPOSE 8001 diff --git a/skyroster-frontend/Dockerfile b/skyroster-frontend/Dockerfile index 2f8a3a1..cde7252 100644 --- a/skyroster-frontend/Dockerfile +++ b/skyroster-frontend/Dockerfile @@ -15,10 +15,10 @@ ENV VITE_API_URL=$VITE_API_URL RUN npm run build # Stage 2: Runtime -FROM nginx:alpine +FROM nginxinc/nginx-unprivileged:alpine COPY --from=build /app/dist /usr/share/nginx/html COPY nginx.conf /etc/nginx/conf.d/default.conf -EXPOSE 80 +EXPOSE 8080 CMD ["nginx", "-g", "daemon off;"] diff --git a/skyroster-frontend/nginx.conf b/skyroster-frontend/nginx.conf index a6dde32..6a27492 100644 --- a/skyroster-frontend/nginx.conf +++ b/skyroster-frontend/nginx.conf @@ -1,5 +1,5 @@ server { - listen 80; + listen 8080; server_name _; root /usr/share/nginx/html; index index.html;