diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5ec25d5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +target/ +src/frontend/node_modules/ +src/frontend/dist/ +.git/ +.gitignore +*.md \ No newline at end of file diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..f4d5c4d --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,51 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - main + - develop + pull_request: + branches: + - main + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: git.avatic.de + username: ${{ gitea.actor }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: git.avatic.de/${{ gitea.repository_owner }}/lcc + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=sha,prefix={{branch}}- + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=registry,ref=git.avatic.de/${{ gitea.repository_owner }}/lcc:buildcache + cache-to: type=registry,ref=git.avatic.de/${{ gitea.repository_owner }}/lcc:buildcache,mode=max \ No newline at end of file diff --git a/.gitignore b/.gitignore index d03081f..63af83a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ target/ .settings .springBeans .sts4-cache +.env.example +.env ### IntelliJ IDEA ### .idea diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2267be1 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,54 @@ +services: + mysql: + image: mysql:8.0 + container_name: lcc-mysql + environment: + MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD} + MYSQL_DATABASE: ${DB_DATABASE} + MYSQL_USER: ${DB_USER} + MYSQL_PASSWORD: ${DB_PASSWORD} + volumes: + - mysql-data:/var/lib/mysql + ports: + - "3306:3306" + networks: + - lcc-network + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + + lcc-app: + #image: git.avatic.de/avatic/lcc:latest + build: . + container_name: lcc-app + depends_on: + mysql: + condition: service_healthy + environment: + DB_DATABASE: ${DB_DATABASE} + DB_USER: ${DB_USER} + DB_PASSWORD: ${DB_PASSWORD} + ALLOWED_CORS_DOMAIN: ${ALLOWED_CORS_DOMAIN} + LCC_BASE_URL: ${LCC_BASE_URL} + AZURE_MAPS_CLIENT_ID: ${AZURE_MAPS_CLIENT_ID} + AZURE_MAPS_SUBSCRIPTION_KEY: ${AZURE_MAPS_SUBSCRIPTION_KEY} + AZURE_TENANT_ID: ${AZURE_TENANT_ID} + AZURE_CLIENT_ID: ${AZURE_CLIENT_ID} + AZURE_CLIENT_SECRET: ${AZURE_CLIENT_SECRET} + JWT_SECRET: ${JWT_SECRET} + SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/${DB_DATABASE} + SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE} + ports: + - "8080:8080" + networks: + - lcc-network + restart: unless-stopped + +volumes: + mysql-data: + +networks: + lcc-network: + driver: bridge \ No newline at end of file diff --git a/dockerfile b/dockerfile new file mode 100644 index 0000000..d077963 --- /dev/null +++ b/dockerfile @@ -0,0 +1,20 @@ +FROM node:20-alpine AS frontend-build +WORKDIR /app/frontend +COPY src/frontend/package*.json ./ +RUN npm ci +COPY src/frontend/ ./ +RUN npm run build + +FROM maven:3.9-eclipse-temurin-23 AS backend-build +WORKDIR /app +COPY pom.xml ./ +COPY src ./src +# copy frontend +COPY --from=frontend-build /app/frontend/dist ./src/main/resources/static +RUN mvn clean package -DskipTests + +FROM eclipse-temurin:23-jre-alpine +WORKDIR /app +COPY --from=backend-build /app/target/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/pom.xml b/pom.xml index 5373598..479d724 100644 --- a/pom.xml +++ b/pom.xml @@ -174,56 +174,6 @@ - - - exec-maven-plugin - org.codehaus.mojo - 3.1.0 - - - npm build the vue app - generate-resources - - exec - - - src/frontend - npm - - run - build - - - - - - - - com.microsoft.azure - azure-container-apps-maven-plugin - 0.1.0 - - your-subscription-id - your-resource-group - your-app-environment-name - your-region - lcc - - - code - ${project.basedir} - - - - true - 8080 - - - 0 - 10 - - - org.springframework.boot spring-boot-maven-plugin diff --git a/src/frontend/src/components/layout/report/Report.vue b/src/frontend/src/components/layout/report/Report.vue index 2ff71ac..855b606 100644 --- a/src/frontend/src/components/layout/report/Report.vue +++ b/src/frontend/src/components/layout/report/Report.vue @@ -90,7 +90,7 @@
-
Airfreight costs
+
Air freight costs
{{ report.costs.air_freight_cost.total.toFixed(2) }}
{{ (report.costs.air_freight_cost.percentage * 100).toFixed(2) }}
diff --git a/src/frontend/vite.config.js b/src/frontend/vite.config.js index 5fdfabb..75fd831 100644 --- a/src/frontend/vite.config.js +++ b/src/frontend/vite.config.js @@ -26,7 +26,7 @@ export default defineConfig({ ], base: '/', build: { - outDir: '../../src/main/resources/static', + outDir: 'dist', emptyOutDir: true, assetsDir: 'assets', }, diff --git a/src/main/java/de/avatic/lcc/controller/token/TokenController.java b/src/main/java/de/avatic/lcc/controller/token/TokenController.java index 8255fc3..f60a022 100644 --- a/src/main/java/de/avatic/lcc/controller/token/TokenController.java +++ b/src/main/java/de/avatic/lcc/controller/token/TokenController.java @@ -1,11 +1,8 @@ package de.avatic.lcc.controller.token; -import de.avatic.lcc.dto.users.AppDTO; import de.avatic.lcc.model.db.users.App; -import de.avatic.lcc.model.db.users.Group; import de.avatic.lcc.repositories.users.JwtTokenService; import de.avatic.lcc.service.apps.AppsService; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; @@ -14,6 +11,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.Map; +import java.util.Optional; @RestController @RequestMapping("/oauth2/token") @@ -35,26 +33,25 @@ public class TokenController { @RequestParam("client_secret") String clientSecret, @RequestParam(value = "scope", required = false) String scope) { + long expiration = 3600; + if (!"client_credentials".equals(grantType)) { return ResponseEntity.badRequest() .body(Map.of("error", "unsupported_grant_type")); } - App app = appService.validateApp(clientId, clientSecret); - if (app == null) { + Optional app = appService.validateApp(clientId, clientSecret); + if (app.isEmpty()) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED) .body(Map.of("error", "invalid_client")); } - String token = jwtTokenService.createApplicationToken( - clientId, - app.getGroups().stream().map(Group::getName).toList() - ); + String token = jwtTokenService.createApplicationToken(app.get(), expiration); return ResponseEntity.ok(Map.of( "access_token", token, "token_type", "Bearer", - "expires_in", 3600 + "expires_in", expiration )); } } \ No newline at end of file diff --git a/src/main/java/de/avatic/lcc/repositories/users/JwtTokenService.java b/src/main/java/de/avatic/lcc/repositories/users/JwtTokenService.java index 2a31686..5b3b683 100644 --- a/src/main/java/de/avatic/lcc/repositories/users/JwtTokenService.java +++ b/src/main/java/de/avatic/lcc/repositories/users/JwtTokenService.java @@ -1,5 +1,7 @@ package de.avatic.lcc.repositories.users; +import de.avatic.lcc.model.db.users.App; +import de.avatic.lcc.model.db.users.Group; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; @@ -24,17 +26,17 @@ public class JwtTokenService { } - public String createApplicationToken(String clientId, List groups) { - long expirationMs = 3600000; + public String createApplicationToken(App app, long expiration) { + return Jwts.builder() .issuer(baseUrl) - .subject(clientId) + .subject(app.getClientId()) .audience().add(baseUrl + "/api").and() .issuedAt(new Date()) - .expiration(new Date(System.currentTimeMillis() + expirationMs)) - .claim("client_id", clientId) - .claim("groups", groups) + .expiration(new Date(System.currentTimeMillis() + (expiration*1000))) + .claim("client_id", app.getId()) + .claim("groups", app.getGroups().stream().map(Group::getName).toList()) .claim("token_type", "ext_app") .signWith(signingKey) .compact(); diff --git a/src/main/java/de/avatic/lcc/service/apps/AppsService.java b/src/main/java/de/avatic/lcc/service/apps/AppsService.java index bf639a3..d15e4c9 100644 --- a/src/main/java/de/avatic/lcc/service/apps/AppsService.java +++ b/src/main/java/de/avatic/lcc/service/apps/AppsService.java @@ -8,6 +8,7 @@ import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import java.util.List; +import java.util.Optional; @Service public class AppsService { @@ -39,15 +40,15 @@ public class AppsService { } - public App validateApp(String clientId, String clientSecret) { + public Optional validateApp(String clientId, String clientSecret) { var app = appRepository.getByClientId(clientId); if (app.isPresent() && passwordEncoder.matches(clientSecret, app.get().getClientSecret())) { - return app.get(); + return app; } - return null; + return Optional.empty(); } diff --git a/src/main/java/de/avatic/lcc/service/report/ReportingService.java b/src/main/java/de/avatic/lcc/service/report/ReportingService.java index a78a518..774e08f 100644 --- a/src/main/java/de/avatic/lcc/service/report/ReportingService.java +++ b/src/main/java/de/avatic/lcc/service/report/ReportingService.java @@ -58,6 +58,8 @@ public class ReportingService { var periodId = tuple.get().periodId(); var setId = tuple.get().propertySetId(); + //TODO check user node id and node id for null + var jobs = new ArrayList<>(nodeIds.stream().map(nodeId -> calculationJobRepository.getCalculationJobWithJobStateValid(periodId, setId, nodeId, materialId)).filter(Optional::isPresent).map(Optional::get).toList()); jobs.addAll(userNodeIds.stream().map(nodeId -> calculationJobRepository.getCalculationJobWithJobStateValidUserNodeId(periodId, setId,nodeId ,materialId)).filter(Optional::isPresent).map(Optional::get).toList()); diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index a56307b..488df12 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,4 +1,4 @@ -spring.config.import=classpath:env.properties +spring.profiles.active=${SPRING_PROFILES_ACTIVE} spring.application.name=lcc spring.datasource.url=jdbc:mysql://localhost:3306/${DB_DATABASE} spring.datasource.username=${DB_USER}