diff --git a/.gitignore b/.gitignore index aba61dc..143f0c2 100644 --- a/.gitignore +++ b/.gitignore @@ -87,3 +87,4 @@ package-lock.json ### MINE ### frontend/src/api +generated diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts new file mode 100644 index 0000000..5052774 --- /dev/null +++ b/backend/build.gradle.kts @@ -0,0 +1,40 @@ +plugins { + java + id("org.springframework.boot") version "3.4.4" + id("io.spring.dependency-management") version "1.1.7" +} + +group = "com.alttd.altitudeweb" +version = "0.0.1-SNAPSHOT" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +configurations { + compileOnly { + extendsFrom(configurations.annotationProcessor.get()) + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation(project(":open_api")) + implementation("org.springframework.boot:spring-boot-starter-web") + compileOnly("org.projectlombok:lombok") + runtimeOnly("org.mariadb.jdbc:mariadb-java-client") + annotationProcessor("org.projectlombok:lombok") + testImplementation("org.springframework.boot:spring-boot-starter-test") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + implementation("org.mybatis:mybatis:3.5.13") + implementation("org.springframework.boot:spring-boot-configuration-processor") +} + +tasks.withType { + useJUnitPlatform() +} diff --git a/src/main/java/com/alttd/altitudeweb/AltitudeWebApplication.java b/backend/src/main/java/com/alttd/altitudeweb/AltitudeWebApplication.java similarity index 100% rename from src/main/java/com/alttd/altitudeweb/AltitudeWebApplication.java rename to backend/src/main/java/com/alttd/altitudeweb/AltitudeWebApplication.java diff --git a/backend/src/main/java/com/alttd/altitudeweb/controllers/CorsConfig.java b/backend/src/main/java/com/alttd/altitudeweb/controllers/CorsConfig.java new file mode 100644 index 0000000..6f9feea --- /dev/null +++ b/backend/src/main/java/com/alttd/altitudeweb/controllers/CorsConfig.java @@ -0,0 +1,28 @@ +package com.alttd.altitudeweb.controllers; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Slf4j +@Configuration +public class CorsConfig implements WebMvcConfigurer { + + @Value("${cors.allowed-origins}") + private String[] allowedOrigins; + + public CorsConfig() { + log.info("test"); + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins(allowedOrigins) + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true); + } +} diff --git a/backend/src/main/java/com/alttd/altitudeweb/controllers/TeamApiController.java b/backend/src/main/java/com/alttd/altitudeweb/controllers/TeamApiController.java new file mode 100644 index 0000000..718ddfe --- /dev/null +++ b/backend/src/main/java/com/alttd/altitudeweb/controllers/TeamApiController.java @@ -0,0 +1,18 @@ +package com.alttd.altitudeweb.controllers; + +import com.alttd.altitudeweb.api.TeamApi; +import com.alttd.altitudeweb.model.TeamMemberDto; +import com.alttd.altitudeweb.model.TeamMembersDto; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class TeamApiController implements TeamApi { + + @Override + public ResponseEntity getTeamMembers(String group) { + TeamMembersDto teamMemberDtos = new TeamMembersDto(); + teamMemberDtos.add(new TeamMemberDto("test", "good")); + return ResponseEntity.ok().body(teamMemberDtos); + } +} diff --git a/backend/src/main/java/com/alttd/altitudeweb/database/Connection.java b/backend/src/main/java/com/alttd/altitudeweb/database/Connection.java new file mode 100644 index 0000000..9551545 --- /dev/null +++ b/backend/src/main/java/com/alttd/altitudeweb/database/Connection.java @@ -0,0 +1,98 @@ +package com.alttd.altitudeweb.database; + +import com.alttd.altitudeweb.database.web_db.DatabaseSettings; +import com.alttd.altitudeweb.database.web_db.SettingsMapper; +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.datasource.pooled.PooledDataSource; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.SqlSession; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; + +import java.util.HashMap; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +@Slf4j +public class Connection { + + private static final HashMap connections = new HashMap<>(); + private SqlSessionFactory sqlSessionFactory; + private final DatabaseSettings settings; + private final AddMappers addMappers; + + private Connection(DatabaseSettings settings, AddMappers addMappers) { + this.settings = settings; + this.addMappers = addMappers; + } + + @FunctionalInterface + public interface AddMappers { + void apply(Configuration configuration); + } + + public static CompletableFuture getConnection(Databases database, AddMappers addMappers) { + if (connections.containsKey(database)) { + return CompletableFuture.completedFuture(connections.get(database)); + } + if (database == Databases.DEFAULT) { + return loadDefaultDatabase(addMappers); + } + CompletableFuture settingsFuture = new CompletableFuture<>(); + getConnection(Databases.DEFAULT, (mapper -> mapper.addMapper(DatabaseSettings.class))).thenApply(connection -> { + connection.runQuery(session -> { + DatabaseSettings loadedSettings = session.getMapper(SettingsMapper.class).getSettings(database.getInternalName()); + settingsFuture.complete(loadedSettings); + }); + return null; + }); + return settingsFuture.thenApply(loadedSettings -> { + Connection connection = new Connection(loadedSettings, addMappers); + connections.put(database, connection); + return connection; + }); + } + + private static CompletableFuture loadDefaultDatabase(AddMappers addMappers) { + DatabaseSettings databaseSettings = new DatabaseSettings( + System.getenv("DB_HOST"), + Integer.parseInt(System.getenv("DB_PORT")), + System.getenv("DB_NAME"), + System.getenv("DB_USER"), + System.getenv("DB_PASS") + ); + Connection connection = new Connection(databaseSettings, addMappers); + return CompletableFuture.completedFuture(connection); + } + + public void runQuery(Consumer consumer) { + new Thread(() -> { + if (sqlSessionFactory == null) { + sqlSessionFactory = createSqlSessionFactory(settings, addMappers); + } + + try (SqlSession session = sqlSessionFactory.openSession()) { + consumer.accept(session); + } catch (Exception e) { + log.error("Failed to run discord query", e); + } + }).start(); + } + + private SqlSessionFactory createSqlSessionFactory(DatabaseSettings settings, AddMappers addMappers) { + PooledDataSource dataSource = new PooledDataSource(); + dataSource.setDriver("com.mysql.cj.jdbc.Driver"); + dataSource.setUrl(String.format("jdbc:mysql://%s:%d/%s", settings.host(), + settings.port(), settings.name())); + dataSource.setUsername(settings.username()); + dataSource.setPassword(settings.password()); + Environment environment = new Environment("production", new JdbcTransactionFactory(), dataSource); + Configuration configuration = new Configuration(environment); + addMappers.apply(configuration); + + return new SqlSessionFactoryBuilder().build(configuration); + } + +} diff --git a/backend/src/main/java/com/alttd/altitudeweb/database/Databases.java b/backend/src/main/java/com/alttd/altitudeweb/database/Databases.java new file mode 100644 index 0000000..8656a51 --- /dev/null +++ b/backend/src/main/java/com/alttd/altitudeweb/database/Databases.java @@ -0,0 +1,15 @@ +package com.alttd.altitudeweb.database; + +import lombok.Getter; + +@Getter +public enum Databases { + DEFAULT("web_db"), + LUCK_PERMS("luckperms"); + + private final String internalName; + + Databases(String internalName) { + this.internalName = internalName; + } +} diff --git a/backend/src/main/java/com/alttd/altitudeweb/database/luckperms/PlayerGroup.java b/backend/src/main/java/com/alttd/altitudeweb/database/luckperms/PlayerGroup.java new file mode 100644 index 0000000..ae9d30e --- /dev/null +++ b/backend/src/main/java/com/alttd/altitudeweb/database/luckperms/PlayerGroup.java @@ -0,0 +1,3 @@ +package com.alttd.altitudeweb.database.luckperms; + +public record PlayerGroup(String username, String groupName) {} diff --git a/backend/src/main/java/com/alttd/altitudeweb/database/luckperms/TeamMemberMapper.java b/backend/src/main/java/com/alttd/altitudeweb/database/luckperms/TeamMemberMapper.java new file mode 100644 index 0000000..67adaaa --- /dev/null +++ b/backend/src/main/java/com/alttd/altitudeweb/database/luckperms/TeamMemberMapper.java @@ -0,0 +1,14 @@ +package com.alttd.altitudeweb.database.luckperms; + +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +public interface TeamMemberMapper { + @Select(""" + SELECT players.username, #{groupName} AS group_name + FROM luckperms_user_permissions AS permissions + INNER JOIN luckperms_players AS players ON players.uuid = permissions.uuid + WHERE permission = 'group.'#{groupName}""") + List getTeamMembers(String groupName); +} diff --git a/backend/src/main/java/com/alttd/altitudeweb/database/web_db/DatabaseSettings.java b/backend/src/main/java/com/alttd/altitudeweb/database/web_db/DatabaseSettings.java new file mode 100644 index 0000000..6030e7b --- /dev/null +++ b/backend/src/main/java/com/alttd/altitudeweb/database/web_db/DatabaseSettings.java @@ -0,0 +1,4 @@ +package com.alttd.altitudeweb.database.web_db; + +public record DatabaseSettings(String host, int port, String name, String username, String password) { +} diff --git a/backend/src/main/java/com/alttd/altitudeweb/database/web_db/SettingsMapper.java b/backend/src/main/java/com/alttd/altitudeweb/database/web_db/SettingsMapper.java new file mode 100644 index 0000000..0aef6f1 --- /dev/null +++ b/backend/src/main/java/com/alttd/altitudeweb/database/web_db/SettingsMapper.java @@ -0,0 +1,8 @@ +package com.alttd.altitudeweb.database.web_db; + +import org.apache.ibatis.annotations.Select; + +public interface SettingsMapper { + @Select("SELECT * FROM database_settings WHERE name = #{database}") + DatabaseSettings getSettings(String database); +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties new file mode 100644 index 0000000..64d14c8 --- /dev/null +++ b/backend/src/main/resources/application.properties @@ -0,0 +1,7 @@ +spring.application.name=AltitudeWeb +database.name=${DB_NAME:web_db} +database.port=${DB_PORT:3306} +database.host=${DB_HOST:localhost} +database.user=${DB_USER:root} +database.password=${DB_PASSWORD:root} +cors.allowed-origins=${CORS:http://localhost:4200} diff --git a/backend/src/test/java/com/alttd/altitudeweb/AltitudeWebApplicationTests.java b/backend/src/test/java/com/alttd/altitudeweb/AltitudeWebApplicationTests.java new file mode 100644 index 0000000..4f8f232 --- /dev/null +++ b/backend/src/test/java/com/alttd/altitudeweb/AltitudeWebApplicationTests.java @@ -0,0 +1,13 @@ +package com.alttd.altitudeweb; + +//import org.junit.jupiter.api.Test; +//import org.springframework.boot.test.context.SpringBootTest; +// +//@SpringBootTest +//class AltitudeWebApplicationTests { +// +// @Test +// void contextLoads() { +// } +// +//} diff --git a/build.gradle.kts b/build.gradle.kts deleted file mode 100644 index 7cec30a..0000000 --- a/build.gradle.kts +++ /dev/null @@ -1,143 +0,0 @@ -import org.openapitools.generator.gradle.plugin.tasks.GenerateTask - -plugins { - java - id("org.springframework.boot") version "3.4.4" - id("io.spring.dependency-management") version "1.1.7" - id("org.openapi.generator") version "7.12.0" -} - -group = "com.alttd" -version = "0.0.1-SNAPSHOT" - -java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) - } -} - -configurations { - compileOnly { - extendsFrom(configurations.annotationProcessor.get()) - } -} - -repositories { - mavenCentral() -} - -dependencies { - implementation("org.springframework.boot:spring-boot-starter-web") - compileOnly("org.projectlombok:lombok") - runtimeOnly("org.mariadb.jdbc:mariadb-java-client") - annotationProcessor("org.projectlombok:lombok") - testImplementation("org.springframework.boot:spring-boot-starter-test") - testRuntimeOnly("org.junit.platform:junit-platform-launcher") -} - -tasks.withType { - useJUnitPlatform() -} - -// Generate Java API using OpenAPI Generator (Spring) -tasks.register("generateJavaApi") { - generatorName.set("spring") - inputSpec.set("$projectDir/open_api/api.yml") - val myBuildDir = layout.buildDirectory.get().asFile.absolutePath - outputDir.set("$myBuildDir/generated-sources/java") - apiPackage.set("com.alttd.api") - modelPackage.set("com.alttd.model") - configOptions.set(mapOf("interfaceOnly" to "true")) -} - -// Make java compiling wait until after the API is generated -tasks.named("compileJava") { - dependsOn("generateJavaApi") -} - -// Generate Angular API client using OpenAPI Generator (TypeScript-Angular) -tasks.register("generateFrontendApi") { - generatorName.set("typescript-angular") - inputSpec.set("$projectDir/open_api/api.yml") - outputDir.set("$projectDir/frontend/src/api") - configOptions.set( - mapOf( - "npmName" to "generated-api", - "supportsES6" to "true" - ) - ) -} - -// Task to install npm dependencies in the frontend folder -tasks.register("npmInstall") { - workingDir = file("$projectDir/frontend") - commandLine("npm.cmd", "install") -// commandLine("npm", "install") -} - -tasks.named("jar") { - dependsOn("copyFrontend") -} - -tasks.named("resolveMainClassName") { - dependsOn("copyFrontend") // Declare `copyFrontend` as a dependency -} - -// Task to build the Angular frontend; depends on npmInstall and generateFrontendApi -tasks.register("ngBuild") { - dependsOn("npmInstall", "generateFrontendApi") - workingDir = file("$projectDir/frontend") -// commandLine("npm", "run", "build") - commandLine("npm.cmd", "run", "build") -} - -// Task to copy built frontend into "static" folder for Spring Boot -tasks.register("copyFrontend") { - - val myBuildDir = layout.buildDirectory.get().asFile.absolutePath - - dependsOn("ngBuild") // Make sure frontend build runs first - from("$projectDir/frontend/dist") // Source directory for Angular build - into("$myBuildDir/resources/main/static") // Destination in Spring Boot's static folder - - // Optional: Explicitly mark inputs and outputs for cache optimization - inputs.dir("$projectDir/frontend/dist") - outputs.dir("$myBuildDir/resources/main/static") -} - -tasks.named("compileTestJava") { - dependsOn("copyFrontend") // Ensure `copyFrontend` runs before `compileTestJava` -} - -// Ensure that the frontend is built and copied before bootJar -tasks.named("bootJar") { - dependsOn("copyFrontend") // Make bootJar depend on frontend build -} - -//tasks.register("checkNpm") { -// doLast { -// // Print PATH for debugging -// val gradlePath = System.getenv("PATH") -// // Add location of npm to the PATH -// val nodePath = "C:\\Program Files\\nodejs" -// val updatedPath = "$gradlePath;$nodePath" -// -// // Check if npm is accessible -// try { -// val processBuilder = ProcessBuilder("npm", "-v") -// processBuilder.environment()["PATH"] = updatedPath // Explicitly set PATH -// val npmVersion = ProcessBuilder("npm", "-v") -// .redirectErrorStream(true) // Redirect error output to standard output -// .start() -// .inputStream -// .bufferedReader() -// .readText() -// .trim() -// -// println("npm version: $npmVersion") -// } catch (e: Exception) { -// println("Error: npm not found or not accessible. Make sure npm is added to your PATH.") -// e.printStackTrace() -// } -// } -//} diff --git a/open_api/build.gradle.kts b/open_api/build.gradle.kts new file mode 100644 index 0000000..b39bed0 --- /dev/null +++ b/open_api/build.gradle.kts @@ -0,0 +1,75 @@ +import org.openapitools.generator.gradle.plugin.tasks.GenerateTask +import org.springframework.boot.gradle.tasks.bundling.BootJar + +plugins { + id("org.openapi.generator") version "7.12.0" + id("org.springframework.boot") version "3.4.4" + id("io.spring.dependency-management") version "1.1.7" + java +} + +repositories { + mavenCentral() +} + +tasks.named("compileJava") { + dependsOn("generateJavaApi") + dependsOn("generateFrontendApi") +} + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +tasks.named("bootJar") { + enabled = false +} + +tasks.jar { + enabled = true +} + + +sourceSets { + main { + java { + srcDir("${projectDir}/build/generated-resources/model/src/main/java") + exclude("org/openapitools/configuration/SpringDocConfiguration.java") + } + } +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("io.swagger.core.v3:swagger-annotations:2.2.20") + implementation("org.openapitools:jackson-databind-nullable:0.2.6") + implementation("org.springframework.hateoas:spring-hateoas:2.2.0") +} + +tasks.register< GenerateTask>("generateJavaApi") { + generatorName.set("spring") + library.set("spring-boot") + inputSpec.set("${projectDir}/src/main/resources/api.yml") + configFile.set("${projectDir}/src/main/resources/config_backend.json") + outputDir.set("${projectDir}/build/generated-resources/model") + typeMappings.put("OffsetDateTime", "Instant") + importMappings.put("java.time.OffsetDateTime", "java.time.Instant") + modelNameSuffix.set("Dto") + generateModelTests.set(false) + generateModelDocumentation.set(false) + generateApiTests.set(false) + generateApiDocumentation.set(false) + generateAliasAsModel.set(true) + modelPackage.set("com.alttd.altitudeweb.model") + generateApiTests.set(false) +} + +tasks.register("generateFrontendApi") { + generatorName.set("typescript-angular") + inputSpec.set("$rootDir/open_api/src/main/resources/api.yml") + outputDir.set("${projectDir}/../frontend/src/api") + configFile.set("$rootDir/open_api/src/main/resources/config_frontend.json") + generateApiTests.set(false) +} diff --git a/open_api/api.yml b/open_api/src/main/resources/api.yml similarity index 57% rename from open_api/api.yml rename to open_api/src/main/resources/api.yml index 1227611..5a74d18 100644 --- a/open_api/api.yml +++ b/open_api/src/main/resources/api.yml @@ -10,6 +10,33 @@ tags: - name: player description: Retrieve player information paths: + /player/teamMembers/{group}: + get: + tags: + - team + summary: Get team members + description: Retrieve players who are part of the specified team + operationId: getTeamMembers + parameters: + - name: group + in: path + required: true + description: The group name of the team + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/TeamMembers' + default: + description: Unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" /player/history: post: tags: @@ -59,6 +86,22 @@ components: duration: type: integer format: int64 + TeamMembers: + type: array + items: + $ref: '#/components/schemas/TeamMember' + TeamMember: + type: object + properties: + name: + type: string + description: The name of the team member + group: + type: string + description: The group to which the team member belongs + required: + - name + - group Error: type: object properties: diff --git a/open_api/src/main/resources/config_backend.json b/open_api/src/main/resources/config_backend.json new file mode 100644 index 0000000..eec124e --- /dev/null +++ b/open_api/src/main/resources/config_backend.json @@ -0,0 +1,33 @@ +{ + "library": "spring-boot", + "hideGenerationTimestamp": true, + "modelPackage": "com.alttd.altitudeweb.api.model", + "apiPackage": "com.alttd.altitudeweb.api", + "invokerPackage": "com.alttd.altitudeweb.api", + "serializableModel": true, + "openApiNullable": false, + "useTags": true, + "useGzipFeature" : true, + "hateoas": true, + "unhandledException": true, + "useSwaggerUI": true, + "importMappings": { + "ResourceSupport":"org.springframework.hateoas.RepresentationModel", + "Link": "org.springframework.hateoas.Link" + }, + "generateApis": true, + "generateApiTests": false, + "generateApiDocumentation": false, + "generateModels": true, + "generateModelTests": false, + "generateSupportingFiles": false, + "modelNameSuffix": "Dto", + "generateTests": false, + "skipSpringdoc": true, + "dateLibrary": "java8", + "interfaceOnly": true, + "swaggerDocketConfig": false, + "skipDefaultInterface": true, + "enumUnknownDefaultCase": true, + "useSpringBoot3": true +} diff --git a/open_api/src/main/resources/config_frontend.json b/open_api/src/main/resources/config_frontend.json new file mode 100644 index 0000000..0006138 --- /dev/null +++ b/open_api/src/main/resources/config_frontend.json @@ -0,0 +1,4 @@ +{ + "npmName": "generated-api", + "supportsES6": true +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 7700d4d..482e96b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,2 @@ rootProject.name = "AltitudeWeb" +include("open_api", "backend", "frontend") diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties deleted file mode 100644 index 985c9cf..0000000 --- a/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=AltitudeWeb diff --git a/src/test/java/com/alttd/altitudeweb/AltitudeWebApplicationTests.java b/src/test/java/com/alttd/altitudeweb/AltitudeWebApplicationTests.java deleted file mode 100644 index 35a4856..0000000 --- a/src/test/java/com/alttd/altitudeweb/AltitudeWebApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.alttd.altitudeweb; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class AltitudeWebApplicationTests { - - @Test - void contextLoads() { - } - -}