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; diff --git a/skyroster-frontend/src/stores/aircraft.js b/skyroster-frontend/src/stores/aircraft.js index 9938c35..f87446e 100644 --- a/skyroster-frontend/src/stores/aircraft.js +++ b/skyroster-frontend/src/stores/aircraft.js @@ -14,6 +14,46 @@ async function getAircraftList() { return res.data; } +function mapAircraftResponse(a) { + return { + id: a.id, + rejestracja: a.registrationNumber, + typ: a.aircraftType.icaoCode, + baza: a.operationalBase.icaoCode + }; +} + +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) { + // 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) { + // 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'; + } + return 'Nie udało się zapisać samolotu'; +} + export const useAircraftStore = defineStore('aircraft', () => { const aircraft = ref([]); const aircraftTypeOptions = AIRCRAFT_TYPES @@ -30,41 +70,53 @@ 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); } } - 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) { - 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) { 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 + }) } }