Added backend and fixed openapi generation

This commit is contained in:
Teriuihi 2025-04-08 22:24:24 +02:00
parent 21496baab5
commit 67a3162ae3
21 changed files with 405 additions and 157 deletions

1
.gitignore vendored
View File

@ -87,3 +87,4 @@ package-lock.json
### MINE ###
frontend/src/api
generated

40
backend/build.gradle.kts Normal file
View File

@ -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<Test> {
useJUnitPlatform()
}

View File

@ -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);
}
}

View File

@ -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<TeamMembersDto> getTeamMembers(String group) {
TeamMembersDto teamMemberDtos = new TeamMembersDto();
teamMemberDtos.add(new TeamMemberDto("test", "good"));
return ResponseEntity.ok().body(teamMemberDtos);
}
}

View File

@ -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<Databases, Connection> 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<Connection> getConnection(Databases database, AddMappers addMappers) {
if (connections.containsKey(database)) {
return CompletableFuture.completedFuture(connections.get(database));
}
if (database == Databases.DEFAULT) {
return loadDefaultDatabase(addMappers);
}
CompletableFuture<DatabaseSettings> 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<Connection> 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<SqlSession> 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);
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,3 @@
package com.alttd.altitudeweb.database.luckperms;
public record PlayerGroup(String username, String groupName) {}

View File

@ -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<PlayerGroup> getTeamMembers(String groupName);
}

View File

@ -0,0 +1,4 @@
package com.alttd.altitudeweb.database.web_db;
public record DatabaseSettings(String host, int port, String name, String username, String password) {
}

View File

@ -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);
}

View File

@ -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}

View File

@ -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() {
// }
//
//}

View File

@ -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<Test> {
useJUnitPlatform()
}
// Generate Java API using OpenAPI Generator (Spring)
tasks.register<GenerateTask>("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<GenerateTask>("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<Exec>("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<Exec>("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<Copy>("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()
// }
// }
//}

75
open_api/build.gradle.kts Normal file
View File

@ -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>("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<GenerateTask>("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)
}

View File

@ -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:

View File

@ -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
}

View File

@ -0,0 +1,4 @@
{
"npmName": "generated-api",
"supportsES6": true
}

View File

@ -1 +1,2 @@
rootProject.name = "AltitudeWeb"
include("open_api", "backend", "frontend")

View File

@ -1 +0,0 @@
spring.application.name=AltitudeWeb

View File

@ -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() {
}
}