commit 5b2500010fd69fa6de502c21186dbdb5d6a1e964 Author: Snoweuph Date: Sat Feb 1 14:18:34 2025 +0100 chore: setup diff --git a/.forgejo/pull_request_template.yml b/.forgejo/pull_request_template.yml new file mode 100644 index 0000000..0f221b4 --- /dev/null +++ b/.forgejo/pull_request_template.yml @@ -0,0 +1,29 @@ +name: Feature +title: '[Feature]: ' +about: 'Vorlage Für ein Feature Ticket' +ref: 'trunk' +body: + - type: input + id: ticket + attributes: + label: Ticket + description: Für Welches Ticket ist diese Pull Request? + placeholder: TD-1 + validations: + required: true + - type: textarea + id: beschreibung + attributes: + label: Beschreibung + description: Was hast du genau gemacht in diesem Ticket? + placeholder: ... + validations: + required: true + - type: textarea + id: more + attributes: + label: Weitere Infos + description: Gibt es noch etwas das ich wissen muss um das Ticket zu Reviewn? + placeholder: ... + validations: + required: false diff --git a/.forgejo/workflows/Dockerfile b/.forgejo/workflows/Dockerfile new file mode 100644 index 0000000..0f6536e --- /dev/null +++ b/.forgejo/workflows/Dockerfile @@ -0,0 +1,7 @@ +FROM amazoncorretto:21-alpine + +ARG VERSION + +ADD tower_defence_server-$VERSION.jar tower_defence_server.jar + +ENTRYPOINT ["java","-jar","/tower_defence_server.jar"] diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml new file mode 100644 index 0000000..a7db8cf --- /dev/null +++ b/.forgejo/workflows/build.yml @@ -0,0 +1,92 @@ +name: Build Application + +on: + push: + tags: + - 'v[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?' + - "v*" + +jobs: + build: + runs-on: stable + container: + image: git.euph.dev/actions/runner-java-21:latest + steps: + - name: "Checkout" + uses: "https://git.euph.dev/actions/checkout@v3" + - uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: "Prepare Gradle" + run: gradle clean + - name: "Build Jar" + run: gradle bootJar -Pversion=${{ github.ref_name }} + - name: Upload Jar as Artifact + uses: "https://git.euph.dev/actions/upload-artifact@v3" + with: + name: tower_defence_server.jar + path: build/libs/Tower-Defence-Server-${{ github.ref_name }}.jar + - name: "Stop Gradle" + run: gradle --stop + + build-docker: + runs-on: docker + needs: + - build + steps: + - name: "Checkout" + uses: "https://git.euph.dev/actions/checkout@v3" + - name: Download artifact from previous job + uses: "https://git.euph.dev/actions/download-artifact@v3" + with: + name: tower_defence_server.jar + path: .forgejo/workflows/ + - name: Login to Registry + uses: "https://git.euph.dev/actions/docker-login@v3" + with: + registry: git.euph.dev + username: ${{ secrets.DEPLOY_USER }} + password: ${{ secrets.DEPLOY_SECRET }} + - name: Build and push Web Image + uses: "https://git.euph.dev/actions/docker-build-push@v5" + with: + context: ".forgejo/workflows/" + push: true + build-args: PACKAGE_VERSION=${{ github.ref_name }} + tags: | + git.euph.dev/towerdefence/server:${{ github.ref_name }} + ${{ contains(github.ref_name, 'rc') == false && 'git.euph.dev/towerdefence/server:latest' || '' }} + + release: + runs-on: stable + container: + image: git.euph.dev/actions/runner-basic:latest + needs: + - build + - build-docker + steps: + - name: Download Server Jar + uses: "https://git.euph.dev/actions/download-artifact@v3" + with: + name: tower_defence_server.jar + path: release + - name: Create Release + uses: "https://git.euph.dev/actions/release@v2" + with: + direction: upload + tag: ${{ github.ref_name }} + token: ${{ secrets.DEPLOY_TOKEN }} + prerelease: ${{ contains( github.ref_name, "rc") }} + release-dir: release + release-notes: | + # Tower Defence - Server ${{ github.ref_name }} + + Read the [Documentation](https://git.euph.dev/TowerDefence/Dokumentation/wiki/Server/Config) to see how to setup the server. + + + diff --git a/.forgejo/workflows/qs.yml b/.forgejo/workflows/qs.yml new file mode 100644 index 0000000..6d48d25 --- /dev/null +++ b/.forgejo/workflows/qs.yml @@ -0,0 +1,108 @@ +name: "Quality Check" + +on: + - push + - pull_request + +jobs: + oas: + name: "Validate OAS" + runs-on: stable + container: + image: "git.euph.dev/actions/runner-java-21:latest" + steps: + - name: "Checkout" + uses: "https://git.euph.dev/actions/checkout@v3" + - uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: "Prepare Gradle" + run: gradle clean + - name: "Validate OAS Spec" + run: gradle validateSwagger + - name: "Stop Gradle" + run: gradle --stop + + linting: + name: "Linting" + runs-on: stable + container: + image: "git.euph.dev/actions/runner-java-21:latest" + steps: + - name: "Checkout" + uses: "https://git.euph.dev/actions/checkout@v3" + - uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: "Prepare Gradle" + run: gradle clean + - name: "Generate OAS Boilerplate" + run: gradle generateSwaggerCode + - name: "Linting Main" + run: gradle checkstyleMain + - name: "Linting Test" + run: gradle checkstyleTest + - name: "Stop Gradle" + run: gradle --stop + + static: + name: "Static Analysis" + runs-on: stable + container: + image: "git.euph.dev/actions/runner-java-21:latest" + steps: + - name: "Checkout" + uses: "https://git.euph.dev/actions/checkout@v3" + - uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: "Prepare Gradle" + run: gradle clean + - name: "Generate OAS Boilerplate" + run: gradle generateSwaggerCode + - name: "Static Analysis Main" + run: gradle spotbugsMain + - name: "Static Analysis Test" + run: gradle spotbugsTest + - name: "Stop Gradle" + run: gradle --stop + + test: + name: "Testing" + runs-on: stable + container: + image: "git.euph.dev/actions/runner-java-21:latest" + steps: + - name: "Checkout" + uses: "https://git.euph.dev/actions/checkout@v3" + - uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: "Prepare Gradle" + run: gradle clean + - name: "Generate OAS Boilerplate" + run: gradle generateSwaggerCode + - name: "Run Tests" + run: gradle test + - name: "Stop Gradle" + run: gradle --stop diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..915f81b --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +.gradle +gradlew +gradlew.bat +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/.run/Check.run.xml b/.run/Check.run.xml new file mode 100644 index 0000000..8863b18 --- /dev/null +++ b/.run/Check.run.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.run/Docker.run.xml b/.run/Docker.run.xml new file mode 100644 index 0000000..1c8948b --- /dev/null +++ b/.run/Docker.run.xml @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file diff --git a/.run/Main Lint.run.xml b/.run/Main Lint.run.xml new file mode 100644 index 0000000..4c51697 --- /dev/null +++ b/.run/Main Lint.run.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/.run/Main Static Analysis.run.xml b/.run/Main Static Analysis.run.xml new file mode 100644 index 0000000..91509c5 --- /dev/null +++ b/.run/Main Static Analysis.run.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/.run/OAS Generate.run.xml b/.run/OAS Generate.run.xml new file mode 100644 index 0000000..067de04 --- /dev/null +++ b/.run/OAS Generate.run.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/.run/OAS Validate.run.xml b/.run/OAS Validate.run.xml new file mode 100644 index 0000000..9f6ffaf --- /dev/null +++ b/.run/OAS Validate.run.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/.run/Run.run.xml b/.run/Run.run.xml new file mode 100644 index 0000000..3694c09 --- /dev/null +++ b/.run/Run.run.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/.run/Test Lint.run.xml b/.run/Test Lint.run.xml new file mode 100644 index 0000000..ded77ff --- /dev/null +++ b/.run/Test Lint.run.xml @@ -0,0 +1,24 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/.run/Test Static Analysis.run.xml b/.run/Test Static Analysis.run.xml new file mode 100644 index 0000000..8e123d1 --- /dev/null +++ b/.run/Test Static Analysis.run.xml @@ -0,0 +1,22 @@ + + + + + + + true + true + false + false + + + \ No newline at end of file diff --git a/.run/Test.run.xml b/.run/Test.run.xml new file mode 100644 index 0000000..667c096 --- /dev/null +++ b/.run/Test.run.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/License.md b/License.md new file mode 100644 index 0000000..ee0327f --- /dev/null +++ b/License.md @@ -0,0 +1 @@ +See License [here](https://git.euph.dev/TowerDefence/.profile/src/branch/main/License.md) diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..6d5b135 --- /dev/null +++ b/Readme.md @@ -0,0 +1,2 @@ +[![QS Badge](https://git.euph.dev/TowerDefence/Server/actions/workflows/qs.yml/badge.svg?branch=trunk&style=for-the-badge&label=QS)](https://git.euph.dev/TowerDefence/Server/actions?workflow=qs.yml) +# Tower Defence - Server diff --git a/api/.gen-ignore b/api/.gen-ignore new file mode 100644 index 0000000..48804ec --- /dev/null +++ b/api/.gen-ignore @@ -0,0 +1,5 @@ +**/*ApiController.java +**/*OpenAPISpringBoot.java +**/*application.properties +**/io/swagger/configuration/HomeController.java +**/io/swagger/configuration/SwaggerUiConfiguration.java \ No newline at end of file diff --git a/api/api.yml b/api/api.yml new file mode 100644 index 0000000..b0cc553 --- /dev/null +++ b/api/api.yml @@ -0,0 +1,110 @@ +openapi: 3.0.0 +info: + title: Tower Defence Server + description: An API for talking to the Tower Defence Server + version: 0.0.1 +servers: + - url: /api/v1 + - url: http://localhost:8080/api/v1 +security: + - JWTAuth: [] + +components: + securitySchemes: + JWTAuth: + type: http + scheme: bearer + bearerFormat: JWT + schemas: + ############################################# + # UUID # + ############################################# + UUID: + description: Unique identifier compatible with [RFC9562](https://datatracker.ietf.org/doc/html/rfc9562) + type: string + format: uuid + example: f0981749-f550-46cd-b9ce-b6ca7cd0251f + ############################################# + # AdminAuthInfo # + ############################################# + ServerHealth: + type: object + properties: + okay: + type: boolean + required: + - okay + ############################################# + # AdminAuthInfo # + ############################################# + AdminAuthInfo: + type: object + properties: + username: + type: string + required: + - username + responses: + 401Unauthorized: + description: "401 - Unauthorized" + 404NotFound: + description: "Not Found" + content: + text/plain: + schema: + type: string + 409Conflict: + description: "409 - Conflict" + content: + text/plain: + schema: + type: string + 500InternalError: + description: "500 - Internal Server Error" + content: + text/plain: + schema: + type: string + 503ServiceUnavailable: + description: "503 - Service Unavailable" + content: + text/plain: + schema: + type: string +paths: + /server/health: + get: + operationId: "ServerGetHealthcheck" + tags: + - server + description: "Endpoint for doing a Healthcheck of the Server" + responses: + 200: + description: "A Health-Report of the server" + content: + application/json: + schema: + $ref: "#/components/schemas/ServerHealth" + 500: + $ref: "#/components/responses/500InternalError" + 503: + $ref: "#/components/responses/503ServiceUnavailable" + /admin/authenticated: + get: + operationId: "AdminGetAuthenticated" + tags: + - admin + description: "Endpoint for Checking if you're authenticated as an admin" + responses: + 200: + description: "A Minimal Admin Info for testing if the admin is logged in" + content: + application/json: + schema: + $ref: "#/components/schemas/AdminAuthInfo" + 401: + $ref: "#/components/responses/401Unauthorized" + 500: + $ref: "#/components/responses/500InternalError" + 503: + $ref: "#/components/responses/503ServiceUnavailable" \ No newline at end of file diff --git a/api/gen-config.json b/api/gen-config.json new file mode 100644 index 0000000..744164f --- /dev/null +++ b/api/gen-config.json @@ -0,0 +1,11 @@ +{ + "modelPackage": "de.towerdefence.server.oas.models", + "apiPackage": "de.towerdefence.server.oas", + "invokerPackage": "de.towerdefence.server", + "java8": false, + "java11": true, + "dateLibrary": "java8-localdatetime", + "library": "spring-boot3", + "defaultInterfaces": false, + "serializableModel": true +} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..b2897fe --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,126 @@ +import com.github.spotbugs.snom.Confidence +import com.github.spotbugs.snom.Effort +import com.github.spotbugs.snom.SpotBugsTask +import org.hidetake.gradle.swagger.generator.GenerateSwaggerCode + +repositories { + mavenCentral() +} + +plugins { + java + checkstyle + id("com.github.spotbugs") version "6.0.23" + id("org.springframework.boot") version "3.4.2" + id("io.spring.dependency-management") version "1.1.7" + id("org.hidetake.swagger.generator") version "2.19.2" +} + +checkstyle { + toolVersion = "10.21.2" + configDirectory = file("src/main/resources/") + configFile = file("src/main/resources/checkstyle.xml") +} + +spotbugs { + toolVersion = "4.8.6" + effort.set(Effort.MAX) + reportLevel.set(Confidence.LOW) +} + +group = "de.towerdefence" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +configurations { + compileOnly { + extendsFrom(configurations.annotationProcessor.get()) + } +} + +dependencies { + // Spring + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-websocket") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-oauth2-client") + implementation("org.springframework.boot:spring-boot-starter-oauth2-resource-server") + developmentOnly("org.springframework.boot:spring-boot-devtools") + + // Postgres + runtimeOnly("org.postgresql:postgresql") + + // Lombok + compileOnly("org.projectlombok:lombok") + annotationProcessor("org.projectlombok:lombok") + + // Test + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.springframework.boot:spring-boot-testcontainers") + testImplementation("org.springframework.security:spring-security-test") + testImplementation("org.testcontainers:junit-jupiter") + testImplementation("org.testcontainers:postgresql") + testRuntimeOnly("com.h2database:h2") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + + //OAS + swaggerCodegen("io.swagger.codegen.v3:swagger-codegen-cli:3.0.61") + implementation("io.swagger.core.v3:swagger-annotations:2.2.22") + implementation("jakarta.xml.bind:jakarta.xml.bind-api") //Needed for XML/HTML Validation +} + +tasks.withType { + useJUnitPlatform() +} + +swaggerSources { + register("api") { + setInputFile(file("${rootDir}/api/api.yml")) + code.configFile = file("${rootDir}/api/gen-config.json") + val validationTask = validation + code(delegateClosureOf { + language = "spring" + code.rawOptions = + listOf("--ignore-file-override=" + file("${rootDir}/api/.gen-ignore").absolutePath) + dependsOn(validationTask) + }) + } +} + + + +tasks { + withType { + reports { + xml.required.set(true) + html.required.set(false) + } + } + withType { + excludeFilter.set(file("${rootDir}/src/main/resources/spotbugs-exclude.xml")) + } + processResources { + dependsOn(generateSwaggerCode) + } + withType { + useJUnitPlatform() + } + named("compileJava").configure { + dependsOn(swaggerSources.getByName("api").code) + } + +} + +sourceSets { + main { + java.srcDir("${swaggerSources.getByName("api").code.outputDir}/src/main/java") + resources.srcDir("${swaggerSources.getByName("api").code.outputDir}/src/main/resources") + } +} + diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..8dde08f --- /dev/null +++ b/compose.yml @@ -0,0 +1,26 @@ +services: + postgres: + container_name: tower_defence_postgres + image: postgres:16 + environment: + - POSTGRES_DB=td + - POSTGRES_USER=td_user + - POSTGRES_PASSWORD=td123 + ports: + - "5432:5432" + volumes: + - "tower_defence_data:/var/lib/postgresql/data" + swagger: + container_name: tower_defence_swagger + image: swaggerapi/swagger-ui:latest + environment: + SWAGGER_JSON: "/data/api.yml" + ports: + - "8090:8080" + volumes: + - "./api/api.yml:/data/api.yml:Z" + + +volumes: + tower_defence_data: + name: tower_defence_data \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..59c5002 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +version="0.0.1-SNAPSHOT" \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..a4b76b9 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e18bc25 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/http/getToken.http b/http/getToken.http new file mode 100644 index 0000000..a569c88 --- /dev/null +++ b/http/getToken.http @@ -0,0 +1,6 @@ +POST https://keycloak.szut.dev/auth/realms/szut/protocol/openid-connect/token +Content-Type: application/x-www-form-urlencoded + +grant_type=password&client_id=employee-management-service&username=user&password=test + +> {%client.log(response.body.access_token)%} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..93074a1 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "Tower-Defence-Server" diff --git a/src/main/java/de/towerdefence/server/Config.java b/src/main/java/de/towerdefence/server/Config.java new file mode 100644 index 0000000..1e8a59d --- /dev/null +++ b/src/main/java/de/towerdefence/server/Config.java @@ -0,0 +1,30 @@ +package de.towerdefence.server; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.Arrays; + +@Configuration +public class Config implements WebMvcConfigurer { + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } + + @Bean + CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Arrays.asList("*")); + configuration.setAllowedMethods(Arrays.asList("*")); + configuration.setAllowedHeaders(Arrays.asList("*")); + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} diff --git a/src/main/java/de/towerdefence/server/ServerApplication.java b/src/main/java/de/towerdefence/server/ServerApplication.java new file mode 100644 index 0000000..03bd63f --- /dev/null +++ b/src/main/java/de/towerdefence/server/ServerApplication.java @@ -0,0 +1,50 @@ +package de.towerdefence.server; + +import io.swagger.configuration.LocalDateConverter; +import io.swagger.configuration.LocalDateTimeConverter; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.ExitCodeGenerator; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.format.FormatterRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport; + +import java.io.Serial; + +@SpringBootApplication +@ComponentScan(basePackages = { "de.towerdefence.server", "de.towerdefence.server.oas" , "io.swagger.configuration"}) +public class ServerApplication implements CommandLineRunner { + + @Override + public void run(String... arg0) throws Exception { + if (arg0.length > 0 && arg0[0].equals("exitcode")) { + throw new ExitException(); + } + } + + public static void main(String[] args) throws Exception { + new SpringApplication(ServerApplication.class).run(args); + } + + @Configuration + static class CustomDateConfig extends WebMvcConfigurationSupport { + @Override + public void addFormatters(FormatterRegistry registry) { + registry.addConverter(new LocalDateConverter("yyyy-MM-dd")); + registry.addConverter(new LocalDateTimeConverter("yyyy-MM-dd'T'HH:mm:ss.SSS")); + } + } + + static class ExitException extends RuntimeException implements ExitCodeGenerator { + @Serial + private static final long serialVersionUID = 1L; + + @Override + public int getExitCode() { + return 10; + } + + } +} diff --git a/src/main/java/de/towerdefence/server/admin/AdminApiController.java b/src/main/java/de/towerdefence/server/admin/AdminApiController.java new file mode 100644 index 0000000..a04f9b6 --- /dev/null +++ b/src/main/java/de/towerdefence/server/admin/AdminApiController.java @@ -0,0 +1,39 @@ +package de.towerdefence.server.admin; + + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.towerdefence.server.auth.UserSession; +import de.towerdefence.server.oas.AdminApi; +import de.towerdefence.server.oas.models.AdminAuthInfo; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.util.Optional; + +@Controller +@RequestMapping("${openapi.api.base-path:/api/v1}") +public class AdminApiController implements AdminApi { + + @Autowired + UserSession userSession; + + @Override + public Optional getObjectMapper() { + return Optional.empty(); + } + + @Override + public Optional getRequest() { + return Optional.empty(); + } + + @Override + public ResponseEntity adminGetAuthenticated() { + AdminAuthInfo authInfo = new AdminAuthInfo(); + authInfo.setUsername(this.userSession.getUsername()); + return ResponseEntity.ok(authInfo); + } +} diff --git a/src/main/java/de/towerdefence/server/auth/AuthConfig.java b/src/main/java/de/towerdefence/server/auth/AuthConfig.java new file mode 100644 index 0000000..8bf7f7f --- /dev/null +++ b/src/main/java/de/towerdefence/server/auth/AuthConfig.java @@ -0,0 +1,60 @@ +package de.towerdefence.server.auth; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.session.SessionRegistry; +import org.springframework.security.core.session.SessionRegistryImpl; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy; +import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; +import org.springframework.security.web.session.HttpSessionEventPublisher; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +public class AuthConfig { + + private static final String API_VERSION = "v1"; + + private final JWT jwt; + + AuthConfig(JWT jwt) { + this.jwt = jwt; + } + + @Bean + public SessionRegistry sessionRegistry() { + return new SessionRegistryImpl(); + } + + @Bean + protected SessionAuthenticationStrategy sessionAuthenticationStrategy() { + return new RegisterSessionAuthenticationStrategy(sessionRegistry()); + } + + @Bean + public HttpSessionEventPublisher httpSessionEventPublisher() { + return new HttpSessionEventPublisher(); + } + + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + return http + .csrf(Customizer.withDefaults()) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/" + API_VERSION + "/admin/**") + .authenticated() + .anyRequest() + .permitAll() + ) + .oauth2ResourceServer(resourceServer -> resourceServer + .jwt(jwt -> jwt.jwtAuthenticationConverter(this.jwt.jwtAuthenticationConverter())) + ) + .build(); + } +} diff --git a/src/main/java/de/towerdefence/server/auth/JWT.java b/src/main/java/de/towerdefence/server/auth/JWT.java new file mode 100644 index 0000000..5ec8ebc --- /dev/null +++ b/src/main/java/de/towerdefence/server/auth/JWT.java @@ -0,0 +1,71 @@ +package de.towerdefence.server.auth; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.core.oidc.user.OidcUser; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.web.authentication.logout.LogoutHandler; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Component +public class JWT implements LogoutHandler { + private static final String REALM_ACCESS_CLAIM = "realm_access"; + private static final String ROLES_CLAIM = "roles"; + private static final String ROLE_PREFIX = "ROLE_"; + private static final String OIDC_LOGOUT_ROUTE = "/protocol/openid-connect/logout"; + private static final String OIDC_TOKEN_HINT_QUERY_PARAMETER = "id_token_hin"; + + @Autowired + private RestTemplate template; + + @Override + public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + OidcUser user = (OidcUser) authentication.getPrincipal(); + String endSessionEndpoint = user.getIssuer() + OIDC_LOGOUT_ROUTE; + UriComponentsBuilder builder = UriComponentsBuilder + .fromUriString(endSessionEndpoint) + .queryParam(OIDC_TOKEN_HINT_QUERY_PARAMETER, user.getIdToken().getTokenValue()); + + ResponseEntity logoutResponse = template.getForEntity(builder.toUriString(), String.class); + if (logoutResponse.getStatusCode().is2xxSuccessful()) { + System.out.println("Logged out successfully"); + } else { + System.out.println("Failed to logout"); + } + } + + public JwtAuthenticationConverter jwtAuthenticationConverter() { + JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); + jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(jwt -> { + List grantedAuthorities = new ArrayList<>(); + + Map realmAccess = jwt.getClaim(REALM_ACCESS_CLAIM); + if (realmAccess == null || !realmAccess.containsKey(ROLES_CLAIM)) { + return grantedAuthorities; + } + + Object rolesClaim = realmAccess.get(ROLES_CLAIM); + if (!(rolesClaim instanceof List)) { + return grantedAuthorities; + } + for (Object role : (List) rolesClaim) { + assert role instanceof String; + grantedAuthorities.add(new SimpleGrantedAuthority(ROLE_PREFIX + role)); + } + + return grantedAuthorities; + }); + return jwtAuthenticationConverter; + } +} diff --git a/src/main/java/de/towerdefence/server/auth/UserSession.java b/src/main/java/de/towerdefence/server/auth/UserSession.java new file mode 100644 index 0000000..3b81c6d --- /dev/null +++ b/src/main/java/de/towerdefence/server/auth/UserSession.java @@ -0,0 +1,20 @@ +package de.towerdefence.server.auth; + +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.security.oauth2.jwt.Jwt; + +@Component +public class UserSession { + public String getToken() { + return getPrincipal().getTokenValue(); + } + + public String getUsername() { + return (String) getPrincipal().getClaims().get("preferred_username"); + } + + private Jwt getPrincipal() { + return (Jwt) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + } +} diff --git a/src/main/java/de/towerdefence/server/server/ServerApiController.java b/src/main/java/de/towerdefence/server/server/ServerApiController.java new file mode 100644 index 0000000..01f966e --- /dev/null +++ b/src/main/java/de/towerdefence/server/server/ServerApiController.java @@ -0,0 +1,32 @@ +package de.towerdefence.server.server; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.towerdefence.server.oas.ServerApi; +import de.towerdefence.server.oas.models.ServerHealth; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.util.Optional; + +@Controller +@RequestMapping("${openapi.api.base-path:/api/v1}") +public class ServerApiController implements ServerApi { + @Override + public Optional getObjectMapper() { + return Optional.empty(); + } + + @Override + public Optional getRequest() { + return Optional.empty(); + } + + @Override + public ResponseEntity serverGetHealthcheck() { + ServerHealth health = new ServerHealth(); + health.setOkay(true); + return ResponseEntity.ok(health); + } +} diff --git a/src/main/java/de/towerdefence/server/server/ServerWebsocketHandler.java b/src/main/java/de/towerdefence/server/server/ServerWebsocketHandler.java new file mode 100644 index 0000000..d8eba5e --- /dev/null +++ b/src/main/java/de/towerdefence/server/server/ServerWebsocketHandler.java @@ -0,0 +1,47 @@ +package de.towerdefence.server.server; + +import org.springframework.web.socket.TextMessage; +import org.springframework.web.socket.WebSocketSession; +import org.springframework.web.socket.handler.TextWebSocketHandler; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.*; + +public class ServerWebsocketHandler extends TextWebSocketHandler { + private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + private final Map> sessionTaskMap = new ConcurrentHashMap<>(); + + @Override + public void afterConnectionEstablished(WebSocketSession session) throws Exception { + ScheduledFuture scheduledTask = scheduler.scheduleAtFixedRate( + () -> sendCurrentTime(session), + 0, + 1, + TimeUnit.MILLISECONDS + ); + sessionTaskMap.put(session, scheduledTask); + } + + @Override + public void handleTextMessage(WebSocketSession session, TextMessage message) { + try { + String responseMessage = "You are Connected to the Tower Defence Server Websocket"; + session.sendMessage(new TextMessage(responseMessage)); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private void sendCurrentTime(WebSocketSession session) { + ScheduledFuture task = sessionTaskMap.get(session); + try { + session.sendMessage(new TextMessage(String.valueOf(System.currentTimeMillis()))); + } catch (IllegalStateException | IOException e) { + task.cancel(true); + sessionTaskMap.remove(session); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/de/towerdefence/server/server/WebSocketConfig.java b/src/main/java/de/towerdefence/server/server/WebSocketConfig.java new file mode 100644 index 0000000..40447fb --- /dev/null +++ b/src/main/java/de/towerdefence/server/server/WebSocketConfig.java @@ -0,0 +1,15 @@ +package de.towerdefence.server.server; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.socket.config.annotation.EnableWebSocket; +import org.springframework.web.socket.config.annotation.WebSocketConfigurer; +import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry; + +@Configuration +@EnableWebSocket +public class WebSocketConfig implements WebSocketConfigurer { + @Override + public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { + registry.addHandler(new ServerWebsocketHandler(), "/ws/server").setAllowedOrigins("*"); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..88e9b20 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,18 @@ +# General +spring.application.name=Tower Defence Server +server.port=8080 + +# DB +spring.datasource.url=jdbc:postgresql://${TD_DB_HOST:localhost}:${TD_DB_PORT:5432}/${TD_DB_NAME:td} +spring.datasource.username=${TD_DB_USER:td_user} +spring.datasource.password=${TD_DB_PASS:td123} +spring.jpa.hibernate.ddl-auto=create-drop + +# TODO: Replace with our own IAM (After completion of the project) +# JWT Auth +spring.security.oauth2.client.registration.keycloak.client-id=employee-management-service +spring.security.oauth2.client.registration.keycloak.authorization-grant-type=authorization_code +spring.security.oauth2.client.registration.keycloak.scope=openid +spring.security.oauth2.client.provider.keycloak.issuer-uri=https://keycloak.szut.dev/auth/realms/szut +spring.security.oauth2.client.provider.keycloak.user-name-attribute=preferred_username +spring.security.oauth2.resourceserver.jwt.issuer-uri=https://keycloak.szut.dev/auth/realms/szut diff --git a/src/main/resources/checkstyle-ignore.xml b/src/main/resources/checkstyle-ignore.xml new file mode 100644 index 0000000..50c0f84 --- /dev/null +++ b/src/main/resources/checkstyle-ignore.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/main/resources/checkstyle.xml b/src/main/resources/checkstyle.xml new file mode 100644 index 0000000..ae6ecfc --- /dev/null +++ b/src/main/resources/checkstyle.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/spotbugs-exclude.xml b/src/main/resources/spotbugs-exclude.xml new file mode 100644 index 0000000..91bc76f --- /dev/null +++ b/src/main/resources/spotbugs-exclude.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/src/test/java/de/towerdefence/server/IntegrationTest.java b/src/test/java/de/towerdefence/server/IntegrationTest.java new file mode 100644 index 0000000..70e8bba --- /dev/null +++ b/src/test/java/de/towerdefence/server/IntegrationTest.java @@ -0,0 +1,35 @@ +package de.towerdefence.server; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +@SpringBootTest +@AutoConfigureMockMvc(addFilters = false) +@ActiveProfiles("test") +public abstract class IntegrationTest { + + protected final static String baseUri = "/api/v1"; + + @Autowired + protected MockMvc mvc; + @Autowired + protected ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + //repository.deleteAll(); + } + + @AfterEach + void cleanUp() { + //repository.deleteAll(); + } + + } + diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties new file mode 100644 index 0000000..6be43ca --- /dev/null +++ b/src/test/resources/application-test.properties @@ -0,0 +1,6 @@ +spring.datasource.url=jdbc:h2:mem:test_db +spring.datasource.username=test +spring.datasource.password=test +spring.datasource.driver-class-name=org.h2.Driver +spring.jpa.hibernate.ddl-auto=create-drop +