Compare commits
193
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8bfcdb6ccc | ||
|
|
64ea68ab39 | ||
|
|
f117cb2477 | ||
|
|
d84d0c7fef | ||
|
|
00bf7caec2 | ||
|
|
41dab473b0 | ||
|
|
b71ea7da8b | ||
|
|
a55806e5dd | ||
|
|
7e25cc583c | ||
|
|
894dfac0c6 | ||
|
|
300d33da7d | ||
|
|
6f6801c728 | ||
|
|
f8157e997a | ||
|
|
74e8697fef | ||
|
|
8ad87da47e | ||
|
|
29a28e712e | ||
|
|
6ad3b5221a | ||
|
|
1b697fcaa3 | ||
|
|
ed9d41cdc6 | ||
|
|
5eaeb3552a | ||
|
|
8e9e267fb0 | ||
|
|
6d8f73201f | ||
|
|
91e5a2a9a0 | ||
|
|
0005b3b6d4 | ||
|
|
b3999b3389 | ||
|
|
5a4df2572d | ||
|
|
e697f7ca90 | ||
|
|
3da46c203b | ||
|
|
e3fd0944df | ||
|
|
cd34cd93ad | ||
|
|
a9294d1115 | ||
|
|
745dab4d80 | ||
|
|
dc65b19a8f | ||
|
|
01dab905d4 | ||
|
|
311d77fcb2 | ||
|
|
cdbf862ecf | ||
|
|
643b15f2e0 | ||
|
|
f886609a0e | ||
|
|
2a0f38aa28 | ||
|
|
4878ad9f0d | ||
|
|
80cb2d0ad1 | ||
|
|
53f67c0b67 | ||
|
|
1f1f1793e3 | ||
|
|
4962d16abd | ||
|
|
cad574b8fb | ||
|
|
c75f0cdb15 | ||
|
|
fe545972e3 | ||
|
|
eab1c9322b | ||
|
|
ffddffa8dc | ||
|
|
0b4c1ccebf | ||
|
|
2e89fcec66 | ||
|
|
42b11eecf1 | ||
|
|
d1da1296bb | ||
|
|
523bf3d43f | ||
|
|
4ccce7e190 | ||
|
|
641083732d | ||
|
|
da17cf9696 | ||
|
|
de1876c90c | ||
|
|
c3f3b20807 | ||
|
|
37fb49cda1 | ||
|
|
db642103ed | ||
|
|
f026f24263 | ||
|
|
eaee31ab2b | ||
|
|
24d7cfe913 | ||
|
|
770a2e0d14 | ||
|
|
101794d8f2 | ||
|
|
eb72ce14cc | ||
|
|
d1ba89acc8 | ||
|
|
d28b4a2b62 | ||
|
|
56f4ccf40e | ||
|
|
d73f057596 | ||
|
|
e825d83124 | ||
|
|
238c5d9644 | ||
|
|
4222df87a3 | ||
|
|
16cc57d774 | ||
|
|
c536bfbf30 | ||
|
|
f67cb50f41 | ||
|
|
bdb38e5011 | ||
|
|
ae1e972438 | ||
|
|
737b26a6c7 | ||
|
|
5013b9a204 | ||
|
|
fcb64db137 | ||
|
|
d2e064e2b4 | ||
|
|
f50f2dc6c2 | ||
|
|
c277306c2c | ||
|
|
1f03a4bdc3 | ||
|
|
7f1c59d102 | ||
|
|
f968a64dd4 | ||
|
|
c25364caf7 | ||
|
|
15c3cc7f26 | ||
|
|
2b96957876 | ||
|
|
b16fab26e7 | ||
|
|
28fd05a656 | ||
|
|
ff1b09be92 | ||
|
|
8a839ac922 | ||
|
|
3f76a98409 | ||
|
|
871615702b | ||
|
|
291c9df5c6 | ||
|
|
4150324d75 | ||
|
|
4267c782a7 | ||
|
|
343964eda8 | ||
|
|
1ce2088cae | ||
|
|
0b952e07f7 | ||
|
|
c2b9a8a574 | ||
|
|
d3ef296784 | ||
|
|
5a792463cc | ||
|
|
974d50d7cd | ||
|
|
62f837914c | ||
|
|
ace969ba3b | ||
|
|
2fc6ba53f6 | ||
|
|
4c38b070ea | ||
|
|
db394beda6 | ||
|
|
76cb3cd89c | ||
|
|
5d8ab2deef | ||
|
|
aef32a8982 | ||
|
|
42f0961f13 | ||
|
|
04310e1cce | ||
|
|
54e747118c | ||
|
|
43430cfbef | ||
|
|
cce83a08de | ||
|
|
f0faa63ca7 | ||
|
|
dfea91d8ca | ||
|
|
73916f0aae | ||
|
|
ebe66c87c0 | ||
|
|
c42fc38b2c | ||
|
|
213f9987d9 | ||
|
|
48cac607de | ||
|
|
6ed2e15017 | ||
|
|
7fc25f46f3 | ||
|
|
c72703ea32 | ||
|
|
e837a9216d | ||
|
|
d4363b3a8a | ||
|
|
1e5862bae6 | ||
|
|
daf88ea437 | ||
|
|
9abd570b87 | ||
|
|
5284d498f3 | ||
|
|
c3a7be82e9 | ||
|
|
fdb57289f8 | ||
|
|
60c1329163 | ||
|
|
0e71c0f581 | ||
|
|
eb67a33331 | ||
|
|
39f20796ce | ||
|
|
e00165c56f | ||
|
|
02c6497700 | ||
|
|
0efd476676 | ||
|
|
237518638c | ||
|
|
fea1a98cea | ||
|
|
ecd9b3d824 | ||
|
|
9808b5d63d | ||
|
|
c13b7077a7 | ||
|
|
023ae809ef | ||
|
|
3a6f137c9a | ||
|
|
cb8447a096 | ||
|
|
3e98e1a498 | ||
|
|
4c31a91bb4 | ||
|
|
d6faaba01c | ||
|
|
56175e62d6 | ||
|
|
283838f444 | ||
|
|
49fe335c73 | ||
|
|
1875f050c6 | ||
|
|
52d8658be3 | ||
|
|
32a454c034 | ||
|
|
07646e8c42 | ||
|
|
174ed834ca | ||
|
|
20dcebbab9 | ||
|
|
c4c17b3adc | ||
|
|
cf758bfe60 | ||
|
|
8c7ec0a237 | ||
|
|
80462218a7 | ||
|
|
26b5f86983 | ||
|
|
ba6cf6d938 | ||
|
|
643545a18a | ||
|
|
b922487d76 | ||
|
|
54eb1ea735 | ||
|
|
9043c774f7 | ||
|
|
43b75b8e74 | ||
|
|
07c77f5485 | ||
|
|
d54a7e51ee | ||
|
|
6be6944dea | ||
|
|
3babde5513 | ||
|
|
a1912b96d8 | ||
|
|
e9880f8864 | ||
|
|
3449cf24b1 | ||
|
|
43edd49273 | ||
|
|
3d4d77bc73 | ||
|
|
5b158ae3f7 | ||
|
|
bbedd2257e | ||
|
|
6f2a48338b | ||
|
|
e6df93ff5c | ||
|
|
680506997e | ||
|
|
235f4fb60f | ||
|
|
6dbb36399f | ||
|
|
25e8dc8e8e |
@@ -90,3 +90,4 @@ frontend/src/api
|
||||
generated
|
||||
|
||||
liquibase*.properties
|
||||
node
|
||||
|
||||
+28
-42
@@ -2,7 +2,6 @@ plugins {
|
||||
java
|
||||
id("org.springframework.boot") version "3.4.4"
|
||||
id("io.spring.dependency-management") version "1.1.7"
|
||||
// id("com.github.johnrengelman.shadow") version "8.1.1"
|
||||
}
|
||||
|
||||
group = "com.alttd.altitudeweb"
|
||||
@@ -27,14 +26,23 @@ repositories {
|
||||
dependencies {
|
||||
implementation(project(":open_api"))
|
||||
implementation(project(":database"))
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation(project(":frontend"))
|
||||
implementation(project(":discord"))
|
||||
annotationProcessor("org.projectlombok:lombok")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
implementation("com.mysql:mysql-connector-j:8.0.32")
|
||||
implementation("org.mybatis:mybatis:3.5.13")
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("org.springframework.boot:spring-boot-configuration-processor")
|
||||
implementation("org.springframework.boot:spring-boot-starter-hateoas")
|
||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||
implementation("org.springframework.security:spring-security-oauth2-resource-server")
|
||||
implementation("org.springframework.security:spring-security-oauth2-jose")
|
||||
implementation("org.springframework.boot:spring-boot-starter-mail:3.1.5")
|
||||
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
|
||||
|
||||
//Open API
|
||||
implementation("io.swagger.core.v3:swagger-annotations:2.2.37")
|
||||
implementation("io.swagger.core.v3:swagger-models:2.2.37")
|
||||
|
||||
//AOP
|
||||
implementation("org.aspectj:aspectjrt:1.9.19")
|
||||
@@ -42,6 +50,12 @@ dependencies {
|
||||
implementation("org.springframework:spring-aop")
|
||||
implementation("org.springframework:spring-aspects")
|
||||
|
||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
}
|
||||
|
||||
tasks.compileJava {
|
||||
dependsOn(":frontend:npmBuild")
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
@@ -54,42 +68,14 @@ tasks.bootJar {
|
||||
archiveClassifier.set("")
|
||||
}
|
||||
|
||||
tasks.processResources {
|
||||
dependsOn("includeFrontend")
|
||||
}
|
||||
|
||||
//tasks.withType<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar> {
|
||||
// mergeServiceFiles()
|
||||
// dependencies {
|
||||
// include(dependency("com.mysql:mysql-connector-j"))
|
||||
// }
|
||||
//}
|
||||
|
||||
//tasks.withType<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar> {
|
||||
// manifest {
|
||||
// attributes["Main-Class"] = "com.alttd.altitudeweb.AltitudeWebApplication"
|
||||
// }
|
||||
// archiveBaseName.set("altitudeweb")
|
||||
// archiveClassifier.set("")
|
||||
// mergeServiceFiles()
|
||||
//
|
||||
// // Include everything
|
||||
// from(sourceSets.main.get().output)
|
||||
//
|
||||
// // Include all project dependencies
|
||||
// configurations = listOf(project.configurations.runtimeClasspath.get())
|
||||
//
|
||||
// // Ensure MySQL is included (even though it should be part of runtimeClasspath already)
|
||||
// dependencies {
|
||||
// include(dependency("com.mysql:mysql-connector-j"))
|
||||
// }
|
||||
//
|
||||
// // Enable zip64 mode for large JARs
|
||||
// isZip64 = true
|
||||
//}
|
||||
//
|
||||
//// Make the shadowJar task the default jar task
|
||||
//tasks.named("jar") {
|
||||
// enabled = false
|
||||
//}
|
||||
//
|
||||
//tasks.named("assemble") {
|
||||
// dependsOn("shadowJar")
|
||||
//}
|
||||
tasks.register<Copy>("includeFrontend") {
|
||||
description = "Copy the built frontend to the Spring Boot static resources directory"
|
||||
dependsOn(":frontend:npmBuild")
|
||||
from("${project.rootDir}/frontend/dist")
|
||||
into(layout.buildDirectory.dir("resources/main/static"))
|
||||
doNotTrackState("Cannot reliably track state in the static directory")
|
||||
}
|
||||
|
||||
@@ -5,13 +5,13 @@ import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||
|
||||
@SpringBootApplication
|
||||
@SpringBootApplication(scanBasePackages = {"com.alttd.altitudeweb"})
|
||||
@EnableAspectJAutoProxy
|
||||
public class AltitudeWebApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AltitudeWebApplication.class, args);
|
||||
Connection.initDatabases();
|
||||
SpringApplication.run(AltitudeWebApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.alttd.altitudeweb.config;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class SecurityAuthFailureHandler implements AccessDeniedHandler, AuthenticationEntryPoint {
|
||||
|
||||
@Override
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response,
|
||||
AccessDeniedException accessDeniedException) throws IOException {
|
||||
log.warn("Access denied: User '{}' attempted to access '{}' without proper permissions",
|
||||
request.getUserPrincipal() != null ? request.getUserPrincipal().getName() : "unknown",
|
||||
request.getRequestURI());
|
||||
|
||||
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException authException) throws IOException {
|
||||
log.warn("Authentication failure: Unauthenticated user attempted to access secured endpoint '{}'",
|
||||
request.getRequestURI());
|
||||
|
||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication Required");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package com.alttd.altitudeweb.config;
|
||||
|
||||
import com.alttd.altitudeweb.controllers.login.KeyPairService;
|
||||
import com.alttd.altitudeweb.model.PermissionClaimDto;
|
||||
import com.nimbusds.jose.jwk.JWK;
|
||||
import com.nimbusds.jose.jwk.JWKSet;
|
||||
import com.nimbusds.jose.jwk.RSAKey;
|
||||
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
|
||||
import com.nimbusds.jose.jwk.source.JWKSource;
|
||||
import com.nimbusds.jose.proc.SecurityContext;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
|
||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
import java.security.KeyPair;
|
||||
import java.security.interfaces.RSAPrivateKey;
|
||||
import java.security.interfaces.RSAPublicKey;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final KeyPairService keyPairService;
|
||||
private final SecurityAuthFailureHandler securityAuthFailureHandler;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
return http
|
||||
.authorizeHttpRequests(
|
||||
auth -> auth
|
||||
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
||||
.requestMatchers("/api/form/**").authenticated()
|
||||
.requestMatchers("/api/login/getUsername").authenticated()
|
||||
.requestMatchers("/api/mail/**").authenticated()
|
||||
.requestMatchers("/api/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||
.requestMatchers("/api/particles/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||
.requestMatchers("/api/files/save/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||
.requestMatchers("/api/history/admin/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||
.requestMatchers("/api/login/userLogin/**").permitAll()
|
||||
.anyRequest().permitAll()
|
||||
)
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.oauth2ResourceServer(
|
||||
oauth2 -> oauth2
|
||||
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
|
||||
.authenticationEntryPoint(securityAuthFailureHandler)
|
||||
.accessDeniedHandler(securityAuthFailureHandler)
|
||||
)
|
||||
.exceptionHandling(
|
||||
ex -> ex
|
||||
.authenticationEntryPoint(securityAuthFailureHandler)
|
||||
.accessDeniedHandler(securityAuthFailureHandler)
|
||||
)
|
||||
.sessionManagement(
|
||||
session -> session
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JwtEncoder jwtEncoder() {
|
||||
KeyPair keyPair = keyPairService.getJwtSigningKeyPair();
|
||||
JWK jwk = new RSAKey.Builder((RSAPublicKey) keyPair.getPublic())
|
||||
.privateKey((RSAPrivateKey) keyPair.getPrivate())
|
||||
.build();
|
||||
JWKSource<SecurityContext> jwkSource = new ImmutableJWKSet<>(new JWKSet(jwk));
|
||||
return new NimbusJwtEncoder(jwkSource);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JwtDecoder jwtDecoder() {
|
||||
KeyPair keyPair = keyPairService.getJwtSigningKeyPair();
|
||||
return NimbusJwtDecoder.withPublicKey((RSAPublicKey) keyPair.getPublic()).build();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Bean
|
||||
public JwtAuthenticationConverter jwtAuthenticationConverter() {
|
||||
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
|
||||
converter.setJwtGrantedAuthoritiesConverter(jwt -> {
|
||||
Map<String, Object> claims = jwt.getClaims();
|
||||
|
||||
Object authoritiesClaim = claims.get("authorities");
|
||||
if (authoritiesClaim instanceof List<?> authorities) {
|
||||
Collection<GrantedAuthority> authorityList = authorities.stream()
|
||||
.map(Object::toString)
|
||||
.map(SimpleGrantedAuthority::new)
|
||||
.collect(Collectors.toList());
|
||||
log.debug("Authorities found in authorities: {}", authorityList);
|
||||
return authorityList;
|
||||
}
|
||||
|
||||
Object scopeClaim = claims.get("scope");
|
||||
if (scopeClaim instanceof String scopeString) {
|
||||
Collection<GrantedAuthority> authorityList = Arrays.stream(scopeString.split(" "))
|
||||
.map(scope -> new SimpleGrantedAuthority("SCOPE_" + scope))
|
||||
.collect(Collectors.toList());
|
||||
log.debug("Authorities found in authorities scope string: {}", authorityList);
|
||||
return authorityList;
|
||||
}
|
||||
|
||||
if (scopeClaim instanceof List<?> scopeList) {
|
||||
Collection<GrantedAuthority> authorityList = scopeList.stream()
|
||||
.map(Object::toString)
|
||||
.map(scope -> new SimpleGrantedAuthority("SCOPE_" + scope))
|
||||
.collect(Collectors.toList());
|
||||
log.debug("Authorities found in authorities scope list: {}", authorityList);
|
||||
return authorityList;
|
||||
}
|
||||
|
||||
log.debug("No granted authorities found");
|
||||
return Collections.emptyList();
|
||||
});
|
||||
return converter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.alttd.altitudeweb.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.resource.PathResourceResolver;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Slf4j @Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry.addResourceHandler("/**")
|
||||
.addResourceLocations("classpath:/static/browser")
|
||||
.resourceChain(true)
|
||||
.addResolver(new PathResourceResolver() {
|
||||
@Override
|
||||
protected Resource getResource(String resourcePath, Resource location) throws IOException {
|
||||
Resource requestedResource = location.createRelative(resourcePath);
|
||||
|
||||
if (requestedResource.exists() && requestedResource.isReadable()) {
|
||||
log.debug("Serving resource {} from {}", resourcePath, location);
|
||||
return requestedResource;
|
||||
}
|
||||
|
||||
log.debug("Resource {} not found in {}, serving index.html", resourcePath, location);
|
||||
|
||||
return new ClassPathResource("/static/browser/index.html");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Controller
|
||||
public static class HomeController {
|
||||
@GetMapping("/")
|
||||
public String index() {
|
||||
return "forward:/index.html";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,7 +18,7 @@ public class CorsConfig implements WebMvcConfigurer {
|
||||
log.info("Registering CORS mappings for {}", String.join(", ", allowedOrigins));
|
||||
registry.addMapping("/**")
|
||||
.allowedOrigins(allowedOrigins)
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true);
|
||||
}
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
package com.alttd.altitudeweb.controllers.data_from_auth;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.oauth2.jwt.Jwt;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class AuthenticatedUuid {
|
||||
@Value("${UNSECURED:#{false}}")
|
||||
private boolean unsecured;
|
||||
|
||||
/**
|
||||
* Extracts and validates the authenticated user's UUID from the JWT token.
|
||||
*
|
||||
* @return The UUID of the authenticated user
|
||||
* @throws ResponseStatusException with 401 status if authentication is invalid
|
||||
*/
|
||||
public UUID getAuthenticatedUserUuid() {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
if (authentication == null || !(authentication.getPrincipal() instanceof Jwt jwt)) {
|
||||
if (unsecured) {
|
||||
return UUID.fromString("55e46bc3-2a29-4c53-850f-dbd944dc5c5f");
|
||||
}
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Authentication required");
|
||||
}
|
||||
|
||||
String stringUuid = jwt.getSubject();
|
||||
|
||||
try {
|
||||
return UUID.fromString(stringUuid);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid UUID format");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package com.alttd.altitudeweb.controllers.forms;
|
||||
|
||||
import com.alttd.altitudeweb.api.AppealsApi;
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.litebans.HistoryRecord;
|
||||
import com.alttd.altitudeweb.database.litebans.HistoryType;
|
||||
import com.alttd.altitudeweb.database.litebans.IdHistoryMapper;
|
||||
import com.alttd.altitudeweb.database.web_db.forms.Appeal;
|
||||
import com.alttd.altitudeweb.database.web_db.forms.AppealMapper;
|
||||
import com.alttd.altitudeweb.database.web_db.mail.EmailVerification;
|
||||
import com.alttd.altitudeweb.database.web_db.mail.EmailVerificationMapper;
|
||||
import com.alttd.altitudeweb.mappers.AppealDataMapper;
|
||||
import com.alttd.altitudeweb.model.DiscordAppealDto;
|
||||
import com.alttd.altitudeweb.model.FormResponseDto;
|
||||
import com.alttd.altitudeweb.model.MinecraftAppealDto;
|
||||
import com.alttd.altitudeweb.model.UpdateMailDto;
|
||||
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||
import com.alttd.altitudeweb.services.mail.AppealMail;
|
||||
import com.alttd.altitudeweb.setup.Connection;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatusCode;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@RateLimit(limit = 30, timeValue = 1, timeUnit = TimeUnit.HOURS)
|
||||
public class AppealController implements AppealsApi {
|
||||
|
||||
private final AppealDataMapper mapper;
|
||||
private final AppealMail appealMail;
|
||||
private final com.alttd.altitudeweb.services.discord.AppealDiscord appealDiscord;
|
||||
|
||||
@RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "discordAppeal")
|
||||
@Override
|
||||
public ResponseEntity<FormResponseDto> submitDiscordAppeal(DiscordAppealDto discordAppealDto) {
|
||||
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Discord appeals are not yet supported");
|
||||
}
|
||||
|
||||
@RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "minecraftAppeal")
|
||||
@Override
|
||||
public ResponseEntity<FormResponseDto> submitMinecraftAppeal(MinecraftAppealDto minecraftAppealDto) {
|
||||
boolean success = true;
|
||||
CompletableFuture<Appeal> appealCompletableFuture = new CompletableFuture<>();
|
||||
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sqlSession -> {
|
||||
log.debug("Loading history by id");
|
||||
try {
|
||||
Appeal appeal = mapper.minecraftAppealDtoToAppeal(minecraftAppealDto);
|
||||
sqlSession.getMapper(AppealMapper.class).createAppeal(appeal);
|
||||
appealCompletableFuture.complete(appeal);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load history count", e);
|
||||
appealCompletableFuture.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
Appeal appeal = appealCompletableFuture.join();
|
||||
HistoryRecord history = getHistory(appeal.historyType(), appeal.historyId());
|
||||
if (history == null) {
|
||||
throw new ResponseStatusException(HttpStatusCode.valueOf(404), "History not found");
|
||||
}
|
||||
// Send to Discord channels
|
||||
try {
|
||||
appealDiscord.sendAppealToDiscord(appeal, history);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send appeal {} to Discord", appeal.id(), e);
|
||||
success = false;
|
||||
}
|
||||
|
||||
appealMail.sendAppealNotification(appeal, history);
|
||||
|
||||
CompletableFuture<Optional<EmailVerification>> emailVerificationCompletableFuture = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sqlSession -> {
|
||||
log.debug("Retrieving mail by uuid and address");
|
||||
|
||||
EmailVerification verifiedMail = sqlSession.getMapper(EmailVerificationMapper.class)
|
||||
.findByUserAndEmail(appeal.uuid(), appeal.email().toLowerCase());
|
||||
emailVerificationCompletableFuture.complete(Optional.ofNullable(verifiedMail));
|
||||
});
|
||||
Optional<EmailVerification> optionalEmailVerification = emailVerificationCompletableFuture.join();
|
||||
|
||||
if (optionalEmailVerification.isEmpty()) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
EmailVerification emailVerification = optionalEmailVerification.get();
|
||||
if (!emailVerification.verified()) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
if (!success) {
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sqlSession -> {
|
||||
log.debug("Marking appeal {} as sent", appeal.id());
|
||||
sqlSession.getMapper(AppealMapper.class)
|
||||
.markAppealAsSent(appeal.id());
|
||||
});
|
||||
FormResponseDto appealResponseDto = new FormResponseDto(
|
||||
appeal.id().toString(),
|
||||
"Your appeal has been submitted. You will be notified when it has been reviewed.",
|
||||
true);
|
||||
return ResponseEntity.ok().body(appealResponseDto);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<FormResponseDto> updateMail(UpdateMailDto updateMailDto) {
|
||||
//TODO move to its own endpoint
|
||||
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Updating mail is not yet supported");
|
||||
}
|
||||
|
||||
private HistoryRecord getHistory(String type, int id) {
|
||||
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||
CompletableFuture<HistoryRecord> historyRecordCompletableFuture = new CompletableFuture<>();
|
||||
|
||||
Connection.getConnection(Databases.LITE_BANS)
|
||||
.runQuery(sqlSession -> {
|
||||
log.debug("Loading history by id");
|
||||
try {
|
||||
HistoryRecord punishment = sqlSession.getMapper(IdHistoryMapper.class)
|
||||
.getRecentHistory(historyTypeEnum, id);
|
||||
historyRecordCompletableFuture.complete(punishment);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load history count", e);
|
||||
historyRecordCompletableFuture.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
return historyRecordCompletableFuture.join();
|
||||
}
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
package com.alttd.altitudeweb.controllers.forms;
|
||||
|
||||
import com.alttd.altitudeweb.api.ApplicationsApi;
|
||||
import com.alttd.altitudeweb.controllers.data_from_auth.AuthenticatedUuid;
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.luckperms.UUIDUsernameMapper;
|
||||
import com.alttd.altitudeweb.database.web_db.forms.StaffApplication;
|
||||
import com.alttd.altitudeweb.database.web_db.forms.StaffApplicationMapper;
|
||||
import com.alttd.altitudeweb.database.web_db.mail.EmailVerification;
|
||||
import com.alttd.altitudeweb.database.web_db.mail.EmailVerificationMapper;
|
||||
import com.alttd.altitudeweb.mappers.StaffApplicationDataMapper;
|
||||
import com.alttd.altitudeweb.model.FormResponseDto;
|
||||
import com.alttd.altitudeweb.model.StaffApplicationDto;
|
||||
import com.alttd.altitudeweb.services.discord.StaffApplicationDiscord;
|
||||
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||
import com.alttd.altitudeweb.services.mail.StaffApplicationMail;
|
||||
import com.alttd.altitudeweb.setup.Connection;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@AllArgsConstructor
|
||||
@RateLimit(limit = 30, timeValue = 1, timeUnit = TimeUnit.HOURS)
|
||||
public class ApplicationController implements ApplicationsApi {
|
||||
|
||||
private final AuthenticatedUuid authenticatedUuid;
|
||||
private final StaffApplicationDataMapper staffApplicationDataMapper;
|
||||
private final StaffApplicationMail staffApplicationMail;
|
||||
private final StaffApplicationDiscord staffApplicationDiscord;
|
||||
|
||||
private final Instant open = Instant.parse("2025-10-18T00:00:00Z");
|
||||
private final Instant close = Instant.parse("2025-10-26T00:00:00Z");
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Boolean> getStaffApplicationsIsOpen() {
|
||||
return ResponseEntity.ok(isOpen());
|
||||
}
|
||||
|
||||
private boolean isOpen() {
|
||||
Instant now = Instant.now();
|
||||
return !now.isBefore(open) && !now.isAfter(close);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<FormResponseDto> submitStaffApplication(StaffApplicationDto staffApplicationDto) {
|
||||
if (!isOpen()) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
UUID userUuid = authenticatedUuid.getAuthenticatedUserUuid();
|
||||
|
||||
String email = staffApplicationDto.getEmail() == null ? null : staffApplicationDto.getEmail().toLowerCase();
|
||||
Optional<EmailVerification> optionalEmail = fetchEmailVerification(userUuid, email);
|
||||
if (optionalEmail.isEmpty() || !optionalEmail.get().verified()) {
|
||||
log.warn("User {} attempted to submit an application without a verified email {}", userUuid, email);
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
// Map and persist application
|
||||
StaffApplication application = staffApplicationDataMapper.map(userUuid, staffApplicationDto);
|
||||
saveApplication(application);
|
||||
|
||||
String username = getUsername(userUuid);
|
||||
|
||||
try {
|
||||
if (!staffApplicationMail.sendApplicationEmail(username, application)) {
|
||||
log.warn("Failed to send staff application email for {}", application.id());
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error while sending staff application email for {}", application.id(), e);
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
|
||||
try {
|
||||
staffApplicationDiscord.sendApplicationToDiscord(username, application);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send staff application {} to Discord", application.id(), e);
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
try {
|
||||
markAsSent(application.id());
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to mark application {} as sent", application.id(), e);
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
FormResponseDto response = buildResponse(application);
|
||||
return ResponseEntity.status(200).body(response);
|
||||
}
|
||||
|
||||
private void saveApplication(StaffApplication application) {
|
||||
CompletableFuture<Void> saveFuture = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sqlSession -> {
|
||||
try {
|
||||
sqlSession.getMapper(StaffApplicationMapper.class).insert(application);
|
||||
saveFuture.complete(null);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to insert staff application", e);
|
||||
saveFuture.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
saveFuture.join();
|
||||
}
|
||||
|
||||
private Optional<EmailVerification> fetchEmailVerification(UUID userUuid, String email) {
|
||||
CompletableFuture<Optional<EmailVerification>> emailVerificationFuture = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sqlSession -> {
|
||||
EmailVerification verifiedMail = sqlSession.getMapper(EmailVerificationMapper.class)
|
||||
.findByUserAndEmail(userUuid, email);
|
||||
emailVerificationFuture.complete(Optional.ofNullable(verifiedMail));
|
||||
});
|
||||
return emailVerificationFuture.join();
|
||||
}
|
||||
|
||||
private void markAsSent(UUID applicationId) {
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sqlSession -> sqlSession.getMapper(StaffApplicationMapper.class).markAsSent(applicationId));
|
||||
}
|
||||
|
||||
private FormResponseDto buildResponse(StaffApplication application) {
|
||||
String message = "Your staff application has been submitted. You will be notified when it has been reviewed.";
|
||||
return new FormResponseDto(
|
||||
application.id().toString(),
|
||||
message,
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
private String getUsername(UUID uuid) {
|
||||
CompletableFuture<String> usernameFuture = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.LUCK_PERMS)
|
||||
.runQuery(sqlSession -> {
|
||||
log.debug("Loading username for uuid {}", uuid);
|
||||
try {
|
||||
String username = sqlSession.getMapper(UUIDUsernameMapper.class).getUsernameFromUUID(uuid.toString());
|
||||
usernameFuture.complete(username);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load username for uuid {}", uuid, e);
|
||||
usernameFuture.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
return usernameFuture.join();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package com.alttd.altitudeweb.controllers.forms;
|
||||
|
||||
import com.alttd.altitudeweb.api.MailApi;
|
||||
import com.alttd.altitudeweb.controllers.data_from_auth.AuthenticatedUuid;
|
||||
import com.alttd.altitudeweb.database.web_db.mail.EmailVerification;
|
||||
import com.alttd.altitudeweb.model.MailResponseDto;
|
||||
import com.alttd.altitudeweb.model.SubmitEmailDto;
|
||||
import com.alttd.altitudeweb.model.VerifyCodeDto;
|
||||
import com.alttd.altitudeweb.model.EmailEntryDto;
|
||||
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||
import com.alttd.altitudeweb.services.mail.MailVerificationService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RateLimit(limit = 60, timeValue = 1, timeUnit = TimeUnit.HOURS)
|
||||
public class MailController implements MailApi {
|
||||
|
||||
private final MailVerificationService mailVerificationService;
|
||||
private final AuthenticatedUuid authenticatedUuid;
|
||||
|
||||
@Override
|
||||
@RateLimit(limit = 5, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "mailSubmit")
|
||||
public ResponseEntity<MailResponseDto> submitEmailForVerification(SubmitEmailDto submitEmailDto) {
|
||||
UUID uuid = authenticatedUuid.getAuthenticatedUserUuid();
|
||||
boolean emailAlreadyVerified = mailVerificationService.listAll(uuid).stream()
|
||||
.filter(EmailVerification::verified)
|
||||
.map(EmailVerification::email)
|
||||
.anyMatch(mail -> mail.equalsIgnoreCase(submitEmailDto.getEmail()));
|
||||
if (emailAlreadyVerified) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Email already verified for user");
|
||||
}
|
||||
|
||||
EmailVerification saved = mailVerificationService.submitEmail(uuid, submitEmailDto.getEmail());
|
||||
MailResponseDto response = new MailResponseDto()
|
||||
.email(saved.email())
|
||||
.message("Verification email sent")
|
||||
.verified(false);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RateLimit(limit = 20, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "mailVerify")
|
||||
public ResponseEntity<MailResponseDto> verifyEmailCode(VerifyCodeDto verifyCodeDto) {
|
||||
UUID uuid = authenticatedUuid.getAuthenticatedUserUuid();
|
||||
Optional<EmailVerification> optionalEmailVerification = mailVerificationService.verifyCode(uuid, verifyCodeDto.getCode());
|
||||
if (optionalEmailVerification.isEmpty()) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid verification code");
|
||||
}
|
||||
EmailVerification emailVerification = optionalEmailVerification.get();
|
||||
MailResponseDto response = new MailResponseDto()
|
||||
.email(emailVerification.email())
|
||||
.message("Email verified successfully")
|
||||
.verified(true);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RateLimit(limit = 5, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "mailResend")
|
||||
public ResponseEntity<MailResponseDto> resendVerificationEmail(SubmitEmailDto submitEmailDto) {
|
||||
UUID uuid = authenticatedUuid.getAuthenticatedUserUuid();
|
||||
EmailVerification updated = mailVerificationService.resend(uuid, submitEmailDto.getEmail());
|
||||
if (updated == null) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Email not found for user");
|
||||
}
|
||||
MailResponseDto response = new MailResponseDto()
|
||||
.email(updated.email())
|
||||
.message("Verification email resent")
|
||||
.verified(false);
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
@RateLimit(limit = 10, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "mailDelete")
|
||||
public ResponseEntity<Void> deleteEmail(SubmitEmailDto submitEmailDto) {
|
||||
UUID uuid = authenticatedUuid.getAuthenticatedUserUuid();
|
||||
boolean deleted = mailVerificationService.delete(uuid, submitEmailDto.getEmail());
|
||||
if (!deleted) {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Email not found for user");
|
||||
}
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<List<EmailEntryDto>> getUserEmails() {
|
||||
UUID uuid = authenticatedUuid.getAuthenticatedUserUuid();
|
||||
List<EmailVerification> emails = mailVerificationService.listAll(uuid);
|
||||
List<EmailEntryDto> result = emails.stream()
|
||||
.map(ev -> new EmailEntryDto().email(ev.email()).verified(ev.verified()))
|
||||
.toList();
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
}
|
||||
+185
-32
@@ -1,16 +1,19 @@
|
||||
package com.alttd.altitudeweb.controllers.history;
|
||||
|
||||
import com.alttd.altitudeweb.api.HistoryApi;
|
||||
import com.alttd.altitudeweb.controllers.limits.RateLimit;
|
||||
import com.alttd.altitudeweb.controllers.data_from_auth.AuthenticatedUuid;
|
||||
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||
import com.alttd.altitudeweb.model.HistoryCountDto;
|
||||
import com.alttd.altitudeweb.model.PunishmentHistoryListDto;
|
||||
import com.alttd.altitudeweb.setup.Connection;
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.litebans.*;
|
||||
import com.alttd.altitudeweb.model.PunishmentHistoryDto;
|
||||
import com.alttd.altitudeweb.model.PunishmentHistoryInnerDto;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
@@ -19,19 +22,22 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RateLimit(limit = 30, timeValue = 1, timeUnit = TimeUnit.MINUTES)
|
||||
@RateLimit(limit = 30, timeValue = 10, timeUnit = TimeUnit.SECONDS)
|
||||
@RequiredArgsConstructor
|
||||
public class HistoryApiController implements HistoryApi {
|
||||
|
||||
private final AuthenticatedUuid authenticatedUuid;
|
||||
|
||||
@Override
|
||||
public ResponseEntity<PunishmentHistoryDto> getHistoryForAll(String userType, String type, Integer page) {
|
||||
public ResponseEntity<PunishmentHistoryListDto> getHistoryForAll(String userType, String type, Integer page) {
|
||||
return getHistoryForUsers(userType, type, "", page);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<PunishmentHistoryDto> getHistoryForUsers(String userType, String type, String user, Integer page) {
|
||||
public ResponseEntity<PunishmentHistoryListDto> getHistoryForUsers(String userType, String type, String user, Integer page) {
|
||||
UserType userTypeEnum = UserType.getUserType(userType);
|
||||
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||
PunishmentHistoryDto punishmentHistory = new PunishmentHistoryDto();
|
||||
PunishmentHistoryListDto punishmentHistoryList = new PunishmentHistoryListDto();
|
||||
CompletableFuture<List<HistoryRecord>> historyRecords = new CompletableFuture<>();
|
||||
|
||||
Connection.getConnection(Databases.LITE_BANS)
|
||||
@@ -46,14 +52,14 @@ public class HistoryApiController implements HistoryApi {
|
||||
historyRecords.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
return mapPunishmentHistory(punishmentHistory, historyRecords);
|
||||
return mapPunishmentHistory(punishmentHistoryList, historyRecords);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<PunishmentHistoryDto> getHistoryForUuid(String userType, String type, String uuid, Integer page) {
|
||||
public ResponseEntity<PunishmentHistoryListDto> getHistoryForUuid(String userType, String type, String uuid, Integer page) {
|
||||
UserType userTypeEnum = UserType.getUserType(userType);
|
||||
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||
PunishmentHistoryDto punishmentHistory = new PunishmentHistoryDto();
|
||||
PunishmentHistoryListDto punishmentHistoryList = new PunishmentHistoryListDto();
|
||||
CompletableFuture<List<HistoryRecord>> historyRecords = new CompletableFuture<>();
|
||||
|
||||
Connection.getConnection(Databases.LITE_BANS)
|
||||
@@ -68,7 +74,7 @@ public class HistoryApiController implements HistoryApi {
|
||||
historyRecords.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
return mapPunishmentHistory(punishmentHistory, historyRecords);
|
||||
return mapPunishmentHistory(punishmentHistoryList, historyRecords);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -153,6 +159,44 @@ public class HistoryApiController implements HistoryApi {
|
||||
return ResponseEntity.ok().body(searchResultCountCompletableFuture.join());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<PunishmentHistoryListDto> getAllHistoryForUUID(String uuid) {
|
||||
PunishmentHistoryListDto punishmentHistoryList = new PunishmentHistoryListDto();
|
||||
CompletableFuture<List<HistoryRecord>> historyRecordsCompletableFuture = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.LITE_BANS).runQuery(sqlSession -> {
|
||||
log.debug("Loading all history for uuid {}", uuid);
|
||||
try {
|
||||
List<HistoryRecord> punishments = sqlSession.getMapper(UUIDHistoryMapper.class)
|
||||
.getAllHistoryForUUID(UUID.fromString(uuid));
|
||||
historyRecordsCompletableFuture.complete(punishments);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load all history for uuid {}", uuid, e);
|
||||
historyRecordsCompletableFuture.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
return mapPunishmentHistory(punishmentHistoryList, historyRecordsCompletableFuture);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<PunishmentHistoryDto> getHistoryById(String type, Integer id) {
|
||||
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||
CompletableFuture<HistoryRecord> historyRecordCompletableFuture = new CompletableFuture<>();
|
||||
|
||||
Connection.getConnection(Databases.LITE_BANS)
|
||||
.runQuery(sqlSession -> {
|
||||
log.debug("Loading history by id");
|
||||
try {
|
||||
HistoryRecord punishment = sqlSession.getMapper(IdHistoryMapper.class)
|
||||
.getRecentHistory(historyTypeEnum, id);
|
||||
historyRecordCompletableFuture.complete(punishment);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load history count", e);
|
||||
historyRecordCompletableFuture.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
return ResponseEntity.ok().body(mapPunishmentHistory(historyRecordCompletableFuture.join()));
|
||||
}
|
||||
|
||||
private ResponseEntity<HistoryCountDto> mapHistoryCount(HistoryCount historyCount) {
|
||||
HistoryCountDto historyCountDto = new HistoryCountDto();
|
||||
historyCountDto.setBans(historyCount.getBans());
|
||||
@@ -162,28 +206,137 @@ public class HistoryApiController implements HistoryApi {
|
||||
return ResponseEntity.ok().body(historyCountDto);
|
||||
}
|
||||
|
||||
private ResponseEntity<PunishmentHistoryDto> mapPunishmentHistory(PunishmentHistoryDto punishmentHistory, CompletableFuture<List<HistoryRecord>> historyRecords) {
|
||||
private ResponseEntity<PunishmentHistoryListDto> mapPunishmentHistory(PunishmentHistoryListDto punishmentHistoryList, CompletableFuture<List<HistoryRecord>> historyRecords) {
|
||||
historyRecords.join().forEach(historyRecord -> {
|
||||
PunishmentHistoryInnerDto.TypeEnum type = switch (historyRecord.getType().toLowerCase()) {
|
||||
case "ban" -> PunishmentHistoryInnerDto.TypeEnum.BAN;
|
||||
case "mute" -> PunishmentHistoryInnerDto.TypeEnum.MUTE;
|
||||
case "warn" -> PunishmentHistoryInnerDto.TypeEnum.WARN;
|
||||
case "kick" -> PunishmentHistoryInnerDto.TypeEnum.KICK;
|
||||
default -> throw new IllegalStateException("Unexpected value: " + historyRecord.getType());
|
||||
};
|
||||
PunishmentHistoryInnerDto innerDto = new PunishmentHistoryInnerDto()
|
||||
.uuid(historyRecord.getUuid())
|
||||
.username(historyRecord.getPunishedName())
|
||||
.reason(historyRecord.getReason())
|
||||
.punishedByUuid(historyRecord.getBannedByUuid())
|
||||
.punishedBy(historyRecord.getBannedByName())
|
||||
.removedBy(historyRecord.getRemovedByName())
|
||||
.punishmentTime(historyRecord.getTime())
|
||||
.expiryTime(historyRecord.getUntil())
|
||||
.removedReason(historyRecord.getRemovedByReason())
|
||||
.type(type);
|
||||
punishmentHistory.add(innerDto);
|
||||
PunishmentHistoryDto innerDto = mapPunishmentHistory(historyRecord);
|
||||
punishmentHistoryList.add(innerDto);
|
||||
});
|
||||
return ResponseEntity.ok().body(punishmentHistory);
|
||||
return ResponseEntity.ok().body(punishmentHistoryList);
|
||||
}
|
||||
|
||||
private static PunishmentHistoryDto mapPunishmentHistory(HistoryRecord historyRecord) {
|
||||
PunishmentHistoryDto.TypeEnum type = switch (historyRecord.getType().toLowerCase()) {
|
||||
case "ban" -> PunishmentHistoryDto.TypeEnum.BAN;
|
||||
case "mute" -> PunishmentHistoryDto.TypeEnum.MUTE;
|
||||
case "warn" -> PunishmentHistoryDto.TypeEnum.WARN;
|
||||
case "kick" -> PunishmentHistoryDto.TypeEnum.KICK;
|
||||
default -> throw new IllegalStateException("Unexpected value: " + historyRecord.getType());
|
||||
};
|
||||
return new PunishmentHistoryDto()
|
||||
.uuid(historyRecord.getUuid())
|
||||
.username(historyRecord.getPunishedName())
|
||||
.reason(historyRecord.getReason())
|
||||
.punishedByUuid(historyRecord.getBannedByUuid())
|
||||
.punishedBy(historyRecord.getBannedByName())
|
||||
.removedBy(historyRecord.getRemovedByName())
|
||||
.punishmentTime(historyRecord.getTime())
|
||||
.expiryTime(historyRecord.getUntil())
|
||||
.removedReason(historyRecord.getRemovedByReason())
|
||||
.type(type)
|
||||
.id(historyRecord.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<PunishmentHistoryDto> updatePunishmentReason(String type, Integer id, String reason) {
|
||||
log.debug("Updating reason for {} id {} to {}", type, id, reason);
|
||||
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||
CompletableFuture<PunishmentHistoryDto> result = new CompletableFuture<>();
|
||||
|
||||
Connection.getConnection(Databases.LITE_BANS).runQuery(sqlSession -> {
|
||||
try {
|
||||
IdHistoryMapper idMapper = sqlSession.getMapper(IdHistoryMapper.class);
|
||||
EditHistoryMapper editMapper = sqlSession.getMapper(EditHistoryMapper.class);
|
||||
HistoryRecord before = idMapper.getRecentHistory(historyTypeEnum, id);
|
||||
if (before == null) {
|
||||
result.complete(null);
|
||||
return;
|
||||
}
|
||||
int changed = editMapper.setReason(historyTypeEnum, id, reason);
|
||||
HistoryRecord after = idMapper.getRecentHistory(historyTypeEnum, id);
|
||||
UUID actor = authenticatedUuid.getAuthenticatedUserUuid();
|
||||
log.info("[Punishment Edit] Actor={} Type={} Id={} Reason: '{}' -> '{}' (rows={})",
|
||||
actor, historyTypeEnum, id, before.getReason(), after != null ? after.getReason() : null, changed);
|
||||
result.complete(after != null ? mapPunishmentHistory(after) : null);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to update reason for {} id {}", type, id, e);
|
||||
result.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
PunishmentHistoryDto body = result.join();
|
||||
if (body == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<PunishmentHistoryDto> updatePunishmentUntil(String type, Integer id, Long until) {
|
||||
log.debug("Updating until for {} id {} to {}", type, id, until);
|
||||
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||
CompletableFuture<PunishmentHistoryDto> result = new CompletableFuture<>();
|
||||
|
||||
Connection.getConnection(Databases.LITE_BANS).runQuery(sqlSession -> {
|
||||
try {
|
||||
IdHistoryMapper idMapper = sqlSession.getMapper(IdHistoryMapper.class);
|
||||
EditHistoryMapper editMapper = sqlSession.getMapper(EditHistoryMapper.class);
|
||||
HistoryRecord before = idMapper.getRecentHistory(historyTypeEnum, id);
|
||||
if (before == null) {
|
||||
result.complete(null);
|
||||
return;
|
||||
}
|
||||
int changed = editMapper.setUntil(historyTypeEnum, id, until);
|
||||
HistoryRecord after = idMapper.getRecentHistory(historyTypeEnum, id);
|
||||
UUID actor = authenticatedUuid.getAuthenticatedUserUuid();
|
||||
log.info("[Punishment Edit] Actor={} Type={} Id={} Until: '{}' -> '{}' (rows={})",
|
||||
actor, historyTypeEnum, id, before.getUntil(), after != null ? after.getUntil() : null, changed);
|
||||
result.complete(after != null ? mapPunishmentHistory(after) : null);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Invalid until edit for type {} id {}: {}", type, id, e.getMessage());
|
||||
result.complete(null);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to update until for {} id {}", type, id, e);
|
||||
result.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
PunishmentHistoryDto body = result.join();
|
||||
if (body == null) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.ok(body);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> removePunishment(String type, Integer id) {
|
||||
log.debug("Removing punishment for {} id {}", type, id);
|
||||
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||
CompletableFuture<Boolean> result = new CompletableFuture<>();
|
||||
|
||||
Connection.getConnection(Databases.LITE_BANS).runQuery(sqlSession -> {
|
||||
try {
|
||||
IdHistoryMapper idMapper = sqlSession.getMapper(IdHistoryMapper.class);
|
||||
EditHistoryMapper editMapper = sqlSession.getMapper(EditHistoryMapper.class);
|
||||
HistoryRecord before = idMapper.getRecentHistory(historyTypeEnum, id);
|
||||
if (before == null) {
|
||||
result.complete(false);
|
||||
return;
|
||||
}
|
||||
UUID actorUuid = authenticatedUuid.getAuthenticatedUserUuid();
|
||||
String actorName = sqlSession.getMapper(RecentNamesMapper.class).getUsername(actorUuid.toString());
|
||||
int changed = editMapper.remove(historyTypeEnum, id);
|
||||
log.info("[Punishment Remove] Actor={} ({}) Type={} Id={} Before(active={} removedBy={} reason='{}') (rows={})",
|
||||
actorName, actorUuid, historyTypeEnum, id,
|
||||
before.getRemovedByName() == null ? 1 : 0, before.getRemovedByName(), before.getRemovedByReason(),
|
||||
changed);
|
||||
result.complete(changed > 0);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to remove punishment for {} id {}", type, id, e);
|
||||
result.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
Boolean ok = result.join();
|
||||
if (ok == null || !ok) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.alttd.altitudeweb.controllers.login;
|
||||
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.web_db.KeyPairEntity;
|
||||
import com.alttd.altitudeweb.database.web_db.KeyPairMapper;
|
||||
import com.alttd.altitudeweb.setup.Connection;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.security.*;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class KeyPairService {
|
||||
|
||||
private KeyPair cachedKeyPair = null;
|
||||
private static final String RSA_ALGORITHM = "RSA";
|
||||
private static final int RSA_KEY_SIZE = 2048;
|
||||
|
||||
public KeyPair getJwtSigningKeyPair() {
|
||||
if (cachedKeyPair != null) {
|
||||
return cachedKeyPair;
|
||||
}
|
||||
KeyPair keyPair = getOrCreateKeyPair();
|
||||
if (keyPair != null) {
|
||||
cachedKeyPair = keyPair;
|
||||
return cachedKeyPair;
|
||||
}
|
||||
throw new IllegalStateException("Failed to generate or load key pair");
|
||||
}
|
||||
|
||||
public KeyPair getOrCreateKeyPair() {
|
||||
CompletableFuture<KeyPairEntity> keyPairFuture = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sqlSession -> {
|
||||
log.debug("Loading key pair");
|
||||
try {
|
||||
KeyPairEntity entity = sqlSession.getMapper(KeyPairMapper.class).getKeyPair();
|
||||
keyPairFuture.complete(entity);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to key pair", e);
|
||||
keyPairFuture.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
KeyPairEntity keyPairEntity = keyPairFuture.join();
|
||||
if (keyPairEntity != null) {
|
||||
try {
|
||||
byte[] privateKeyBytes = Base64.getDecoder().decode(keyPairEntity.getPrivateKey());
|
||||
byte[] publicKeyBytes = Base64.getDecoder().decode(keyPairEntity.getPublicKey());
|
||||
|
||||
KeyFactory keyFactory = KeyFactory.getInstance(RSA_ALGORITHM);
|
||||
PrivateKey privateKey = keyFactory.generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes));
|
||||
PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(publicKeyBytes));
|
||||
|
||||
return new KeyPair(publicKey, privateKey);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load key pair from database", e);
|
||||
}
|
||||
}
|
||||
|
||||
KeyPair keyPair = generateKeyPair();
|
||||
|
||||
try {
|
||||
KeyPairEntity entity = new KeyPairEntity();
|
||||
entity.setPrivateKey(Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded()));
|
||||
entity.setPublicKey(Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()));
|
||||
entity.setCreatedAt(Instant.now());
|
||||
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sqlSession -> {
|
||||
log.debug("Saving key pair");
|
||||
try {
|
||||
sqlSession.getMapper(KeyPairMapper.class).save(entity);
|
||||
log.info("Generated and saved new key pair");
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to key pair", e);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to save key pair to database", e);
|
||||
}
|
||||
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
private KeyPair generateKeyPair() {
|
||||
try {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(RSA_ALGORITHM);
|
||||
keyPairGenerator.initialize(RSA_KEY_SIZE);
|
||||
return keyPairGenerator.generateKeyPair();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new IllegalStateException("Error generating key pair", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
package com.alttd.altitudeweb.controllers.login;
|
||||
|
||||
import com.alttd.altitudeweb.api.LoginApi;
|
||||
import com.alttd.altitudeweb.controllers.data_from_auth.AuthenticatedUuid;
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.litebans.RecentNamesMapper;
|
||||
import com.alttd.altitudeweb.database.web_db.PrivilegedUser;
|
||||
import com.alttd.altitudeweb.database.web_db.PrivilegedUserMapper;
|
||||
import com.alttd.altitudeweb.model.PermissionClaimDto;
|
||||
import com.alttd.altitudeweb.model.UsernameDto;
|
||||
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||
import com.alttd.altitudeweb.setup.Connection;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@EnableScheduling
|
||||
@RestController
|
||||
public class LoginController implements LoginApi {
|
||||
|
||||
private final JwtEncoder jwtEncoder;
|
||||
private final AuthenticatedUuid authenticatedUuid;
|
||||
|
||||
@Value("${login.secret:#{null}}")
|
||||
private String loginSecret;
|
||||
|
||||
@Value("${my-server.address:#{null}}")
|
||||
private String serverAddress;
|
||||
|
||||
private record CacheEntry(UUID uuid, Instant expiry) {
|
||||
}
|
||||
|
||||
private static final ConcurrentMap<String, CacheEntry> cache = new ConcurrentHashMap<>();
|
||||
|
||||
@Scheduled(fixedRate = 300000) // 5 minutes in milliseconds
|
||||
protected void clearExpiredCacheEntries() {
|
||||
Instant now = Instant.now();
|
||||
int initialCacheSize = cache.size();
|
||||
cache.entrySet().removeIf(entry -> entry.getValue().expiry().isBefore(now));
|
||||
log.info("Cleared {} expired cache entries", initialCacheSize - cache.size());
|
||||
}
|
||||
|
||||
@RateLimit(limit = 100, timeValue = 1, timeUnit = TimeUnit.MINUTES, key = "addLogin")
|
||||
@Override
|
||||
public ResponseEntity<String> requestLogin(String authorization, String uuid) {
|
||||
UUID uuidFromString;
|
||||
try {
|
||||
uuidFromString = UUID.fromString(uuid);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
log.info("{} is requesting a login code", uuid);
|
||||
|
||||
if (authorization == null || !authorization.startsWith("SECRET ")) {
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
|
||||
String secret = authorization.substring("SECRET ".length());
|
||||
if (!isValidSecret(secret)) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Invalid secret");
|
||||
}
|
||||
|
||||
Optional<String> key = cache.entrySet().stream()
|
||||
.filter(entry -> entry.getValue().uuid.equals(uuidFromString))
|
||||
.map(Map.Entry::getKey)
|
||||
.findFirst();
|
||||
|
||||
if (key.isPresent()) {
|
||||
log.info("{} got cached key: {}", uuid, key.get());
|
||||
return ResponseEntity.ok(key.get());
|
||||
}
|
||||
|
||||
String loginCode = generateLoginCode(uuidFromString);
|
||||
log.info("{} received login code: {}", uuid, loginCode);
|
||||
return ResponseEntity.ok(loginCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<UsernameDto> getUsername() {
|
||||
log.debug("Loading username for logged in user");
|
||||
|
||||
try {
|
||||
// Get authenticated UUID using the utility method
|
||||
UUID uuid = authenticatedUuid.getAuthenticatedUserUuid();
|
||||
log.debug("Loaded username for logged in user {}", uuid);
|
||||
|
||||
// Create response with username
|
||||
UsernameDto usernameDto = new UsernameDto();
|
||||
usernameDto.setUsername(getUsername(uuid));
|
||||
log.debug("Loaded username for logged in user {}", usernameDto.getUsername());
|
||||
|
||||
return ResponseEntity.ok(usernameDto);
|
||||
|
||||
} catch (ResponseStatusException e) {
|
||||
// The utility method already throws proper exceptions, we just need to convert them to ResponseEntity
|
||||
return ResponseEntity.status(e.getStatusCode()).build();
|
||||
}
|
||||
}
|
||||
|
||||
private String getUsername(UUID uuid) {
|
||||
CompletableFuture<String> username = new CompletableFuture<>();
|
||||
|
||||
Connection.getConnection(Databases.LITE_BANS)
|
||||
.runQuery(sqlSession -> {
|
||||
log.debug("Loading all history through logged in uuid");
|
||||
try {
|
||||
String temp = sqlSession
|
||||
.getMapper(RecentNamesMapper.class)
|
||||
.getUsername(uuid.toString());
|
||||
username.complete(temp);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to find username for uuid {}", uuid, e);
|
||||
username.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
return username.join();
|
||||
}
|
||||
|
||||
@Value("${UNSECURED:#{false}}")
|
||||
private boolean unsecured;
|
||||
|
||||
@RateLimit(limit = 5, timeValue = 1, timeUnit = TimeUnit.MINUTES, key = "login")
|
||||
@Override
|
||||
public ResponseEntity<String> login(String code) {
|
||||
if (unsecured) {
|
||||
log.warn("Unsecured login is enabled, skipping login validation!");
|
||||
} else {
|
||||
log.info("Received login request with code {}", code);
|
||||
}
|
||||
if (code == null) {
|
||||
log.warn("Received null login code");
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
CacheEntry cacheEntry = cache.get(code);
|
||||
if (!unsecured && (cacheEntry == null || cacheEntry.expiry().isBefore(Instant.now()))) {
|
||||
log.warn("Received invalid login code {}", code);
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
|
||||
if (unsecured && cacheEntry == null) {
|
||||
cacheEntry = new CacheEntry(UUID.fromString("55e46bc3-2a29-4c53-850f-dbd944dc5c5f"), Instant.now().plusSeconds(TimeUnit.DAYS.toSeconds(1)));
|
||||
}
|
||||
|
||||
String token = generateToken(cacheEntry.uuid);
|
||||
log.debug("Generated token for user {} with token {}", cacheEntry.uuid, token);
|
||||
|
||||
cache.remove(code);
|
||||
|
||||
log.debug("Generated token for user {}", cacheEntry.uuid);
|
||||
return ResponseEntity.ok(token);
|
||||
}
|
||||
|
||||
private String generateLoginCode(UUID uuid) {
|
||||
String characters = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
|
||||
StringBuilder loginCode = new StringBuilder();
|
||||
for (int i = 0; i < 8; i++) {
|
||||
int index = (int) (Math.random() * characters.length());
|
||||
loginCode.append(characters.charAt(index));
|
||||
}
|
||||
CacheEntry cacheEntry = new CacheEntry(uuid,
|
||||
Instant.now().plusSeconds(TimeUnit.MINUTES.toSeconds(15)));
|
||||
cache.put(loginCode.toString(), cacheEntry);
|
||||
return loginCode.toString();
|
||||
}
|
||||
|
||||
private boolean isValidSecret(String secret) {
|
||||
if (loginSecret == null) {
|
||||
log.warn("No login secret set, skipping secret validation");
|
||||
return false;
|
||||
}
|
||||
if (loginSecret.length() < 16) {
|
||||
log.warn("Login secret is too short, skipping secret validation");
|
||||
return false;
|
||||
}
|
||||
if (!loginSecret.equals(secret)) {
|
||||
log.info("Received invalid secret attempt");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private String generateToken(UUID uuid) {
|
||||
Instant now = Instant.now();
|
||||
//TODO make a JWT for renewing and one for storing permissions for a session (expiry 1 hour)
|
||||
Instant expiryTime = now.plusSeconds(TimeUnit.DAYS.toSeconds(30));
|
||||
CompletableFuture<Optional<PrivilegedUser>> privilegedUserCompletableFuture = new CompletableFuture<>();
|
||||
List<PermissionClaimDto> claimList = new ArrayList<>();
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sqlSession -> {
|
||||
try {
|
||||
log.debug("Loading user by uuid {}", uuid.toString());
|
||||
PrivilegedUserMapper mapper = sqlSession.getMapper(PrivilegedUserMapper.class);
|
||||
Optional<PrivilegedUser> privilegedUser = mapper
|
||||
.getUserByUuid(uuid);
|
||||
|
||||
if (privilegedUser.isEmpty()) {
|
||||
int privilegedUserId = mapper.createPrivilegedUser(uuid);
|
||||
privilegedUserCompletableFuture.complete(
|
||||
Optional.of(new PrivilegedUser(privilegedUserId, uuid, List.of())));
|
||||
} else {
|
||||
privilegedUserCompletableFuture.complete(privilegedUser);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load user by uuid", e);
|
||||
privilegedUserCompletableFuture.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
Optional<PrivilegedUser> privilegedUser = privilegedUserCompletableFuture.join();
|
||||
claimList.add(PermissionClaimDto.USER);
|
||||
privilegedUser.ifPresent(user -> user.getPermissions().forEach(permission -> {
|
||||
try {
|
||||
claimList.add(PermissionClaimDto.fromValue(permission));
|
||||
log.debug("Added permission claim {}", permission);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Received invalid permission claim: {}", permission);
|
||||
}
|
||||
}));
|
||||
log.debug("Generated token for user {} with claims {}", uuid.toString(),
|
||||
claimList.stream().map(PermissionClaimDto::getValue).toList());
|
||||
JwtClaimsSet claims = JwtClaimsSet.builder()
|
||||
.issuer(serverAddress)
|
||||
.claim("authorities",
|
||||
claimList.stream().map(PermissionClaimDto::getValue).toList())
|
||||
.issuedAt(now)
|
||||
.expiresAt(expiryTime)
|
||||
.subject(uuid.toString())
|
||||
.build();
|
||||
|
||||
return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
|
||||
}
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
package com.alttd.altitudeweb.controllers.particles;
|
||||
|
||||
import com.alttd.altitudeweb.api.ParticlesApi;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
public class ParticleController implements ParticlesApi {
|
||||
|
||||
@Value("${login.secret:#{null}}")
|
||||
private String loginSecret;
|
||||
|
||||
@Value("${particles.file_path}")
|
||||
private String particlesFilePath;
|
||||
|
||||
@Value("${notification.server.url:http://localhost:8080}")
|
||||
private String notificationServerUrl;
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Resource> downloadFile(String authorization, String filename) throws Exception {
|
||||
if (authorization == null || !authorization.equals(loginSecret)) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
File file = new File(particlesFilePath);
|
||||
if (!file.exists() || !file.isDirectory()) {
|
||||
log.error("Particles file path {} is not a directory, not downloading particles file", particlesFilePath);
|
||||
return ResponseEntity.status(404).build();
|
||||
}
|
||||
File targetFile = new File(file, filename);
|
||||
return getFileForDownload(targetFile, filename);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Resource> downloadFileForUser(String authorization, String uuid, String filename) throws Exception {
|
||||
if (authorization == null || !authorization.equals(loginSecret)) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
File file = new File(particlesFilePath);
|
||||
if (!file.exists() || !file.isDirectory()) {
|
||||
log.error("Particles file path {} is not a directory, not downloading particles user file", particlesFilePath);
|
||||
return ResponseEntity.status(404).build();
|
||||
}
|
||||
File targetDir = new File(file, uuid);
|
||||
if (targetDir.exists()) {
|
||||
return getFileForDownload(targetDir, filename);
|
||||
} else {
|
||||
log.warn("User {} does not have a directory for particles files", uuid);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
|
||||
private ResponseEntity<Resource> getFileForDownload(File file, String filename) {
|
||||
File targetFile = new File(file, filename);
|
||||
if (!targetFile.exists()) {
|
||||
log.warn("Particles file {} does not exist", targetFile.getAbsolutePath());
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
if (!targetFile.isFile()) {
|
||||
log.warn("Particles file {} is not a file", targetFile.getAbsolutePath());
|
||||
return ResponseEntity.status(404).build();
|
||||
}
|
||||
|
||||
try {
|
||||
Path path = targetFile.toPath();
|
||||
ByteArrayResource resource = new ByteArrayResource(Files.readAllBytes(path));
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.contentLength(targetFile.length())
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
|
||||
.body(resource);
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to read particles file {}: {}", targetFile.getAbsolutePath(), e.getMessage(), e);
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> saveFile(String filename, MultipartFile content) throws Exception {
|
||||
File file = new File(particlesFilePath);
|
||||
if (!file.exists() || !file.isDirectory()) {
|
||||
log.error("Particles file path {} is not a directory, not saving particles file", particlesFilePath);
|
||||
return ResponseEntity.status(404).build();
|
||||
}
|
||||
ResponseEntity<Void> voidResponseEntity = writeContentToFile(file, filename, content);
|
||||
notifyServerOfFileUpload(filename);
|
||||
return voidResponseEntity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> saveFileForUser(String uuid, String filename, MultipartFile content) throws Exception {
|
||||
File file = new File(particlesFilePath);
|
||||
if (!file.exists() || !file.isDirectory()) {
|
||||
log.error("Particles file path {} is not a directory, not saving particles user file", particlesFilePath);
|
||||
return ResponseEntity.status(404).build();
|
||||
}
|
||||
File targetDir = new File(file, uuid);
|
||||
if (!file.exists()) {
|
||||
log.debug("Creating particles directory {}", targetDir.getAbsolutePath());
|
||||
if (targetDir.mkdirs()) {
|
||||
log.info("Created particles user directory {}", targetDir.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
ResponseEntity<Void> voidResponseEntity = writeContentToFile(file, filename, content);
|
||||
notifyServerOfFileUpload(uuid, filename);
|
||||
return voidResponseEntity;
|
||||
}
|
||||
|
||||
private void notifyServerOfFileUpload(String filename) {
|
||||
String notificationUrl = String.format("%s/notify/%s.json", notificationServerUrl, filename);
|
||||
sendNotification(notificationUrl, String.format("file upload: %s", filename));
|
||||
}
|
||||
|
||||
private void notifyServerOfFileUpload(String uuid, String filename) {
|
||||
String notificationUrl = String.format("%s/notify/%s/%s.json", notificationServerUrl, uuid, filename);
|
||||
sendNotification(notificationUrl, String.format("file upload for user %s: %s", uuid, filename));
|
||||
}
|
||||
|
||||
private void sendNotification(String notificationUrl, String logDescription) {
|
||||
try {
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
ResponseEntity<String> response = restTemplate.getForEntity(notificationUrl, String.class);
|
||||
|
||||
if (response.getStatusCode().is2xxSuccessful()) {
|
||||
log.info("Successfully notified server of {}", logDescription);
|
||||
} else {
|
||||
log.warn("Failed to notify server of {}, status: {}",
|
||||
logDescription, response.getStatusCode());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error notifying server of {}", logDescription, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private ResponseEntity<Void> writeContentToFile(File dir, String filename, MultipartFile content) {
|
||||
File targetFile = new File(dir, filename);
|
||||
if (!Files.isWritable(targetFile.toPath())) {
|
||||
log.error("Particles file {} is not writable", targetFile.getAbsolutePath());
|
||||
return ResponseEntity.status(403).build();
|
||||
}
|
||||
|
||||
if (targetFile.exists()) {
|
||||
log.warn("Overwriting existing particles file {}", targetFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
try {
|
||||
content.transferTo(targetFile);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to write particles file {}", targetFile.getAbsolutePath(), e);
|
||||
return ResponseEntity.status(500).build();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.alttd.altitudeweb.controllers.site;
|
||||
|
||||
import com.alttd.altitudeweb.api.SiteApi;
|
||||
import com.alttd.altitudeweb.controllers.data_from_auth.AuthenticatedUuid;
|
||||
import com.alttd.altitudeweb.model.VoteDataDto;
|
||||
import com.alttd.altitudeweb.model.VoteStatsDto;
|
||||
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||
import com.alttd.altitudeweb.services.site.VoteService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RateLimit(limit = 2, timeValue = 10, timeUnit = TimeUnit.SECONDS)
|
||||
public class SiteController implements SiteApi {
|
||||
|
||||
private final VoteService voteService;
|
||||
private final AuthenticatedUuid authenticatedUuid;
|
||||
|
||||
@Override
|
||||
public ResponseEntity<VoteDataDto> getVoteStats() {
|
||||
UUID uuid = authenticatedUuid.getAuthenticatedUserUuid();
|
||||
Optional<VoteDataDto> optionalVoteDataDto = voteService.getVoteStats(uuid);
|
||||
if (optionalVoteDataDto.isEmpty()) {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
VoteDataDto voteDataDto = optionalVoteDataDto.get();
|
||||
return ResponseEntity.ok(voteDataDto);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.alttd.altitudeweb.controllers.team;
|
||||
|
||||
import com.alttd.altitudeweb.api.TeamApi;
|
||||
import com.alttd.altitudeweb.controllers.limits.RateLimit;
|
||||
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||
import com.alttd.altitudeweb.setup.Connection;
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.luckperms.Player;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.alttd.altitudeweb.mappers;
|
||||
|
||||
import com.alttd.altitudeweb.database.web_db.forms.Appeal;
|
||||
import com.alttd.altitudeweb.model.MinecraftAppealDto;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class AppealDataMapper {
|
||||
public MinecraftAppealDto appealToMinecraftAppealDto(Appeal appeal) {
|
||||
MinecraftAppealDto minecraftAppealDto = new MinecraftAppealDto();
|
||||
minecraftAppealDto.setAppeal(appeal.reason());
|
||||
minecraftAppealDto.setUsername(appeal.username());
|
||||
minecraftAppealDto.setUuid(appeal.uuid());
|
||||
minecraftAppealDto.setEmail(appeal.email());
|
||||
return minecraftAppealDto;
|
||||
}
|
||||
|
||||
public Appeal minecraftAppealDtoToAppeal(MinecraftAppealDto minecraftAppealDto) {
|
||||
return new Appeal(
|
||||
UUID.randomUUID(),
|
||||
minecraftAppealDto.getUuid(),
|
||||
minecraftAppealDto.getPunishmentType().toString(),
|
||||
minecraftAppealDto.getPunishmentId(),
|
||||
minecraftAppealDto.getUsername(),
|
||||
minecraftAppealDto.getAppeal(),
|
||||
Instant.now(),
|
||||
null,
|
||||
minecraftAppealDto.getEmail(),
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.alttd.altitudeweb.mappers;
|
||||
|
||||
import com.alttd.altitudeweb.database.web_db.forms.StaffApplication;
|
||||
import com.alttd.altitudeweb.model.StaffApplicationDto;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class StaffApplicationDataMapper {
|
||||
|
||||
/**
|
||||
* Maps the incoming DTO and the authenticated user's UUID to a StaffApplication entity.
|
||||
* Normalizes and prepares fields as needed (lowercase email, join availableDays, timestamps, ids).
|
||||
*/
|
||||
public StaffApplication map(UUID userUuid, StaffApplicationDto dto) {
|
||||
String email = dto.getEmail() == null ? null : dto.getEmail().toLowerCase();
|
||||
String availableDaysJoined = joinList(dto.getAvailableDays());
|
||||
|
||||
return new StaffApplication(
|
||||
UUID.randomUUID(),
|
||||
userUuid,
|
||||
email,
|
||||
dto.getAge(),
|
||||
dto.getDiscordUsername(),
|
||||
Boolean.TRUE.equals(dto.getMeetsRequirements()),
|
||||
dto.getPronouns(),
|
||||
dto.getJoinDate(),
|
||||
dto.getWeeklyPlaytime(),
|
||||
availableDaysJoined,
|
||||
dto.getAvailableTimes(),
|
||||
dto.getPreviousExperience(),
|
||||
dto.getPluginExperience(),
|
||||
dto.getModeratorExpectations(),
|
||||
dto.getAdditionalInfo(),
|
||||
Instant.now(),
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private String joinList(List<String> list) {
|
||||
if (list == null) return null;
|
||||
// Avoid NPEs and trim entries
|
||||
return list.stream()
|
||||
.filter(s -> s != null && !s.isBlank())
|
||||
.map(String::trim)
|
||||
.collect(Collectors.joining(","));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.alttd.altitudeweb.mappers;
|
||||
|
||||
import com.alttd.altitudeweb.database.votingplugin.VotingStatsRow;
|
||||
import com.alttd.altitudeweb.model.VoteDataDto;
|
||||
import com.alttd.altitudeweb.model.VoteInfoDto;
|
||||
import com.alttd.altitudeweb.model.VoteStatsDto;
|
||||
import com.alttd.altitudeweb.model.VoteStreakDto;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class VotingStatsRowToVoteDataDto {
|
||||
|
||||
public VoteDataDto map(VotingStatsRow votingStatsRow) {
|
||||
VoteDataDto voteDataDto = new VoteDataDto();
|
||||
voteDataDto.setVoteStats(getVoteStats(votingStatsRow));
|
||||
voteDataDto.setVoteStreak(getVoteStreak(votingStatsRow));
|
||||
voteDataDto.setBestVoteStreak(getBestVoteStreak(votingStatsRow));
|
||||
voteDataDto.setAllVoteInfo(getVoteInfo(votingStatsRow.lastVotes()));
|
||||
return voteDataDto;
|
||||
}
|
||||
|
||||
private VoteStreakDto getVoteStreak(VotingStatsRow votingStatsRow) {
|
||||
VoteStreakDto voteStreakDto = new VoteStreakDto();
|
||||
voteStreakDto.setDailyStreak(votingStatsRow.dailyStreak());
|
||||
voteStreakDto.setWeeklyStreak(votingStatsRow.weeklyStreak());
|
||||
voteStreakDto.setMonthlyStreak(votingStatsRow.monthlyStreak());
|
||||
return voteStreakDto;
|
||||
}
|
||||
|
||||
private VoteStreakDto getBestVoteStreak(VotingStatsRow votingStatsRow) {
|
||||
VoteStreakDto voteStreakDto = new VoteStreakDto();
|
||||
voteStreakDto.setDailyStreak(votingStatsRow.bestDailyStreak());
|
||||
voteStreakDto.setWeeklyStreak(votingStatsRow.bestWeeklyStreak());
|
||||
voteStreakDto.setMonthlyStreak(votingStatsRow.bestMonthlyStreak());
|
||||
return voteStreakDto;
|
||||
}
|
||||
|
||||
|
||||
private VoteStatsDto getVoteStats(VotingStatsRow votingStatsRow) {
|
||||
VoteStatsDto voteStatsDto = new VoteStatsDto();
|
||||
voteStatsDto.setDaily(votingStatsRow.totalVotesToday());
|
||||
voteStatsDto.setWeekly(votingStatsRow.totalVotesThisWeek());
|
||||
voteStatsDto.setMonthly(votingStatsRow.totalVotesThisMonth());
|
||||
voteStatsDto.setTotal(votingStatsRow.totalVotesAllTime());
|
||||
return voteStatsDto;
|
||||
}
|
||||
|
||||
private List<VoteInfoDto> getVoteInfo(String lastVotes) {
|
||||
return Arrays.stream(lastVotes.split("%line%"))
|
||||
.map(voteInfo -> {
|
||||
String[] siteAndTimestamp = voteInfo.split("//");
|
||||
VoteInfoDto voteInfoDto = new VoteInfoDto();
|
||||
voteInfoDto.setSiteName(siteAndTimestamp[0]);
|
||||
voteInfoDto.setLastVoteTimestamp(Long.parseLong(siteAndTimestamp[1]));
|
||||
return voteInfoDto;
|
||||
})
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package com.alttd.altitudeweb.services.discord;
|
||||
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.discord.OutputChannel;
|
||||
import com.alttd.altitudeweb.database.discord.OutputChannelMapper;
|
||||
import com.alttd.altitudeweb.database.litebans.HistoryCountMapper;
|
||||
import com.alttd.altitudeweb.database.litebans.HistoryRecord;
|
||||
import com.alttd.altitudeweb.database.litebans.HistoryType;
|
||||
import com.alttd.altitudeweb.database.litebans.UserType;
|
||||
import com.alttd.altitudeweb.database.web_db.forms.Appeal;
|
||||
import com.alttd.altitudeweb.setup.Connection;
|
||||
import com.alttd.webinterface.send_message.DiscordSender;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class AppealDiscord {
|
||||
|
||||
private static final String OUTPUT_TYPE = "APPEAL";
|
||||
|
||||
public void sendAppealToDiscord(Appeal appeal, HistoryRecord history) {
|
||||
// Fetch channels
|
||||
CompletableFuture<List<OutputChannel>> channelsFuture = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.DISCORD).runQuery(sql -> {
|
||||
try {
|
||||
List<OutputChannel> channels = sql.getMapper(OutputChannelMapper.class)
|
||||
.getChannelsWithOutputType(OUTPUT_TYPE);
|
||||
channelsFuture.complete(channels);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load output channels for {}", OUTPUT_TYPE, e);
|
||||
channelsFuture.complete(new ArrayList<>());
|
||||
}
|
||||
});
|
||||
|
||||
CompletableFuture<Integer> bansF = getCountAsync(HistoryType.BAN, appeal.uuid());
|
||||
CompletableFuture<Integer> mutesF = getCountAsync(HistoryType.MUTE, appeal.uuid());
|
||||
CompletableFuture<Integer> warnsF = getCountAsync(HistoryType.WARN, appeal.uuid());
|
||||
CompletableFuture<Integer> kicksF = getCountAsync(HistoryType.KICK, appeal.uuid());
|
||||
|
||||
List<OutputChannel> channels = channelsFuture.join();
|
||||
int bans = bansF.join();
|
||||
int mutes = mutesF.join();
|
||||
int warns = warnsF.join();
|
||||
int kicks = kicksF.join();
|
||||
|
||||
if (channels.isEmpty()) {
|
||||
log.warn("No Discord output channels found for type {}. Skipping Discord send.", OUTPUT_TYPE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build embed
|
||||
boolean active = history.getUntil() == null || history.getUntil() <= 0 || history.getUntil() > System.currentTimeMillis();
|
||||
String createdAt = formatInstant(appeal.createdAt());
|
||||
|
||||
List<DiscordSender.EmbedField> fields = new ArrayList<>();
|
||||
// Group: User
|
||||
fields.add(new DiscordSender.EmbedField(
|
||||
"User",
|
||||
"Username: " + safe(appeal.username()) + "\n" +
|
||||
"UUID: " + safe(String.valueOf(appeal.uuid())) + "\n" +
|
||||
"Email: " + safe(appeal.email()) + "\n" +
|
||||
"Submitted: " + createdAt,
|
||||
false
|
||||
));
|
||||
// Group: Punishment
|
||||
fields.add(new DiscordSender.EmbedField(
|
||||
"Punishment",
|
||||
"Type: " + safe(String.valueOf(appeal.historyType())) + "\n" +
|
||||
"ID: " + safe(String.valueOf(appeal.historyId())) + "\n" +
|
||||
"Reason: " + safe(history.getReason()) + "\n" +
|
||||
"Active: " + active,
|
||||
false
|
||||
));
|
||||
// Group: Previous punishments
|
||||
fields.add(new DiscordSender.EmbedField(
|
||||
"Previous punishments",
|
||||
"Bans: " + bans + "\n" +
|
||||
"Mutes: " + mutes + "\n" +
|
||||
"Warnings: " + warns + "\n" +
|
||||
"Kicks: " + kicks,
|
||||
true
|
||||
));
|
||||
|
||||
String description = safe(appeal.reason());
|
||||
|
||||
List<Long> channelIds = channels.stream()
|
||||
.map(OutputChannel::channel)
|
||||
.toList();
|
||||
|
||||
// colorRgb = null (use default), timestamp = appeal.createdAt if available
|
||||
Instant timestamp = appeal.createdAt() != null ? appeal.createdAt() : Instant.now();
|
||||
DiscordSender.getInstance().sendEmbedToChannels(
|
||||
channelIds,
|
||||
"New Appeal Submitted",
|
||||
description,
|
||||
fields,
|
||||
null,
|
||||
timestamp,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private CompletableFuture<Integer> getCountAsync(HistoryType type, java.util.UUID uuid) {
|
||||
CompletableFuture<Integer> future = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.LITE_BANS).runQuery(sql -> {
|
||||
try {
|
||||
Integer count = sql.getMapper(HistoryCountMapper.class)
|
||||
.getUuidPunishmentCount(type, UserType.PLAYER, uuid);
|
||||
future.complete(count == null ? 0 : count);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load punishment count for {} ({})", type, uuid, e);
|
||||
future.complete(0);
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
private String safe(String s) {
|
||||
return s == null ? "unknown" : s;
|
||||
}
|
||||
|
||||
private String formatInstant(Instant instant) {
|
||||
if (instant == null) return "unknown";
|
||||
return instant.atZone(ZoneId.of("UTC"))
|
||||
.format(DateTimeFormatter.ofPattern("yyyy MMMM dd hh:mm a '(UTC)'"));
|
||||
}
|
||||
}
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
package com.alttd.altitudeweb.services.discord;
|
||||
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.discord.OutputChannel;
|
||||
import com.alttd.altitudeweb.database.discord.OutputChannelMapper;
|
||||
import com.alttd.altitudeweb.database.web_db.forms.StaffApplication;
|
||||
import com.alttd.altitudeweb.setup.Connection;
|
||||
import com.alttd.webinterface.send_message.DiscordSender;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class StaffApplicationDiscord {
|
||||
|
||||
private static final String OUTPUT_TYPE = "STAFF_APPLICATION";
|
||||
|
||||
public void sendApplicationToDiscord(String username, StaffApplication application) {
|
||||
// Fetch channels for staff applications
|
||||
CompletableFuture<List<OutputChannel>> channelsFuture = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.DISCORD).runQuery(sql -> {
|
||||
try {
|
||||
List<OutputChannel> channels = sql.getMapper(OutputChannelMapper.class)
|
||||
.getChannelsWithOutputType(OUTPUT_TYPE);
|
||||
channelsFuture.complete(channels);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to load output channels for {}", OUTPUT_TYPE, e);
|
||||
channelsFuture.complete(new ArrayList<>());
|
||||
}
|
||||
});
|
||||
|
||||
List<OutputChannel> channels = channelsFuture.join();
|
||||
if (channels.isEmpty()) {
|
||||
log.warn("No Discord output channels found for type {}. Skipping Discord send.", OUTPUT_TYPE);
|
||||
return;
|
||||
}
|
||||
|
||||
// Build embed content
|
||||
List<DiscordSender.EmbedField> fields = new ArrayList<>();
|
||||
fields.add(new DiscordSender.EmbedField(
|
||||
"Applicant",
|
||||
"Username: " + safe(username) + "\n" +
|
||||
"Discord: " + safe(application.discordUsername()) + "\n" +
|
||||
"Email: " + safe(application.email()) + "\n" +
|
||||
"Age: " + safe(String.valueOf(application.age())) + "\n" +
|
||||
"Meets reqs: " + (application.meetsRequirements() != null && application.meetsRequirements()),
|
||||
false
|
||||
));
|
||||
fields.add(new DiscordSender.EmbedField(
|
||||
"Availability",
|
||||
"Days: " + safe(application.availableDays()) + "\n" +
|
||||
"Times: " + safe(application.availableTimes()),
|
||||
false
|
||||
));
|
||||
fields.add(new DiscordSender.EmbedField(
|
||||
"Experience",
|
||||
"Previous: " + safe(application.previousExperience()) + "\n" +
|
||||
"Plugins: " + safe(application.pluginExperience()) + "\n" +
|
||||
"Expectations: " + safe(application.moderatorExpectations()),
|
||||
false
|
||||
));
|
||||
if (application.additionalInfo() != null && !application.additionalInfo().isBlank()) {
|
||||
fields.add(new DiscordSender.EmbedField(
|
||||
"Additional Info",
|
||||
application.additionalInfo(),
|
||||
false
|
||||
));
|
||||
}
|
||||
|
||||
List<Long> channelIds = channels.stream()
|
||||
.map(OutputChannel::channel)
|
||||
.toList();
|
||||
|
||||
Instant timestamp = application.createdAt() != null ? application.createdAt() : Instant.now();
|
||||
DiscordSender.getInstance().sendEmbedToChannels(
|
||||
channelIds,
|
||||
"New Staff Application Submitted",
|
||||
"Join date: " + (application.joinDate() != null ? application.joinDate().toString() : "unknown") +
|
||||
"\nSubmitted: " + formatInstant(timestamp),
|
||||
fields,
|
||||
null,
|
||||
timestamp,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private String safe(String s) {
|
||||
return s == null ? "unknown" : s;
|
||||
}
|
||||
|
||||
private String formatInstant(Instant instant) {
|
||||
if (instant == null) return "unknown";
|
||||
return instant.atZone(ZoneId.of("UTC"))
|
||||
.format(DateTimeFormatter.ofPattern("yyyy MMMM dd hh:mm a '(UTC)'"));
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.alttd.altitudeweb.controllers.limits;
|
||||
package com.alttd.altitudeweb.services.limits;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.alttd.altitudeweb.controllers.limits;
|
||||
package com.alttd.altitudeweb.services.limits;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
package com.alttd.altitudeweb.controllers.limits;
|
||||
package com.alttd.altitudeweb.services.limits;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -26,8 +26,8 @@ public class RateLimitAspect {
|
||||
private final InMemoryRateLimiterService rateLimiterService;
|
||||
|
||||
@Around("""
|
||||
@annotation(com.alttd.altitudeweb.controllers.limits.RateLimit)
|
||||
|| @within(com.alttd.altitudeweb.controllers.limits.RateLimit)""")
|
||||
@annotation(com.alttd.altitudeweb.services.limits.RateLimit)
|
||||
|| @within(com.alttd.altitudeweb.services.limits.RateLimit)""")
|
||||
public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (requestAttributes == null) {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package com.alttd.altitudeweb.controllers.limits;
|
||||
package com.alttd.altitudeweb.services.limits;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.alttd.altitudeweb.services.mail;
|
||||
|
||||
import com.alttd.altitudeweb.database.litebans.HistoryRecord;
|
||||
import com.alttd.altitudeweb.database.web_db.forms.Appeal;
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.thymeleaf.context.Context;
|
||||
import org.thymeleaf.spring6.SpringTemplateEngine;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AppealMail {
|
||||
|
||||
private final JavaMailSender mailSender;
|
||||
private final SpringTemplateEngine templateEngine;
|
||||
|
||||
@Value("${spring.mail.username}")
|
||||
private String fromEmail;
|
||||
|
||||
private static final String APPEAL_EMAIL = "[email protected]";
|
||||
|
||||
/**
|
||||
* Sends an email notification about the appeal to both the user and the appeals team.
|
||||
*
|
||||
* @param appeal The appeal object containing all necessary information
|
||||
*/
|
||||
public void sendAppealNotification(Appeal appeal, HistoryRecord history) {
|
||||
try {
|
||||
sendEmailToAppealsTeam(appeal, history);
|
||||
|
||||
log.info("Appeal notification emails sent successfully for appeal ID: {}", appeal.id());
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send appeal notification emails for appeal ID: {}", appeal.id(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendEmailToAppealsTeam(Appeal appeal, HistoryRecord history) throws MessagingException {
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true);
|
||||
|
||||
helper.setFrom(fromEmail);
|
||||
helper.setTo(APPEAL_EMAIL);
|
||||
helper.setReplyTo(appeal.email());
|
||||
helper.setSubject("New Appeal Submitted - " + appeal.username());
|
||||
|
||||
Context context = new Context();
|
||||
context.setVariable("appeal", appeal);
|
||||
context.setVariable("history", history);
|
||||
context.setVariable("createdAt", appeal.createdAt()
|
||||
.atZone(ZoneId.of("UTC"))
|
||||
.format(DateTimeFormatter.ofPattern("yyyy MMMM dd hh:mm a '(UTC)'")));
|
||||
context.setVariable("active", history.getUntil() <= 0 || history.getUntil() > System.currentTimeMillis());
|
||||
String content = templateEngine.process("appeal-email", context);
|
||||
|
||||
helper.setText(content, true);
|
||||
mailSender.send(message);
|
||||
}
|
||||
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
package com.alttd.altitudeweb.services.mail;
|
||||
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.web_db.mail.EmailVerification;
|
||||
import com.alttd.altitudeweb.database.web_db.mail.EmailVerificationMapper;
|
||||
import com.alttd.altitudeweb.setup.Connection;
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class MailVerificationService {
|
||||
|
||||
private final JavaMailSender mailSender;
|
||||
|
||||
@Value("${spring.mail.username}")
|
||||
private String fromEmail;
|
||||
|
||||
public java.util.List<EmailVerification> listAll(UUID userUuid) {
|
||||
java.util.concurrent.CompletableFuture<java.util.List<EmailVerification>> future = new java.util.concurrent.CompletableFuture<>();
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sql -> {
|
||||
EmailVerificationMapper mapper = sql.getMapper(EmailVerificationMapper.class);
|
||||
future.complete(mapper.findAllByUser(userUuid));
|
||||
});
|
||||
return future.join();
|
||||
}
|
||||
|
||||
public EmailVerification submitEmail(UUID userUuid, String email) {
|
||||
String code = generateCode();
|
||||
Instant now = Instant.now();
|
||||
final String finalEmail = email.toLowerCase();
|
||||
|
||||
CompletableFuture<EmailVerification> future = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sql -> {
|
||||
EmailVerificationMapper mapper = sql.getMapper(EmailVerificationMapper.class);
|
||||
EmailVerification existing = mapper.findByUserAndEmail(userUuid, finalEmail);
|
||||
EmailVerification toPersist;
|
||||
if (existing == null) {
|
||||
toPersist = new EmailVerification(UUID.randomUUID(), userUuid, finalEmail, code, false, now, null, now);
|
||||
mapper.insert(toPersist);
|
||||
} else {
|
||||
mapper.updateCodeAndLastSent(existing.id(), code, now);
|
||||
toPersist = new EmailVerification(existing.id(), userUuid, finalEmail, code, false, existing.createdAt(), null, now);
|
||||
}
|
||||
future.complete(toPersist);
|
||||
});
|
||||
EmailVerification saved = future.join();
|
||||
sendVerificationEmail(saved);
|
||||
return saved;
|
||||
}
|
||||
|
||||
public Optional<EmailVerification> verifyCode(UUID userUuid, String code) {
|
||||
CompletableFuture<Optional<EmailVerification>> future = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sql -> {
|
||||
EmailVerificationMapper mapper = sql.getMapper(EmailVerificationMapper.class);
|
||||
EmailVerification found = mapper.findByUserAndCode(userUuid, code);
|
||||
if (found == null) {
|
||||
future.complete(Optional.empty());
|
||||
return;
|
||||
}
|
||||
mapper.markVerified(found.id(), Instant.now());
|
||||
future.complete(Optional.of(found));
|
||||
});
|
||||
return future.join();
|
||||
}
|
||||
|
||||
public EmailVerification resend(UUID userUuid, String email) {
|
||||
String code = generateCode();
|
||||
Instant now = Instant.now();
|
||||
final String finalEmail = email.toLowerCase();
|
||||
CompletableFuture<EmailVerification> future = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sql -> {
|
||||
EmailVerificationMapper mapper = sql.getMapper(EmailVerificationMapper.class);
|
||||
EmailVerification existing = mapper.findByUserAndEmail(userUuid, finalEmail);
|
||||
if (existing != null) {
|
||||
mapper.updateCodeAndLastSent(existing.id(), code, now);
|
||||
future.complete(new EmailVerification(existing.id(), userUuid,
|
||||
finalEmail, code, false, existing.createdAt(), null, now));
|
||||
} else {
|
||||
future.complete(null);
|
||||
}
|
||||
});
|
||||
EmailVerification updated = future.join();
|
||||
if (updated != null) {
|
||||
sendVerificationEmail(updated);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
public boolean delete(UUID userUuid, String email) {
|
||||
final String finalEmail = email.toLowerCase();
|
||||
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sql -> {
|
||||
EmailVerificationMapper mapper = sql.getMapper(EmailVerificationMapper.class);
|
||||
EmailVerification existing = mapper.findByUserAndEmail(userUuid, finalEmail);
|
||||
if (existing != null) {
|
||||
mapper.deleteByUserAndEmail(userUuid, finalEmail);
|
||||
future.complete(true);
|
||||
} else {
|
||||
future.complete(false);
|
||||
}
|
||||
});
|
||||
return future.join();
|
||||
}
|
||||
|
||||
private void sendVerificationEmail(EmailVerification emailVerification) {
|
||||
try {
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true);
|
||||
helper.setFrom(fromEmail);
|
||||
helper.setTo(emailVerification.email().toLowerCase());
|
||||
helper.setSubject("Your verification code");
|
||||
helper.setText("Your verification code is: " + emailVerification.verificationCode(), false);
|
||||
mailSender.send(message);
|
||||
} catch (MessagingException e) {
|
||||
log.error("Failed to send verification email to {}", emailVerification.email(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private String generateCode() {
|
||||
// 6-digit numeric code
|
||||
Random random = new Random();
|
||||
int num = 100000 + random.nextInt(899999);
|
||||
return String.valueOf(num);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.alttd.altitudeweb.services.mail;
|
||||
|
||||
import com.alttd.altitudeweb.database.web_db.forms.StaffApplication;
|
||||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.thymeleaf.context.Context;
|
||||
import org.thymeleaf.spring6.SpringTemplateEngine;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class StaffApplicationMail {
|
||||
|
||||
private final JavaMailSender mailSender;
|
||||
private final SpringTemplateEngine templateEngine;
|
||||
|
||||
@Value("${spring.mail.username}")
|
||||
private String fromEmail;
|
||||
|
||||
private static final String STAFF_APPLICATION_EMAIL = "[email protected]";
|
||||
|
||||
/**
|
||||
* Sends an email with the staff application details to the staff applications team mailbox.
|
||||
* Returns true if the email was sent successfully.
|
||||
*/
|
||||
public boolean sendApplicationEmail(String username, StaffApplication application) {
|
||||
try {
|
||||
doSend(username, application);
|
||||
log.info("Staff application email sent successfully for application ID: {}", application.id());
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to send staff application email for application ID: {}", application.id(), e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void doSend(String username, StaffApplication application) throws MessagingException {
|
||||
MimeMessage message = mailSender.createMimeMessage();
|
||||
MimeMessageHelper helper = new MimeMessageHelper(message, true);
|
||||
|
||||
helper.setFrom(fromEmail);
|
||||
helper.setTo(STAFF_APPLICATION_EMAIL);
|
||||
helper.setReplyTo(application.email());
|
||||
helper.setSubject("Staff Application: " + safe(application.discordUsername()));
|
||||
|
||||
// Prepare template context
|
||||
String createdAt = application.createdAt()
|
||||
.atZone(ZoneId.of("UTC"))
|
||||
.format(DateTimeFormatter.ofPattern("yyyy MMMM dd hh:mm a '(UTC)'"));
|
||||
Context context = new Context();
|
||||
context.setVariable("application", application);
|
||||
context.setVariable("createdAt", createdAt);
|
||||
context.setVariable("username", username);
|
||||
|
||||
String content = templateEngine.process("staff-application-email", context);
|
||||
helper.setText(content, true);
|
||||
mailSender.send(message);
|
||||
}
|
||||
|
||||
private String safe(String s) {
|
||||
return s == null ? "unknown" : s;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.alttd.altitudeweb.services.site;
|
||||
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.votingplugin.VotingPluginUsersMapper;
|
||||
import com.alttd.altitudeweb.database.votingplugin.VotingStatsRow;
|
||||
import com.alttd.altitudeweb.mappers.VotingStatsRowToVoteDataDto;
|
||||
import com.alttd.altitudeweb.model.VoteDataDto;
|
||||
import com.alttd.altitudeweb.setup.Connection;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class VoteService {
|
||||
|
||||
private final VotingStatsRowToVoteDataDto votingStatsRowToVoteDataDto;
|
||||
|
||||
public Optional<VoteDataDto> getVoteStats(UUID uuid) {
|
||||
CompletableFuture<Optional<VoteDataDto>> voteDataDtoFuture = new CompletableFuture<>();
|
||||
Connection.getConnection(Databases.VOTING_PLUGIN).runQuery(sqlSession -> {
|
||||
try {
|
||||
VotingPluginUsersMapper votingPluginUsersMapper = sqlSession.getMapper(VotingPluginUsersMapper.class);
|
||||
Optional<VotingStatsRow> optionalVotingStatsRow = votingPluginUsersMapper.getStatsByUuid(uuid);
|
||||
if (optionalVotingStatsRow.isEmpty()) {
|
||||
log.debug("No voting stats found for {}", uuid);
|
||||
voteDataDtoFuture.complete(Optional.empty());
|
||||
return;
|
||||
}
|
||||
|
||||
VotingStatsRow votingStatsRow = optionalVotingStatsRow.get();
|
||||
|
||||
VoteDataDto voteDataDto = votingStatsRowToVoteDataDto.map(votingStatsRow);
|
||||
voteDataDtoFuture.complete(Optional.of(voteDataDto));
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to get vote data for {}", uuid, e);
|
||||
voteDataDtoFuture.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
return voteDataDtoFuture.join();//TODO handle exception
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.alttd.altitudeweb.services.user;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserDetailsServiceImpl implements UserDetailsService {
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String uuid) throws UsernameNotFoundException {
|
||||
try {
|
||||
//Validate uuid
|
||||
UUID.fromString(uuid);
|
||||
return new User(uuid, "", Collections.emptyList());
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new UsernameNotFoundException("Invalid UUID format: " + uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
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:https://beta.alttd.com}
|
||||
my-server.address=${SERVER_ADDRESS:https://beta.alttd.com}
|
||||
logging.level.com.alttd.altitudeweb=DEBUG
|
||||
@@ -1,8 +1,4 @@
|
||||
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}
|
||||
cors.allowed-origins=${CORS:http://localhost:4200,http://localhost:8080,http://localhost:80}
|
||||
my-server.address=${SERVER_ADDRESS:http://localhost:8080}
|
||||
logging.level.com.alttd.altitudeweb=DEBUG
|
||||
logging.level.org.springframework.security=DEBUG
|
||||
|
||||
@@ -5,4 +5,16 @@ database.host=${DB_HOST:localhost}
|
||||
database.user=${DB_USER:root}
|
||||
database.password=${DB_PASSWORD:root}
|
||||
cors.allowed-origins=${CORS:https://alttd.com}
|
||||
login.secret=${LOGIN_SECRET:SET_TOKEN}
|
||||
particles.file_path=${user.home}/.altitudeweb/particles
|
||||
notification.server.url=${SERVER_IP:10.0.0.107}:${SERVER_PORT:8080}
|
||||
my-server.address=${SERVER_ADDRESS:https://alttd.com}
|
||||
logging.level.com.alttd.altitudeweb=INFO
|
||||
discord.token=${DISCORD_TOKEN}
|
||||
spring.mail.host=${MAIL_HOST:smtp.zoho.com}
|
||||
spring.mail.port=${MAIL_PORT:465}
|
||||
spring.mail.username=${MAIL_USER}
|
||||
spring.mail.password=${MAIL_PASSWORD}
|
||||
spring.mail.properties.mail.smtp.auth=true
|
||||
spring.mail.properties.mail.smtp.ssl.enable=true
|
||||
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Appeal Notification</title>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'minecraft-title';
|
||||
src: url('https://beta.alttd.com/public/fonts/minecraft-title.ttf') format('truetype'),
|
||||
url('https://beta.alttd.com/public/fonts/minecraft-title.eot') format('embedded-opentype'),
|
||||
url('https://beta.alttd.com/public/fonts/minecraft-title.svg') format('svg'),
|
||||
url('https://beta.alttd.com/public/fonts/minecraft-title.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'minecraft-text';
|
||||
src: url('https://beta.alttd.com/public/fonts/minecraft-text.ttf') format('truetype'),
|
||||
url('https://beta.alttd.com/public/fonts/minecraft-text.eot') format('embedded-opentype'),
|
||||
url('https://beta.alttd.com/public/fonts/minecraft-text.svg') format('svg'),
|
||||
url('https://beta.alttd.com/public/fonts/minecraft-text.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'opensans';
|
||||
src: url('https://beta.alttd.com/public/fonts/opensans.ttf') format('truetype'),
|
||||
url('https://beta.alttd.com/public/fonts/opensans.eot') format('embedded-opentype'),
|
||||
url('https://beta.alttd.com/public/fonts/opensans.svg') format('svg'),
|
||||
url('https://beta.alttd.com/public/fonts/opensans.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'opensans-bold';
|
||||
src: url('https://beta.alttd.com/public/fonts/opensans-bold.ttf') format('truetype'),
|
||||
url('https://beta.alttd.com/public/fonts/opensans-bold.eot') format('embedded-opentype'),
|
||||
url('https://beta.alttd.com/public/fonts/opensans-bold.svg') format('svg'),
|
||||
url('https://beta.alttd.com/public/fonts/opensans-bold.woff') format('woff');
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'minecraft-title', sans-serif;
|
||||
}
|
||||
|
||||
.columnSection {
|
||||
width: 80%;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.columnContainer {
|
||||
flex: 1 1 200px;
|
||||
min-width: 200px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
margin: auto;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
|
||||
li, p {
|
||||
font-family: 'opensans', sans-serif;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1150px) {
|
||||
.columnContainer, .columnSection {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 690px) {
|
||||
.columnContainer {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<img id="header-img" src="https://beta.alttd.com/public/img/logos/logo.png" alt="The Altitude Minecraft Server" height="159"
|
||||
width="275">
|
||||
<h1 style="text-align: center;" th:text="'Appeal by ' + ${appeal.username}">Appeal by Username</h1>
|
||||
<section class="columnSection">
|
||||
<div class="columnContainer">
|
||||
<div>
|
||||
<h2>User information</h2>
|
||||
<ul>
|
||||
<li><strong>Username:</strong> <span th:text="${appeal.username}">username</span></li>
|
||||
<li><strong>UUID:</strong> <span th:text="${appeal.uuid}">uuid</span></li>
|
||||
<li><strong>Email:</strong> <span th:text="${appeal.email}">email</span></li>
|
||||
<li><strong>Submitted at:</strong> <span th:text="${createdAt}">date</span></li>
|
||||
<li><strong>Reason:</strong> <span th:text="${history.reason}">reason</span></li>
|
||||
<li><strong>Active:</strong> <span th:text="${active}">unknown</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columnContainer">
|
||||
<div>
|
||||
<h2>Appeal:</h2>
|
||||
<p th:text="${appeal.reason}">Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.
|
||||
Lorem ipsum dolor sit amet consectetur adipiscing elit. Quisque faucibus ex sapien vitae pellentesque sem placerat. In id cursus mi pretium tellus duis convallis. Tempus leo eu aenean sed diam urna tempor. Pulvinar vivamus fringilla lacus nec metus bibendum egestas. Iaculis massa nisl malesuada lacinia integer nunc posuere. Ut hendrerit semper vel class aptent taciti sociosqu. Ad litora torquent per conubia nostra inceptos himenaeos.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,138 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Staff Application</title>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'minecraft-title';
|
||||
src: url('https://beta.alttd.com/public/fonts/minecraft-title.ttf') format('truetype'),
|
||||
url('https://beta.alttd.com/public/fonts/minecraft-title.eot') format('embedded-opentype'),
|
||||
url('https://beta.alttd.com/public/fonts/minecraft-title.svg') format('svg'),
|
||||
url('https://beta.alttd.com/public/fonts/minecraft-title.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'minecraft-text';
|
||||
src: url('https://beta.alttd.com/public/fonts/minecraft-text.ttf') format('truetype'),
|
||||
url('https://beta.alttd.com/public/fonts/minecraft-text.eot') format('embedded-opentype'),
|
||||
url('https://beta.alttd.com/public/fonts/minecraft-text.svg') format('svg'),
|
||||
url('https://beta.alttd.com/public/fonts/minecraft-text.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'opensans';
|
||||
src: url('https://beta.alttd.com/public/fonts/opensans.ttf') format('truetype'),
|
||||
url('https://beta.alttd.com/public/fonts/opensans.eot') format('embedded-opentype'),
|
||||
url('https://beta.alttd.com/public/fonts/opensans.svg') format('svg'),
|
||||
url('https://beta.alttd.com/public/fonts/opensans.woff') format('woff');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'opensans-bold';
|
||||
src: url('https://beta.alttd.com/public/fonts/opensans-bold.ttf') format('truetype'),
|
||||
url('https://beta.alttd.com/public/fonts/opensans-bold.eot') format('embedded-opentype'),
|
||||
url('https://beta.alttd.com/public/fonts/opensans-bold.svg') format('svg'),
|
||||
url('https://beta.alttd.com/public/fonts/opensans-bold.woff') format('woff');
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'minecraft-title', sans-serif;
|
||||
}
|
||||
|
||||
.columnSection {
|
||||
width: 80%;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.columnContainer {
|
||||
flex: 1 1 200px;
|
||||
min-width: 200px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
margin: auto;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
|
||||
li, p {
|
||||
font-family: 'opensans', sans-serif;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1150px) {
|
||||
.columnContainer, .columnSection {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 690px) {
|
||||
.columnContainer {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<img id="header-img" src="https://beta.alttd.com/public/img/logos/logo.png" alt="The Altitude Minecraft Server" height="159" width="275">
|
||||
<h1 style="text-align: center;" th:text="'Staff application by ' + ${application.discordUsername}">Staff application</h1>
|
||||
|
||||
<section class="columnSection">
|
||||
<div class="columnContainer">
|
||||
<div>
|
||||
<h2>Applicant</h2>
|
||||
<ul>
|
||||
<li><strong>Username:</strong> <span th:text="${username}">uuid</span></li>
|
||||
<li><strong>Email:</strong> <span th:text="${application.email}">email</span></li>
|
||||
<li><strong>Discord:</strong> <span th:text="${application.discordUsername}">discord</span></li>
|
||||
<li><strong>Age:</strong> <span th:text="${application.age}">age</span></li>
|
||||
<li><strong>Pronouns:</strong> <span th:text="${application.pronouns}">pronouns</span></li>
|
||||
<li><strong>Join date:</strong> <span th:text="${application.joinDate}">date</span></li>
|
||||
<li><strong>Submitted at:</strong> <span th:text="${createdAt}">date</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columnContainer">
|
||||
<div>
|
||||
<h2>Availability</h2>
|
||||
<ul>
|
||||
<li><strong>Days:</strong> <span th:text="${application.availableDays}">days</span></li>
|
||||
<li><strong>Times:</strong> <span th:text="${application.availableTimes}">times</span></li>
|
||||
<li><strong>Weekly playtime:</strong> <span th:text="${application.weeklyPlaytime}">0</span> hours</li>
|
||||
</ul>
|
||||
|
||||
<h2>Experience</h2>
|
||||
<p><strong>Previous:</strong><br><span th:text="${application.previousExperience}">previous experience</span></p>
|
||||
<p><strong>Plugins:</strong><br><span th:text="${application.pluginExperience}">plugin experience</span></p>
|
||||
<p><strong>Expectations:</strong><br><span th:text="${application.moderatorExpectations}">moderator expectations</span></p>
|
||||
|
||||
<div th:if="${application.additionalInfo} != null and ${application.additionalInfo} != ''">
|
||||
<h2>Additional info</h2>
|
||||
<p th:text="${application.additionalInfo}">additional info</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -16,6 +16,7 @@ dependencies {
|
||||
implementation("org.mybatis:mybatis:3.5.13")
|
||||
compileOnly("org.slf4j:slf4j-api:2.0.17")
|
||||
compileOnly("org.slf4j:slf4j-simple:2.0.17")
|
||||
compileOnly("org.jetbrains:annotations:26.0.2")
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
|
||||
@@ -6,7 +6,9 @@ import lombok.Getter;
|
||||
public enum Databases {
|
||||
DEFAULT("web_db"),
|
||||
LUCK_PERMS("luckperms"),
|
||||
LITE_BANS("litebans");
|
||||
LITE_BANS("litebans"),
|
||||
DISCORD("discordLink"),
|
||||
VOTING_PLUGIN("votingplugin");
|
||||
|
||||
private final String internalName;
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.alttd.altitudeweb.database.discord;
|
||||
|
||||
public record OutputChannel(long guild, String outputType, long channel, String channelType) {
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.alttd.altitudeweb.database.discord;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface OutputChannelMapper {
|
||||
@Select("""
|
||||
SELECT guild, output_type, channel, channel_type
|
||||
FROM output_channels
|
||||
WHERE output_type = #{outputType}
|
||||
""")
|
||||
List<OutputChannel> getChannelsWithOutputType(@Param("outputType") String outputType);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.alttd.altitudeweb.database.litebans;
|
||||
|
||||
import org.apache.ibatis.annotations.Delete;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public interface EditHistoryMapper {
|
||||
|
||||
@Update("""
|
||||
UPDATE ${table_name}
|
||||
SET reason = #{reason}
|
||||
WHERE id = #{id}
|
||||
""")
|
||||
int updateReason(@Param("table_name") String tableName,
|
||||
@Param("id") int id,
|
||||
@Param("reason") String reason);
|
||||
|
||||
@Update("""
|
||||
UPDATE ${table_name}
|
||||
SET until = #{until}
|
||||
WHERE id = #{id}
|
||||
""")
|
||||
int updateUntil(@Param("table_name") String tableName,
|
||||
@Param("id") int id,
|
||||
@Param("until") Long until);
|
||||
|
||||
@Delete("""
|
||||
DELETE FROM ${table_name}
|
||||
WHERE id = #{id}
|
||||
""")
|
||||
int deletePunishment(@Param("table_name") String tableName,
|
||||
@Param("id") int id);
|
||||
|
||||
default int setReason(@NotNull HistoryType type, int id, String reason) {
|
||||
return switch (type) {
|
||||
case ALL -> throw new IllegalArgumentException("HistoryType.ALL is not supported");
|
||||
case BAN -> updateReason("litebans_bans", id, reason);
|
||||
case MUTE -> updateReason("litebans_mutes", id, reason);
|
||||
case KICK -> updateReason("litebans_kicks", id, reason);
|
||||
case WARN -> updateReason("litebans_warnings", id, reason);
|
||||
};
|
||||
}
|
||||
|
||||
default int setUntil(@NotNull HistoryType type, int id, Long until) {
|
||||
return switch (type) {
|
||||
case ALL -> throw new IllegalArgumentException("HistoryType.ALL is not supported");
|
||||
case BAN -> updateUntil("litebans_bans", id, until);
|
||||
case MUTE -> updateUntil("litebans_mutes", id, until);
|
||||
case KICK -> throw new IllegalArgumentException("KICK has no until");
|
||||
case WARN -> throw new IllegalArgumentException("WARN has no until");
|
||||
};
|
||||
}
|
||||
|
||||
default int remove(@NotNull HistoryType type, int id) {
|
||||
return switch (type) {
|
||||
case ALL -> throw new IllegalArgumentException("HistoryType.ALL is not supported");
|
||||
case BAN -> deletePunishment("litebans_bans", id);
|
||||
case MUTE -> deletePunishment("litebans_mutes", id);
|
||||
case KICK -> deletePunishment("litebans_kicks", id);
|
||||
case WARN -> deletePunishment("litebans_warnings", id);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import lombok.Data;
|
||||
@Data
|
||||
public class HistoryRecord {
|
||||
private String uuid;
|
||||
private int id;
|
||||
private String punishedName;
|
||||
private String reason;
|
||||
private String bannedByUuid;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.alttd.altitudeweb.database.litebans;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Result;
|
||||
import org.apache.ibatis.annotations.Results;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
public interface IdHistoryMapper {
|
||||
@Results({
|
||||
@Result(property = "id", column = "id"),
|
||||
@Result(property = "uuid", column = "uuid"),
|
||||
@Result(property = "punishedName", column = "punished_name"),
|
||||
@Result(property = "reason", column = "reason"),
|
||||
@Result(property = "bannedByUuid", column = "banned_by_uuid"),
|
||||
@Result(property = "bannedByName", column = "banned_by_name"),
|
||||
@Result(property = "removedByName", column = "removed_by_name"),
|
||||
@Result(property = "time", column = "time"),
|
||||
@Result(property = "until", column = "until"),
|
||||
@Result(property = "removedByReason", column = "removed_by_reason")
|
||||
})
|
||||
@Select("""
|
||||
SELECT punishment.id,
|
||||
punishment.uuid,
|
||||
user_lookup.name AS punished_name,
|
||||
punishment.reason,
|
||||
punishment.banned_by_uuid,
|
||||
punishment.banned_by_name,
|
||||
punishment.removed_by_name,
|
||||
punishment.time,
|
||||
punishment.until,
|
||||
punishment.removed_by_reason
|
||||
FROM (SELECT *
|
||||
FROM ${table_name} AS punishment
|
||||
WHERE punishment.id = #{id})
|
||||
AS punishment
|
||||
INNER JOIN user_lookup
|
||||
ON user_lookup.uuid = punishment.uuid
|
||||
""")
|
||||
HistoryRecord getHistoryForId(@Param("table_name") String tableName,
|
||||
@Param("type") String type,
|
||||
@Param("id") int id);
|
||||
|
||||
default HistoryRecord getRecentHistory(HistoryType historyType, int id) {
|
||||
HistoryRecord historyRecord = switch (historyType) {
|
||||
case ALL -> throw new IllegalArgumentException("HistoryType.ALL is not supported");
|
||||
case BAN -> getHistoryForId("litebans_bans", "ban", id);
|
||||
case MUTE -> getHistoryForId("litebans_mutes", "mute", id);
|
||||
case KICK -> getHistoryForId("litebans_kicks", "kick", id);
|
||||
case WARN -> getHistoryForId("litebans_warnings", "warn", id);
|
||||
};
|
||||
historyRecord.setType(historyType.name().toLowerCase());
|
||||
return historyRecord;
|
||||
}
|
||||
}
|
||||
+44
-36
@@ -4,6 +4,7 @@ import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Result;
|
||||
import org.apache.ibatis.annotations.Results;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@@ -24,6 +25,7 @@ public interface NameHistoryMapper {
|
||||
* order
|
||||
*/
|
||||
@Results({
|
||||
@Result(property = "id", column = "id"),
|
||||
@Result(property = "uuid", column = "uuid"),
|
||||
@Result(property = "punishedName", column = "punished_name"),
|
||||
@Result(property = "reason", column = "reason"),
|
||||
@@ -36,7 +38,8 @@ public interface NameHistoryMapper {
|
||||
@Result(property = "type", column = "type")
|
||||
})
|
||||
@Select("""
|
||||
SELECT all_punishments.uuid,
|
||||
SELECT id,
|
||||
all_punishments.uuid,
|
||||
user_lookup.name AS punished_name,
|
||||
reason,
|
||||
banned_by_uuid,
|
||||
@@ -62,7 +65,6 @@ public interface NameHistoryMapper {
|
||||
* Retrieves a list of all types of recent punishment history records sorted
|
||||
* in descending time order. This result does NOT contain kicks history
|
||||
*
|
||||
* @param nameColumn the column name in the database indicating the name to use for filtering
|
||||
* @param limit the maximum number of records to fetch
|
||||
* @param offset the starting offset position of the result set
|
||||
*
|
||||
@@ -70,6 +72,7 @@ public interface NameHistoryMapper {
|
||||
* order
|
||||
*/
|
||||
@Results({
|
||||
@Result(property = "id", column = "id"),
|
||||
@Result(property = "uuid", column = "uuid"),
|
||||
@Result(property = "punishedName", column = "punished_name"),
|
||||
@Result(property = "reason", column = "reason"),
|
||||
@@ -82,17 +85,19 @@ public interface NameHistoryMapper {
|
||||
@Result(property = "type", column = "type")
|
||||
})
|
||||
@Select("""
|
||||
SELECT punishments.uuid,
|
||||
SELECT punishment.id,
|
||||
punishment.uuid,
|
||||
user_lookup.name AS punished_name,
|
||||
punishments.reason,
|
||||
punishments.banned_by_uuid,
|
||||
punishments.banned_by_name,
|
||||
punishments.removed_by_name,
|
||||
punishments.time,
|
||||
punishments.until,
|
||||
punishments.removed_by_reason,
|
||||
punishments.type
|
||||
punishment.reason,
|
||||
punishment.banned_by_uuid,
|
||||
punishment.banned_by_name,
|
||||
punishment.removed_by_name,
|
||||
punishment.time,
|
||||
punishment.until,
|
||||
punishment.removed_by_reason,
|
||||
punishment.type
|
||||
FROM (SELECT uuid,
|
||||
id,
|
||||
reason,
|
||||
banned_by_uuid,
|
||||
banned_by_name,
|
||||
@@ -103,9 +108,9 @@ public interface NameHistoryMapper {
|
||||
type
|
||||
FROM all_punishments
|
||||
ORDER BY time DESC
|
||||
LIMIT #{limit} OFFSET #{offset}) AS punishments
|
||||
LIMIT #{limit} OFFSET #{offset}) AS punishment
|
||||
INNER JOIN user_lookup
|
||||
ON user_lookup.uuid = punishments.uuid;
|
||||
ON user_lookup.uuid = punishment.uuid;
|
||||
""")
|
||||
List<HistoryRecord> getRecentAllHistory(@Param("limit") int limit,
|
||||
@Param("offset") int offset);
|
||||
@@ -124,6 +129,7 @@ public interface NameHistoryMapper {
|
||||
* order
|
||||
*/
|
||||
@Results({
|
||||
@Result(property = "id", column = "id"),
|
||||
@Result(property = "uuid", column = "uuid"),
|
||||
@Result(property = "punishedName", column = "punished_name"),
|
||||
@Result(property = "reason", column = "reason"),
|
||||
@@ -135,22 +141,24 @@ public interface NameHistoryMapper {
|
||||
@Result(property = "removedByReason", column = "removed_by_reason")
|
||||
})
|
||||
@Select("""
|
||||
SELECT punishment.uuid,
|
||||
SELECT punishment.id,
|
||||
punishment.uuid,
|
||||
user_lookup.name AS punished_name,
|
||||
reason,
|
||||
banned_by_uuid,
|
||||
banned_by_name,
|
||||
removed_by_name,
|
||||
time,
|
||||
until,
|
||||
removed_by_reason
|
||||
FROM ${tableName} AS punishment
|
||||
INNER JOIN user_lookup ON user_lookup.uuid = punishment.uuid
|
||||
punishment.reason,
|
||||
punishment.banned_by_uuid,
|
||||
punishment.banned_by_name,
|
||||
punishment.removed_by_name,
|
||||
punishment.time,
|
||||
punishment.until,
|
||||
punishment.removed_by_reason
|
||||
FROM ${table_name} AS punishment
|
||||
INNER JOIN user_lookup
|
||||
ON user_lookup.uuid = punishment.uuid
|
||||
WHERE ${name_column} LIKE #{partialName}
|
||||
ORDER BY time DESC
|
||||
LIMIT #{limit} OFFSET #{offset}
|
||||
""")
|
||||
List<HistoryRecord> getRecentHistoryForName(@Param("tableName") String tableName,
|
||||
List<HistoryRecord> getRecentHistoryForName(@Param("table_name") String tableName,
|
||||
@Param("partialName") String partialName,
|
||||
@Param("name_column") String nameColumn,
|
||||
@Param("limit") int limit,
|
||||
@@ -161,8 +169,6 @@ public interface NameHistoryMapper {
|
||||
* parameters. The records are sorted in descending order by time and limited to a specified range.
|
||||
*
|
||||
* @param tableName the name of the database table to query the history records from
|
||||
* @param partialName a partial or complete name of the user to filter the records, case-insensitive
|
||||
* @param nameColumn the column name in the database indicating the name to use for filtering
|
||||
* @param limit the maximum number of records to fetch
|
||||
* @param offset the starting offset position of the result set
|
||||
*
|
||||
@@ -170,6 +176,7 @@ public interface NameHistoryMapper {
|
||||
* order
|
||||
*/
|
||||
@Results({
|
||||
@Result(property = "id", column = "id"),
|
||||
@Result(property = "uuid", column = "uuid"),
|
||||
@Result(property = "punishedName", column = "punished_name"),
|
||||
@Result(property = "reason", column = "reason"),
|
||||
@@ -181,7 +188,8 @@ public interface NameHistoryMapper {
|
||||
@Result(property = "removedByReason", column = "removed_by_reason")
|
||||
})
|
||||
@Select("""
|
||||
SELECT punishment.uuid,
|
||||
SELECT punishment.id,
|
||||
punishment.uuid,
|
||||
user_lookup.name AS punished_name,
|
||||
punishment.reason,
|
||||
punishment.banned_by_uuid,
|
||||
@@ -191,7 +199,8 @@ public interface NameHistoryMapper {
|
||||
punishment.until,
|
||||
punishment.removed_by_reason
|
||||
FROM (
|
||||
SELECT uuid,
|
||||
SELECT id,
|
||||
uuid,
|
||||
reason,
|
||||
banned_by_uuid,
|
||||
banned_by_name,
|
||||
@@ -199,14 +208,13 @@ public interface NameHistoryMapper {
|
||||
time,
|
||||
until,
|
||||
removed_by_reason
|
||||
FROM ${tableName} AS punishment
|
||||
WHERE ${name_column} LIKE #{partialName}
|
||||
FROM ${table_name} AS punishment
|
||||
ORDER BY time DESC
|
||||
LIMIT #{limit} OFFSET #{offset}
|
||||
) AS punishment
|
||||
INNER JOIN user_lookup ON user_lookup.uuid = punishment.uuid
|
||||
""")
|
||||
List<HistoryRecord> getRecentHistory(@Param("tableName") String tableName,
|
||||
List<HistoryRecord> getRecentHistory(@Param("table_name") String tableName,
|
||||
@Param("limit") int limit,
|
||||
@Param("offset") int offset);
|
||||
|
||||
@@ -254,23 +262,23 @@ public interface NameHistoryMapper {
|
||||
}
|
||||
}
|
||||
|
||||
private List<HistoryRecord> getRecentBans(UserType userType, String partialName, int page) {
|
||||
private @NotNull List<HistoryRecord> getRecentBans(UserType userType, String partialName, int page) {
|
||||
return addType(getRecent("litebans_bans", userType, partialName, page), "ban");
|
||||
}
|
||||
|
||||
private List<HistoryRecord> getRecentKicks(UserType userType, String partialName, int page) {
|
||||
private @NotNull List<HistoryRecord> getRecentKicks(UserType userType, String partialName, int page) {
|
||||
return addType(getRecent("litebans_kicks", userType, partialName, page), "kick");
|
||||
}
|
||||
|
||||
private List<HistoryRecord> getRecentMutes(UserType userType, String partialName, int page) {
|
||||
private @NotNull List<HistoryRecord> getRecentMutes(UserType userType, String partialName, int page) {
|
||||
return addType(getRecent("litebans_mutes", userType, partialName, page), "mute");
|
||||
}
|
||||
|
||||
private List<HistoryRecord> getRecentWarns(UserType userType, String partialName, int page) {
|
||||
private @NotNull List<HistoryRecord> getRecentWarns(UserType userType, String partialName, int page) {
|
||||
return addType(getRecent("litebans_warnings", userType, partialName, page), "warn");
|
||||
}
|
||||
|
||||
private List<HistoryRecord> addType(List<HistoryRecord> historyRecords, String type) {
|
||||
private @NotNull List<HistoryRecord> addType(@NotNull List<HistoryRecord> historyRecords, String type) {
|
||||
historyRecords.forEach(historyRecord -> historyRecord.setType(type));
|
||||
return historyRecords;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,17 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public interface RecentNamesMapper {
|
||||
|
||||
@Select("""
|
||||
SELECT DISTINCT name AS username
|
||||
FROM litebans_history
|
||||
WHERE uuid = #{uuid}
|
||||
ORDER BY date DESC
|
||||
LIMIT 1;
|
||||
""")
|
||||
String getUsername(@Param("uuid") String uuid);
|
||||
|
||||
|
||||
@Select("""
|
||||
SELECT DISTINCT user_lookup.name AS punished_name
|
||||
FROM ${tableName} AS punishment
|
||||
|
||||
+25
-15
@@ -1,10 +1,11 @@
|
||||
package com.alttd.altitudeweb.database.litebans;
|
||||
|
||||
import com.alttd.altitudeweb.type_handler.UUIDTypeHandler;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Result;
|
||||
import org.apache.ibatis.annotations.Results;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
@@ -14,10 +15,11 @@ public interface UUIDHistoryMapper {
|
||||
int PAGE_SIZE = 10;
|
||||
|
||||
@Results({
|
||||
@Result(property = "uuid", column = "uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class),
|
||||
@Result(property = "id", column = "id"),
|
||||
@Result(property = "uuid", column = "uuid"),
|
||||
@Result(property = "punishedName", column = "punished_name"),
|
||||
@Result(property = "reason", column = "reason"),
|
||||
@Result(property = "bannedByUuid", column = "banned_by_uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class),
|
||||
@Result(property = "bannedByUuid", column = "banned_by_uuid"),
|
||||
@Result(property = "bannedByName", column = "banned_by_name"),
|
||||
@Result(property = "removedByName", column = "removed_by_name"),
|
||||
@Result(property = "time", column = "time"),
|
||||
@@ -26,7 +28,7 @@ public interface UUIDHistoryMapper {
|
||||
@Result(property = "type", column = "type")
|
||||
})
|
||||
@Select("""
|
||||
SELECT all_punishments.uuid, user_lookup.name AS punished_name, reason, banned_by_uuid, banned_by_name,
|
||||
SELECT id, all_punishments.uuid, user_lookup.name AS punished_name, reason, banned_by_uuid, banned_by_name,
|
||||
removed_by_name, time, until, removed_by_reason, type
|
||||
FROM all_punishments
|
||||
INNER JOIN user_lookup
|
||||
@@ -41,10 +43,11 @@ public interface UUIDHistoryMapper {
|
||||
@Param("offset") int offset);
|
||||
|
||||
@Results({
|
||||
@Result(property = "uuid", column = "uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class),
|
||||
@Result(property = "id", column = "id"),
|
||||
@Result(property = "uuid", column = "uuid"),
|
||||
@Result(property = "punishedName", column = "punished_name"),
|
||||
@Result(property = "reason", column = "reason"),
|
||||
@Result(property = "bannedByUuid", column = "banned_by_uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class),
|
||||
@Result(property = "bannedByUuid", column = "banned_by_uuid"),
|
||||
@Result(property = "bannedByName", column = "banned_by_name"),
|
||||
@Result(property = "removedByName", column = "removed_by_name"),
|
||||
@Result(property = "time", column = "time"),
|
||||
@@ -52,7 +55,7 @@ public interface UUIDHistoryMapper {
|
||||
@Result(property = "removedByReason", column = "removed_by_reason")
|
||||
})
|
||||
@Select("""
|
||||
SELECT punishment.uuid, user_lookup.name AS punished_name, reason, banned_by_uuid, banned_by_name,
|
||||
SELECT punishment.id, punishment.uuid, user_lookup.name AS punished_name, reason, banned_by_uuid, banned_by_name,
|
||||
removed_by_name, time, until, removed_by_reason
|
||||
FROM ${tableName} AS punishment
|
||||
INNER JOIN user_lookup ON user_lookup.uuid = punishment.uuid
|
||||
@@ -66,7 +69,8 @@ public interface UUIDHistoryMapper {
|
||||
@Param("limit") int limit,
|
||||
@Param("offset") int offset);
|
||||
|
||||
default List<HistoryRecord> getRecent(HistoryType historyType, UserType userType, UUID uuid, int page) {
|
||||
default List<HistoryRecord> getRecent(@NotNull HistoryType historyType, @NotNull UserType userType,
|
||||
@NotNull UUID uuid, int page) {
|
||||
return switch (historyType) {
|
||||
case ALL -> getRecentAll(userType, uuid, page);
|
||||
case BAN -> getRecentBans(userType, uuid, page);
|
||||
@@ -76,7 +80,12 @@ public interface UUIDHistoryMapper {
|
||||
};
|
||||
}
|
||||
|
||||
private List<HistoryRecord> getRecent(String tableName, UserType userType, UUID uuid, int page) {
|
||||
default List<HistoryRecord> getAllHistoryForUUID(@NotNull UUID uuid) {
|
||||
return getRecentAllHistory(uuid.toString(), "uuid", 100, 0);
|
||||
}
|
||||
|
||||
private List<HistoryRecord> getRecent(@NotNull String tableName, @NotNull UserType userType,
|
||||
@NotNull UUID uuid, int page) {
|
||||
int offset = page * PAGE_SIZE;
|
||||
int limit = PAGE_SIZE;
|
||||
return switch (userType) {
|
||||
@@ -85,7 +94,7 @@ public interface UUIDHistoryMapper {
|
||||
};
|
||||
}
|
||||
|
||||
private List<HistoryRecord> getRecentAll(UserType userType, UUID uuid, int page) {
|
||||
private List<HistoryRecord> getRecentAll(@NotNull UserType userType, @NotNull UUID uuid, int page) {
|
||||
int offset = page * PAGE_SIZE;
|
||||
int limit = PAGE_SIZE;
|
||||
return switch (userType) {
|
||||
@@ -96,23 +105,24 @@ public interface UUIDHistoryMapper {
|
||||
};
|
||||
}
|
||||
|
||||
private List<HistoryRecord> getRecentBans(UserType userType, UUID uuid, int page) {
|
||||
private @NotNull List<HistoryRecord> getRecentBans(@NotNull UserType userType, @NotNull UUID uuid, int page) {
|
||||
return addType(getRecent("litebans_bans", userType, uuid, page), "ban");
|
||||
}
|
||||
|
||||
private List<HistoryRecord> getRecentKicks(UserType userType, UUID uuid, int page) {
|
||||
private @NotNull List<HistoryRecord> getRecentKicks(@NotNull UserType userType, @NotNull UUID uuid, int page) {
|
||||
return addType(getRecent("litebans_kicks", userType, uuid, page), "kick");
|
||||
}
|
||||
|
||||
private List<HistoryRecord> getRecentMutes(UserType userType, UUID uuid, int page) {
|
||||
private @NotNull List<HistoryRecord> getRecentMutes(@NotNull UserType userType, @NotNull UUID uuid, int page) {
|
||||
return addType(getRecent("litebans_mutes", userType, uuid, page), "mute");
|
||||
}
|
||||
|
||||
private List<HistoryRecord> getRecentWarns(UserType userType, UUID uuid, int page) {
|
||||
private @NotNull List<HistoryRecord> getRecentWarns(@NotNull UserType userType, @NotNull UUID uuid, int page) {
|
||||
return addType(getRecent("litebans_warnings", userType, uuid, page), "warn");
|
||||
}
|
||||
|
||||
private List<HistoryRecord> addType(List<HistoryRecord> historyRecords, String type) {
|
||||
@Contract("_, _ -> param1")
|
||||
private @NotNull List<HistoryRecord> addType(@NotNull List<HistoryRecord> historyRecords, @NotNull String type) {
|
||||
historyRecords.forEach(historyRecord -> historyRecord.setType(type));
|
||||
return historyRecords;
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ public interface TeamMemberMapper {
|
||||
FROM luckperms_user_permissions AS permissions
|
||||
INNER JOIN luckperms_players AS players ON players.uuid = permissions.uuid
|
||||
WHERE permission = #{groupPermission}
|
||||
AND world = 'global'
|
||||
""")
|
||||
List<Player> getTeamMembers(@Param("groupPermission") String groupPermission);
|
||||
}
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.alttd.altitudeweb.database.luckperms;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
public interface UUIDUsernameMapper {
|
||||
@Select("""
|
||||
SELECT username
|
||||
FROM luckperms_players
|
||||
WHERE uuid = #{uuid}
|
||||
""")
|
||||
String getUsernameFromUUID(@Param("uuid") String uuid);
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.alttd.altitudeweb.database.votingplugin;
|
||||
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface VotingPluginUsersMapper {
|
||||
|
||||
@Select("""
|
||||
SELECT
|
||||
LastVotes as lastVotes,
|
||||
BestDayVoteStreak as bestDailyStreak,
|
||||
BestWeekVoteStreak as bestWeeklyStreak,
|
||||
BestMonthVoteStreak as bestMonthlyStreak,
|
||||
DayVoteStreak as dailyStreak,
|
||||
WeekVoteStreak as weeklyStreak,
|
||||
MonthVoteStreak as monthlyStreak,
|
||||
DailyTotal as totalVotesToday,
|
||||
WeeklyTotal as totalVotesThisWeek,
|
||||
MonthTotal as totalVotesThisMonth,
|
||||
AllTimeTotal as totalVotesAllTime
|
||||
FROM votingplugin.votingplugin_users
|
||||
WHERE uuid = #{uuid}
|
||||
""")
|
||||
Optional<VotingStatsRow> getStatsByUuid(@Param("uuid") UUID uuid);
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package com.alttd.altitudeweb.database.votingplugin;
|
||||
|
||||
public record VotingStatsRow(
|
||||
String lastVotes,
|
||||
Integer bestDailyStreak,
|
||||
Integer bestWeeklyStreak,
|
||||
Integer bestMonthlyStreak,
|
||||
Integer dailyStreak,
|
||||
Integer weeklyStreak,
|
||||
Integer monthlyStreak,
|
||||
Integer totalVotesToday,
|
||||
Integer totalVotesThisWeek,
|
||||
Integer totalVotesThisMonth,
|
||||
Integer totalVotesAllTime
|
||||
) { }
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.alttd.altitudeweb.database.web_db;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
import java.time.Instant;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class KeyPairEntity {
|
||||
private int id;
|
||||
private String privateKey;
|
||||
private String publicKey;
|
||||
private Instant createdAt;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.alttd.altitudeweb.database.web_db;
|
||||
|
||||
import org.apache.ibatis.annotations.*;
|
||||
|
||||
public interface KeyPairMapper {
|
||||
|
||||
@Select("""
|
||||
SELECT id, private_key AS privateKey, public_key AS publicKey, created_at AS createdAt
|
||||
FROM key_pair
|
||||
ORDER BY id
|
||||
DESC LIMIT 1
|
||||
""")
|
||||
KeyPairEntity getKeyPair();
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO key_pair (id, private_key, public_key, created_at)
|
||||
VALUES (#{id}, #{privateKey}, #{publicKey}, #{createdAt})
|
||||
""")
|
||||
void save(KeyPairEntity keyPair);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.alttd.altitudeweb.database.web_db;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class PrivilegedUser {
|
||||
private Integer id;
|
||||
private UUID uuid;
|
||||
private List<String> permissions;
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package com.alttd.altitudeweb.database.web_db;
|
||||
|
||||
import org.apache.ibatis.annotations.*;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface PrivilegedUserMapper {
|
||||
|
||||
/**
|
||||
* Retrieves a user by their UUID along with their permissions
|
||||
* @param uuid The UUID of the user to retrieve
|
||||
* @return The optional PrivilegedUser with their permissions
|
||||
*/
|
||||
@Select("""
|
||||
SELECT id, uuid
|
||||
FROM privileged_users
|
||||
WHERE uuid = #{uuid}
|
||||
""")
|
||||
@Results({
|
||||
@Result(property = "id", column = "id"),
|
||||
@Result(property = "uuid", column = "uuid"),
|
||||
@Result(property = "permissions", column = "id", javaType = List.class,
|
||||
many = @Many(select = "getPermissionsForUser"))
|
||||
})
|
||||
Optional<PrivilegedUser> getUserByUuid(@Param("uuid") UUID uuid);
|
||||
|
||||
/**
|
||||
* Retrieves all privileged users with their permissions
|
||||
* @return List of all privileged users with their permissions
|
||||
*/
|
||||
@Select("""
|
||||
SELECT id, uuid
|
||||
FROM privileged_users
|
||||
""")
|
||||
@Results({
|
||||
@Result(property = "id", column = "id"),
|
||||
@Result(property = "uuid", column = "uuid"),
|
||||
@Result(property = "permissions", column = "id", javaType = List.class,
|
||||
many = @Many(select = "getPermissionsForUser"))
|
||||
})
|
||||
List<PrivilegedUser> getAllUsers();
|
||||
|
||||
/**
|
||||
* Gets all permissions for a specific user
|
||||
* @param userId The ID of the user
|
||||
* @return List of permission strings
|
||||
*/
|
||||
@Select("""
|
||||
SELECT privileges
|
||||
FROM privileges
|
||||
WHERE user_id = #{userId}
|
||||
""")
|
||||
List<String> getPermissionsForUser(@Param("userId") int userId);
|
||||
|
||||
/**
|
||||
* Adds a new privileged user
|
||||
* @param user The PrivilegedUser object to add
|
||||
* @return The number of rows affected
|
||||
*/
|
||||
@Insert("""
|
||||
INSERT INTO privileged_users (uuid)
|
||||
VALUES (#{user.uuid})
|
||||
""")
|
||||
@Options(useGeneratedKeys = true, keyProperty = "user.id", keyColumn = "id")
|
||||
int addUser(@Param("user") PrivilegedUser user);
|
||||
|
||||
/**
|
||||
* Deletes a privileged user by their UUID
|
||||
* @param uuid The UUID of the user to delete
|
||||
* @return The number of rows affected
|
||||
*/
|
||||
@Delete("""
|
||||
DELETE FROM privileged_users
|
||||
WHERE uuid = #{uuid}
|
||||
""")
|
||||
int deleteUserByUuid(@Param("uuid") String uuid);
|
||||
|
||||
/**
|
||||
* Adds a permission to a user
|
||||
* @param userId The ID of the user
|
||||
* @param permission The permission to add
|
||||
* @return The number of rows affected
|
||||
*/
|
||||
@Insert("""
|
||||
INSERT INTO privileges (user_id, privileges)
|
||||
VALUES (#{userId}, #{permission})
|
||||
""")
|
||||
int addPermissionToUser(@Param("userId") int userId, @Param("permission") String permission);
|
||||
|
||||
/**
|
||||
* Removes a permission from a user
|
||||
* @param userId The ID of the user
|
||||
* @param permission The permission to remove
|
||||
* @return The number of rows affected
|
||||
*/
|
||||
@Delete("""
|
||||
DELETE FROM privileges
|
||||
WHERE user_id = #{userId} AND privileges = #{permission}
|
||||
""")
|
||||
int removePermissionFromUser(@Param("userId") int userId, @Param("permission") String permission);
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO privileged_users (uuid)
|
||||
VALUES (#{uuid})
|
||||
""")
|
||||
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
|
||||
int createPrivilegedUser(UUID uuid);
|
||||
}
|
||||
@@ -3,6 +3,6 @@ package com.alttd.altitudeweb.database.web_db;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
public interface SettingsMapper {
|
||||
@Select("SELECT host, port, name, username, password FROM db_connection_settings WHERE name = #{database}")
|
||||
@Select("SELECT host, port, name, username, password FROM db_connection_settings WHERE internal_name = #{database}")
|
||||
DatabaseSettings getSettings(String database);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.alttd.altitudeweb.database.web_db.forms;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
public record Appeal(
|
||||
UUID id,
|
||||
UUID uuid,
|
||||
String historyType,
|
||||
Integer historyId,
|
||||
String username,
|
||||
String reason,
|
||||
Instant createdAt,
|
||||
Instant sendAt,
|
||||
String email,
|
||||
Long assignedTo
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.alttd.altitudeweb.database.web_db.forms;
|
||||
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface AppealMapper {
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO appeals (uuid, username, historyType, historyId, reason, created_at, send_at, e_mail, assigned_to)
|
||||
VALUES (#{uuid}, #{username}, #{historyType}, #{historyId}, #{reason}, #{createdAt}, #{sendAt}, #{email}, #{assignedTo})
|
||||
""")
|
||||
void createAppeal(Appeal appeal);
|
||||
|
||||
@Select("""
|
||||
SELECT id, uuid, historyType, historyId, reason, created_at AS createdAt, send_at AS sendAt, e_mail AS email, assigned_to AS assignedTo
|
||||
FROM appeals
|
||||
WHERE id = #{id}
|
||||
""")
|
||||
Appeal getAppealById(int id);
|
||||
|
||||
@Select("""
|
||||
SELECT id, uuid, historyType, historyId, reason, created_at AS createdAt, send_at AS sendAt, e_mail AS email, assigned_to AS assignedTo
|
||||
FROM appeals
|
||||
WHERE uuid = #{uuid}
|
||||
""")
|
||||
List<Appeal> getAppealsByUuid(String uuid);
|
||||
|
||||
@Update("""
|
||||
UPDATE appeals SET send_at = NOW()
|
||||
WHERE id = #{id}
|
||||
""")
|
||||
void markAppealAsSent(UUID id);
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package com.alttd.altitudeweb.database.web_db.forms;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public record StaffApplication(
|
||||
UUID id,
|
||||
UUID uuid,
|
||||
String email,
|
||||
Integer age,
|
||||
String discordUsername,
|
||||
Boolean meetsRequirements,
|
||||
String pronouns,
|
||||
LocalDate joinDate,
|
||||
Integer weeklyPlaytime,
|
||||
String availableDays,
|
||||
String availableTimes,
|
||||
String previousExperience,
|
||||
String pluginExperience,
|
||||
String moderatorExpectations,
|
||||
String additionalInfo,
|
||||
Instant createdAt,
|
||||
Instant sendAt,
|
||||
Long assignedTo
|
||||
) {
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package com.alttd.altitudeweb.database.web_db.forms;
|
||||
|
||||
import org.apache.ibatis.annotations.*;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface StaffApplicationMapper {
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO staff_applications (
|
||||
id, uuid, email, age, discord_username, meets_requirements, pronouns, join_date,
|
||||
weekly_playtime, available_days, available_times, previous_experience, plugin_experience,
|
||||
moderator_expectations, additional_info, created_at, send_at, assigned_to
|
||||
) VALUES (
|
||||
#{id}, #{uuid}, #{email}, #{age}, #{discordUsername}, #{meetsRequirements}, #{pronouns}, #{joinDate},
|
||||
#{weeklyPlaytime},
|
||||
#{availableDays},
|
||||
#{availableTimes}, #{previousExperience}, #{pluginExperience},
|
||||
#{moderatorExpectations}, #{additionalInfo}, #{createdAt}, #{sendAt}, #{assignedTo}
|
||||
)
|
||||
""")
|
||||
void insert(StaffApplication application);
|
||||
|
||||
@Update("""
|
||||
UPDATE staff_applications SET send_at = NOW()
|
||||
WHERE id = #{id}
|
||||
""")
|
||||
void markAsSent(@Param("id") UUID id);
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package com.alttd.altitudeweb.database.web_db.mail;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
public record EmailVerification(
|
||||
UUID id,
|
||||
UUID userUuid,
|
||||
String email,
|
||||
String verificationCode,
|
||||
boolean verified,
|
||||
Instant createdAt,
|
||||
Instant verifiedAt,
|
||||
Instant lastSentAt
|
||||
) {
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package com.alttd.altitudeweb.database.web_db.mail;
|
||||
|
||||
import org.apache.ibatis.annotations.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface EmailVerificationMapper {
|
||||
|
||||
@Insert("""
|
||||
INSERT INTO user_emails (id, user_uuid, email, verification_code, verified, created_at, last_sent_at)
|
||||
VALUES (#{id}, #{userUuid}, #{email}, #{verificationCode}, #{verified}, #{createdAt}, #{lastSentAt})
|
||||
""")
|
||||
void insert(EmailVerification emailVerification);
|
||||
|
||||
@Select("""
|
||||
SELECT id, user_uuid AS userUuid, email, verification_code AS verificationCode, verified,
|
||||
created_at AS createdAt, verified_at AS verifiedAt, last_sent_at AS lastSentAt
|
||||
FROM user_emails
|
||||
WHERE user_uuid = #{userUuid} AND email = #{email}
|
||||
""")
|
||||
EmailVerification findByUserAndEmail(@Param("userUuid") UUID userUuid, @Param("email") String email);
|
||||
|
||||
@Select("""
|
||||
SELECT id, user_uuid AS userUuid, email, verification_code AS verificationCode, verified,
|
||||
created_at AS createdAt, verified_at AS verifiedAt, last_sent_at AS lastSentAt
|
||||
FROM user_emails
|
||||
WHERE user_uuid = #{userUuid} AND verification_code = #{code}
|
||||
ORDER BY created_at DESC LIMIT 1
|
||||
""")
|
||||
EmailVerification findByUserAndCode(@Param("userUuid") UUID userUuid, @Param("code") String code);
|
||||
|
||||
@Select("""
|
||||
SELECT id, user_uuid AS userUuid, email, verification_code AS verificationCode, verified,
|
||||
created_at AS createdAt, verified_at AS verifiedAt, last_sent_at AS lastSentAt
|
||||
FROM user_emails
|
||||
WHERE user_uuid = #{userUuid}
|
||||
ORDER BY created_at ASC
|
||||
""")
|
||||
java.util.List<EmailVerification> findAllByUser(@Param("userUuid") UUID userUuid);
|
||||
|
||||
@Update("""
|
||||
UPDATE user_emails SET verified = 1, verified_at = #{verifiedAt}
|
||||
WHERE id = #{id}
|
||||
""")
|
||||
void markVerified(@Param("id") UUID id, @Param("verifiedAt") Instant verifiedAt);
|
||||
|
||||
@Update("""
|
||||
UPDATE user_emails SET verification_code = #{code}, last_sent_at = #{lastSentAt}, verified = 0, verified_at = NULL
|
||||
WHERE id = #{id}
|
||||
""")
|
||||
void updateCodeAndLastSent(@Param("id") UUID id, @Param("code") String code, @Param("lastSentAt") Instant lastSentAt);
|
||||
|
||||
@Delete("""
|
||||
DELETE FROM user_emails WHERE user_uuid = #{userUuid} AND email = #{email}
|
||||
""")
|
||||
void deleteByUserAndEmail(@Param("userUuid") UUID userUuid, @Param("email") String email);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.alttd.altitudeweb.setup;
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.web_db.DatabaseSettings;
|
||||
import com.alttd.altitudeweb.database.web_db.SettingsMapper;
|
||||
import com.alttd.altitudeweb.type_handler.UUIDTypeHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.datasource.pooled.PooledDataSource;
|
||||
import org.apache.ibatis.mapping.Environment;
|
||||
@@ -11,9 +12,12 @@ 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 org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@Slf4j
|
||||
@@ -33,6 +37,8 @@ public class Connection {
|
||||
InitializeWebDb.init();
|
||||
InitializeLiteBans.init();
|
||||
InitializeLuckPerms.init();
|
||||
InitializeDiscord.init();
|
||||
InitializeVotingPlugin.init();
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
@@ -54,25 +60,46 @@ public class Connection {
|
||||
if (database == Databases.DEFAULT) {
|
||||
return loadDefaultDatabase(addMappers);
|
||||
}
|
||||
|
||||
log.debug("Loading settings for database {}", database.getInternalName());
|
||||
CompletableFuture<DatabaseSettings> settingsFuture = new CompletableFuture<>();
|
||||
|
||||
getConnection(Databases.DEFAULT, (mapper -> mapper.addMapper(SettingsMapper.class))).thenApply(connection -> {
|
||||
log.debug("Loading settings for database {}", database.getInternalName());
|
||||
connection.runQuery(session -> {
|
||||
log.debug("Running query to load settings for database");
|
||||
DatabaseSettings loadedSettings = session.getMapper(SettingsMapper.class).getSettings(database.getInternalName());
|
||||
if (loadedSettings == null) {
|
||||
log.error("Failed to load settings for database {}", database.getInternalName());
|
||||
try {
|
||||
log.debug("Running query to load settings for database {}", database.getInternalName());
|
||||
DatabaseSettings loadedSettings = session.getMapper(SettingsMapper.class).getSettings(database.getInternalName());
|
||||
|
||||
if (loadedSettings == null) {
|
||||
log.error("Failed to load settings for database {}. No settings found in db_connection_settings table.",
|
||||
database.getInternalName());
|
||||
settingsFuture.completeExceptionally(new IllegalStateException(
|
||||
"Database settings for " + database.getInternalName() + " not found in db_connection_settings table"));
|
||||
} else {
|
||||
log.debug("Loaded settings for database {}: host={}, port={}, name={}",
|
||||
database.getInternalName(), loadedSettings.host(), loadedSettings.port(), loadedSettings.name());
|
||||
settingsFuture.complete(loadedSettings);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error occurred while loading database settings for {}", database.getInternalName(), e);
|
||||
settingsFuture.completeExceptionally(e);
|
||||
}
|
||||
log.debug("Loaded settings {}", loadedSettings);
|
||||
settingsFuture.complete(loadedSettings);
|
||||
});
|
||||
return null;
|
||||
}).exceptionally(ex -> {
|
||||
log.error("Failed to access DEFAULT database to load settings for {}", database.getInternalName(), ex);
|
||||
settingsFuture.completeExceptionally(ex);
|
||||
return null;
|
||||
});
|
||||
|
||||
return settingsFuture.thenApply(loadedSettings -> {
|
||||
log.debug("Storing connection for database {}", database.getInternalName());
|
||||
Connection connection = new Connection(loadedSettings, addMappers);
|
||||
connections.put(database, connection);
|
||||
return connection;
|
||||
}).exceptionally(ex -> {
|
||||
log.error("Failed to create connection for database {}", database.getInternalName(), ex);
|
||||
throw new CompletionException("Failed to initialize database connection for " + database.getInternalName(), ex);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -87,6 +114,7 @@ public class Connection {
|
||||
log.debug("Loaded default database settings {}", databaseSettings);
|
||||
Connection connection = new Connection(databaseSettings, addMappers);
|
||||
log.debug("Created default database connection {}", connection);
|
||||
connections.put(Databases.DEFAULT, connection);
|
||||
return CompletableFuture.completedFuture(connection);
|
||||
}
|
||||
|
||||
@@ -96,26 +124,66 @@ public class Connection {
|
||||
sqlSessionFactory = createSqlSessionFactory(settings, addMappers);
|
||||
}
|
||||
|
||||
try (SqlSession session = sqlSessionFactory.openSession()) {
|
||||
SqlSession session = null;
|
||||
try {
|
||||
session = sqlSessionFactory.openSession();
|
||||
consumer.accept(session);
|
||||
session.commit();
|
||||
} catch (Exception e) {
|
||||
if (session != null) {
|
||||
session.rollback();
|
||||
}
|
||||
log.error("Failed to run query", e);
|
||||
} finally {
|
||||
if (session != null) {
|
||||
session.close();
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private SqlSessionFactory createSqlSessionFactory(DatabaseSettings settings, AddMappers addMappers) {
|
||||
try {
|
||||
Configuration configuration = getConfiguration(settings);
|
||||
configuration.getTypeHandlerRegistry().register(UUID.class, UUIDTypeHandler.class);
|
||||
addMappers.apply(configuration);
|
||||
|
||||
return new SqlSessionFactoryBuilder().build(configuration);
|
||||
} catch (Exception e) {
|
||||
log.error("""
|
||||
Failed to create sql session factory with
|
||||
\thost {}
|
||||
\tport: {}
|
||||
\tname: {}
|
||||
\tusername: {}
|
||||
""", settings.host(), settings.port(), settings.name(), settings.username(), e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private static @NotNull Configuration getConfiguration(DatabaseSettings settings) {
|
||||
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()));
|
||||
|
||||
String url = String.format(
|
||||
"jdbc:mysql://%s:%d/%s?useSSL=true&tcpKeepAlive=true&socketTimeout=60000&connectTimeout=10000&autoReconnect=false&useUnicode=true&characterEncoding=utf8",
|
||||
settings.host(),
|
||||
settings.port(),
|
||||
settings.name()
|
||||
);
|
||||
dataSource.setUrl(url);
|
||||
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);
|
||||
dataSource.setPoolMaximumActiveConnections(10);
|
||||
dataSource.setPoolMaximumIdleConnections(5);
|
||||
dataSource.setPoolTimeToWait(20000);
|
||||
dataSource.setPoolPingEnabled(true);
|
||||
dataSource.setPoolPingQuery("SELECT 1");
|
||||
dataSource.setPoolPingConnectionsNotUsedFor(300000); // 5 min
|
||||
|
||||
Environment environment = new Environment("production", new JdbcTransactionFactory(), dataSource);
|
||||
return new Configuration(environment);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.alttd.altitudeweb.setup;
|
||||
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.discord.OutputChannelMapper;
|
||||
import com.alttd.altitudeweb.database.luckperms.TeamMemberMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class InitializeDiscord {
|
||||
|
||||
protected static void init() {
|
||||
log.info("Initializing Discord");
|
||||
Connection.getConnection(Databases.DISCORD, (configuration) -> {
|
||||
configuration.addMapper(OutputChannelMapper.class);
|
||||
}).join();
|
||||
log.debug("Initialized Discord");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,6 +18,8 @@ public class InitializeLiteBans {
|
||||
configuration.addMapper(NameHistoryMapper.class);
|
||||
configuration.addMapper(UUIDHistoryMapper.class);
|
||||
configuration.addMapper(HistoryCountMapper.class);
|
||||
configuration.addMapper(IdHistoryMapper.class);
|
||||
configuration.addMapper(EditHistoryMapper.class);
|
||||
}).join()
|
||||
.runQuery(sqlSession -> {
|
||||
createAllPunishmentsView(sqlSession);
|
||||
@@ -29,15 +31,15 @@ public class InitializeLiteBans {
|
||||
private static void createAllPunishmentsView(SqlSession sqlSession) {
|
||||
String query = """
|
||||
CREATE VIEW IF NOT EXISTS all_punishments AS
|
||||
SELECT uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
|
||||
SELECT id, uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
|
||||
'ban' as type
|
||||
FROM litebans_bans
|
||||
UNION ALL
|
||||
SELECT uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
|
||||
SELECT id, uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
|
||||
'mute' as type
|
||||
FROM litebans_mutes
|
||||
UNION ALL
|
||||
SELECT uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
|
||||
SELECT id, uuid, reason, banned_by_uuid, banned_by_name, removed_by_name, time, until, removed_by_reason,
|
||||
'warn' as type
|
||||
FROM litebans_warnings
|
||||
ORDER BY time DESC;
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.alttd.altitudeweb.setup;
|
||||
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.luckperms.TeamMemberMapper;
|
||||
import com.alttd.altitudeweb.database.luckperms.UUIDUsernameMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@@ -11,6 +12,7 @@ public class InitializeLuckPerms {
|
||||
log.info("Initializing LuckPerms");
|
||||
Connection.getConnection(Databases.LUCK_PERMS, (configuration) -> {
|
||||
configuration.addMapper(TeamMemberMapper.class);
|
||||
configuration.addMapper(UUIDUsernameMapper.class);
|
||||
}).join();
|
||||
log.debug("Initialized LuckPerms");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.alttd.altitudeweb.setup;
|
||||
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.votingplugin.VotingPluginUsersMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class InitializeVotingPlugin {
|
||||
|
||||
protected static void init() {
|
||||
log.info("Initializing VotingPlugin");
|
||||
Connection.getConnection(Databases.VOTING_PLUGIN, (configuration) -> {
|
||||
configuration.addMapper(VotingPluginUsersMapper.class);
|
||||
}).join();
|
||||
log.debug("Initialized VotingPlugin");
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,15 @@
|
||||
package com.alttd.altitudeweb.setup;
|
||||
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.web_db.KeyPairMapper;
|
||||
import com.alttd.altitudeweb.database.web_db.PrivilegedUserMapper;
|
||||
import com.alttd.altitudeweb.database.web_db.SettingsMapper;
|
||||
import com.alttd.altitudeweb.database.web_db.forms.AppealMapper;
|
||||
import com.alttd.altitudeweb.database.web_db.forms.StaffApplicationMapper;
|
||||
import com.alttd.altitudeweb.database.web_db.mail.EmailVerificationMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.session.SqlSession;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
@@ -12,15 +18,28 @@ import java.sql.Statement;
|
||||
public class InitializeWebDb {
|
||||
|
||||
protected static void init() {
|
||||
log.info("Initializing LiteBans");
|
||||
log.info("Initializing WebDb");
|
||||
Connection.getConnection(Databases.DEFAULT, (configuration) -> {
|
||||
configuration.addMapper(SettingsMapper.class);
|
||||
}).join()
|
||||
.runQuery(InitializeWebDb::createSettingsTable);
|
||||
log.debug("Initialized LuckPerms");
|
||||
configuration.addMapper(SettingsMapper.class);
|
||||
configuration.addMapper(KeyPairMapper.class);
|
||||
configuration.addMapper(PrivilegedUserMapper.class);
|
||||
configuration.addMapper(AppealMapper.class);
|
||||
configuration.addMapper(StaffApplicationMapper.class);
|
||||
configuration.addMapper(EmailVerificationMapper.class);
|
||||
}).join()
|
||||
.runQuery(sqlSession -> {
|
||||
createSettingsTable(sqlSession);
|
||||
createKeyTable(sqlSession);
|
||||
createPrivilegedUsersTable(sqlSession);
|
||||
createPrivilegesTable(sqlSession);
|
||||
createAppealTable(sqlSession);
|
||||
createStaffApplicationsTable(sqlSession);
|
||||
createUserEmailsTable(sqlSession);
|
||||
});
|
||||
log.debug("Initialized WebDb");
|
||||
}
|
||||
|
||||
private static void createSettingsTable(SqlSession sqlSession) {
|
||||
private static void createSettingsTable(@NotNull SqlSession sqlSession) {
|
||||
String query = """
|
||||
CREATE TABLE IF NOT EXISTS db_connection_settings
|
||||
(
|
||||
@@ -40,4 +59,128 @@ public class InitializeWebDb {
|
||||
}
|
||||
}
|
||||
|
||||
private static void createKeyTable(@NotNull SqlSession sqlSession) {
|
||||
String query = """
|
||||
CREATE TABLE IF NOT EXISTS key_pair (
|
||||
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
private_key TEXT NOT NULL,
|
||||
public_key TEXT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL
|
||||
);
|
||||
""";
|
||||
try (Statement statement = sqlSession.getConnection().createStatement()) {
|
||||
statement.execute(query);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createPrivilegedUsersTable(@NotNull SqlSession sqlSession) {
|
||||
String query = """
|
||||
CREATE TABLE IF NOT EXISTS privileged_users (
|
||||
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
uuid UUID UNIQUE NOT NULL
|
||||
);
|
||||
""";
|
||||
try (Statement statement = sqlSession.getConnection().createStatement()) {
|
||||
statement.execute(query);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createPrivilegesTable(@NotNull SqlSession sqlSession) {
|
||||
String query = """
|
||||
CREATE TABLE IF NOT EXISTS privileges (
|
||||
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id int NOT NULL,
|
||||
privileges VARCHAR(36) NOT NULL,
|
||||
CONSTRAINT fk_privileges_user FOREIGN KEY (user_id)
|
||||
REFERENCES privileged_users(id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE CASCADE
|
||||
);
|
||||
""";
|
||||
try (Statement statement = sqlSession.getConnection().createStatement()) {
|
||||
statement.execute(query);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createUserEmailsTable(@NotNull SqlSession sqlSession) {
|
||||
String query = """
|
||||
CREATE TABLE IF NOT EXISTS user_emails (
|
||||
id UUID NOT NULL DEFAULT (UUID()) PRIMARY KEY,
|
||||
user_uuid UUID NOT NULL,
|
||||
email VARCHAR(255) NOT NULL,
|
||||
verification_code VARCHAR(16) NOT NULL,
|
||||
verified BOOLEAN NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
verified_at TIMESTAMP NULL,
|
||||
last_sent_at TIMESTAMP NULL,
|
||||
FOREIGN KEY (user_uuid) REFERENCES privileged_users(uuid) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
""";
|
||||
try (Statement statement = sqlSession.getConnection().createStatement()) {
|
||||
statement.execute(query);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createStaffApplicationsTable(@NotNull SqlSession sqlSession) {
|
||||
String query = """
|
||||
CREATE TABLE IF NOT EXISTS staff_applications (
|
||||
id UUID NOT NULL DEFAULT (UUID()) PRIMARY KEY,
|
||||
uuid UUID NOT NULL,
|
||||
email VARCHAR(320) NOT NULL,
|
||||
age INT NOT NULL,
|
||||
discord_username VARCHAR(32) NOT NULL,
|
||||
meets_requirements BOOLEAN NOT NULL,
|
||||
pronouns VARCHAR(32) NULL,
|
||||
join_date DATE NOT NULL,
|
||||
weekly_playtime INT NOT NULL,
|
||||
available_days TEXT NOT NULL,
|
||||
available_times TEXT NOT NULL,
|
||||
previous_experience TEXT NOT NULL,
|
||||
plugin_experience TEXT NOT NULL,
|
||||
moderator_expectations TEXT NOT NULL,
|
||||
additional_info TEXT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
send_at TIMESTAMP NULL,
|
||||
assigned_to BIGINT UNSIGNED NULL,
|
||||
FOREIGN KEY (uuid) REFERENCES privileged_users(uuid) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
""";
|
||||
try (Statement statement = sqlSession.getConnection().createStatement()) {
|
||||
statement.execute(query);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createAppealTable(@NotNull SqlSession sqlSession) {
|
||||
String query = """
|
||||
CREATE TABLE IF NOT EXISTS appeals (
|
||||
id UUID NOT NULL DEFAULT (UUID()) PRIMARY KEY,
|
||||
uuid UUID NOT NULL,
|
||||
historyType VARCHAR(16) NOT NULL,
|
||||
historyId BIGINT UNSIGNED NOT NULL,
|
||||
username VARCHAR(16) NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
send_at TIMESTAMP NULL,
|
||||
e_mail TEXT NOT NULL,
|
||||
assigned_to BIGINT UNSIGNED NULL,
|
||||
FOREIGN KEY (uuid) REFERENCES privileged_users(uuid) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
""";
|
||||
try (Statement statement = sqlSession.getConnection().createStatement()) {
|
||||
statement.execute(query);
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
plugins {
|
||||
id("java")
|
||||
}
|
||||
|
||||
group = "com.alttd.webinterface"
|
||||
version = "unspecified"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation(platform("org.junit:junit-bom:5.10.0"))
|
||||
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||
// JDA
|
||||
implementation("net.dv8tion:JDA:6.0.0-rc.2") {
|
||||
exclude("opus-java") // exclude audio
|
||||
exclude("tink") // exclude audio
|
||||
}
|
||||
compileOnly("org.projectlombok:lombok:1.18.38")
|
||||
annotationProcessor("org.projectlombok:lombok:1.18.38")
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.alttd.webinterface;
|
||||
|
||||
import com.alttd.webinterface.bot.DiscordBotInstance;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class DiscordBot {
|
||||
|
||||
public static void main(String[] args) {
|
||||
String discordToken = System.getProperty("DISCORD_TOKEN");
|
||||
if (discordToken == null) {
|
||||
log.error("Discord token not found, put it in the DISCORD_TOKEN environment variable");
|
||||
System.exit(1);
|
||||
}
|
||||
DiscordBotInstance discordBotInstance = new DiscordBotInstance();
|
||||
discordBotInstance.start(discordToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.alttd.webinterface.bot;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.dv8tion.jda.api.JDA;
|
||||
import net.dv8tion.jda.api.JDABuilder;
|
||||
import net.dv8tion.jda.api.requests.GatewayIntent;
|
||||
|
||||
@Slf4j
|
||||
public class DiscordBotInstance {
|
||||
|
||||
@Getter
|
||||
private JDA jda;
|
||||
private volatile boolean ready = false;
|
||||
|
||||
public synchronized void start(String token) {
|
||||
if (jda != null) {
|
||||
return;
|
||||
}
|
||||
jda = JDABuilder.createDefault(token,
|
||||
GatewayIntent.GUILD_MEMBERS,
|
||||
GatewayIntent.GUILD_PRESENCES,
|
||||
GatewayIntent.GUILD_MESSAGES,
|
||||
GatewayIntent.MESSAGE_CONTENT)
|
||||
.addEventListeners(new ReadyListener(() -> {
|
||||
ready = true;
|
||||
}))
|
||||
.build();
|
||||
}
|
||||
|
||||
public boolean isReady() {
|
||||
return ready && jda != null && jda.getStatus() == JDA.Status.CONNECTED;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.alttd.webinterface.bot;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.dv8tion.jda.api.events.session.ReadyEvent;
|
||||
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class ReadyListener extends ListenerAdapter {
|
||||
|
||||
private final Runnable onReadyCallback;
|
||||
|
||||
@Override
|
||||
public void onReady(@NotNull ReadyEvent event) {
|
||||
log.info("JDA is ready. Guilds loaded: {}", event.getJDA().getGuilds().size());
|
||||
if (onReadyCallback != null) {
|
||||
try {
|
||||
onReadyCallback.run();
|
||||
} catch (Exception e) {
|
||||
log.error("Error running onReady callback", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.alttd.webinterface.send_message;
|
||||
|
||||
import com.alttd.webinterface.bot.DiscordBotInstance;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.dv8tion.jda.api.EmbedBuilder;
|
||||
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
|
||||
|
||||
import java.awt.*;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
public class DiscordSender {
|
||||
|
||||
private static final DiscordSender INSTANCE = new DiscordSender();
|
||||
|
||||
private final DiscordBotInstance botInstance = new DiscordBotInstance();
|
||||
|
||||
private DiscordSender() {}
|
||||
|
||||
public static DiscordSender getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private void ensureStarted() {
|
||||
if (botInstance.getJda() != null) return;
|
||||
String token = Optional.ofNullable(System.getenv("DISCORD_TOKEN"))
|
||||
.orElse(System.getProperty("DISCORD_TOKEN"));
|
||||
if (token == null || token.isBlank()) {
|
||||
log.error("Discord token not found. Set DISCORD_TOKEN as an environment variable or system property.");
|
||||
return;
|
||||
}
|
||||
botInstance.start(token);
|
||||
}
|
||||
|
||||
public void sendMessageToChannels(List<Long> channelIds, String message) {
|
||||
ensureStarted();
|
||||
if (botInstance.getJda() == null) {
|
||||
log.error("JDA not initialized; cannot send Discord message.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!botInstance.isReady()) {
|
||||
botInstance.getJda().awaitReady();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (Exception e) {
|
||||
log.warn("Error while waiting for JDA ready state", e);
|
||||
}
|
||||
|
||||
channelIds.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.forEach(id -> {
|
||||
TextChannel channel = botInstance.getJda().getChannelById(TextChannel.class, id);
|
||||
if (channel == null) {
|
||||
log.warn("TextChannel with id {} not found", id);
|
||||
return;
|
||||
}
|
||||
channel.sendMessage(message).queue(
|
||||
success -> log.debug("Sent message to channel {}", id),
|
||||
error -> log.error("Failed sending message to channel {}", id, error)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public void sendEmbedToChannels(List<Long> channelIds, String title, String description, List<EmbedField> fields,
|
||||
Integer colorRgb, Instant timestamp, String footer) {
|
||||
ensureStarted();
|
||||
if (botInstance.getJda() == null) {
|
||||
log.error("JDA not initialized; cannot send Discord embed.");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (!botInstance.isReady()) {
|
||||
botInstance.getJda().awaitReady();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (Exception e) {
|
||||
log.warn("Error while waiting for JDA ready state", e);
|
||||
}
|
||||
|
||||
EmbedBuilder eb = new EmbedBuilder();
|
||||
if (title != null && !title.isBlank()) eb.setTitle(title);
|
||||
if (description != null && !description.isBlank()) eb.setDescription(description);
|
||||
if (colorRgb != null) eb.setColor(new Color(colorRgb)); else eb.setColor(new Color(0xFF8C00)); // default orange
|
||||
eb.setTimestamp(timestamp != null ? timestamp : Instant.now());
|
||||
if (footer != null && !footer.isBlank()) eb.setFooter(footer);
|
||||
|
||||
if (fields != null) {
|
||||
for (EmbedField f : fields) {
|
||||
if (f == null) continue;
|
||||
String name = f.getName() == null ? "" : f.getName();
|
||||
String value = f.getValue() == null ? "" : f.getValue();
|
||||
// JDA field value max is 1024; truncate to be safe
|
||||
if (value.length() > 1024) value = value.substring(0, 1021) + "...";
|
||||
eb.addField(new MessageEmbed.Field(name, value, f.isInline()));
|
||||
}
|
||||
}
|
||||
|
||||
MessageEmbed embed = eb.build();
|
||||
|
||||
channelIds.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.forEach(id -> {
|
||||
TextChannel channel = botInstance.getJda().getChannelById(TextChannel.class, id);
|
||||
if (channel == null) {
|
||||
log.warn("TextChannel with id {} not found", id);
|
||||
return;
|
||||
}
|
||||
channel.sendMessageEmbeds(embed).queue(
|
||||
success -> log.debug("Sent embed to channel {}", id),
|
||||
error -> log.error("Failed sending embed to channel {}", id, error)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class EmbedField {
|
||||
private String name;
|
||||
private String value;
|
||||
private boolean inline;
|
||||
}
|
||||
}
|
||||
+59
-14
@@ -15,11 +15,12 @@
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"outputPath": "dist",
|
||||
"outputPath": {
|
||||
"base": "dist"
|
||||
},
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": [
|
||||
"zone.js"
|
||||
],
|
||||
@@ -34,33 +35,51 @@
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
"scripts": [],
|
||||
"browser": "src/main.ts"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
"with": "src/environments/environment.production.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"logging": false,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true
|
||||
"namedChunks": false
|
||||
},
|
||||
"development": {
|
||||
"sourceMap": true,
|
||||
"optimization": false
|
||||
"optimization": false,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.development.ts"
|
||||
}
|
||||
]
|
||||
},
|
||||
"beta": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.beta.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"options": {
|
||||
"proxyConfig": "proxy.conf.json"
|
||||
},
|
||||
"builder": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "frontend:build:production"
|
||||
@@ -72,10 +91,10 @@
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n"
|
||||
"builder": "@angular/build:extract-i18n"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"builder": "@angular/build:karma",
|
||||
"options": {
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
@@ -101,5 +120,31 @@
|
||||
},
|
||||
"cli": {
|
||||
"analytics": false
|
||||
},
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"type": "component"
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"type": "directive"
|
||||
},
|
||||
"@schematics/angular:service": {
|
||||
"type": "service"
|
||||
},
|
||||
"@schematics/angular:guard": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:interceptor": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:module": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:pipe": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:resolver": {
|
||||
"typeSeparator": "."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+54
-10
@@ -1,14 +1,58 @@
|
||||
tasks.register<Exec>("npmInstall") {
|
||||
commandLine("npm.cmd", "install")
|
||||
// commandLine("npm", "install")
|
||||
import com.github.gradle.node.NodeExtension
|
||||
import com.github.gradle.node.npm.task.NpmTask
|
||||
|
||||
plugins {
|
||||
id("com.github.node-gradle.node") version "7.1.0"
|
||||
id("base") // This adds the standard Gradle lifecycle tasks like "build"
|
||||
}
|
||||
|
||||
tasks.register<Exec>("ngBuild") {
|
||||
dependsOn("npmInstall", "generateFrontendApi")
|
||||
// commandLine("npm", "run", "build")
|
||||
commandLine("npm.cmd", "run", "build")
|
||||
node {
|
||||
download.set(true)
|
||||
// Update to the version that's compatible with your environment requirements
|
||||
version.set("20.19.0")
|
||||
npmVersion.set("10.2.3") // A compatible npm version for Node.js 20.19.0
|
||||
workDir.set(file("${project.projectDir}/node"))
|
||||
npmWorkDir.set(file("${project.projectDir}/node"))
|
||||
}
|
||||
|
||||
//dependencies {
|
||||
// implementation(project(":backend"))
|
||||
//}
|
||||
// Clean the distribution directory
|
||||
tasks.register<Delete>("cleanDist") {
|
||||
description = "Clean the distribution directory"
|
||||
delete("dist")
|
||||
}
|
||||
|
||||
// Create a task that will run npm build
|
||||
tasks.register<com.github.gradle.node.npm.task.NpmTask>("npmBuild") {
|
||||
description = "Run 'npm run build'"
|
||||
group = "build"
|
||||
|
||||
// Determine which build script to run based on the OS
|
||||
val isWindows = System.getProperty("os.name").lowercase().contains("windows")
|
||||
npmCommand.set(listOf("run", if (isWindows) "build:dev" else "build:beta"))
|
||||
|
||||
dependsOn("npmInstall")
|
||||
}
|
||||
|
||||
// Add a new task to check Node.js and npm versions
|
||||
tasks.register<com.github.gradle.node.task.NodeTask>("nodeVersionCheck") {
|
||||
description = "Check Node.js and npm versions"
|
||||
script.set(file("${projectDir}/node-version-check.js"))
|
||||
|
||||
doFirst {
|
||||
// Create a temporary script to check versions
|
||||
file("${projectDir}/node-version-check.js").writeText("""
|
||||
console.log('Node.js version:', process.version);
|
||||
console.log('npm version:', require('npm/package.json').version);
|
||||
console.log('Build command that would be used:', process.platform === 'win32' ? 'build:dev' : 'build:beta');
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
doLast {
|
||||
// Clean up the temporary script
|
||||
delete("${projectDir}/node-version-check.js")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("assemble") {
|
||||
dependsOn("npmBuild")
|
||||
}
|
||||
|
||||
+21
-13
@@ -6,26 +6,34 @@
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
"test": "ng test",
|
||||
"build:beta": "ng build --configuration=beta",
|
||||
"build:prod": "ng build --configuration=production",
|
||||
"build:dev": "ng build --configuration=development"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/common": "^19.2.0",
|
||||
"@angular/compiler": "^19.2.0",
|
||||
"@angular/core": "^19.2.0",
|
||||
"@angular/forms": "^19.2.0",
|
||||
"@angular/platform-browser": "^19.2.0",
|
||||
"@angular/platform-browser-dynamic": "^19.2.0",
|
||||
"@angular/router": "^19.2.0",
|
||||
"ngx-cookie-service": "^19.1.2",
|
||||
"@angular/cdk": "^20.1.3",
|
||||
"@angular/common": "^20.1.0",
|
||||
"@angular/compiler": "^20.1.0",
|
||||
"@angular/core": "^20.1.0",
|
||||
"@angular/forms": "^20.1.0",
|
||||
"@angular/material": "^20.1.3",
|
||||
"@angular/platform-browser": "^20.1.0",
|
||||
"@angular/platform-browser-dynamic": "^20.1.0",
|
||||
"@angular/router": "^20.1.0",
|
||||
"@auth0/angular-jwt": "^5.2.0",
|
||||
"@types/three": "^0.177.0",
|
||||
"ngx-cookie-service": "^20.0.1",
|
||||
"rxjs": "~7.8.0",
|
||||
"three": "^0.177.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^19.2.5",
|
||||
"@angular/cli": "^19.2.5",
|
||||
"@angular/compiler-cli": "^19.2.0",
|
||||
"@angular/build": "^20.1.0",
|
||||
"@angular/cli": "^20.1.0",
|
||||
"@angular/compiler-cli": "^20.1.0",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"jasmine-core": "~5.6.0",
|
||||
"karma": "~6.4.0",
|
||||
@@ -33,6 +41,6 @@
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.7.2"
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"/api": {
|
||||
"target": "http://localhost:8080",
|
||||
"secure": false,
|
||||
"changeOrigin": true
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
+1
-5
@@ -4,8 +4,4 @@
|
||||
# New page
|
||||
|
||||
- ng generate component <name>
|
||||
- Go to the component (<name>.component.ts)
|
||||
- add standalone: false to the @Component
|
||||
- remove imports: [] from that same block
|
||||
- Go to app.module.ts and add the new page in declaration
|
||||
- Add the new file to routes and give it a path (like 'new_page')
|
||||
- Go to app.routes.ts and give it a path
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {Meta, Title} from '@angular/platform-browser';
|
||||
import {ALTITUDE_VERSION} from './constant';
|
||||
import {ALTITUDE_VERSION} from '@custom-types/constant';
|
||||
import {Router, RouterOutlet} from '@angular/router';
|
||||
import {FooterComponent} from './footer/footer.component';
|
||||
import {FooterComponent} from '@pages/footer/footer/footer.component';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -10,8 +10,8 @@ import {FooterComponent} from './footer/footer.component';
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.scss',
|
||||
imports: [
|
||||
FooterComponent,
|
||||
RouterOutlet
|
||||
RouterOutlet,
|
||||
FooterComponent
|
||||
]
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import {ApplicationConfig, provideZoneChangeDetection} from '@angular/core';
|
||||
import {provideRouter} from '@angular/router';
|
||||
|
||||
import {routes} from './app.routes';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [provideZoneChangeDetection({eventCoalescing: true}), provideRouter(routes)]
|
||||
};
|
||||
+123
-14
@@ -1,50 +1,159 @@
|
||||
import {Routes} from '@angular/router';
|
||||
import {AuthGuard} from './guards/auth.guard';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
|
||||
loadComponent: () => import('./pages/home/home.component').then(m => m.HomeComponent)
|
||||
},
|
||||
{
|
||||
path: 'particles',
|
||||
loadComponent: () => import('./pages/particles/particles.component').then(m => m.ParticlesComponent),
|
||||
canActivate: [AuthGuard],
|
||||
data: {
|
||||
requiredAuthorizations: ['SCOPE_head_mod']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'map',
|
||||
loadComponent: () => import('./map/map.component').then(m => m.MapComponent)
|
||||
loadComponent: () => import('./pages/features/map/map.component').then(m => m.MapComponent)
|
||||
},
|
||||
{
|
||||
path: 'rules',
|
||||
loadComponent: () => import('./rules/rules.component').then(m => m.RulesComponent)
|
||||
loadComponent: () => import('./pages/reference/rules/rules.component').then(m => m.RulesComponent)
|
||||
},
|
||||
{
|
||||
path: 'vote',
|
||||
loadComponent: () => import('./vote/vote.component').then(m => m.VoteComponent)
|
||||
loadComponent: () => import('./pages/vote/vote.component').then(m => m.VoteComponent)
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
loadComponent: () => import('./about/about.component').then(m => m.AboutComponent)
|
||||
loadComponent: () => import('./pages/altitude/about/about.component').then(m => m.AboutComponent)
|
||||
},
|
||||
{
|
||||
path: 'socials',
|
||||
loadComponent: () => import('./socials/socials.component').then(m => m.SocialsComponent)
|
||||
loadComponent: () => import('./pages/altitude/socials/socials.component').then(m => m.SocialsComponent)
|
||||
},
|
||||
{
|
||||
path: 'team',
|
||||
loadComponent: () => import('./team/team.component').then(m => m.TeamComponent)
|
||||
loadComponent: () => import('./pages/altitude/team/team.component').then(m => m.TeamComponent)
|
||||
},
|
||||
{
|
||||
path: 'birthdays',
|
||||
loadComponent: () => import('./birthdays/birthdays.component').then(m => m.BirthdaysComponent)
|
||||
loadComponent: () => import('./pages/altitude/birthdays/birthdays.component').then(m => m.BirthdaysComponent)
|
||||
},
|
||||
{
|
||||
path: 'terms',
|
||||
loadComponent: () => import('./terms/terms.component').then(m => m.TermsComponent)
|
||||
loadComponent: () => import('./pages/footer/terms/terms.component').then(m => m.TermsComponent)
|
||||
},
|
||||
{
|
||||
path: 'privacy',
|
||||
loadComponent: () => import('./privacy/privacy.component').then(m => m.PrivacyComponent)
|
||||
loadComponent: () => import('./pages/footer/privacy/privacy.component').then(m => m.PrivacyComponent)
|
||||
},
|
||||
{
|
||||
path: 'bans',
|
||||
loadComponent: () => import('./bans/bans.component').then(m => m.BansComponent)
|
||||
}
|
||||
loadComponent: () => import('./pages/reference/bans/bans.component').then(m => m.BansComponent)
|
||||
},
|
||||
{
|
||||
path: 'bans/:type/:id',
|
||||
loadComponent: () => import('./pages/reference/bans/details/details.component').then(m => m.DetailsComponent)
|
||||
},
|
||||
{
|
||||
path: 'economy',
|
||||
loadComponent: () => import('./pages/features/economy/economy.component').then(m => m.EconomyComponent)
|
||||
},
|
||||
{
|
||||
path: 'claiming',
|
||||
loadComponent: () => import('./pages/features/claiming/claiming.component').then(m => m.ClaimingComponent)
|
||||
},
|
||||
{
|
||||
path: 'mypet',
|
||||
loadComponent: () => import('./pages/features/mypet/mypet.component').then(m => m.MypetComponent)
|
||||
},
|
||||
{
|
||||
path: 'warps',
|
||||
loadComponent: () => import('./pages/features/warps/warps.component').then(m => m.WarpsComponent)
|
||||
},
|
||||
{
|
||||
path: 'skyblock',
|
||||
loadComponent: () => import('./pages/features/skyblock/skyblock.component').then(m => m.SkyblockComponent)
|
||||
},
|
||||
{
|
||||
path: 'customfeatures',
|
||||
loadComponent: () => import('./pages/features/customfeatures/customfeatures.component').then(m => m.CustomfeaturesComponent)
|
||||
},
|
||||
{
|
||||
path: 'guide',
|
||||
loadComponent: () => import('./pages/reference/guide/guide.component').then(m => m.GuideComponent)
|
||||
},
|
||||
{
|
||||
path: 'ranks',
|
||||
loadComponent: () => import('./pages/reference/ranks/ranks.component').then(m => m.RanksComponent)
|
||||
},
|
||||
{
|
||||
path: 'commandlist',
|
||||
loadComponent: () => import('./pages/reference/commandlist/commandlist.component').then(m => m.CommandlistComponent)
|
||||
},
|
||||
{
|
||||
path: 'mapart',
|
||||
loadComponent: () => import('./pages/reference/mapart/mapart.component').then(m => m.MapartComponent)
|
||||
},
|
||||
{
|
||||
path: 'lag',
|
||||
loadComponent: () => import('./pages/reference/lag/lag.component').then(m => m.LagComponent)
|
||||
},
|
||||
{
|
||||
path: 'staffpowers',
|
||||
loadComponent: () => import('./pages/reference/staffpowers/staffpowers.component').then(m => m.StaffpowersComponent)
|
||||
},
|
||||
{
|
||||
path: 'forms',
|
||||
loadComponent: () => import('./pages/forms/forms.component').then(m => m.FormsComponent)
|
||||
},
|
||||
{
|
||||
path: 'appeal',
|
||||
redirectTo: 'forms/appeal',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'forms/appeal',
|
||||
loadComponent: () => import('./pages/forms/appeal/appeal.component').then(m => m.AppealComponent),
|
||||
canActivate: [AuthGuard],
|
||||
data: {
|
||||
requiredAuthorizations: ['SCOPE_user']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'forms/sent',
|
||||
loadComponent: () => import('./pages/forms/sent/sent.component').then(m => m.SentComponent),
|
||||
canActivate: [AuthGuard],
|
||||
data: {
|
||||
requiredAuthorizations: ['SCOPE_user']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'apply',
|
||||
redirectTo: 'forms/staff-application',
|
||||
pathMatch: 'full'
|
||||
},
|
||||
{
|
||||
path: 'forms/staff-application',
|
||||
loadComponent: () => import('./pages/forms/staff-application/staff-application.component').then(m => m.StaffApplicationComponent),
|
||||
canActivate: [AuthGuard],
|
||||
data: {
|
||||
requiredAuthorizations: ['SCOPE_user']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'community',
|
||||
loadComponent: () => import('./pages/altitude/community/community.component').then(m => m.CommunityComponent)
|
||||
},
|
||||
{
|
||||
path: 'nicknames',
|
||||
loadComponent: () => import('./pages/reference/nicknames/nicknames.component').then(m => m.NicknamesComponent)
|
||||
},
|
||||
{
|
||||
path: 'nickgenerator',
|
||||
loadComponent: () => import('./pages/reference/nickgenerator/nickgenerator.component').then(m => m.NickgeneratorComponent)
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="'bans'" height="200px" background_image="/public/img/backgrounds/staff.png"
|
||||
[overlay_gradient]="0.5">>
|
||||
<div class="title" header-content>
|
||||
<h1>Minecraft Punishments</h1>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<main>
|
||||
<section class="darkmodeSection">
|
||||
<div class="container">
|
||||
<div class="columnSection">
|
||||
<div class="historyButtonContainer">
|
||||
<div [id]="getCurrentButtonId('all')" class="button-outer" (click)="changeHistoryPunishment('all')">
|
||||
<span class="button-inner"
|
||||
[ngClass]="active">All</span>
|
||||
</div>
|
||||
<div [id]="getCurrentButtonId('ban')" class="button-outer" (click)="changeHistoryPunishment('ban')">
|
||||
<span class="button-inner"
|
||||
[ngClass]="active">Bans</span>
|
||||
</div>
|
||||
<div [id]="getCurrentButtonId('mute')" class="button-outer" (click)="changeHistoryPunishment('mute')">
|
||||
<span class="button-inner"
|
||||
[ngClass]="active">Mutes</span>
|
||||
</div>
|
||||
<div [id]="getCurrentButtonId('warn')" class="button-outer" (click)="changeHistoryPunishment('warn')">
|
||||
<span class="button-inner"
|
||||
[ngClass]="active">Warnings</span>
|
||||
</div>
|
||||
<div [id]="getCurrentUserTypeButtonId('player')" class="button-outer" (click)="changeUserType('player')"
|
||||
style="margin-left: 120px;">
|
||||
<span class="button-inner"
|
||||
[ngClass]="active">Player</span>
|
||||
</div>
|
||||
<div [id]="getCurrentUserTypeButtonId('staff')" class="button-outer" (click)="changeUserType('staff')">
|
||||
<span class="button-inner"
|
||||
[ngClass]="active">Staff</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="historySearchContainer">
|
||||
<input class="historySearch"
|
||||
type="search"
|
||||
placeholder="Search.."
|
||||
[(ngModel)]="searchTerm"
|
||||
(input)="filterNames()"
|
||||
(keyup.enter)="search()"
|
||||
>
|
||||
<div class="dropdown-results" *ngIf="filteredNames.length > 0 && searchTerm">
|
||||
<div
|
||||
class="dropdown-item"
|
||||
*ngFor="let name of filteredNames"
|
||||
(mousedown)="selectName(name)">
|
||||
{{ name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="historyTable">
|
||||
<app-history [userType]="userType" [punishmentType]="punishmentType"
|
||||
[page]="page" [searchTerm]="finalSearchTerm" (pageChange)="updatePageSize($event)">
|
||||
</app-history>
|
||||
</div>
|
||||
<div class="changePageButtons">
|
||||
<button [ngClass]="{'active': buttonActive(0), 'disabled': !buttonActive(0)}"
|
||||
[disabled]="!buttonActive(0)"
|
||||
(click)="setPage(0)" class="historyPageButton">
|
||||
First page
|
||||
</button>
|
||||
<button [ngClass]="{'active': buttonActive(0), 'disabled': !buttonActive(0)}"
|
||||
[disabled]="!buttonActive(0)"
|
||||
(click)="previousPage()" class="historyPageButton">
|
||||
Previous page
|
||||
</button>
|
||||
<span class="pageNumber">{{ this.page }} / {{ getMaxPage() }}</span>
|
||||
<button [ngClass]="{'active': buttonActive(getMaxPage()), 'disabled': !buttonActive(getMaxPage())}"
|
||||
[disabled]="!buttonActive(getMaxPage())"
|
||||
(click)="nextPage()" class="historyPageButton">
|
||||
Next page
|
||||
</button>
|
||||
<button [ngClass]="{'active': buttonActive(getMaxPage()), 'disabled': !buttonActive(getMaxPage())}"
|
||||
[disabled]="!buttonActive(getMaxPage())"
|
||||
(click)="setPage(getMaxPage())" class="historyPageButton">
|
||||
Last page
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</ng-container>
|
||||
@@ -1,45 +0,0 @@
|
||||
<ng-container *ngIf="history.length === 0">
|
||||
<p>No history found</p>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="history.length > 0">
|
||||
<table [cellSpacing]="0">
|
||||
<div class="historyTableHead">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="historyType">Type</th>
|
||||
<th class="historyPlayer">Player</th>
|
||||
<th class="historyPlayer">Banned By</th>
|
||||
<th class="historyReason">Reason</th>
|
||||
<th class="historyDate">Date</th>
|
||||
<th class="historyDate">Expires</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</div>
|
||||
<div>
|
||||
<tbody>
|
||||
<tr class="historyPlayerRow" *ngFor="let entry of history">
|
||||
<td class="historyType">{{ getType(entry) }}</td>
|
||||
<td class="historyPlayer">
|
||||
<div class="playerContainer">
|
||||
<img class="avatar" [ngSrc]="getAvatarUrl(entry.uuid)" width="25" height="25"
|
||||
alt="{{entry.username}}'s Minecraft skin">
|
||||
<span class="username">{{ entry.username }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="historyPlayer">
|
||||
<div class="playerContainer">
|
||||
<img class="avatar" [ngSrc]="getAvatarUrl(entry.punishedByUuid)" width="25" height="25"
|
||||
alt="{{entry.punishedBy}}'s Minecraft skin">
|
||||
<span>{{ entry.punishedBy }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="historyReason">{{ entry.reason | removeTrailingPeriod }}</td>
|
||||
<td class="historyDate">{{ getPunishmentTime(entry) }}</td>
|
||||
<td class="historyDate">{{ getExpiredTime(entry) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</div>
|
||||
</table>
|
||||
</ng-container>
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree} from '@angular/router';
|
||||
import {map, Observable} from 'rxjs';
|
||||
import {AuthService} from '@services/auth.service';
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
import {LoginDialogComponent} from '@shared-components/login/login.component';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthGuard implements CanActivate {
|
||||
|
||||
constructor(
|
||||
private authService: AuthService,
|
||||
private router: Router,
|
||||
private dialog: MatDialog
|
||||
) {
|
||||
}
|
||||
|
||||
canActivate(
|
||||
route: ActivatedRouteSnapshot,
|
||||
state: RouterStateSnapshot
|
||||
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
|
||||
if (!this.authService.isAuthenticated$()) {
|
||||
this.router.createUrlTree(['/']);
|
||||
const dialogRef = this.dialog.open(LoginDialogComponent, {
|
||||
width: '400px',
|
||||
})
|
||||
return dialogRef.afterClosed().pipe(
|
||||
map(result => {
|
||||
if (result) {
|
||||
return this.router.createUrlTree([state.url]);
|
||||
}
|
||||
return this.router.createUrlTree(['/']);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const requiredAuthorizations = route.data['requiredAuthorizations'] as string[];
|
||||
|
||||
if (!requiredAuthorizations || requiredAuthorizations.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const userAuthorizations = this.authService.getUserAuthorizations();
|
||||
const hasAccess = requiredAuthorizations.some(auth => userAuthorizations.includes(auth));
|
||||
|
||||
if (!hasAccess) {
|
||||
return this.router.createUrlTree(['/']);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+4
-5
@@ -1,15 +1,14 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {ScrollService} from '../scroll/scroll.service';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {HeaderComponent} from '../header/header.component';
|
||||
import {ScrollService} from '@services/scroll.service';
|
||||
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-about',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
HeaderComponent
|
||||
],
|
||||
],
|
||||
templateUrl: './about.component.html',
|
||||
styleUrl: './about.component.scss'
|
||||
})
|
||||
+4
-5
@@ -1,15 +1,14 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {ScrollService} from '../scroll/scroll.service';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {HeaderComponent} from '../header/header.component';
|
||||
import {ScrollService} from '@services/scroll.service';
|
||||
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-birthdays',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
HeaderComponent
|
||||
],
|
||||
],
|
||||
templateUrl: './birthdays.component.html',
|
||||
styleUrl: './birthdays.component.scss'
|
||||
})
|
||||
@@ -0,0 +1,79 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="'community'" height="460px" background_image="/public/img/backgrounds/community.jpg"
|
||||
[overlay_gradient]="0.5">
|
||||
<div class="title" header-content>
|
||||
<h1>Community</h1>
|
||||
<h2>Talented people who help Altitude in more than one way.</h2>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<main>
|
||||
<section class="darkmodeSection">
|
||||
<div class="customContainer">
|
||||
<h2>Current Nitro Boosters</h2>
|
||||
</div>
|
||||
</section>
|
||||
<section id="social" class="darkmodeSectionThree">
|
||||
<div class="container" style="padding: 50px 0 0 0; justify-content: center;">
|
||||
<h2 class="sectionTitle">Social Media</h2>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: center; padding-bottom: 30px;">
|
||||
<p style="text-align: center;">We're currently not looking for more people to help manage our socials.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section id="crateTeam" class="darkmodeSection">
|
||||
<div class="container" style="padding: 50px 0 0 0; justify-content: center;">
|
||||
<h2 class="sectionTitle">Crate Team</h2>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSectionThree">
|
||||
<div class="container" style="padding: 50px 0 0 0; justify-content: center;">
|
||||
<h2 class="sectionTitle">Event Leaders</h2>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: center; padding-bottom: 30px;">
|
||||
<p style="text-align: center;">We're currently not looking for more Event Leaders.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSection">
|
||||
<div class="container" style="padding: 50px 0 0 0; justify-content: center;">
|
||||
<h2 class="sectionTitle">Event Team</h2>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: center; padding-bottom: 30px;">
|
||||
<div style="flex-direction: column;">
|
||||
<p style="text-align: center;">We occasionally open applications for the event team.</p>
|
||||
<p style="text-align: center;">If you're interested in joining you simply need to keep an eye on the Discord
|
||||
announcements.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSectionThree">
|
||||
<div class="container" style="padding: 50px 0 0 0; justify-content: center;">
|
||||
<h2 class="sectionTitle">YouTubers & Streamers</h2>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: center; padding-bottom: 30px;">
|
||||
<div style="flex-direction: column;">
|
||||
<p style="text-align: center;"><a style="cursor: pointer;" id="reqButton">Show Requirements...</a></p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="req" class="hide" style="display: flex; justify-content: center; padding-bottom: 30px;">
|
||||
<div style="flex-direction: column; justify-content: center; max-width: 800px;">
|
||||
<p style="text-align: center;"><span style="font-family: 'opensans-bold', sans-serif;">Requirements:</span>
|
||||
</p>
|
||||
<p style="text-align: center;">You need to have at least one recent stream/video on Altitude which we can use
|
||||
to gauge if your audience enjoys your content on Altitude.</p>
|
||||
<br>
|
||||
<p style="text-align: center;">Twitch: You need to be affiliate and get at least 5 viewers on average while
|
||||
streaming on Altitude.</p>
|
||||
<p style="text-align: center;">YouTube videos: You need at least 500 subs and have at least 200 views per
|
||||
video within a week on average for Altitude content.</p>
|
||||
<p style="text-align: center;">YouTube streamers: You need at least 500 subs and have at least 5 viewers on
|
||||
average while streaming on Altitude.</p>
|
||||
<br>
|
||||
<p style="text-align: center;"><span style="font-family: 'opensans-bold', sans-serif;">Note:</span> Before
|
||||
accepting or denying you we will watch your latest video/stream on Altitude (so keep your broadcasts public
|
||||
on twitch).</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,12 @@
|
||||
.customContainer {
|
||||
width: 80%;
|
||||
max-width: 1020px;
|
||||
margin: auto;
|
||||
padding: 80px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CommunityComponent } from './community.component';
|
||||
|
||||
describe('CommunityComponent', () => {
|
||||
let component: CommunityComponent;
|
||||
let fixture: ComponentFixture<CommunityComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CommunityComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CommunityComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {HeaderComponent} from "@header/header.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-community',
|
||||
imports: [
|
||||
HeaderComponent
|
||||
],
|
||||
templateUrl: './community.component.html',
|
||||
styleUrl: './community.component.scss'
|
||||
})
|
||||
export class CommunityComponent {
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user