Compare commits
163
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
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 | ||
|
|
5759d6c0dc | ||
|
|
96cc4610dd | ||
|
|
f53fc9746e | ||
|
|
ecee377f01 | ||
|
|
ee66bdda83 | ||
|
|
905373093c | ||
|
|
21f2b3e4a5 | ||
|
|
53f67a5075 | ||
|
|
87a0b9ad1b | ||
|
|
a01038e86c | ||
|
|
e3eaab708c | ||
|
|
a3570b6833 | ||
|
|
48fd133b6d | ||
|
|
21b87fbf68 | ||
|
|
3a2a54050f | ||
|
|
a5adb031bd | ||
|
|
d535dd1ba9 | ||
|
|
13122559db | ||
|
|
a4d8fc07e9 | ||
|
|
3ed5fcfc0f | ||
|
|
81807b107c | ||
|
|
5e75c82994 | ||
|
|
2383c291c8 | ||
|
|
a70159e88d | ||
|
|
fd1115086a | ||
|
|
6a70428c75 | ||
|
|
1264003a44 | ||
|
|
a1b42060b0 | ||
|
|
3499f331a0 | ||
|
|
2b27fb6955 | ||
|
|
66d641b825 | ||
|
|
0a2338e42e | ||
|
|
aa2797c51c | ||
|
|
c83f95a3de | ||
|
|
df9c3ac996 | ||
|
|
02c69b318a | ||
|
|
4b5a49b82a | ||
|
|
6526bad733 | ||
|
|
9076f6d150 | ||
|
|
44d28494e5 | ||
|
|
807569a0a1 | ||
|
|
e7fde81f49 | ||
|
|
ce9260ede6 | ||
|
|
23367b6dea | ||
|
|
97e5d202bb | ||
|
|
2289b14b5a | ||
|
|
4b891dd672 | ||
|
|
3c6141f7ad | ||
|
|
0f9761da3a | ||
|
|
43a5dafd82 | ||
|
|
2b818fc297 | ||
|
|
7fa84fec81 | ||
|
|
1585011143 | ||
|
|
2137459e9b | ||
|
|
23f298b8c9 | ||
|
|
02f21169b4 | ||
|
|
f10c3ab2d3 | ||
|
|
d5daa8458b | ||
|
|
67a3162ae3 | ||
|
|
21496baab5 | ||
|
|
c2c81de9d0 | ||
|
|
9615139554 | ||
|
|
e7ac2d0462 | ||
|
|
ab3abe7f6b | ||
|
|
7b377a3667 | ||
|
|
d61a783f7f | ||
|
|
b1f2a89a2e | ||
|
|
b256b9f66b | ||
|
|
6f0c667bdd | ||
|
|
d4e231f67b | ||
|
|
b4d78690d9 | ||
|
|
d981b7dcff | ||
|
|
d006a47cf3 | ||
|
|
85c73c22c8 | ||
|
|
c0f2a38f4d | ||
|
|
6e459767ed | ||
|
|
2eb619b253 | ||
|
|
7b5002a475 | ||
|
|
25931d05d7 | ||
|
|
b029153657 | ||
|
|
4e77f50d70 | ||
|
|
0652e5e55e | ||
|
|
1a6e489f9a | ||
|
|
849f09d10a | ||
|
|
6f3445dc01 | ||
|
|
13089ba437 | ||
|
|
1af614323e | ||
|
|
04c5a3e846 | ||
|
|
4f2118a83a | ||
|
|
e158c30bf1 | ||
|
|
18cc111b26 | ||
|
|
a2f1a079cf | ||
|
|
9761923190 | ||
|
|
46c755040d | ||
|
|
297bd473a6 | ||
|
|
dee84cc1a9 | ||
|
|
c628f1e05b | ||
|
|
de989ad5a0 | ||
|
|
44a7a2140e | ||
|
|
42dd1a184c | ||
|
|
5aec42320a | ||
|
|
8d0da93c99 | ||
|
|
a5ef610a09 | ||
|
|
a993c1cc3e | ||
|
|
5bf9c725f4 | ||
|
|
50d28e62d9 | ||
|
|
ba258fbf30 | ||
|
|
ca96b36246 |
@@ -87,3 +87,7 @@ package-lock.json
|
|||||||
|
|
||||||
### MINE ###
|
### MINE ###
|
||||||
frontend/src/api
|
frontend/src/api
|
||||||
|
generated
|
||||||
|
|
||||||
|
liquibase*.properties
|
||||||
|
node
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
plugins {
|
||||||
|
java
|
||||||
|
id("org.springframework.boot") version "3.4.4"
|
||||||
|
id("io.spring.dependency-management") version "1.1.7"
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "com.alttd.altitudeweb"
|
||||||
|
version = "0.0.1-SNAPSHOT"
|
||||||
|
|
||||||
|
java {
|
||||||
|
toolchain {
|
||||||
|
languageVersion = JavaLanguageVersion.of(21)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
compileOnly {
|
||||||
|
extendsFrom(configurations.annotationProcessor.get())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":open_api"))
|
||||||
|
implementation(project(":database"))
|
||||||
|
implementation(project(":frontend"))
|
||||||
|
annotationProcessor("org.projectlombok:lombok")
|
||||||
|
implementation("com.mysql:mysql-connector-j:8.0.32")
|
||||||
|
implementation("org.mybatis:mybatis:3.5.13")
|
||||||
|
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")
|
||||||
|
|
||||||
|
//AOP
|
||||||
|
implementation("org.aspectj:aspectjrt:1.9.19")
|
||||||
|
implementation("org.aspectj:aspectjweaver:1.9.19")
|
||||||
|
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> {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.bootJar {
|
||||||
|
mainClass.set("com.alttd.altitudeweb.AltitudeWebApplication")
|
||||||
|
archiveBaseName.set("altitudeweb")
|
||||||
|
archiveClassifier.set("")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.processResources {
|
||||||
|
dependsOn("includeFrontend")
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
+4
@@ -1,12 +1,16 @@
|
|||||||
package com.alttd.altitudeweb;
|
package com.alttd.altitudeweb;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.setup.Connection;
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
|
@EnableAspectJAutoProxy
|
||||||
public class AltitudeWebApplication {
|
public class AltitudeWebApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
Connection.initDatabases();
|
||||||
SpringApplication.run(AltitudeWebApplication.class, args);
|
SpringApplication.run(AltitudeWebApplication.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
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 org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
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.http.SessionCreationPolicy;
|
||||||
|
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.web.SecurityFilterChain;
|
||||||
|
|
||||||
|
import java.security.KeyPair;
|
||||||
|
import java.security.interfaces.RSAPrivateKey;
|
||||||
|
import java.security.interfaces.RSAPublicKey;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
private final KeyPairService keyPairService;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
|
return http
|
||||||
|
.authorizeHttpRequests(auth -> auth
|
||||||
|
.requestMatchers("/login/userLogin/**", "/login/requestNewUserLogin/**").permitAll()
|
||||||
|
.requestMatchers("/team/**", "/history/**").permitAll()
|
||||||
|
.requestMatchers("/form/**").hasAuthority(PermissionClaimDto.USER.getValue())
|
||||||
|
.requestMatchers("/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||||
|
.anyRequest().permitAll()
|
||||||
|
)
|
||||||
|
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
|
||||||
|
.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.alttd.altitudeweb.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
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;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class WebConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
|
registry.addResourceHandler("/**")
|
||||||
|
.addResourceLocations("classpath:/static/")
|
||||||
|
.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()) {
|
||||||
|
return requestedResource;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ClassPathResource("/static/index.html");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.alttd.altitudeweb.controllers;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Configuration
|
||||||
|
public class CorsConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
|
@Value("${cors.allowed-origins}")
|
||||||
|
private String[] allowedOrigins;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addCorsMappings(CorsRegistry registry) {
|
||||||
|
log.info("Registering CORS mappings for {}", String.join(", ", allowedOrigins));
|
||||||
|
registry.addMapping("/**")
|
||||||
|
.allowedOrigins(allowedOrigins)
|
||||||
|
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||||
|
.allowedHeaders("*")
|
||||||
|
.allowCredentials(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.alttd.altitudeweb.controllers;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.model.ApiErrorDto;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
|
||||||
|
|
||||||
|
@ControllerAdvice
|
||||||
|
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<ApiErrorDto> handleException(Exception ex) {
|
||||||
|
ApiErrorDto error = new ApiErrorDto();
|
||||||
|
error.reason("Error: " + ex.getMessage());
|
||||||
|
logger.error(ex.getMessage(), ex);
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
package com.alttd.altitudeweb.controllers.application;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.api.AppealsApi;
|
||||||
|
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||||
|
import com.alttd.altitudeweb.model.AppealResponseDto;
|
||||||
|
import com.alttd.altitudeweb.model.DiscordAppealDto;
|
||||||
|
import com.alttd.altitudeweb.model.MinecraftAppealDto;
|
||||||
|
import com.alttd.altitudeweb.model.UpdateMailDto;
|
||||||
|
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.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RateLimit(limit = 30, timeValue = 1, timeUnit = TimeUnit.HOURS)
|
||||||
|
public class AppealController implements AppealsApi {
|
||||||
|
|
||||||
|
@RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "discordAppeal")
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<MinecraftAppealDto> 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<AppealResponseDto> submitMinecraftAppeal(MinecraftAppealDto minecraftAppealDto) {
|
||||||
|
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Minecraft appeals are not yet supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<AppealResponseDto> updateMail(UpdateMailDto updateMailDto) {
|
||||||
|
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Updating mail is not yet supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
+232
@@ -0,0 +1,232 @@
|
|||||||
|
package com.alttd.altitudeweb.controllers.history;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.api.HistoryApi;
|
||||||
|
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 lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RateLimit(limit = 30, timeValue = 10, timeUnit = TimeUnit.SECONDS)
|
||||||
|
public class HistoryApiController implements HistoryApi {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<PunishmentHistoryListDto> getHistoryForAll(String userType, String type, Integer page) {
|
||||||
|
return getHistoryForUsers(userType, type, "", page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<PunishmentHistoryListDto> getHistoryForUsers(String userType, String type, String user, Integer page) {
|
||||||
|
UserType userTypeEnum = UserType.getUserType(userType);
|
||||||
|
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||||
|
PunishmentHistoryListDto punishmentHistoryList = new PunishmentHistoryListDto();
|
||||||
|
CompletableFuture<List<HistoryRecord>> historyRecords = new CompletableFuture<>();
|
||||||
|
|
||||||
|
Connection.getConnection(Databases.LITE_BANS)
|
||||||
|
.runQuery(sqlSession -> {
|
||||||
|
log.debug("Loading history through name for type {}", type);
|
||||||
|
try {
|
||||||
|
List<HistoryRecord> temp = sqlSession.getMapper(NameHistoryMapper.class)
|
||||||
|
.getRecent(historyTypeEnum, userTypeEnum, user, page);
|
||||||
|
historyRecords.complete(temp);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to load history through name for type {}", type, e);
|
||||||
|
historyRecords.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return mapPunishmentHistory(punishmentHistoryList, historyRecords);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<PunishmentHistoryListDto> getHistoryForUuid(String userType, String type, String uuid, Integer page) {
|
||||||
|
UserType userTypeEnum = UserType.getUserType(userType);
|
||||||
|
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||||
|
PunishmentHistoryListDto punishmentHistoryList = new PunishmentHistoryListDto();
|
||||||
|
CompletableFuture<List<HistoryRecord>> historyRecords = new CompletableFuture<>();
|
||||||
|
|
||||||
|
Connection.getConnection(Databases.LITE_BANS)
|
||||||
|
.runQuery(sqlSession -> {
|
||||||
|
log.debug("Loading history through uuid for type {}", type);
|
||||||
|
try {
|
||||||
|
List<HistoryRecord> temp = sqlSession.getMapper(UUIDHistoryMapper.class)
|
||||||
|
.getRecent(historyTypeEnum, userTypeEnum, UUID.fromString(uuid), page);
|
||||||
|
historyRecords.complete(temp);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to load history through uuid for type {}", type, e);
|
||||||
|
historyRecords.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return mapPunishmentHistory(punishmentHistoryList, historyRecords);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<List<String>> getUserNames(String userType, String type) {
|
||||||
|
UserType userTypeEnum = UserType.getUserType(userType);
|
||||||
|
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||||
|
CompletableFuture<List<String>> playerGroupFuture = new CompletableFuture<>();
|
||||||
|
|
||||||
|
Connection.getConnection(Databases.LITE_BANS)
|
||||||
|
.runQuery(sqlSession -> {
|
||||||
|
log.debug("Loading user names for type {}", type);
|
||||||
|
try {
|
||||||
|
List<String> temp = sqlSession.getMapper(RecentNamesMapper.class)
|
||||||
|
.getRecent(historyTypeEnum, userTypeEnum);
|
||||||
|
playerGroupFuture.complete(temp);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to load user names for type {}", type, e);
|
||||||
|
playerGroupFuture.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return ResponseEntity.ok().body(playerGroupFuture.join());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<HistoryCountDto> getTotalPunishments() {
|
||||||
|
CompletableFuture<HistoryCount> historyCountCompletableFuture = new CompletableFuture<>();
|
||||||
|
|
||||||
|
Connection.getConnection(Databases.LITE_BANS)
|
||||||
|
.runQuery(sqlSession -> {
|
||||||
|
log.debug("Loading history count");
|
||||||
|
try {
|
||||||
|
HistoryCount punishmentCounts = sqlSession.getMapper(HistoryCountMapper.class)
|
||||||
|
.getPunishmentCounts();
|
||||||
|
historyCountCompletableFuture.complete(punishmentCounts);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to load history count", e);
|
||||||
|
historyCountCompletableFuture.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return mapHistoryCount(historyCountCompletableFuture.join());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<Integer> getTotalResultsForUserSearch(String userType, String type, String user) {
|
||||||
|
UserType userTypeEnum = UserType.getUserType(userType);
|
||||||
|
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||||
|
CompletableFuture<Integer> searchResultCountCompletableFuture = new CompletableFuture<>();
|
||||||
|
|
||||||
|
Connection.getConnection(Databases.LITE_BANS)
|
||||||
|
.runQuery(sqlSession -> {
|
||||||
|
log.debug("Loading name search result count");
|
||||||
|
try {
|
||||||
|
Integer punishmentCount = sqlSession.getMapper(HistoryCountMapper.class)
|
||||||
|
.getNamePunishmentCount(historyTypeEnum, userTypeEnum, user);
|
||||||
|
searchResultCountCompletableFuture.complete(punishmentCount);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to load history count", e);
|
||||||
|
searchResultCountCompletableFuture.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return ResponseEntity.ok().body(searchResultCountCompletableFuture.join());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<Integer> getTotalResultsForUuidSearch(String userType, String type, String uuid) {
|
||||||
|
UserType userTypeEnum = UserType.getUserType(userType);
|
||||||
|
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||||
|
CompletableFuture<Integer> searchResultCountCompletableFuture = new CompletableFuture<>();
|
||||||
|
|
||||||
|
Connection.getConnection(Databases.LITE_BANS)
|
||||||
|
.runQuery(sqlSession -> {
|
||||||
|
log.debug("Loading uuid search result count");
|
||||||
|
try {
|
||||||
|
Integer punishmentCount = sqlSession.getMapper(HistoryCountMapper.class)
|
||||||
|
.getUuidPunishmentCount(historyTypeEnum, userTypeEnum, UUID.fromString(uuid));
|
||||||
|
searchResultCountCompletableFuture.complete(punishmentCount);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to load history count", e);
|
||||||
|
searchResultCountCompletableFuture.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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());
|
||||||
|
historyCountDto.setMutes(historyCount.getMutes());
|
||||||
|
historyCountDto.setWarnings(historyCount.getWarnings());
|
||||||
|
historyCountDto.setKicks(historyCount.getKicks());
|
||||||
|
return ResponseEntity.ok().body(historyCountDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseEntity<PunishmentHistoryListDto> mapPunishmentHistory(PunishmentHistoryListDto punishmentHistoryList, CompletableFuture<List<HistoryRecord>> historyRecords) {
|
||||||
|
historyRecords.join().forEach(historyRecord -> {
|
||||||
|
PunishmentHistoryDto innerDto = mapPunishmentHistory(historyRecord);
|
||||||
|
punishmentHistoryList.add(innerDto);
|
||||||
|
});
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,172 @@
|
|||||||
|
package com.alttd.altitudeweb.controllers.login;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.api.LoginApi;
|
||||||
|
import com.alttd.altitudeweb.model.PermissionClaimDto;
|
||||||
|
import com.alttd.altitudeweb.database.Databases;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.PrivilegedUser;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.PrivilegedUserMapper;
|
||||||
|
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.Scheduled;
|
||||||
|
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||||
|
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
|
||||||
|
@RestController
|
||||||
|
public class LoginController implements LoginApi {
|
||||||
|
|
||||||
|
private final JwtEncoder jwtEncoder;
|
||||||
|
|
||||||
|
@Value("${login.secret:#{null}}")
|
||||||
|
private String loginSecret;
|
||||||
|
|
||||||
|
private record CacheEntry(UUID uuid, Instant expiry) {}
|
||||||
|
|
||||||
|
private static final ConcurrentMap<String, CacheEntry> cache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Scheduled(fixedRate = 300000) // 5 minutes in milliseconds
|
||||||
|
private 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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()) {
|
||||||
|
return ResponseEntity.ok(key.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
String loginCode = generateLoginCode(uuidFromString);
|
||||||
|
return ResponseEntity.ok(loginCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
@RateLimit(limit = 5, timeValue = 1, timeUnit = TimeUnit.MINUTES, key = "login")
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<String> login(String code) {
|
||||||
|
CacheEntry cacheEntry1 = new CacheEntry(UUID.fromString("55e46bc3-2a29-4c53-850f-dbd944dc5c5f"), Instant.now().plusSeconds(TimeUnit.DAYS.toSeconds(1)));
|
||||||
|
cache.put("23232323", cacheEntry1);
|
||||||
|
if (code == null) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
CacheEntry cacheEntry = cache.get(code);
|
||||||
|
if (cacheEntry == null || cacheEntry.expiry().isBefore(Instant.now())) {
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
String token = generateToken(cacheEntry.uuid);
|
||||||
|
|
||||||
|
cache.remove(code);
|
||||||
|
|
||||||
|
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<PrivilegedUser> privilegedUserCompletableFuture = new CompletableFuture<>();
|
||||||
|
List<PermissionClaimDto> claimList = new ArrayList<>();
|
||||||
|
Connection.getConnection(Databases.DEFAULT)
|
||||||
|
.runQuery(sqlSession -> {
|
||||||
|
try {
|
||||||
|
PrivilegedUser privilegedUser = sqlSession.getMapper(PrivilegedUserMapper.class)
|
||||||
|
.getUserByUuid(uuid.toString());
|
||||||
|
|
||||||
|
privilegedUserCompletableFuture.complete(privilegedUser);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to load user by uuid", e);
|
||||||
|
privilegedUserCompletableFuture.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
PrivilegedUser privilegedUser = privilegedUserCompletableFuture.join();
|
||||||
|
claimList.add(PermissionClaimDto.USER);
|
||||||
|
if (privilegedUser != null) {
|
||||||
|
privilegedUser.getPermissions().forEach(permission -> {
|
||||||
|
try {
|
||||||
|
claimList.add(PermissionClaimDto.valueOf(permission));
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
log.warn("Received invalid permission claim: {}", permission);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
JwtClaimsSet claims = JwtClaimsSet.builder()
|
||||||
|
.issuer("altitudeweb")
|
||||||
|
.claim("authorities", claimList.stream().map(PermissionClaimDto::getValue).toList())
|
||||||
|
.issuedAt(now)
|
||||||
|
.expiresAt(expiryTime)
|
||||||
|
.subject(uuid.toString())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.alttd.altitudeweb.controllers.team;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.api.TeamApi;
|
||||||
|
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;
|
||||||
|
import com.alttd.altitudeweb.database.luckperms.TeamMemberMapper;
|
||||||
|
import com.alttd.altitudeweb.model.PlayerDto;
|
||||||
|
import com.alttd.altitudeweb.model.TeamMembersDto;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RateLimit(limit = 30, timeValue = 1, timeUnit = TimeUnit.MINUTES)
|
||||||
|
public class TeamApiController implements TeamApi {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<TeamMembersDto> getTeamMembers(String group) {
|
||||||
|
TeamMembersDto teamMemberDtos = new TeamMembersDto();
|
||||||
|
CompletableFuture<List<Player>> playerGroupFuture = new CompletableFuture<>();
|
||||||
|
Connection.getConnection(Databases.LUCK_PERMS)
|
||||||
|
.runQuery(sqlSession -> {
|
||||||
|
log.debug("Loading team members for group {}", group);
|
||||||
|
try {
|
||||||
|
List<Player> players = sqlSession.getMapper(TeamMemberMapper.class).getTeamMembers("group." + group);
|
||||||
|
playerGroupFuture.complete(players);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to load team members for group {}", group, e);
|
||||||
|
playerGroupFuture.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
List<Player> join = playerGroupFuture.join();
|
||||||
|
join.forEach(player -> teamMemberDtos.add(new PlayerDto(player.username(), player.uuid().toString())));
|
||||||
|
return ResponseEntity.ok().body(teamMemberDtos);
|
||||||
|
}
|
||||||
|
}
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
package com.alttd.altitudeweb.services.limits;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class InMemoryRateLimiterService {
|
||||||
|
|
||||||
|
private final Map<String, RequestCounter> limiters = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
public boolean tryAcquire(String key, int limit, Duration duration) {
|
||||||
|
RequestCounter counter = limiters.computeIfAbsent(key, k -> new RequestCounter());
|
||||||
|
return counter.tryAcquire(limit, duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Duration getNextResetTime(String key, Duration duration) {
|
||||||
|
RequestCounter counter = limiters.get(key);
|
||||||
|
if (counter == null) {
|
||||||
|
return Duration.ZERO;
|
||||||
|
}
|
||||||
|
return counter.getNextResetTime(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRemainingTokens(String key, int limit, Duration duration) {
|
||||||
|
RequestCounter counter = limiters.get(key);
|
||||||
|
if (counter == null) {
|
||||||
|
return limit;
|
||||||
|
}
|
||||||
|
return Math.max(0, limit - counter.getCurrentCount(duration));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.alttd.altitudeweb.services.limits;
|
||||||
|
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
public @interface RateLimit {
|
||||||
|
int limit() default 20;
|
||||||
|
long timeValue() default 60;
|
||||||
|
TimeUnit timeUnit() default TimeUnit.SECONDS;
|
||||||
|
String key() default "";
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package com.alttd.altitudeweb.services.limits;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.Around;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.aspectj.lang.reflect.MethodSignature;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
import java.time.Duration;
|
||||||
|
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class RateLimitAspect {
|
||||||
|
|
||||||
|
private final InMemoryRateLimiterService rateLimiterService;
|
||||||
|
|
||||||
|
@Around("""
|
||||||
|
@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) {
|
||||||
|
return joinPoint.proceed();
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpServletRequest request = requestAttributes.getRequest();
|
||||||
|
HttpServletResponse response = requestAttributes.getResponse();
|
||||||
|
|
||||||
|
String clientIp = request.getRemoteAddr();
|
||||||
|
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||||
|
Method method = signature.getMethod();
|
||||||
|
|
||||||
|
RateLimit rateLimit = method.getAnnotation(RateLimit.class);
|
||||||
|
if (rateLimit == null) {
|
||||||
|
rateLimit = method.getDeclaringClass().getAnnotation(RateLimit.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rateLimit == null) {
|
||||||
|
return joinPoint.proceed();
|
||||||
|
}
|
||||||
|
|
||||||
|
int limit = rateLimit.limit();
|
||||||
|
Duration duration = Duration.ofSeconds(rateLimit.timeUnit().toSeconds(rateLimit.timeValue()));
|
||||||
|
String customKey = rateLimit.key();
|
||||||
|
|
||||||
|
String key = clientIp + "-" + (customKey.isEmpty() ? method.getName() : customKey);
|
||||||
|
|
||||||
|
boolean allowed = rateLimiterService.tryAcquire(key, limit, duration);
|
||||||
|
|
||||||
|
if (allowed) {
|
||||||
|
if (response != null) {
|
||||||
|
response.setHeader("X-Rate-Limit-Limit", String.valueOf(limit));
|
||||||
|
response.setHeader("X-Rate-Limit-Remaining",
|
||||||
|
String.valueOf(rateLimiterService.getRemainingTokens(key, limit, duration)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return joinPoint.proceed();
|
||||||
|
} else {
|
||||||
|
log.warn("Rate limit exceeded for IP: {}, endpoint: {}", clientIp, request.getRequestURI());
|
||||||
|
|
||||||
|
Duration nextResetTime = rateLimiterService.getNextResetTime(key, duration);
|
||||||
|
|
||||||
|
return ResponseEntity
|
||||||
|
.status(HttpStatus.TOO_MANY_REQUESTS)
|
||||||
|
.header("X-Rate-Limit-Limit", String.valueOf(limit))
|
||||||
|
.header("X-Rate-Limit-Remaining", "0")
|
||||||
|
.header("Retry-After", String.valueOf(nextResetTime.getSeconds()))
|
||||||
|
.header("Access-Control-Expose-Headers", "Retry-After, X-Rate-Limit-Limit, X-Rate-Limit-Remaining")
|
||||||
|
.body(String.format("Rate limit exceeded. Try again in %d seconds.", nextResetTime.getSeconds()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package com.alttd.altitudeweb.services.limits;
|
||||||
|
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Queue;
|
||||||
|
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||||
|
|
||||||
|
public class RequestCounter {
|
||||||
|
private final Queue<Instant> requestTimestamps = new ConcurrentLinkedQueue<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to acquire a permit for a request within the specified rate limit.
|
||||||
|
* This method checks if the number of requests made within the given duration
|
||||||
|
* is less than the specified limit. If the limit has not been exceeded, the
|
||||||
|
* current request is recorded and the method will return true. Otherwise, it
|
||||||
|
* will return false.
|
||||||
|
*
|
||||||
|
* @param limit the maximum number of requests allowed within the specified duration
|
||||||
|
* @param duration the time window during which requests are counted against the limit
|
||||||
|
* @return true if the request is successfully acquired without exceeding the rate limit, false otherwise
|
||||||
|
*/
|
||||||
|
public synchronized boolean tryAcquire(int limit, Duration duration) {
|
||||||
|
Instant currentTime = Instant.now();
|
||||||
|
Instant cutoffTime = currentTime.minus(duration);
|
||||||
|
|
||||||
|
while (!requestTimestamps.isEmpty() && requestTimestamps.peek().isBefore(cutoffTime)) {
|
||||||
|
requestTimestamps.poll();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestTimestamps.size() < limit) {
|
||||||
|
requestTimestamps.add(currentTime);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCurrentCount(Duration duration) {
|
||||||
|
Instant cutoffTime = Instant.now().minus(duration);
|
||||||
|
|
||||||
|
return (int) requestTimestamps.stream()
|
||||||
|
.filter(timestamp -> !timestamp.isBefore(cutoffTime))
|
||||||
|
.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Duration getNextResetTime(Duration duration) {
|
||||||
|
if (requestTimestamps.isEmpty()) {
|
||||||
|
return Duration.ZERO;
|
||||||
|
}
|
||||||
|
|
||||||
|
Instant oldestTimestamp = requestTimestamps.peek();
|
||||||
|
Instant resetTime = oldestTimestamp.plus(duration);
|
||||||
|
Duration timeUntilReset = Duration.between(Instant.now(), resetTime);
|
||||||
|
|
||||||
|
return timeUntilReset.isNegative() ? Duration.ZERO : timeUntilReset;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,8 @@
|
|||||||
|
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}
|
||||||
|
logging.level.com.alttd.altitudeweb=INFO
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
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}
|
||||||
|
logging.level.com.alttd.altitudeweb=DEBUG
|
||||||
@@ -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://alttd.com}
|
||||||
|
login.secret=${LOGIN_SECRET:SET_TOKEN}
|
||||||
|
logging.level.com.alttd.altitudeweb=INFO
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.alttd.altitudeweb;
|
||||||
|
|
||||||
|
//import org.junit.jupiter.api.Test;
|
||||||
|
//import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
//
|
||||||
|
//@SpringBootTest
|
||||||
|
//class AltitudeWebApplicationTests {
|
||||||
|
//
|
||||||
|
// @Test
|
||||||
|
// void contextLoads() {
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
//}
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
import org.openapitools.generator.gradle.plugin.tasks.GenerateTask
|
|
||||||
|
|
||||||
plugins {
|
|
||||||
java
|
|
||||||
id("org.springframework.boot") version "3.4.4"
|
|
||||||
id("io.spring.dependency-management") version "1.1.7"
|
|
||||||
id("org.openapi.generator") version "7.12.0"
|
|
||||||
}
|
|
||||||
|
|
||||||
group = "com.alttd"
|
|
||||||
version = "0.0.1-SNAPSHOT"
|
|
||||||
|
|
||||||
java {
|
|
||||||
toolchain {
|
|
||||||
languageVersion = JavaLanguageVersion.of(21)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
configurations {
|
|
||||||
compileOnly {
|
|
||||||
extendsFrom(configurations.annotationProcessor.get())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
repositories {
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
dependencies {
|
|
||||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
|
||||||
compileOnly("org.projectlombok:lombok")
|
|
||||||
runtimeOnly("org.mariadb.jdbc:mariadb-java-client")
|
|
||||||
annotationProcessor("org.projectlombok:lombok")
|
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
|
||||||
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.withType<Test> {
|
|
||||||
useJUnitPlatform()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate Java API using OpenAPI Generator (Spring)
|
|
||||||
tasks.register<GenerateTask>("generateJavaApi") {
|
|
||||||
generatorName.set("spring")
|
|
||||||
inputSpec.set("$projectDir/open_api/api.yml")
|
|
||||||
val myBuildDir = layout.buildDirectory.get().asFile.absolutePath
|
|
||||||
outputDir.set("$myBuildDir/generated-sources/java")
|
|
||||||
apiPackage.set("com.alttd.api")
|
|
||||||
modelPackage.set("com.alttd.model")
|
|
||||||
configOptions.set(mapOf("interfaceOnly" to "true"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make java compiling wait until after the API is generated
|
|
||||||
tasks.named("compileJava") {
|
|
||||||
dependsOn("generateJavaApi")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate Angular API client using OpenAPI Generator (TypeScript-Angular)
|
|
||||||
tasks.register<GenerateTask>("generateFrontendApi") {
|
|
||||||
generatorName.set("typescript-angular")
|
|
||||||
inputSpec.set("$projectDir/open_api/api.yml")
|
|
||||||
outputDir.set("$projectDir/frontend/src/api")
|
|
||||||
configOptions.set(
|
|
||||||
mapOf(
|
|
||||||
"npmName" to "generated-api",
|
|
||||||
"supportsES6" to "true"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Task to install npm dependencies in the frontend folder
|
|
||||||
tasks.register<Exec>("npmInstall") {
|
|
||||||
workingDir = file("$projectDir/frontend")
|
|
||||||
commandLine("npm.cmd", "install")
|
|
||||||
// commandLine("npm", "install")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named("jar") {
|
|
||||||
dependsOn("copyFrontend")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named("resolveMainClassName") {
|
|
||||||
dependsOn("copyFrontend") // Declare `copyFrontend` as a dependency
|
|
||||||
}
|
|
||||||
|
|
||||||
// Task to build the Angular frontend; depends on npmInstall and generateFrontendApi
|
|
||||||
tasks.register<Exec>("ngBuild") {
|
|
||||||
dependsOn("npmInstall", "generateFrontendApi")
|
|
||||||
workingDir = file("$projectDir/frontend")
|
|
||||||
// commandLine("npm", "run", "build")
|
|
||||||
commandLine("npm.cmd", "run", "build")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Task to copy built frontend into "static" folder for Spring Boot
|
|
||||||
tasks.register<Copy>("copyFrontend") {
|
|
||||||
|
|
||||||
val myBuildDir = layout.buildDirectory.get().asFile.absolutePath
|
|
||||||
|
|
||||||
dependsOn("ngBuild") // Make sure frontend build runs first
|
|
||||||
from("$projectDir/frontend/dist") // Source directory for Angular build
|
|
||||||
into("$myBuildDir/resources/main/static") // Destination in Spring Boot's static folder
|
|
||||||
|
|
||||||
// Optional: Explicitly mark inputs and outputs for cache optimization
|
|
||||||
inputs.dir("$projectDir/frontend/dist")
|
|
||||||
outputs.dir("$myBuildDir/resources/main/static")
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named("compileTestJava") {
|
|
||||||
dependsOn("copyFrontend") // Ensure `copyFrontend` runs before `compileTestJava`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that the frontend is built and copied before bootJar
|
|
||||||
tasks.named("bootJar") {
|
|
||||||
dependsOn("copyFrontend") // Make bootJar depend on frontend build
|
|
||||||
}
|
|
||||||
|
|
||||||
//tasks.register("checkNpm") {
|
|
||||||
// doLast {
|
|
||||||
// // Print PATH for debugging
|
|
||||||
// val gradlePath = System.getenv("PATH")
|
|
||||||
// // Add location of npm to the PATH
|
|
||||||
// val nodePath = "C:\\Program Files\\nodejs"
|
|
||||||
// val updatedPath = "$gradlePath;$nodePath"
|
|
||||||
//
|
|
||||||
// // Check if npm is accessible
|
|
||||||
// try {
|
|
||||||
// val processBuilder = ProcessBuilder("npm", "-v")
|
|
||||||
// processBuilder.environment()["PATH"] = updatedPath // Explicitly set PATH
|
|
||||||
// val npmVersion = ProcessBuilder("npm", "-v")
|
|
||||||
// .redirectErrorStream(true) // Redirect error output to standard output
|
|
||||||
// .start()
|
|
||||||
// .inputStream
|
|
||||||
// .bufferedReader()
|
|
||||||
// .readText()
|
|
||||||
// .trim()
|
|
||||||
//
|
|
||||||
// println("npm version: $npmVersion")
|
|
||||||
// } catch (e: Exception) {
|
|
||||||
// println("Error: npm not found or not accessible. Make sure npm is added to your PATH.")
|
|
||||||
// e.printStackTrace()
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
plugins {
|
||||||
|
id("java")
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "com.alttd.altitudeweb"
|
||||||
|
version = "0.0.1-SNAPSHOT"
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":open_api"))
|
||||||
|
compileOnly("org.projectlombok:lombok:1.18.38")
|
||||||
|
annotationProcessor("org.projectlombok:lombok:1.18.38")
|
||||||
|
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 {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.alttd.altitudeweb;
|
||||||
|
|
||||||
|
public class Main {
|
||||||
|
public static void main(String[] args) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.alttd.altitudeweb.database;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public enum Databases {
|
||||||
|
DEFAULT("web_db"),
|
||||||
|
LUCK_PERMS("luckperms"),
|
||||||
|
LITE_BANS("litebans");
|
||||||
|
|
||||||
|
private final String internalName;
|
||||||
|
|
||||||
|
Databases(String internalName) {
|
||||||
|
this.internalName = internalName;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.alttd.altitudeweb.database.litebans;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class HistoryCount {
|
||||||
|
private int bans;
|
||||||
|
private int mutes;
|
||||||
|
private int warnings;
|
||||||
|
private int kicks;
|
||||||
|
}
|
||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
package com.alttd.altitudeweb.database.litebans;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public interface HistoryCountMapper {
|
||||||
|
/**
|
||||||
|
* Gets the total count of punishments from all LiteBans tables.
|
||||||
|
*
|
||||||
|
* @return A PunishmentCount object containing counts from each table
|
||||||
|
*/
|
||||||
|
@Select({"""
|
||||||
|
SELECT
|
||||||
|
(SELECT COUNT(*) FROM litebans_bans) AS bans,
|
||||||
|
(SELECT COUNT(*) FROM litebans_mutes) AS mutes,
|
||||||
|
(SELECT COUNT(*) FROM litebans_warnings) AS warnings,
|
||||||
|
(SELECT COUNT(*) FROM litebans_kicks) AS kicks
|
||||||
|
"""
|
||||||
|
})
|
||||||
|
HistoryCount getPunishmentCounts();
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM (SELECT punishment.uuid, user_lookup.name AS punished_name
|
||||||
|
FROM all_punishments AS punishment
|
||||||
|
JOIN user_lookup ON user_lookup.uuid = punishment.uuid
|
||||||
|
WHERE user_lookup.name LIKE #{partialName}
|
||||||
|
AND type IN (${typeList})) AS punishment
|
||||||
|
""")
|
||||||
|
Integer getPlayerNamePunishmentCount(@Param("typeList") String typeList,
|
||||||
|
@Param("partialName") String partialName);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT COUNT(*) AS punishment_count
|
||||||
|
FROM (SELECT uuid, banned_by_name
|
||||||
|
FROM all_punishments
|
||||||
|
WHERE banned_by_name LIKE #{partialName}
|
||||||
|
AND type IN (${typeList})) AS punishment
|
||||||
|
""")
|
||||||
|
Integer getStaffNamePunishmentCount(@Param("typeList") String typeList,
|
||||||
|
@Param("partialName") String partialName);
|
||||||
|
|
||||||
|
default Integer getNamePunishmentCount(HistoryType historyTypeEnum, UserType userTypeEnum, String partialName) {
|
||||||
|
final String searchName = partialName.toLowerCase().replace("_", "\\_") + "%";
|
||||||
|
String[] historyType = getHistoryType(historyTypeEnum);
|
||||||
|
String historyTypeSqlList = Arrays.stream(historyType)
|
||||||
|
.map(type -> "'" + type + "'")
|
||||||
|
.collect(Collectors.joining(", "));
|
||||||
|
return switch (userTypeEnum) {
|
||||||
|
case PLAYER -> getPlayerNamePunishmentCount(historyTypeSqlList, searchName);
|
||||||
|
case STAFF -> getStaffNamePunishmentCount(historyTypeSqlList, searchName);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT COUNT(*) AS punishment_count
|
||||||
|
FROM all_punishments
|
||||||
|
WHERE uuid = #{uuid}
|
||||||
|
AND type IN (${typeList})
|
||||||
|
""")
|
||||||
|
Integer getPlayerUuidPunishmentCount(@Param("typeList") String typeList,
|
||||||
|
@Param("uuid") String uuid);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT COUNT(*) AS punishment_count
|
||||||
|
FROM all_punishments
|
||||||
|
WHERE banned_by_uuid = #{uuid}
|
||||||
|
AND type IN (${typeList})
|
||||||
|
""")
|
||||||
|
Integer getStaffUuidPunishmentCount(@Param("typeList") String typeList,
|
||||||
|
@Param("uuid") String uuid);
|
||||||
|
|
||||||
|
default Integer getUuidPunishmentCount(HistoryType historyTypeEnum, UserType userTypeEnum, UUID uuid) {
|
||||||
|
String[] historyType = getHistoryType(historyTypeEnum);
|
||||||
|
String historyTypeSqlList = Arrays.stream(historyType)
|
||||||
|
.map(type -> "'" + type + "'")
|
||||||
|
.collect(Collectors.joining(", "));
|
||||||
|
return switch (userTypeEnum) {
|
||||||
|
case PLAYER -> getPlayerUuidPunishmentCount(historyTypeSqlList, uuid.toString());
|
||||||
|
case STAFF -> getStaffUuidPunishmentCount(historyTypeSqlList, uuid.toString());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] getHistoryType(HistoryType historyTypeEnum) {
|
||||||
|
return switch (historyTypeEnum) {
|
||||||
|
case ALL -> new String[]{"ban", "mute", "warn"};
|
||||||
|
case BAN -> new String[]{"ban"};
|
||||||
|
case MUTE -> new String[]{"mute"};
|
||||||
|
case KICK -> new String[]{"kick"};
|
||||||
|
case WARN -> new String[]{"warn"};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.alttd.altitudeweb.database.litebans;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class HistoryRecord {
|
||||||
|
private String uuid;
|
||||||
|
private int id;
|
||||||
|
private String punishedName;
|
||||||
|
private String reason;
|
||||||
|
private String bannedByUuid;
|
||||||
|
private String bannedByName;
|
||||||
|
private String removedByName;
|
||||||
|
private Long time;
|
||||||
|
private Long until;
|
||||||
|
private String removedByReason;
|
||||||
|
private String type;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.alttd.altitudeweb.database.litebans;
|
||||||
|
|
||||||
|
public enum HistoryType {
|
||||||
|
ALL,
|
||||||
|
BAN,
|
||||||
|
MUTE,
|
||||||
|
KICK,
|
||||||
|
WARN;
|
||||||
|
|
||||||
|
public static HistoryType getHistoryType(String historyType) {
|
||||||
|
return HistoryType.valueOf(historyType.toUpperCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+286
@@ -0,0 +1,286 @@
|
|||||||
|
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;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface NameHistoryMapper {
|
||||||
|
|
||||||
|
int PAGE_SIZE = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a list of all types of recent punishment history records filtered based on the given parameters sorted
|
||||||
|
* in descending time order. This result does NOT contain kicks history
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
*
|
||||||
|
* @return a list of {@link HistoryRecord} objects that match the specified filters, ordered by time in descending
|
||||||
|
* order
|
||||||
|
*/
|
||||||
|
@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"),
|
||||||
|
@Result(property = "type", column = "type")
|
||||||
|
})
|
||||||
|
@Select("""
|
||||||
|
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
|
||||||
|
ON user_lookup.uuid = all_punishments.uuid
|
||||||
|
WHERE ${name_column} LIKE #{partialName}
|
||||||
|
ORDER BY time DESC
|
||||||
|
LIMIT #{limit} OFFSET #{offset}
|
||||||
|
""")
|
||||||
|
List<HistoryRecord> getRecentAllHistoryForName(@Param("partialName") String partialName,
|
||||||
|
@Param("name_column") String nameColumn,
|
||||||
|
@Param("limit") int limit,
|
||||||
|
@Param("offset") int offset);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a list of all types of recent punishment history records sorted
|
||||||
|
* in descending time order. This result does NOT contain kicks history
|
||||||
|
*
|
||||||
|
* @param limit the maximum number of records to fetch
|
||||||
|
* @param offset the starting offset position of the result set
|
||||||
|
*
|
||||||
|
* @return a list of {@link HistoryRecord} objects that match the specified filters, ordered by time in descending
|
||||||
|
* order
|
||||||
|
*/
|
||||||
|
@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"),
|
||||||
|
@Result(property = "type", column = "type")
|
||||||
|
})
|
||||||
|
@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,
|
||||||
|
punishment.type
|
||||||
|
FROM (SELECT uuid,
|
||||||
|
id,
|
||||||
|
reason,
|
||||||
|
banned_by_uuid,
|
||||||
|
banned_by_name,
|
||||||
|
removed_by_name,
|
||||||
|
time,
|
||||||
|
until,
|
||||||
|
removed_by_reason,
|
||||||
|
type
|
||||||
|
FROM all_punishments
|
||||||
|
ORDER BY time DESC
|
||||||
|
LIMIT #{limit} OFFSET #{offset}) AS punishment
|
||||||
|
INNER JOIN user_lookup
|
||||||
|
ON user_lookup.uuid = punishment.uuid;
|
||||||
|
""")
|
||||||
|
List<HistoryRecord> getRecentAllHistory(@Param("limit") int limit,
|
||||||
|
@Param("offset") int offset);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a list of a specific type of recent punishment history records filtered based on the provided
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* @return a list of {@link HistoryRecord} objects that match the specified filters, sorted by time in descending
|
||||||
|
* order
|
||||||
|
*/
|
||||||
|
@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 ${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("table_name") String tableName,
|
||||||
|
@Param("partialName") String partialName,
|
||||||
|
@Param("name_column") String nameColumn,
|
||||||
|
@Param("limit") int limit,
|
||||||
|
@Param("offset") int offset);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a list of a specific type of recent punishment history records filtered based on the provided
|
||||||
|
* 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 limit the maximum number of records to fetch
|
||||||
|
* @param offset the starting offset position of the result set
|
||||||
|
*
|
||||||
|
* @return a list of {@link HistoryRecord} objects that match the specified filters, sorted by time in descending
|
||||||
|
* order
|
||||||
|
*/
|
||||||
|
@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 id,
|
||||||
|
uuid,
|
||||||
|
reason,
|
||||||
|
banned_by_uuid,
|
||||||
|
banned_by_name,
|
||||||
|
removed_by_name,
|
||||||
|
time,
|
||||||
|
until,
|
||||||
|
removed_by_reason
|
||||||
|
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("table_name") String tableName,
|
||||||
|
@Param("limit") int limit,
|
||||||
|
@Param("offset") int offset);
|
||||||
|
|
||||||
|
default List<HistoryRecord> getRecent(HistoryType historyType, UserType userType, String partialName,
|
||||||
|
int page) {
|
||||||
|
return switch (historyType) {
|
||||||
|
case ALL -> getRecentAll(userType, partialName, page);
|
||||||
|
case BAN -> getRecentBans(userType, partialName, page);
|
||||||
|
case MUTE -> getRecentMutes(userType, partialName, page);
|
||||||
|
case KICK -> getRecentKicks(userType, partialName, page);
|
||||||
|
case WARN -> getRecentWarns(userType, partialName, page);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<HistoryRecord> getRecent(String tableName, UserType userType, String partialName,
|
||||||
|
int page) {
|
||||||
|
int offset = page * PAGE_SIZE;
|
||||||
|
int limit = PAGE_SIZE;
|
||||||
|
if (partialName.isEmpty()) {
|
||||||
|
return getRecentHistory(tableName, limit, offset);
|
||||||
|
} else {
|
||||||
|
final String searchName = partialName.toLowerCase().replace("_", "\\_") + "%";
|
||||||
|
return switch (userType) {
|
||||||
|
case PLAYER -> getRecentHistoryForName(tableName, searchName, "name",
|
||||||
|
limit, offset);
|
||||||
|
case STAFF -> getRecentHistoryForName(tableName, searchName, "banned_by_name",
|
||||||
|
limit, offset);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<HistoryRecord> getRecentAll(UserType userType, String partialName, int page) {
|
||||||
|
int offset = page * PAGE_SIZE;
|
||||||
|
int limit = PAGE_SIZE;
|
||||||
|
if (partialName.isEmpty()) {
|
||||||
|
return getRecentAllHistory(limit, offset);
|
||||||
|
} else {
|
||||||
|
final String searchName = partialName.toLowerCase().replace("_", "\\_") + "%";
|
||||||
|
return switch (userType) {
|
||||||
|
case PLAYER -> getRecentAllHistoryForName(searchName, "name",
|
||||||
|
limit, offset);
|
||||||
|
case STAFF -> getRecentAllHistoryForName(searchName, "banned_by_name",
|
||||||
|
limit, offset);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NotNull List<HistoryRecord> getRecentBans(UserType userType, String partialName, int page) {
|
||||||
|
return addType(getRecent("litebans_bans", userType, partialName, page), "ban");
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NotNull List<HistoryRecord> getRecentKicks(UserType userType, String partialName, int page) {
|
||||||
|
return addType(getRecent("litebans_kicks", userType, partialName, page), "kick");
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NotNull List<HistoryRecord> getRecentMutes(UserType userType, String partialName, int page) {
|
||||||
|
return addType(getRecent("litebans_mutes", userType, partialName, page), "mute");
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NotNull List<HistoryRecord> getRecentWarns(UserType userType, String partialName, int page) {
|
||||||
|
return addType(getRecent("litebans_warnings", userType, partialName, page), "warn");
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NotNull List<HistoryRecord> addType(@NotNull List<HistoryRecord> historyRecords, String type) {
|
||||||
|
historyRecords.forEach(historyRecord -> historyRecord.setType(type));
|
||||||
|
return historyRecords;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package com.alttd.altitudeweb.database.litebans;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface RecentNamesMapper {
|
||||||
|
@Select("""
|
||||||
|
SELECT DISTINCT user_lookup.name AS punished_name
|
||||||
|
FROM ${tableName} AS punishment
|
||||||
|
INNER JOIN user_lookup
|
||||||
|
ON user_lookup.uuid = punishment.${uuid_column}
|
||||||
|
WHERE punishment.time > UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 1 YEAR)) * 1000
|
||||||
|
ORDER BY user_lookup.name
|
||||||
|
""")
|
||||||
|
List<String> getRecentHistory(@Param("tableName") String tableName, @Param("uuid_column") String uuidColumn);
|
||||||
|
|
||||||
|
default List<String> getRecent(HistoryType historyType, UserType userType) {
|
||||||
|
return switch (historyType) {
|
||||||
|
case ALL -> getRecentAll(userType);
|
||||||
|
case BAN -> getRecentBans(userType);
|
||||||
|
case MUTE -> getRecentMutes(userType);
|
||||||
|
case KICK -> getRecentKicks(userType);
|
||||||
|
case WARN -> getRecentWarns(userType);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getRecent(String tableName, UserType userType) {
|
||||||
|
switch (userType) {
|
||||||
|
case PLAYER -> {
|
||||||
|
return getRecentHistory(tableName, "uuid");
|
||||||
|
}
|
||||||
|
case STAFF -> {
|
||||||
|
return getRecentHistory(tableName, "banned_by_uuid");
|
||||||
|
}
|
||||||
|
default -> throw new IllegalArgumentException("Invalid user type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getRecentBans(UserType userType) {
|
||||||
|
return getRecent("litebans_bans", userType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getRecentKicks(UserType userType) {
|
||||||
|
return getRecent("litebans_kicks", userType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getRecentMutes(UserType userType) {
|
||||||
|
return getRecent("litebans_mutes", userType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getRecentWarns(UserType userType) {
|
||||||
|
return getRecent("litebans_warnings", userType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> getRecentAll(UserType userType) {
|
||||||
|
List<String> all = new ArrayList<>();
|
||||||
|
all.addAll(getRecentBans(userType));
|
||||||
|
// Removed recent kicks as we do not show those
|
||||||
|
// all.addAll(getRecentKicks(userType));
|
||||||
|
all.addAll(getRecentMutes(userType));
|
||||||
|
all.addAll(getRecentWarns(userType));
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+130
@@ -0,0 +1,130 @@
|
|||||||
|
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;
|
||||||
|
import org.jetbrains.annotations.Contract;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface UUIDHistoryMapper {
|
||||||
|
|
||||||
|
int PAGE_SIZE = 10;
|
||||||
|
|
||||||
|
@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"),
|
||||||
|
@Result(property = "type", column = "type")
|
||||||
|
})
|
||||||
|
@Select("""
|
||||||
|
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
|
||||||
|
ON user_lookup.uuid = all_punishments.uuid
|
||||||
|
WHERE all_punishments.${uuid_column} = #{uuid}
|
||||||
|
ORDER BY time DESC
|
||||||
|
LIMIT #{limit} OFFSET #{offset}
|
||||||
|
""")
|
||||||
|
List<HistoryRecord> getRecentAllHistory(@Param("uuid") String uuid,
|
||||||
|
@Param("uuid_column") String uuidColumn,
|
||||||
|
@Param("limit") int limit,
|
||||||
|
@Param("offset") int offset);
|
||||||
|
|
||||||
|
@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, 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
|
||||||
|
WHERE punishment.${uuid_column} = #{uuid}
|
||||||
|
ORDER BY time DESC
|
||||||
|
LIMIT #{limit} OFFSET #{offset}
|
||||||
|
""")
|
||||||
|
List<HistoryRecord> getRecentHistory(@Param("tableName") String tableName,
|
||||||
|
@Param("uuid") String uuid,
|
||||||
|
@Param("uuid_column") String uuidColumn,
|
||||||
|
@Param("limit") int limit,
|
||||||
|
@Param("offset") int offset);
|
||||||
|
|
||||||
|
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);
|
||||||
|
case MUTE -> getRecentMutes(userType, uuid, page);
|
||||||
|
case KICK -> getRecentKicks(userType, uuid, page);
|
||||||
|
case WARN -> getRecentWarns(userType, uuid, 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) {
|
||||||
|
case PLAYER -> getRecentHistory(tableName, uuid.toString(), "uuid", limit, offset);
|
||||||
|
case STAFF -> getRecentHistory(tableName, uuid.toString(), "banned_by_uuid", limit, offset);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<HistoryRecord> getRecentAll(@NotNull UserType userType, @NotNull UUID uuid, int page) {
|
||||||
|
int offset = page * PAGE_SIZE;
|
||||||
|
int limit = PAGE_SIZE;
|
||||||
|
return switch (userType) {
|
||||||
|
case PLAYER -> getRecentAllHistory(uuid.toString(), "name",
|
||||||
|
limit, offset);
|
||||||
|
case STAFF -> getRecentAllHistory(uuid.toString(), "banned_by_name",
|
||||||
|
limit, offset);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NotNull List<HistoryRecord> getRecentBans(@NotNull UserType userType, @NotNull UUID uuid, int page) {
|
||||||
|
return addType(getRecent("litebans_bans", userType, uuid, page), "ban");
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NotNull List<HistoryRecord> getRecentKicks(@NotNull UserType userType, @NotNull UUID uuid, int page) {
|
||||||
|
return addType(getRecent("litebans_kicks", userType, uuid, page), "kick");
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NotNull List<HistoryRecord> getRecentMutes(@NotNull UserType userType, @NotNull UUID uuid, int page) {
|
||||||
|
return addType(getRecent("litebans_mutes", userType, uuid, page), "mute");
|
||||||
|
}
|
||||||
|
|
||||||
|
private @NotNull List<HistoryRecord> getRecentWarns(@NotNull UserType userType, @NotNull UUID uuid, int page) {
|
||||||
|
return addType(getRecent("litebans_warnings", userType, uuid, page), "warn");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Contract("_, _ -> param1")
|
||||||
|
private @NotNull List<HistoryRecord> addType(@NotNull List<HistoryRecord> historyRecords, @NotNull String type) {
|
||||||
|
historyRecords.forEach(historyRecord -> historyRecord.setType(type));
|
||||||
|
return historyRecords;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.alttd.altitudeweb.database.litebans;
|
||||||
|
|
||||||
|
public enum UserType {
|
||||||
|
PLAYER,
|
||||||
|
STAFF;
|
||||||
|
|
||||||
|
public static UserType getUserType(String userType) {
|
||||||
|
return UserType.valueOf(userType.toUpperCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.alttd.altitudeweb.database.luckperms;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record Player(String username, UUID uuid) {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.alttd.altitudeweb.database.luckperms;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.type_handler.UUIDTypeHandler;
|
||||||
|
import org.apache.ibatis.annotations.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface TeamMemberMapper {
|
||||||
|
@ConstructorArgs({
|
||||||
|
@Arg(column = "username", javaType = String.class),
|
||||||
|
@Arg(column = "uuid", javaType = UUID.class, typeHandler = UUIDTypeHandler.class)
|
||||||
|
})
|
||||||
|
@Select("""
|
||||||
|
SELECT players.username, players.uuid
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.alttd.altitudeweb.database.web_db;
|
||||||
|
|
||||||
|
public record DatabaseSettings(String host, int port, String name, String username, String password) {
|
||||||
|
}
|
||||||
@@ -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,15 @@
|
|||||||
|
package com.alttd.altitudeweb.database.web_db;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.*;
|
||||||
|
|
||||||
|
public interface KeyPairMapper {
|
||||||
|
|
||||||
|
@Select("SELECT * 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,16 @@
|
|||||||
|
package com.alttd.altitudeweb.database.web_db;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class PrivilegedUser {
|
||||||
|
private int id;
|
||||||
|
private String uuid;
|
||||||
|
private List<String> permissions;
|
||||||
|
}
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
package com.alttd.altitudeweb.database.web_db;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface PrivilegedUserMapper {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a user by their UUID along with their permissions
|
||||||
|
* @param uuid The UUID of the user to retrieve
|
||||||
|
* @return The PrivilegedUser with their permissions, or null if not found
|
||||||
|
*/
|
||||||
|
@Select("""
|
||||||
|
SELECT privileged_users.id, privileged_users.uuid, privileges.privileges as permission
|
||||||
|
FROM privileged_users
|
||||||
|
LEFT JOIN privileges ON privileged_users.id = privileges.user_id
|
||||||
|
WHERE privileged_users.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"))
|
||||||
|
})
|
||||||
|
PrivilegedUser getUserByUuid(@Param("uuid") String 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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
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}")
|
||||||
|
DatabaseSettings getSettings(String database);
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
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 lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.ibatis.datasource.pooled.PooledDataSource;
|
||||||
|
import org.apache.ibatis.mapping.Environment;
|
||||||
|
import org.apache.ibatis.session.Configuration;
|
||||||
|
import org.apache.ibatis.session.SqlSession;
|
||||||
|
import org.apache.ibatis.session.SqlSessionFactory;
|
||||||
|
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
|
||||||
|
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class Connection {
|
||||||
|
|
||||||
|
private static final HashMap<Databases, Connection> connections = new HashMap<>();
|
||||||
|
private SqlSessionFactory sqlSessionFactory;
|
||||||
|
private final DatabaseSettings settings;
|
||||||
|
private final AddMappers addMappers;
|
||||||
|
|
||||||
|
private Connection(DatabaseSettings settings, AddMappers addMappers) {
|
||||||
|
this.settings = settings;
|
||||||
|
this.addMappers = addMappers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void initDatabases() {
|
||||||
|
InitializeWebDb.init();
|
||||||
|
InitializeLiteBans.init();
|
||||||
|
InitializeLuckPerms.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface AddMappers {
|
||||||
|
void apply(Configuration configuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Connection getConnection(Databases database) {
|
||||||
|
if (connections.containsKey(database)) {
|
||||||
|
return connections.get(database);
|
||||||
|
}
|
||||||
|
throw new RuntimeException("Database " + database + " has not been initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected static CompletableFuture<Connection> getConnection(Databases database, AddMappers addMappers) {
|
||||||
|
if (connections.containsKey(database)) {
|
||||||
|
return CompletableFuture.completedFuture(connections.get(database));
|
||||||
|
}
|
||||||
|
if (database == Databases.DEFAULT) {
|
||||||
|
return loadDefaultDatabase(addMappers);
|
||||||
|
}
|
||||||
|
CompletableFuture<DatabaseSettings> settingsFuture = new CompletableFuture<>();
|
||||||
|
getConnection(Databases.DEFAULT, (mapper -> mapper.addMapper(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());
|
||||||
|
}
|
||||||
|
log.debug("Loaded settings {}", loadedSettings);
|
||||||
|
settingsFuture.complete(loadedSettings);
|
||||||
|
});
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CompletableFuture<Connection> loadDefaultDatabase(AddMappers addMappers) {
|
||||||
|
DatabaseSettings databaseSettings = new DatabaseSettings(
|
||||||
|
System.getenv("DB_HOST"),
|
||||||
|
Integer.parseInt(System.getenv("DB_PORT")),
|
||||||
|
System.getenv("DB_NAME"),
|
||||||
|
System.getenv("DB_USER"),
|
||||||
|
System.getenv("DB_PASS")
|
||||||
|
);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void runQuery(Consumer<SqlSession> consumer) {
|
||||||
|
new Thread(() -> {
|
||||||
|
if (sqlSessionFactory == null) {
|
||||||
|
sqlSessionFactory = createSqlSessionFactory(settings, addMappers);
|
||||||
|
}
|
||||||
|
|
||||||
|
try (SqlSession session = sqlSessionFactory.openSession()) {
|
||||||
|
consumer.accept(session);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to run query", e);
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private SqlSessionFactory createSqlSessionFactory(DatabaseSettings settings, AddMappers addMappers) {
|
||||||
|
PooledDataSource dataSource = new PooledDataSource();
|
||||||
|
dataSource.setDriver("com.mysql.cj.jdbc.Driver");
|
||||||
|
dataSource.setUrl(String.format("jdbc:mysql://%s:%d/%s", settings.host(),
|
||||||
|
settings.port(), settings.name()));
|
||||||
|
dataSource.setUsername(settings.username());
|
||||||
|
dataSource.setPassword(settings.password());
|
||||||
|
Environment environment = new Environment("production", new JdbcTransactionFactory(), dataSource);
|
||||||
|
Configuration configuration = new Configuration(environment);
|
||||||
|
addMappers.apply(configuration);
|
||||||
|
|
||||||
|
return new SqlSessionFactoryBuilder().build(configuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package com.alttd.altitudeweb.setup;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.database.Databases;
|
||||||
|
import com.alttd.altitudeweb.database.litebans.*;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.ibatis.session.SqlSession;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class InitializeLiteBans {
|
||||||
|
|
||||||
|
protected static void init() {
|
||||||
|
log.info("Initializing LiteBans");
|
||||||
|
Connection.getConnection(Databases.LITE_BANS, (configuration) -> {
|
||||||
|
configuration.addMapper(RecentNamesMapper.class);
|
||||||
|
configuration.addMapper(NameHistoryMapper.class);
|
||||||
|
configuration.addMapper(UUIDHistoryMapper.class);
|
||||||
|
configuration.addMapper(HistoryCountMapper.class);
|
||||||
|
configuration.addMapper(IdHistoryMapper.class);
|
||||||
|
}).join()
|
||||||
|
.runQuery(sqlSession -> {
|
||||||
|
createAllPunishmentsView(sqlSession);
|
||||||
|
createUserLookupView(sqlSession);
|
||||||
|
});
|
||||||
|
log.debug("Initialized LiteBans");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void createAllPunishmentsView(SqlSession sqlSession) {
|
||||||
|
String query = """
|
||||||
|
CREATE VIEW IF NOT EXISTS all_punishments AS
|
||||||
|
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 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 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;
|
||||||
|
""";
|
||||||
|
try (Statement statement = sqlSession.getConnection().createStatement()) {
|
||||||
|
statement.execute(query);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void createUserLookupView(SqlSession sqlSession) {
|
||||||
|
String query = """
|
||||||
|
CREATE VIEW IF NOT EXISTS user_lookup AS
|
||||||
|
SELECT history_1.uuid, history_1.name
|
||||||
|
FROM litebans.litebans_history history_1
|
||||||
|
INNER JOIN (
|
||||||
|
SELECT uuid, MAX(id) as max_id
|
||||||
|
FROM litebans.litebans_history
|
||||||
|
GROUP BY uuid
|
||||||
|
) history_2 ON history_1.uuid = history_2.uuid AND history_1.id = history_2.max_id
|
||||||
|
""";
|
||||||
|
try (Statement statement = sqlSession.getConnection().createStatement()) {
|
||||||
|
statement.execute(query);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.alttd.altitudeweb.setup;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.database.Databases;
|
||||||
|
import com.alttd.altitudeweb.database.luckperms.TeamMemberMapper;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class InitializeLuckPerms {
|
||||||
|
|
||||||
|
protected static void init() {
|
||||||
|
log.info("Initializing LuckPerms");
|
||||||
|
Connection.getConnection(Databases.LUCK_PERMS, (configuration) -> {
|
||||||
|
configuration.addMapper(TeamMemberMapper.class);
|
||||||
|
}).join();
|
||||||
|
log.debug("Initialized LuckPerms");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
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.SettingsMapper;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.apache.ibatis.session.SqlSession;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class InitializeWebDb {
|
||||||
|
|
||||||
|
protected static void init() {
|
||||||
|
log.info("Initializing WebDb");
|
||||||
|
Connection.getConnection(Databases.DEFAULT, (configuration) -> {
|
||||||
|
configuration.addMapper(SettingsMapper.class);
|
||||||
|
configuration.addMapper(KeyPairMapper.class);
|
||||||
|
}).join()
|
||||||
|
.runQuery(SqlSession -> {
|
||||||
|
createSettingsTable(SqlSession);
|
||||||
|
createKeyTable(SqlSession);
|
||||||
|
createPrivilegedUsersTable(SqlSession);
|
||||||
|
createPrivilegesTable(SqlSession);
|
||||||
|
});
|
||||||
|
log.debug("Initialized WebDb");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void createSettingsTable(@NotNull SqlSession sqlSession) {
|
||||||
|
String query = """
|
||||||
|
CREATE TABLE IF NOT EXISTS db_connection_settings
|
||||||
|
(
|
||||||
|
internal_name VARCHAR(255) NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
username VARCHAR(255) NOT NULL,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
host VARCHAR(255) NOT NULL,
|
||||||
|
port INT NOT NULL,
|
||||||
|
CONSTRAINT pk_internal_name PRIMARY KEY (internal_name)
|
||||||
|
);
|
||||||
|
""";
|
||||||
|
try (Statement statement = sqlSession.getConnection().createStatement()) {
|
||||||
|
statement.execute(query);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 VARCHAR(36) 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.alttd.altitudeweb.type_handler;
|
||||||
|
|
||||||
|
import org.apache.ibatis.type.BaseTypeHandler;
|
||||||
|
import org.apache.ibatis.type.JdbcType;
|
||||||
|
import org.apache.ibatis.type.MappedTypes;
|
||||||
|
|
||||||
|
import java.sql.CallableStatement;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@MappedTypes(UUID.class)
|
||||||
|
public class UUIDTypeHandler extends BaseTypeHandler<UUID> {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setNonNullParameter(PreparedStatement ps, int i, UUID parameter, JdbcType jdbcType) throws SQLException {
|
||||||
|
ps.setString(i, parameter.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UUID getNullableResult(ResultSet rs, String columnName) throws SQLException {
|
||||||
|
String uuid = rs.getString(columnName);
|
||||||
|
return uuid == null ? null : UUID.fromString(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UUID getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
|
||||||
|
String uuid = rs.getString(columnIndex);
|
||||||
|
return uuid == null ? null : UUID.fromString(uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UUID getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
|
||||||
|
String uuid = cs.getString(columnIndex);
|
||||||
|
return uuid == null ? null : UUID.fromString(uuid);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
CREATE TABLE db_connection_settings
|
||||||
|
(
|
||||||
|
internal_name VARCHAR(255) NOT NULL,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
username VARCHAR(255) NOT NULL,
|
||||||
|
password VARCHAR(255) NOT NULL,
|
||||||
|
host VARCHAR(255) NOT NULL,
|
||||||
|
port INT NOT NULL,
|
||||||
|
CONSTRAINT pk_internal_name PRIMARY KEY (internal_name)
|
||||||
|
);
|
||||||
+25
-4
@@ -41,21 +41,39 @@
|
|||||||
"fileReplacements": [
|
"fileReplacements": [
|
||||||
{
|
{
|
||||||
"replace": "src/environments/environment.ts",
|
"replace": "src/environments/environment.ts",
|
||||||
"with": "src/environments/environment.prod.ts"
|
"with": "src/environments/environment.production.ts"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"optimization": true,
|
"optimization": true,
|
||||||
"outputHashing": "all",
|
"outputHashing": "all",
|
||||||
"sourceMap": false,
|
"sourceMap": false,
|
||||||
"extractCss": true,
|
|
||||||
"namedChunks": false,
|
"namedChunks": false,
|
||||||
"logging": false,
|
|
||||||
"vendorChunk": false,
|
"vendorChunk": false,
|
||||||
"buildOptimizer": true
|
"buildOptimizer": true
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"sourceMap": true,
|
"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,
|
||||||
|
"vendorChunk": false,
|
||||||
|
"buildOptimizer": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -98,5 +116,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"cli": {
|
||||||
|
"analytics": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
node {
|
||||||
|
download.set(true)
|
||||||
|
version.set("22.14.0")
|
||||||
|
npmVersion.set("10.9.2")
|
||||||
|
workDir.set(file("${project.projectDir}/node"))
|
||||||
|
npmWorkDir.set(file("${project.projectDir}/node"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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("npmBuild") {
|
||||||
|
description = "Run 'npm run build'"
|
||||||
|
group = "build"
|
||||||
|
|
||||||
|
doLast {
|
||||||
|
// Use nodeCommand directly from the plugin
|
||||||
|
project.exec {
|
||||||
|
workingDir(project.projectDir)
|
||||||
|
|
||||||
|
// Use node's npm to ensure it works on all environments
|
||||||
|
val nodeDir = "${project.projectDir}/node"
|
||||||
|
val isWindows = System.getProperty("os.name").lowercase().contains("windows")
|
||||||
|
|
||||||
|
if (isWindows) {
|
||||||
|
val npmCmd = file(nodeDir).listFiles()?.find { it.name.startsWith("npm") && it.isDirectory }?.let {
|
||||||
|
"${it.absolutePath}/npm.cmd"
|
||||||
|
} ?: "$nodeDir/node_modules/npm/bin/npm.cmd"
|
||||||
|
|
||||||
|
commandLine(npmCmd, "run", "build:dev")
|
||||||
|
} else {
|
||||||
|
val npmExecutable = file(nodeDir).listFiles()?.find { it.name.startsWith("npm") && it.isDirectory }?.let {
|
||||||
|
"${it.absolutePath}/bin/npm"
|
||||||
|
} ?: "$nodeDir/node_modules/npm/bin/npm"
|
||||||
|
|
||||||
|
commandLine(npmExecutable, "run", "build:beta")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependsOn("npmInstall")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named("assemble") {
|
||||||
|
dependsOn("npmBuild")
|
||||||
|
}
|
||||||
@@ -6,19 +6,26 @@
|
|||||||
"start": "ng serve",
|
"start": "ng serve",
|
||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
"watch": "ng build --watch --configuration development",
|
"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,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@angular/cdk": "^19.2.18",
|
||||||
"@angular/common": "^19.2.0",
|
"@angular/common": "^19.2.0",
|
||||||
"@angular/compiler": "^19.2.0",
|
"@angular/compiler": "^19.2.0",
|
||||||
"@angular/core": "^19.2.0",
|
"@angular/core": "^19.2.0",
|
||||||
"@angular/forms": "^19.2.0",
|
"@angular/forms": "^19.2.0",
|
||||||
|
"@angular/material": "^19.2.18",
|
||||||
"@angular/platform-browser": "^19.2.0",
|
"@angular/platform-browser": "^19.2.0",
|
||||||
"@angular/platform-browser-dynamic": "^19.2.0",
|
"@angular/platform-browser-dynamic": "^19.2.0",
|
||||||
"@angular/router": "^19.2.0",
|
"@angular/router": "^19.2.0",
|
||||||
|
"@types/three": "^0.177.0",
|
||||||
"ngx-cookie-service": "^19.1.2",
|
"ngx-cookie-service": "^19.1.2",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
|
"three": "^0.177.0",
|
||||||
"tslib": "^2.3.0",
|
"tslib": "^2.3.0",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.15.0"
|
||||||
},
|
},
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M15.7955 15.8111L21 21M18 10.5C18 14.6421 14.6421 18 10.5 18C6.35786 18 3 14.6421 3 10.5C3 6.35786 6.35786 3 10.5 3C14.6421 3 18 6.35786 18 10.5Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 467 B |
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
@@ -1,2 +1,7 @@
|
|||||||
- https://nodejs.org/en
|
- https://nodejs.org/en
|
||||||
- npm install -g @angular/cli
|
- npm install -g @angular/cli
|
||||||
|
|
||||||
|
# New page
|
||||||
|
|
||||||
|
- ng generate component <name>
|
||||||
|
- Go to app.routes.ts and give it a path
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<ng-container>
|
||||||
|
<app-header [current_page]="'about'" height="460px" background_image="/public/img/backgrounds/trees.jpg"
|
||||||
|
[overlay_gradient]="0.5">
|
||||||
|
<div class="title" header-content>
|
||||||
|
<h1>About Us</h1>
|
||||||
|
<h2>We are a simple server born out of nostalgia, and working to maintain a semi-vanilla feel today's Minecraft
|
||||||
|
world.</h2>
|
||||||
|
</div>
|
||||||
|
</app-header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Our Concept</h2>
|
||||||
|
<p>You could say it all started with a bit of nostalgia. Altitude was inspired by a small server back in
|
||||||
|
2012. It was a simple survival and creative server with a community that felt like family, and when the
|
||||||
|
server closed we took that concept and created Altitude.</p>
|
||||||
|
<p>From the very beginning we've been all about our community - adding features suggested by our members,
|
||||||
|
and designing a server that encourages cooperation and playing together. On Altitude, it's about coming
|
||||||
|
together as a community and creating new adventures every day.</p>
|
||||||
|
<p>We want to be the server you call home and always feel welcome to come on and relax for a while.</p>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Our Hardware</h2>
|
||||||
|
<p>As the community has grown and evolved so has the server. We started with a little desktop PC running in
|
||||||
|
the basement and have expanded through the years from Minecraft hosting, to a VPS, and finally to
|
||||||
|
dedicated servers hosted by OVH in Canada.</p>
|
||||||
|
<p>Today we run on a VPS and a gaming server from OVH with <span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">32GB of DDR4 memory</span>, an <span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">Intel i7 6700k</span> processor, <span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">2x 500GB SSD drives</span> and <span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">500Mbps bandwidth</span>. The VPS serves as our proxy
|
||||||
|
and website, while the gaming server hosts our Minecraft servers and SQL databases.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Our History</h2>
|
||||||
|
<p>Altitude began as a private server to get together with friends and it quickly became tradition to come
|
||||||
|
together every afternoon. Eventually we wanted to share our adventures with others, and went public,
|
||||||
|
inviting people from all around the globe to be a part of the community they had formed.</p>
|
||||||
|
<p>In the beginning, the server was named "The Underground", and was a small hangout spot mostly spread by
|
||||||
|
word of mouth. A few months after going public we renamed ourselves to "Altitude" and have kept the name
|
||||||
|
ever since. Altitude has evolved quite a bit over the years, but the concept has never changed - we are
|
||||||
|
home.</p>
|
||||||
|
<p>Today, Altitude thrives on the community that has formed. Many great people have decided to call Altitude
|
||||||
|
home, and many more people come every day. We are constantly learning more about what makes Altitude
|
||||||
|
great, and what we can do to make it even greater.</p>
|
||||||
|
<p>We hope you will join us on this adventure and learn with us as we embark on this amazing journey!</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { AboutComponent } from './about.component';
|
||||||
|
|
||||||
|
describe('AboutComponent', () => {
|
||||||
|
let component: AboutComponent;
|
||||||
|
let fixture: ComponentFixture<AboutComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [AboutComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(AboutComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {ScrollService} from '../scroll/scroll.service';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {HeaderComponent} from '../header/header.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-about',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
HeaderComponent
|
||||||
|
],
|
||||||
|
templateUrl: './about.component.html',
|
||||||
|
styleUrl: './about.component.scss'
|
||||||
|
})
|
||||||
|
export class AboutComponent {
|
||||||
|
constructor(public scrollService: ScrollService) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
|
<app-footer></app-footer>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import {TestBed} from '@angular/core/testing';
|
||||||
import { AppComponent } from './app.component';
|
import {AppComponent} from './app.component';
|
||||||
|
|
||||||
describe('AppComponent', () => {
|
describe('AppComponent', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import {Component, OnInit} from '@angular/core';
|
import {Component, OnInit} from '@angular/core';
|
||||||
import {Meta, Title} from '@angular/platform-browser';
|
import {Meta, Title} from '@angular/platform-browser';
|
||||||
import {ALTITUDE_VERSION} from './constant';
|
import {ALTITUDE_VERSION} from './constant';
|
||||||
import {Router} from '@angular/router';
|
import {Router, RouterOutlet} from '@angular/router';
|
||||||
|
import {FooterComponent} from './footer/footer.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: false,
|
standalone: true,
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrl: './app.component.scss',
|
styleUrl: './app.component.scss',
|
||||||
|
imports: [
|
||||||
|
FooterComponent,
|
||||||
|
RouterOutlet
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit{
|
export class AppComponent implements OnInit {
|
||||||
title = 'Survival Minecraft Server on ' + ALTITUDE_VERSION + '! | Altitude Community';
|
title = 'Survival Minecraft Server on ' + ALTITUDE_VERSION + '! | Altitude Community';
|
||||||
title_og = 'Community-centered ' + ALTITUDE_VERSION + ' Survival Minecraft Server';
|
title_og = 'Community-centered ' + ALTITUDE_VERSION + ' Survival Minecraft Server';
|
||||||
description = 'Start your adventure today! We are a ' +
|
description = 'Start your adventure today! We are a ' +
|
||||||
@@ -28,14 +33,11 @@ export class AppComponent implements OnInit{
|
|||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.titleService.setTitle(this.title)
|
this.titleService.setTitle(this.title)
|
||||||
this.metaService.updateTag({ name: 'description', content: this.description});
|
this.metaService.updateTag({name: 'description', content: this.description});
|
||||||
this.metaService.updateTag({ name: 'twitter:description', content: this.description_og})
|
this.metaService.updateTag({name: 'twitter:description', content: this.description_og})
|
||||||
this.metaService.updateTag({ name: 'og:title', content: this.title_og})
|
this.metaService.updateTag({name: 'og:title', content: this.title_og})
|
||||||
this.metaService.updateTag({ name: 'og:description', content: this.description_og})
|
this.metaService.updateTag({name: 'og:description', content: this.description_og})
|
||||||
this.metaService.updateTag({ name: 'keywords', content: this.keywords})
|
this.metaService.updateTag({name: 'keywords', content: this.keywords})
|
||||||
}
|
}
|
||||||
|
|
||||||
public isHomePage(): boolean {
|
|
||||||
return this.router.url === '/';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
|
import {ApplicationConfig, provideZoneChangeDetection} from '@angular/core';
|
||||||
import { provideRouter } from '@angular/router';
|
import {provideRouter} from '@angular/router';
|
||||||
|
|
||||||
import { routes } from './app.routes';
|
import {routes} from './app.routes';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes)]
|
providers: [provideZoneChangeDetection({eventCoalescing: true}), provideRouter(routes)]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
|
||||||
import { AppComponent } from './app.component';
|
|
||||||
import {RouterModule, Routes} from '@angular/router';
|
|
||||||
import { HeaderComponent } from './header/header.component';
|
|
||||||
import { HomeComponent } from './home/home.component';
|
|
||||||
import {NgOptimizedImage} from '@angular/common';
|
|
||||||
import {ThemeComponent} from "./theme/theme.component";
|
|
||||||
import {CookieService} from 'ngx-cookie-service';
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{ path: '', component: HomeComponent }
|
|
||||||
];
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [
|
|
||||||
AppComponent,
|
|
||||||
HeaderComponent,
|
|
||||||
HomeComponent,
|
|
||||||
ThemeComponent
|
|
||||||
],
|
|
||||||
imports: [
|
|
||||||
BrowserModule,
|
|
||||||
RouterModule.forRoot(routes),
|
|
||||||
NgOptimizedImage,
|
|
||||||
],
|
|
||||||
providers: [CookieService],
|
|
||||||
bootstrap: [AppComponent]
|
|
||||||
})
|
|
||||||
export class AppModule { }
|
|
||||||
@@ -1,3 +1,116 @@
|
|||||||
import { Routes } from '@angular/router';
|
import {Routes} from '@angular/router';
|
||||||
|
|
||||||
export const routes: Routes = [];
|
export const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'particles',
|
||||||
|
loadComponent: () => import('./particles/particles.component').then(m => m.ParticlesComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'map',
|
||||||
|
loadComponent: () => import('./map/map.component').then(m => m.MapComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'rules',
|
||||||
|
loadComponent: () => import('./rules/rules.component').then(m => m.RulesComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'vote',
|
||||||
|
loadComponent: () => import('./vote/vote.component').then(m => m.VoteComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'about',
|
||||||
|
loadComponent: () => import('./about/about.component').then(m => m.AboutComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'socials',
|
||||||
|
loadComponent: () => import('./socials/socials.component').then(m => m.SocialsComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'team',
|
||||||
|
loadComponent: () => import('./team/team.component').then(m => m.TeamComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'birthdays',
|
||||||
|
loadComponent: () => import('./birthdays/birthdays.component').then(m => m.BirthdaysComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'terms',
|
||||||
|
loadComponent: () => import('./terms/terms.component').then(m => m.TermsComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'privacy',
|
||||||
|
loadComponent: () => import('./privacy/privacy.component').then(m => m.PrivacyComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'bans',
|
||||||
|
loadComponent: () => import('./bans/bans.component').then(m => m.BansComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'bans/:type/:id',
|
||||||
|
loadComponent: () => import('./bans/details/details.component').then(m => m.DetailsComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'economy',
|
||||||
|
loadComponent: () => import('./economy/economy.component').then(m => m.EconomyComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'claiming',
|
||||||
|
loadComponent: () => import('./claiming/claiming.component').then(m => m.ClaimingComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'mypet',
|
||||||
|
loadComponent: () => import('./mypet/mypet.component').then(m => m.MypetComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'warps',
|
||||||
|
loadComponent: () => import('./warps/warps.component').then(m => m.WarpsComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'skyblock',
|
||||||
|
loadComponent: () => import('./skyblock/skyblock.component').then(m => m.SkyblockComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'customfeatures',
|
||||||
|
loadComponent: () => import('./customfeatures/customfeatures.component').then(m => m.CustomfeaturesComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'guide',
|
||||||
|
loadComponent: () => import('./guide/guide.component').then(m => m.GuideComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'ranks',
|
||||||
|
loadComponent: () => import('./ranks/ranks.component').then(m => m.RanksComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'commandlist',
|
||||||
|
loadComponent: () => import('./commandlist/commandlist.component').then(m => m.CommandlistComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'mapart',
|
||||||
|
loadComponent: () => import('./mapart/mapart.component').then(m => m.MapartComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'lag',
|
||||||
|
loadComponent: () => import('./lag/lag.component').then(m => m.LagComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'staffpowers',
|
||||||
|
loadComponent: () => import('./staffpowers/staffpowers.component').then(m => m.StaffpowersComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'forms/:form',
|
||||||
|
loadComponent: () => import('./forms/forms.component').then(m => m.FormsComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'forms',
|
||||||
|
loadComponent: () => import('./forms/forms.component').then(m => m.FormsComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'particles',
|
||||||
|
loadComponent: () => import('./particles/particles.component').then(m => m.ParticlesComponent)
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<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)"
|
||||||
|
(selectItem)="setSearch($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>
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
main .container {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.columnSection {
|
||||||
|
padding: 20px 0 20px 0;
|
||||||
|
max-width: 1300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyButtonContainer {
|
||||||
|
width: 80%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historySearchContainer {
|
||||||
|
width: 20%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
text-align: right;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-outer {
|
||||||
|
margin-right: 15px;
|
||||||
|
background: var(--history-link-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-outer.active {
|
||||||
|
color: var(--navlink-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#currentButton {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--white);
|
||||||
|
background: var(--link-color);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.historySearch {
|
||||||
|
font-family: 'open-sans', sans-serif;
|
||||||
|
width: 260px;
|
||||||
|
border: 1px solid #DEDEDE;
|
||||||
|
padding: 9px 4px 9px 40px;
|
||||||
|
background: #DEDEDE url(/public/img/random/search.svg) no-repeat 10px;
|
||||||
|
background-size: 18px 18px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchButton {
|
||||||
|
vertical-align: -15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyTable {
|
||||||
|
font-family: 'open-sans', sans-serif;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
|
||||||
|
border-radius: 5px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-results {
|
||||||
|
font-family: 'open-sans', sans-serif;
|
||||||
|
position: absolute;
|
||||||
|
top: 35px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
background-color: #DEDEDE;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-top: none;
|
||||||
|
z-index: 1050;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
border-bottom-right-radius: 5px;
|
||||||
|
border-bottom-left-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.changePageButtons {
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 10px 0 30px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyPageButton {
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: .25rem;
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.02) 0 1px 3px 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background-color: var(--history-page-button-background);
|
||||||
|
color: var(--font-color);
|
||||||
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
|
font-family: 'opensans', sans-serif;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 1.25;
|
||||||
|
margin: 0 5px 0 5px;
|
||||||
|
min-height: 3rem;
|
||||||
|
padding: calc(.875rem - 1px) calc(1.5rem - 1px);
|
||||||
|
position: relative;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 250ms;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
touch-action: manipulation;
|
||||||
|
vertical-align: baseline;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyPageButton.active:hover,
|
||||||
|
.historyPageButton.active:focus {
|
||||||
|
border-color: rgba(0, 0, 0, 0.15);
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.1) 0 4px 12px;
|
||||||
|
color: var(--font-color-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyPageButton.active:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyPageButton.active:active {
|
||||||
|
border-color: rgba(0, 0, 0, 0.15);
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.06) 0 2px 4px;
|
||||||
|
color: var(--font-color-hover);
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyPageButton.disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background-color: var(--color-quaternary);
|
||||||
|
color: var(--font-color);
|
||||||
|
box-shadow: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent hover effects on disabled buttons */
|
||||||
|
.historyPageButton.disabled:hover,
|
||||||
|
.historyPageButton.disabled:focus {
|
||||||
|
border-color: rgba(0, 0, 0, 0.1);
|
||||||
|
box-shadow: none;
|
||||||
|
transform: none;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageNumber {
|
||||||
|
font-family: 'open-sans', sans-serif;
|
||||||
|
margin: 0 25px 0 25px;
|
||||||
|
color: var(--font-color);
|
||||||
|
transition: 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { BansComponent } from './bans.component';
|
||||||
|
|
||||||
|
describe('BansComponent', () => {
|
||||||
|
let component: BansComponent;
|
||||||
|
let fixture: ComponentFixture<BansComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [BansComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(BansComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
import {Component, OnInit} from '@angular/core';
|
||||||
|
import {HeaderComponent} from "../header/header.component";
|
||||||
|
import {HistoryComponent} from './history/history.component';
|
||||||
|
import {HistoryCount, HistoryService} from '../../api';
|
||||||
|
import {NgClass, NgForOf, NgIf} from '@angular/common';
|
||||||
|
import {FormsModule} from '@angular/forms';
|
||||||
|
import {catchError, map, Observable} from 'rxjs';
|
||||||
|
import {SearchParams} from './search-terms';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-bans',
|
||||||
|
imports: [
|
||||||
|
HeaderComponent,
|
||||||
|
HistoryComponent,
|
||||||
|
NgIf,
|
||||||
|
FormsModule,
|
||||||
|
NgForOf,
|
||||||
|
NgClass
|
||||||
|
],
|
||||||
|
templateUrl: './bans.component.html',
|
||||||
|
styleUrl: './bans.component.scss'
|
||||||
|
})
|
||||||
|
export class BansComponent implements OnInit {
|
||||||
|
|
||||||
|
constructor(public historyApi: HistoryService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private PAGE_SIZE: number = 10;
|
||||||
|
private uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
||||||
|
private actualPage: number = 0;
|
||||||
|
private totalSearchResults: number = -1;
|
||||||
|
|
||||||
|
public active: string = '';
|
||||||
|
public userType: 'player' | 'staff' = "player";
|
||||||
|
public punishmentType: 'all' | 'ban' | 'mute' | 'kick' | 'warn' = "all";
|
||||||
|
public page: number = 0;
|
||||||
|
public names: string[] = [];
|
||||||
|
public finalSearchTerm: string = '';
|
||||||
|
public searchTerm: string = '';
|
||||||
|
public filteredNames: string[] = [];
|
||||||
|
public pageSize = 10;
|
||||||
|
public historyCount: HistoryCount = {
|
||||||
|
bans: 0,
|
||||||
|
mutes: 0,
|
||||||
|
kicks: 0,
|
||||||
|
warnings: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.historyApi.getUserNames(this.userType, this.punishmentType).subscribe(names => {
|
||||||
|
this.names = names;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.historyApi.getTotalPunishments().subscribe(totalPunishments => {
|
||||||
|
this.historyCount = totalPunishments;
|
||||||
|
})
|
||||||
|
|
||||||
|
this.pushState();
|
||||||
|
|
||||||
|
window.addEventListener('popstate', (event) => {
|
||||||
|
if (event.state && event.state.searchTerm !== undefined) {
|
||||||
|
this.searchTerm = event.state.searchTerm;
|
||||||
|
this.finalSearchTerm = event.state.searchTerm;
|
||||||
|
this.page = event.state.page;
|
||||||
|
this.userType = event.state.userType;
|
||||||
|
this.punishmentType = event.state.punishmentType;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCurrentButtonId(options: 'all' | 'ban' | 'mute' | 'kick' | 'warn') {
|
||||||
|
if (options == this.punishmentType) {
|
||||||
|
return 'currentButton'
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCurrentUserTypeButtonId(options: 'player' | 'staff') {
|
||||||
|
if (options == this.userType) {
|
||||||
|
return 'currentButton'
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public filterNames() {
|
||||||
|
if (!this.searchTerm) {
|
||||||
|
this.filteredNames = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.filteredNames = this.names.filter(name =>
|
||||||
|
name.toLowerCase().startsWith(this.searchTerm.toLowerCase())
|
||||||
|
).slice(0, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
public selectName(name: string) {
|
||||||
|
this.searchTerm = name;
|
||||||
|
this.filteredNames = [];
|
||||||
|
this.search();
|
||||||
|
}
|
||||||
|
|
||||||
|
public search() {
|
||||||
|
this.pushState()
|
||||||
|
this.finalSearchTerm = this.searchTerm;
|
||||||
|
this.page = 0;
|
||||||
|
if (this.finalSearchTerm.length === 0) {
|
||||||
|
this.totalSearchResults = -1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalSearchResultsObservable: Observable<number>;
|
||||||
|
if (this.uuidRegex.test(this.finalSearchTerm)) {
|
||||||
|
totalSearchResultsObservable = this.historyApi.getTotalResultsForUuidSearch(this.userType, this.punishmentType, this.finalSearchTerm);
|
||||||
|
} else {
|
||||||
|
totalSearchResultsObservable = this.historyApi.getTotalResultsForUserSearch(this.userType, this.punishmentType, this.finalSearchTerm);
|
||||||
|
}
|
||||||
|
totalSearchResultsObservable.pipe(
|
||||||
|
map(totalSearchResults => {
|
||||||
|
this.totalSearchResults = totalSearchResults;
|
||||||
|
}),
|
||||||
|
catchError(err => {
|
||||||
|
console.error(err);
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
).subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
public changeUserType(type: 'player' | 'staff') {
|
||||||
|
this.pushState();
|
||||||
|
this.userType = type;
|
||||||
|
this.page = 0;
|
||||||
|
this.finalSearchTerm = '';
|
||||||
|
this.searchTerm = '';
|
||||||
|
this.filteredNames = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public changeHistoryPunishment(type: 'all' | 'ban' | 'mute' | 'kick' | 'warn') {
|
||||||
|
this.pushState();
|
||||||
|
this.punishmentType = type;
|
||||||
|
this.page = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextPage() {
|
||||||
|
if (this.page === this.getMaxPage()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setPage(this.page + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
public previousPage() {
|
||||||
|
if (this.page === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setPage(this.page - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
public setPage(page: number) {
|
||||||
|
if (this.actualPage !== this.page) {
|
||||||
|
console.warn('Changing page too quickly, the previous page change has not loaded yet');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.pushState();
|
||||||
|
this.page = page
|
||||||
|
}
|
||||||
|
|
||||||
|
private pushState() {
|
||||||
|
const state = {
|
||||||
|
searchTerm: this.searchTerm,
|
||||||
|
page: this.page,
|
||||||
|
userType: this.userType,
|
||||||
|
punishmentType: this.punishmentType
|
||||||
|
};
|
||||||
|
const title = 'Altitude Bans';
|
||||||
|
const url = window.location.href;
|
||||||
|
window.history.pushState(state, title, url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMaxPage() {
|
||||||
|
const all = this.historyCount.bans + this.historyCount.mutes + this.historyCount.warnings;
|
||||||
|
if (this.finalSearchTerm.length !== 0) {
|
||||||
|
return Math.floor(this.totalSearchResults / this.PAGE_SIZE);
|
||||||
|
}
|
||||||
|
switch (this.punishmentType) {
|
||||||
|
case 'all':
|
||||||
|
return Math.floor(all / this.PAGE_SIZE);
|
||||||
|
case 'ban':
|
||||||
|
return Math.floor(this.historyCount.bans / this.PAGE_SIZE);
|
||||||
|
case 'mute':
|
||||||
|
return Math.floor(this.historyCount.mutes / this.PAGE_SIZE);
|
||||||
|
case 'kick':
|
||||||
|
return Math.floor(this.historyCount.kicks / this.PAGE_SIZE);
|
||||||
|
case 'warn':
|
||||||
|
return Math.floor(this.historyCount.warnings / this.PAGE_SIZE);
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public buttonActive(compare: number): boolean {
|
||||||
|
if (compare !== 0 && this.pageSize < this.PAGE_SIZE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.page !== compare;
|
||||||
|
}
|
||||||
|
|
||||||
|
public updatePageSize(pageSize: number) {
|
||||||
|
if (pageSize < 0) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
this.pageSize = pageSize;
|
||||||
|
this.actualPage = this.page;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public setSearch(searchParams: SearchParams) {
|
||||||
|
this.userType = searchParams.userType
|
||||||
|
this.searchTerm = searchParams.searchTerm
|
||||||
|
this.punishmentType = searchParams.punishmentType
|
||||||
|
this.search();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
<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">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="detailsBackButton">
|
||||||
|
<ng-container *ngIf="punishment === undefined">
|
||||||
|
<p>Loading...</p>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
<a [routerLink]="['/bans']">< Back</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="columnSection center">
|
||||||
|
<ng-container *ngIf="punishment">
|
||||||
|
<div>
|
||||||
|
<span class="tag tagInfo"
|
||||||
|
[ngClass]="{
|
||||||
|
'tagPermanent': this.historyFormat.isPermanent(punishment),
|
||||||
|
'tagExpired': !this.historyFormat.isPermanent(punishment)
|
||||||
|
}">
|
||||||
|
{{ this.historyFormat.getType(punishment) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
class="tag tagInfo"
|
||||||
|
[ngClass]="{
|
||||||
|
'tagActive': this.historyFormat.isActive(punishment),
|
||||||
|
'tagInactive': !this.historyFormat.isActive(punishment)
|
||||||
|
}">
|
||||||
|
{{ this.historyFormat.isActive(punishment) ? 'Active' : 'Inactive' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</section>
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<ng-container *ngIf="punishment">
|
||||||
|
<div class="playerContainer">
|
||||||
|
<h2>Player</h2>
|
||||||
|
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(punishment.uuid, '150')"
|
||||||
|
width="150"
|
||||||
|
height="150"
|
||||||
|
alt="{{punishment.username}}'s Minecraft skin"
|
||||||
|
>
|
||||||
|
<h3 class="detailsUsername">{{ punishment.username }}</h3>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<ng-container *ngIf="punishment">
|
||||||
|
<div class="playerContainer">
|
||||||
|
<h2>Moderator</h2>
|
||||||
|
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(punishment.punishedByUuid, '150')"
|
||||||
|
width="150"
|
||||||
|
height="150"
|
||||||
|
alt="{{punishment.punishedBy}}'s Minecraft skin"
|
||||||
|
>
|
||||||
|
<h3 class="detailsUsername">{{ punishment.punishedBy }}</h3>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<ng-container *ngIf="punishment">
|
||||||
|
<div class="detailsInfo">
|
||||||
|
<h2>Reason</h2>
|
||||||
|
<p>{{ punishment.reason | removeTrailingPeriod }}</p>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<ng-container *ngIf="punishment">
|
||||||
|
<div class="detailsInfo">
|
||||||
|
<h2>Date</h2>
|
||||||
|
<p>{{ this.historyFormat.getPunishmentTime(punishment) }}</p>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
|
||||||
|
<section class="columnSection">
|
||||||
|
<ng-container *ngIf="punishment">
|
||||||
|
<span>Expires</span>
|
||||||
|
<span>{{ this.historyFormat.getExpiredTime(punishment) }}</span>
|
||||||
|
<ng-container *ngIf="punishment.removedBy !== undefined && punishment.removedBy.length > 0">
|
||||||
|
<span>Un{{ this.historyFormat.getType(punishment).toLocaleLowerCase() }} reason</span>
|
||||||
|
<span>{{ punishment.removedReason == null ? 'No reason specified' : punishment.removedReason }}</span>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
</section>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
.detailsBackButton {
|
||||||
|
font-family: open-sans, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.columnSection {
|
||||||
|
padding-top: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailsUsername {
|
||||||
|
font-size: 1.2em;
|
||||||
|
font-family: 'opensans-bold', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailsInfo {
|
||||||
|
padding-top: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detailsInfo p {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 5px 10px;
|
||||||
|
line-height: 1;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: baseline;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-family: 'opensans-bold', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagInfo {
|
||||||
|
margin: 0 20px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagActive {
|
||||||
|
color: #FFFFFF;
|
||||||
|
background-color: #EE5555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagInactive {
|
||||||
|
color: #FFFFFF;
|
||||||
|
background-color: #F79720;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagPermanent {
|
||||||
|
color: #FFFFFF;
|
||||||
|
background-color: #EE5555;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tagExpired {
|
||||||
|
color: #FFFFFF;
|
||||||
|
background-color: #777777
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { DetailsComponent } from './details.component';
|
||||||
|
|
||||||
|
describe('DetailsComponent', () => {
|
||||||
|
let component: DetailsComponent;
|
||||||
|
let fixture: ComponentFixture<DetailsComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [DetailsComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(DetailsComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import {Component, OnInit} from '@angular/core';
|
||||||
|
import {HistoryService, PunishmentHistory} from '../../../api';
|
||||||
|
import {NgClass, NgIf, NgOptimizedImage} from '@angular/common';
|
||||||
|
import {RemoveTrailingPeriodPipe} from '../../util/RemoveTrailingPeriodPipe';
|
||||||
|
import {HistoryFormatService} from '../history-format.service';
|
||||||
|
import {ActivatedRoute, RouterLink} from '@angular/router';
|
||||||
|
import {catchError, map} from 'rxjs';
|
||||||
|
import {HeaderComponent} from '../../header/header.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-details',
|
||||||
|
imports: [
|
||||||
|
NgIf,
|
||||||
|
NgOptimizedImage,
|
||||||
|
RemoveTrailingPeriodPipe,
|
||||||
|
HeaderComponent,
|
||||||
|
RouterLink,
|
||||||
|
NgClass
|
||||||
|
],
|
||||||
|
templateUrl: './details.component.html',
|
||||||
|
styleUrl: './details.component.scss'
|
||||||
|
})
|
||||||
|
export class DetailsComponent implements OnInit {
|
||||||
|
type: 'all' | 'ban' | 'mute' | 'kick' | 'warn' = 'all';
|
||||||
|
id: number = -1;
|
||||||
|
punishment: PunishmentHistory | undefined;
|
||||||
|
|
||||||
|
constructor(private route: ActivatedRoute,
|
||||||
|
private historyApi: HistoryService,
|
||||||
|
public historyFormat: HistoryFormatService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.route.paramMap.subscribe(params => {
|
||||||
|
switch (params.get('type')) {
|
||||||
|
case 'ban':
|
||||||
|
this.type = 'ban';
|
||||||
|
break;
|
||||||
|
case 'mute':
|
||||||
|
this.type = 'mute';
|
||||||
|
break;
|
||||||
|
case 'kick':
|
||||||
|
this.type = 'kick';
|
||||||
|
break;
|
||||||
|
case 'warn':
|
||||||
|
this.type = 'warn';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Invalid type");
|
||||||
|
}
|
||||||
|
this.id = Number(params.get('id') || '-1');
|
||||||
|
this.loadPunishmentData();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadPunishmentData() {
|
||||||
|
if (!this.type || this.type === 'all' || this.id < 0 || isNaN(this.id)) {
|
||||||
|
console.error("Invalid type or id");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.historyApi.getHistoryById(this.type, this.id).pipe(
|
||||||
|
map(punishment => {
|
||||||
|
this.punishment = punishment;
|
||||||
|
}),
|
||||||
|
catchError(err => {
|
||||||
|
console.error(err);
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
).subscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { HistoryFormatService } from './history-format.service';
|
||||||
|
|
||||||
|
describe('HistoryFormatService', () => {
|
||||||
|
let service: HistoryFormatService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(HistoryFormatService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be created', () => {
|
||||||
|
expect(service).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {PunishmentHistory} from '../../api';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class HistoryFormatService {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPunishmentTime(entry: PunishmentHistory) {
|
||||||
|
const date = new Date(entry.punishmentTime);
|
||||||
|
return date.toLocaleString(navigator.language, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public isActive(entry: PunishmentHistory): boolean {
|
||||||
|
if (entry.removedBy !== null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (entry.expiryTime <= 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return entry.expiryTime > Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
public isPermanent(entry: PunishmentHistory): boolean {
|
||||||
|
return entry.expiryTime <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getType(entry: PunishmentHistory): string {
|
||||||
|
return entry.type.charAt(0).toUpperCase() + entry.type.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getExpiredTime(entry: PunishmentHistory): string {
|
||||||
|
let suffix: string = '';
|
||||||
|
if (entry.removedBy !== null && entry.removedBy !== undefined) {
|
||||||
|
suffix = '(Unbanned by ' + entry.removedBy + ')';
|
||||||
|
}
|
||||||
|
if (entry.expiryTime <= 0) {
|
||||||
|
return "Permanent " + this.getType(entry) + " " + suffix;
|
||||||
|
}
|
||||||
|
const date = new Date(entry.expiryTime);
|
||||||
|
return date.toLocaleString(navigator.language, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: false
|
||||||
|
}) + " " + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAvatarUrl(entry: string, size: string = '25'): string {
|
||||||
|
let uuid = entry.replace('-', '');
|
||||||
|
if (uuid === 'C') {
|
||||||
|
uuid = "f78a4d8dd51b4b3998a3230f2de0c670"
|
||||||
|
}
|
||||||
|
return `https://crafatar.com/avatars/${uuid}?size=${size}&overlay`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<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" (click)="showDetailedPunishment(entry)">
|
||||||
|
{{ this.historyFormat.getType(entry) }}
|
||||||
|
</td>
|
||||||
|
<td class="historyPlayer" (click)="setSearch(entry.username, 'player')">
|
||||||
|
<div class="playerContainer">
|
||||||
|
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(entry.uuid)" width="25" height="25"
|
||||||
|
alt="{{entry.username}}'s Minecraft skin">
|
||||||
|
<span class="username">{{ entry.username }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="historyPlayer" (click)="setSearch(entry.punishedBy, 'staff')">
|
||||||
|
<div class="playerContainer">
|
||||||
|
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(entry.punishedByUuid)" width="25" height="25"
|
||||||
|
alt="{{entry.punishedBy}}'s Minecraft skin">
|
||||||
|
<span>{{ entry.punishedBy }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td class="historyReason" (click)="showDetailedPunishment(entry)">
|
||||||
|
{{ entry.reason | removeTrailingPeriod }}
|
||||||
|
</td>
|
||||||
|
<td class="historyDate" (click)="showDetailedPunishment(entry)">
|
||||||
|
{{ this.historyFormat.getPunishmentTime(entry) }}
|
||||||
|
</td>
|
||||||
|
<td class="historyDate" (click)="showDetailedPunishment(entry)">
|
||||||
|
{{ this.historyFormat.getExpiredTime(entry) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</div>
|
||||||
|
</table>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead {
|
||||||
|
height: 25px;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: 5px 0 5px 0;
|
||||||
|
height: 55px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
padding: 5px 0 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 5px 0 8px 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
th.historyType, td.historyType {
|
||||||
|
width: 100px !important;
|
||||||
|
min-width: 100px;
|
||||||
|
max-width: 100px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
th.historyPlayer, td.historyPlayer {
|
||||||
|
width: 180px !important;
|
||||||
|
min-width: 180px;
|
||||||
|
max-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th.historyReason, td.historyReason {
|
||||||
|
width: 540px !important;
|
||||||
|
min-width: 540px;
|
||||||
|
max-width: 540px;
|
||||||
|
}
|
||||||
|
|
||||||
|
th.historyDate, td.historyDate {
|
||||||
|
width: 150px !important;
|
||||||
|
min-width: 150px;
|
||||||
|
max-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
table tr td {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyPlayerRow:hover {
|
||||||
|
background-color: var(--history-table-row-color);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyTableHead {
|
||||||
|
width: 1300px;
|
||||||
|
background-color: var(--history-table-head-color);
|
||||||
|
transition: all 100ms cubic-bezier(0.55, 0.085, 0.68, 0.53), 0.5s background ease;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 4px 8px -4px rgba(0, 0, 0, 0.2), 0 6px 20px -8px rgba(0, 0, 0, 0.19);
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyType {
|
||||||
|
max-width: 100%;
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyPlayer {
|
||||||
|
max-width: 100%;
|
||||||
|
width: 200px;
|
||||||
|
text-overflow: clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playerContainer {
|
||||||
|
display: inline-flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
vertical-align: middle;
|
||||||
|
border-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyReason {
|
||||||
|
width: 650px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.historyDate {
|
||||||
|
width: 170px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { HistoryComponent } from './history.component';
|
||||||
|
|
||||||
|
describe('HistoryComponent', () => {
|
||||||
|
let component: HistoryComponent;
|
||||||
|
let fixture: ComponentFixture<HistoryComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [HistoryComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(HistoryComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import {Component, EventEmitter, Input, OnChanges, OnInit, Output} from '@angular/core';
|
||||||
|
import {BASE_PATH, HistoryService, PunishmentHistory} from '../../../api';
|
||||||
|
import {catchError, map, Observable, shareReplay} from 'rxjs';
|
||||||
|
import {NgForOf, NgIf, NgOptimizedImage} from '@angular/common';
|
||||||
|
import {CookieService} from 'ngx-cookie-service';
|
||||||
|
import {RemoveTrailingPeriodPipe} from '../../util/RemoveTrailingPeriodPipe';
|
||||||
|
import {HttpErrorResponse} from '@angular/common/http';
|
||||||
|
import {environment} from '../../../environments/environment';
|
||||||
|
import {HistoryFormatService} from '../history-format.service';
|
||||||
|
import {SearchParams} from '../search-terms';
|
||||||
|
import {Router} from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-history',
|
||||||
|
imports: [
|
||||||
|
NgIf,
|
||||||
|
NgForOf,
|
||||||
|
NgOptimizedImage,
|
||||||
|
RemoveTrailingPeriodPipe
|
||||||
|
],
|
||||||
|
templateUrl: './history.component.html',
|
||||||
|
styleUrl: './history.component.scss',
|
||||||
|
providers: [
|
||||||
|
CookieService,
|
||||||
|
{provide: BASE_PATH, useValue: environment.apiUrl}
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class HistoryComponent implements OnInit, OnChanges {
|
||||||
|
|
||||||
|
@Input() userType: 'player' | 'staff' = "player";
|
||||||
|
@Input() punishmentType: 'all' | 'ban' | 'mute' | 'kick' | 'warn' = "all";
|
||||||
|
@Input() page: number = 0;
|
||||||
|
@Input() searchTerm: string = '';
|
||||||
|
|
||||||
|
@Output() pageChange = new EventEmitter<number>();
|
||||||
|
@Output() selectItem = new EventEmitter<SearchParams>();
|
||||||
|
|
||||||
|
private uuidRegex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
||||||
|
|
||||||
|
public history: PunishmentHistory[] = []
|
||||||
|
|
||||||
|
constructor(private historyApi: HistoryService, public historyFormat: HistoryFormatService, private router: Router) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges(): void {
|
||||||
|
this.reloadHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.reloadHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
private reloadHistory(): void {
|
||||||
|
let historyObservable: Observable<PunishmentHistory[]>;
|
||||||
|
if (this.searchTerm.length === 0) {
|
||||||
|
historyObservable = this.historyApi.getHistoryForAll(this.userType, this.punishmentType, this.page);
|
||||||
|
} else {
|
||||||
|
if (this.uuidRegex.test(this.searchTerm)) {
|
||||||
|
historyObservable = this.historyApi.getHistoryForUuid(this.userType, this.punishmentType, this.searchTerm, this.page);
|
||||||
|
} else {
|
||||||
|
historyObservable = this.historyApi.getHistoryForUsers(this.userType, this.punishmentType, this.searchTerm, this.page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
historyObservable.pipe(
|
||||||
|
map(history => {
|
||||||
|
this.history = history;
|
||||||
|
this.pageChange.emit(this.history.length);
|
||||||
|
}),
|
||||||
|
catchError(err => {
|
||||||
|
this.pageChange.emit(-1);
|
||||||
|
let retrySeconds = 5;
|
||||||
|
|
||||||
|
if (err instanceof HttpErrorResponse) {
|
||||||
|
if (err.status !== 429) {
|
||||||
|
return this.history;
|
||||||
|
}
|
||||||
|
const headers = err.headers;
|
||||||
|
const retryAfterHeader = headers.get('Retry-After');
|
||||||
|
console.warn(err.error);
|
||||||
|
if (retryAfterHeader) {
|
||||||
|
const retryAfter = parseInt(retryAfterHeader || '0', 10);
|
||||||
|
if (!isNaN(retryAfter) && retryAfter > 0) {
|
||||||
|
retrySeconds = retryAfter + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
setTimeout(() => this.reloadHistory(), retrySeconds * 1000);
|
||||||
|
return this.history;
|
||||||
|
}),
|
||||||
|
shareReplay(1)
|
||||||
|
).subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearch(name: string, userType: 'player' | 'staff') {
|
||||||
|
let searchParams: SearchParams = {
|
||||||
|
userType: userType,
|
||||||
|
punishmentType: 'all',
|
||||||
|
searchTerm: name
|
||||||
|
}
|
||||||
|
this.selectItem.emit(searchParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
public showDetailedPunishment(entry: PunishmentHistory) {
|
||||||
|
this.router.navigate([`bans/${entry.type}/${entry.id}`]).then();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export interface SearchParams {
|
||||||
|
userType: 'player' | 'staff';
|
||||||
|
searchTerm: string;
|
||||||
|
punishmentType: 'all' | 'ban' | 'mute' | 'kick' | 'warn';
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<ng-container>
|
||||||
|
<app-header [current_page]="'birthdays'" height="460px" background_image="/public/img/backgrounds/staff.png"
|
||||||
|
[overlay_gradient]="0.5">
|
||||||
|
<div class="title" header-content>
|
||||||
|
<h1>Famous Altitude Staff Birthdays</h1>
|
||||||
|
<h2>(and previous staff)</h2>
|
||||||
|
</div>
|
||||||
|
</app-header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<div class="container" style="padding: 50px 0 0 0; justify-content: center;">
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
.birthdayMember {
|
||||||
|
width: 33%;
|
||||||
|
min-width: 250px;
|
||||||
|
padding-bottom: 50px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.birthdayMember img {
|
||||||
|
padding-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.birthdayMember p {
|
||||||
|
font-family: 'opensans-bold', sans-serif;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { BirthdaysComponent } from './birthdays.component';
|
||||||
|
|
||||||
|
describe('BirthdaysComponent', () => {
|
||||||
|
let component: BirthdaysComponent;
|
||||||
|
let fixture: ComponentFixture<BirthdaysComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [BirthdaysComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(BirthdaysComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {ScrollService} from '../scroll/scroll.service';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
import {HeaderComponent} from '../header/header.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-birthdays',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
HeaderComponent
|
||||||
|
],
|
||||||
|
templateUrl: './birthdays.component.html',
|
||||||
|
styleUrl: './birthdays.component.scss'
|
||||||
|
})
|
||||||
|
export class BirthdaysComponent {
|
||||||
|
constructor(public scrollService: ScrollService) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
<ng-container>
|
||||||
|
<app-header [current_page]="'claiming'" height="460px" background_image="/public/img/backgrounds/trees.jpg"
|
||||||
|
[overlay_gradient]="0.5">
|
||||||
|
<div class="title" header-content>
|
||||||
|
<h1>Claiming</h1>
|
||||||
|
<h2>Claiming allows you to protect your items and builds from other players.</h2>
|
||||||
|
</div>
|
||||||
|
</app-header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Getting Started</h2>
|
||||||
|
<p>You can “claim” land on Altitude to protect your builds. When you claim a section of land, other players
|
||||||
|
will be unable to place or break blocks, kill animals, take crops, or open chests. Everything will be
|
||||||
|
completely protected!</p>
|
||||||
|
<p>To claim land, you will need some basic tools: a golden shovel, and a stick. You can craft these items
|
||||||
|
yourself or do <b>/claim</b> to receive them for free. The shovel is used for modifying claims and the
|
||||||
|
stick is used for viewing claim information.</p>
|
||||||
|
<img ngSrc="/public/img/items/shovel.png" alt="Minecraft golden shovel" style="width: 25%;" height="114"
|
||||||
|
width="114">
|
||||||
|
<p>Players start with an allowance of 500 “claim blocks” and can purchase additional “claim blocks” with
|
||||||
|
in-game currency using <span style="font-family: 'opensans-bold', sans-serif;">/buyclaimblocks</span>. How
|
||||||
|
many claims you can create depends on the rank you have - the amount can be found on the <a
|
||||||
|
[routerLink]="['/ranks']">ranks</a> page. Additionally, there is also a limit on how many blocks in
|
||||||
|
total you can
|
||||||
|
claim.</p>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Creating a Claim</h2>
|
||||||
|
<p>Before creating any claim, we should check if there are other claims by other players nearby. Claims by
|
||||||
|
different people should be at least 100 blocks apart unless both parties discuss otherwise. To do this,
|
||||||
|
hold a stick in your hand and <b>shift + right click</b> to visualize all nearby claims. Claims are
|
||||||
|
visualized using dotted lines of gold blocks, so look around for any dotted lines on the ground around
|
||||||
|
you.</p>
|
||||||
|
<p>Once you've confirmed that there are no claims too close, we can begin claiming! A claim doesn't go down
|
||||||
|
all the way. The lowest point of the claim starts at 5 blocks below the lower corner you made. It'll keep
|
||||||
|
expanding down as you place blocks lower and lower in the claim, but it may reset when you change the
|
||||||
|
claim size, trust, subclaims etc. If you want to make sure your claim stays extended all the way down,
|
||||||
|
make one of the initial corners at bedrock level. Remember to not claim further down than you need to.</p>
|
||||||
|
<p>If a claim is within 100 blocks you will be unable to create your claim. If the original claim owner is
|
||||||
|
fine with you having a claim closer than the 100 blocks, then they can trust you using <span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/claimnearbytrust</span>. You will need to be trusted
|
||||||
|
in order to claim and resize. The trust expires on every server reboot, this is to remind you that there
|
||||||
|
are nearby claims.</p>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Automatic Claim Deletion</h2>
|
||||||
|
<p>When a player has been inactive for 60 consecutive days, on a server (not cross-server), their claim(s)
|
||||||
|
on that server will be transferred into an admin claim(s). The claim will be in that state for 24 hours
|
||||||
|
after which is will be fully unclaimed. During the time the claim is an admin claim everyone will be
|
||||||
|
trusted. This means that you will be able to raid the claim, but not claim it. If you wish to claim the
|
||||||
|
build you will have to return 24 hours later.</p>
|
||||||
|
<p>If you are planning on being gone this long, all you need to do is join for a couple of minutes within
|
||||||
|
those 60 days to reset the timer. Unclaimed land is fair game to loot, so make sure your claims are active
|
||||||
|
and properly sized!</p>
|
||||||
|
<p>Any containers (i.e shulker, chest) that are placed within an active raid-claim are fair game - this also
|
||||||
|
includes containers that raiders place. This means that items a raider puts into their personal shulker
|
||||||
|
aren't safe while placed down or when on the ground as an item after being broken, as anyone that comes by
|
||||||
|
can take it.</p>
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Useful Commands</h2>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/buyclaimblocks -</span> Buy more claim
|
||||||
|
blocks
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/sellclaimblocks -</span> Sell claim blocks
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/unclaim -</span> Remove a protection</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/claimslist -</span> List all of your
|
||||||
|
protections
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/expandclaim <number> -</span> Expand
|
||||||
|
the claim the direction you're facing (can also be used with negative numbers to downsize the claim)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/containertrust <player> -</span> Give
|
||||||
|
someone access to chests in your claim
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/permissiontrust <player> -</span> Give
|
||||||
|
someone the ability to trust and untrust others in your claim
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/accesstrust <player> -</span> Give
|
||||||
|
someone access to doors and trapdoors in your claim
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/subdivide -</span> Create claims within your
|
||||||
|
main claim that you can trust people to
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/restrictsubclaim -</span> Sets subclaim NOT
|
||||||
|
to inherit perms from the parent claim
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/trust <player> -</span> Allow someone
|
||||||
|
to build and use chests within your claim
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/trustlist -</span> See who is trusted in your
|
||||||
|
claim
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/untrust <player> -</span> Remove a
|
||||||
|
player from your protection
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/kickfromclaim <player> -</span> Kick a
|
||||||
|
player outside of your claim
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petkick -</span> Enter pet kicking mode
|
||||||
|
(right click pets you don't want in your claim)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Resizing Your Claim</h2>
|
||||||
|
<p>You can make your claim bigger or smaller any time by using a golden shovel. The process is similar to
|
||||||
|
creating the claim. To make things easier, start by right clicking within the claim with a stick. This
|
||||||
|
will show the border of the claim so you can see where the corners are.</p>
|
||||||
|
<p>To resize a claim, right click a corner you would like to move. Then, right click again wherever you
|
||||||
|
would like to move that corner to. To make things easier, you don't need to right click the actual block,
|
||||||
|
you can just point your crosshair at the block and right click from a distance.</p>
|
||||||
|
<p>If you don't have the needed amount of claim blocks to expand (or create) your claim, you will need to
|
||||||
|
purchase additional claim blocks. The price for purchasing claim blocks is different depending on how many
|
||||||
|
you have:</p>
|
||||||
|
<ul>
|
||||||
|
<li>0-10,000 blocks = $0.50/block</li>
|
||||||
|
<li>10,000-50,000 blocks = $0.75/block</li>
|
||||||
|
<li>50,000-300,000 blocks = $1.00/block</li>
|
||||||
|
<li>300,000-1,000,000 blocks = $2.00/block</li>
|
||||||
|
<li>1,000,000+ blocks = $5.00/block</li>
|
||||||
|
</ul>
|
||||||
|
<p>If you have left over claim blocks you can sell them using <span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/sellclaimblocks</span>. The return will always be
|
||||||
|
$0.50/block. This means that you need to be mindful of purchasing large amounts of claim blocks; don't
|
||||||
|
purchase them unless you actually need them.</p>
|
||||||
|
<img ngSrc="/public/img/backgrounds/claim.png" alt="Screenshot showing claim border in-game"
|
||||||
|
style="width: 100%;"
|
||||||
|
width="480" height="250">
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Subdividing Claims</h2>
|
||||||
|
<p>Subdividing claims allows you to give players access to specific parts of your claim. This is the best
|
||||||
|
way to manage a town or shop!</p>
|
||||||
|
<p>The idea behind subdivisions is easy. You are creating mini-claims within your main claim so that you can
|
||||||
|
trust people to certain areas, but not the entire claim. You are still in control of the border of these
|
||||||
|
mini-claims (size and location). Once a subdivision is created you can trust people the same way as
|
||||||
|
before. Just stand in the mini-claim and use the trust commands. It will only trust them to the
|
||||||
|
mini-claim. (To trust someone to your main claim you will need to stand outside of any mini-claims you've
|
||||||
|
created.)</p>
|
||||||
|
<p>To create a subdivision you need to go into "subdivision mode". You can access this mode with <b>/subdivideclaims</b>.
|
||||||
|
Once you are in this mode you can create the subdivisions the same way you would make a regular claim,
|
||||||
|
just right click two corners. Subdivisions will be outlined with a white dotted line. To leave subdivision
|
||||||
|
mode use <b>/basicclaims</b>.</p>
|
||||||
|
<img ngSrc="/public/img/backgrounds/townsubdivisions.png" alt="Screenshot of town subdivided"
|
||||||
|
style="width: 100%;"
|
||||||
|
width="480" height="250">
|
||||||
|
</div>
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Trusting Others</h2>
|
||||||
|
<p>You can add other players to your protected area with various levels of access. To trust everyone on the
|
||||||
|
server replace <player> with "all". This works with all of the commands below.</p>
|
||||||
|
<ul>
|
||||||
|
<li>To give someone full access to your claim (break, build, access chests, etc) do: <b>/trust <player></b>
|
||||||
|
</li>
|
||||||
|
<li>To give someone just the ability to access storage items, but NOT build, do: <b>/containertrust <player></b>
|
||||||
|
</li>
|
||||||
|
<li>To give someone visitor access (opening the doors, but not take anything or build anything) do: <b>/accesstrust
|
||||||
|
<player></b></li>
|
||||||
|
<li>To give someone permission to manage your claim (trusting other players) do: <b>/permissiontrust <player></b>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
main ul {
|
||||||
|
font-family: opensans, sans-serif;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
main li {
|
||||||
|
margin-left: 30px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ClaimingComponent } from './claiming.component';
|
||||||
|
|
||||||
|
describe('ClaimingComponent', () => {
|
||||||
|
let component: ClaimingComponent;
|
||||||
|
let fixture: ComponentFixture<ClaimingComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ClaimingComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ClaimingComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {HeaderComponent} from '../header/header.component';
|
||||||
|
import {NgOptimizedImage} from '@angular/common';
|
||||||
|
import {RouterLink} from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-claiming',
|
||||||
|
imports: [
|
||||||
|
HeaderComponent,
|
||||||
|
NgOptimizedImage,
|
||||||
|
RouterLink
|
||||||
|
],
|
||||||
|
templateUrl: './claiming.component.html',
|
||||||
|
styleUrl: './claiming.component.scss'
|
||||||
|
})
|
||||||
|
export class ClaimingComponent {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,496 @@
|
|||||||
|
<ng-container>
|
||||||
|
<app-header [current_page]="'commandlist'" height="460px" background_image="/public/img/backgrounds/trees.jpg"
|
||||||
|
[overlay_gradient]="0.5">
|
||||||
|
<div class="title" header-content>
|
||||||
|
<h1>Command list</h1>
|
||||||
|
<h2>Looking for a specific command, or the description of one? This is where you find it.</h2>
|
||||||
|
</div>
|
||||||
|
</app-header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<p style="font-family: 'opensans-bold', sans-serif; padding-bottom: 5px !important;">Claiming</p>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/claim -</span> Gives you the claim kit</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/buyclaimblocks <amount> -</span> Allows
|
||||||
|
you to buy claimblocks (0.5 in game per block)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/sellclaimblocks <amount> -</span>
|
||||||
|
Allows you to sell claimblocks (0.5 in game per block)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/abandonallclaims -</span> Deletes all your
|
||||||
|
claims
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/abandonclaim -</span> Deletes the
|
||||||
|
claim/subdivision you're currently in
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/delclaim -</span> Deletes the
|
||||||
|
claim/subdivision you're currently in
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/unclaim -</span> Deletes the
|
||||||
|
claim/subdivision you're currently in
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/claimlist -</span> Shows you all your claims
|
||||||
|
and claimblocks
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/accesstrust <username/public> -</span>
|
||||||
|
Gives accesstrust to a user/everyone
|
||||||
|
</li>
|
||||||
|
<li><span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/containertrust <username/public> -</span> Gives
|
||||||
|
containertrust to a user/everyone
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/expandclaim <amount> -</span> Expands
|
||||||
|
claim by the specified amount in the direction you're facing
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/restrictsubclaim -</span> Seperates
|
||||||
|
permissions for the subclaim you are standing in from the main claim
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/subdivide -</span> Turns on subdivide mode
|
||||||
|
(hold golden shovel) to allow you to create subdivisions
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/trustlist -</span> Shows users trusted in the
|
||||||
|
claim/subclaim you're standing it
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/claimnearbytrust -</span> Allows other
|
||||||
|
players to claim within 100 blocks to your claim
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">Creative</p>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot help -</span> Display all creative
|
||||||
|
commands
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot info -</span> Display plot info</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot confirm -</span> Confirm an action</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot claim -</span> Claim the current plot
|
||||||
|
you're standing on
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot auto -</span> Claim a random available
|
||||||
|
plot nearby
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot delete -</span> Delete your plot</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot trust -</span> Allow a player to build
|
||||||
|
in your plot
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot add -</span> Allow a user to build while
|
||||||
|
you are online
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot setowner -</span> Set the plot owner
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot setbiome -</span> Set the plot biome
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot visit -</span> Visit someone (or your
|
||||||
|
own) plot
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot kick -</span> Kick a player from your
|
||||||
|
plot
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot middle -</span> Teleports you to the
|
||||||
|
center of the current plot
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot untrust -</span> Remove a player from
|
||||||
|
your plot
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot merge [auto] -</span> Merge a plot (or
|
||||||
|
multiple)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot unmerge -</span> Unmerge plots</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot sethome -</span> Set the plot home</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/plot clear -</span> Clear a plot</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/nightvision -</span> Enables nightvision</li>
|
||||||
|
</ul>
|
||||||
|
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">Dynamic Map</p>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/dynmap -</span> Sends you a link to the
|
||||||
|
Altitude map page
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/dynmap hide -</span> Hides you on the map
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/dynmap show -</span> Shows you on the map
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">McMMO</p>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/mctop <ability> -</span> Shows the top
|
||||||
|
10 players for Power Level or the specified ability
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/mcrank -</span> Shows where you are on the
|
||||||
|
mctop per skill
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/mcstats -</span> Shows your mcmmo levels in a
|
||||||
|
scoreboard
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/mcability -</span> Toggles ability use on or
|
||||||
|
off
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/acrobatics [?] [page] -</span> Shows your
|
||||||
|
stats for Acrobatics. If you add ? it will show the guide for Acrobatics
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/archery [?] [page] -</span> Shows your stats
|
||||||
|
for Archery. If you add ? it will show the guide for Archery
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/axes [?] [page] -</span> Shows your stats for
|
||||||
|
Axes. If you add ? it will show the guide for Axes
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/excavation [?] [page] -</span> Shows your
|
||||||
|
stats for Excavation. If you add ? it will show the guide for Excavation
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/fishing [?] [page] -</span> Shows your stats
|
||||||
|
for Fishing. If you add ? it will show the guide for Fishing
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/herbalism [?] [page] -</span> Shows your
|
||||||
|
stats for Herbalism. If you add ? it will show the guide for Herbalism
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/mining [?] [page] -</span> Shows your stats
|
||||||
|
for Mining. If you add ? it will show the guide for Mining
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/repair [?] [page] -</span> Shows your stats
|
||||||
|
for Repair. If you add ? it will show the guide for Repair
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/swords [?] [page] -</span> Shows your stats
|
||||||
|
for Swords. If you add ? it will show the guide for Swords
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/taming [?] [page] -</span> Shows your stats
|
||||||
|
for Taming. If you add ? it will show the guide for Taming
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/unarmed [?] [page] -</span> Shows your stats
|
||||||
|
for Unarmed. If you add ? it will show the guide for Unarmed
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/woodcutting [?] [page] -</span> Shows your
|
||||||
|
stats for Woodcutting. If you add ? it will show the guide for Woodcutting
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/inspect <username> -</span> Shows mcmmo
|
||||||
|
levels for specified user in a scoreboard if they are near you
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">MyPet</p>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petinfo -</span> Shows information about your
|
||||||
|
MyPet
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petname <name> -</span> Gives your
|
||||||
|
current MyPet the specified name
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petrelease <name> -</span> Release the
|
||||||
|
specified MyPet (this will despawn it)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petcall (/petc) -</span> Teleports your
|
||||||
|
current MyPet to you
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/psa -</span> Sends your MyPet away, it can
|
||||||
|
still be called using /petc (does not store your pet)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petswitch (/psw) -</span> Switches between
|
||||||
|
current and stored MyPet
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petstore (/pst) -</span> Stores your MyPet
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/pettrade <username> [price] -</span>
|
||||||
|
Trades current MyPet with a player
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petshop -</span> Opens the MyPet shop</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petskill -</span> Shows information about the
|
||||||
|
skills of your MyPet
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petstop -</span> Stops your MyPet from
|
||||||
|
attacking a target if it's not in farm or aggressive behavior modes
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petchooseskilltree (/pcst) -</span> Allows
|
||||||
|
you to select a skilltree for your MyPet
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petcapturehelper (/pch) -</span>
|
||||||
|
Enables/disables the CaptureHelper for MyPet
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petoptions healthbar -</span> Toggles
|
||||||
|
actionbar healthbar on/off
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petoptions idle-volume <%> -</span>
|
||||||
|
Sets volume percentage for the MyPet
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petinventory (/peti) -</span> Opens the
|
||||||
|
inventory for your current MyPet if it has one
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petpickup (/pp) -</span> Toggles pet pickup
|
||||||
|
on/off if your pet has an inventory
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petbehavior [mode] (/pb) -</span> Switches
|
||||||
|
between pet behaviors or to the specified one
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/petbeacon -</span> Opens the beacon editor
|
||||||
|
for your MyPet if it has one
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<p style="font-family: 'opensans-bold', sans-serif; padding-bottom: 5px !important;">Economy</p>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/bal -</span> Shows you your balance</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/cheque <amount> -</span> Creates a
|
||||||
|
cheque with the specified amount of money if you're holding a piece of paper
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/pay <username> <amount> [-confirmed] -</span>
|
||||||
|
Pays the specified amount to the specified user
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/shop -</span> Opens the shop GUI</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/points -</span> Displays your current amount
|
||||||
|
of sell/buy points
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/sell <item> -</span> Tells you if the
|
||||||
|
entered item is sellable at spawn, and at what price
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/buy <item> -</span> Tells you if the
|
||||||
|
entered item is buyable at spawn, and at what price
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/playershop checkstock <radius> [max. stock] -</span>
|
||||||
|
highlights all of your shops which do not meet the minimum amount specified
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">Party Chat</p>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/party help -</span> Display the help menu
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/party create <partyname> <password> -</span>
|
||||||
|
Creates a party
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/party info -</span> Displays information
|
||||||
|
about your current party
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/party invite <username> -</span> Invite
|
||||||
|
a user to your party
|
||||||
|
</li>
|
||||||
|
<li><span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/party join <partyname> [password] -</span> Join
|
||||||
|
a users party (password is required only if the party has one)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/p <message> -</span> Changes chat to
|
||||||
|
party chat. If a message is specified it is sent to party chat
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/party leave -</span> Leave a party</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/party name <newname> -</span> Leave a
|
||||||
|
party
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/party owner <username> -</span> Makes
|
||||||
|
the specified user owner of the party
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/party password <password> -</span> Sets
|
||||||
|
or changes a password for the party
|
||||||
|
</li>
|
||||||
|
<li><span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/party remove (/party kick) <username> -</span>
|
||||||
|
Kicks the specified user from the party
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/party disband -</span> Disband your party
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">Voting</p>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/vote -</span> Sends a link to our vote page
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/votebest -</span> Shows highest total votes
|
||||||
|
you reached per day/week/month
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/votelast -</span> Shows when you last voted
|
||||||
|
on each of the websites
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/votenext -</span> Shows when you can vote
|
||||||
|
again per website
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/votestreak -</span> Shows your current and
|
||||||
|
best vote streak
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/votetop [page] -</span> Shows the vote top
|
||||||
|
page
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/votetotal -</span> Shows you your total votes
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">Warps</p>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/warps [warp] -</span> Opens the warp GUI or
|
||||||
|
teleports you to the specified warp
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/warp apply -</span> Starts application
|
||||||
|
process for a new warp (costs 25k)
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">Altitude</p>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/queue -</span> Shows what server you are
|
||||||
|
queued for
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/find <username> -</span> Shows if and
|
||||||
|
where this user is online
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/back -</span> Teleports you back to where you
|
||||||
|
last teleported from
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/dback -</span> Brings you back to the last
|
||||||
|
location you died at
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/colors -</span> Shows all color and format
|
||||||
|
codes
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/home [name] -</span> Shows your homes (click
|
||||||
|
to teleport), if a home is specified it will teleport you there
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/list -</span> Lists all online players on the
|
||||||
|
server
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/stafflist -</span> Lists all online staff
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/seen <username> -</span> Shows when a
|
||||||
|
player is/was online
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/playtime -</span> Shows your playtime</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/sit -</span> Allows you to sit down</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/cooldown -</span> Shows the cooldown for the
|
||||||
|
rtp portal
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/hat -</span> Puts the current item on your
|
||||||
|
head
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/discord -</span> Sends you a link to the
|
||||||
|
Altitude Discord server
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/ranks -</span> Shows the ranks on the
|
||||||
|
Altitude server
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/contact -</span> Sends a link to our Contact
|
||||||
|
page
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/report -</span> Sends a link to the report
|
||||||
|
form
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/rules -</span> Sends a link to the rules page
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/store -</span> Sends a link to the Altitude
|
||||||
|
store
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/guide -</span> Sends a link to the guide page
|
||||||
|
for getting started
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/info -</span> Sends some links to our website
|
||||||
|
and Discord
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/se <signline> <text> -</span>
|
||||||
|
While looking at a sign changes text on specified line to specified text.
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/editsign -</span> Toggles shift click sign
|
||||||
|
editing
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/kickfromclaim -</span> Kicks specified user
|
||||||
|
from claim
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/kickpet -</span> Toggles kickpet mode, kicks
|
||||||
|
vanilla pets from claims after right clicking it
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/warnings -</span> Shows all active warnings
|
||||||
|
for yourself
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/realname <username/nickname> -</span>
|
||||||
|
Shows the specified users username and nickname
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/mapart remove -</span> Deletes map from the
|
||||||
|
database if you own it and are holding it
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/mapart save -</span> Saves map on the
|
||||||
|
database if you're holding it
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/sneakclickmending -</span> Allows you to mend
|
||||||
|
your items by right clicking while sneaking
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/pu rotateblock -</span> Rotate already
|
||||||
|
placed, direction-specific, blocks
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/pu xpcalc <current xp> <goal xp> -</span>
|
||||||
|
Calculates the amount of XP needed to get from level X to level Y
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/pu xpcheque <xp> -</span> Creates a
|
||||||
|
'cheque' in the form of an XP bottle with the specified amount of XP
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/pt top [daily/weekly/monthly/total]-</span>
|
||||||
|
Displays the players with the highest amount of playtime for the specified time
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/finditem <item> -</span> Enables you to
|
||||||
|
search your chest monster for a specific item
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/local -</span> Toggles local chat, where
|
||||||
|
messages will only be sent to players within a 200 block distance
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/blockitempickup -</span> Prevents picking up
|
||||||
|
items
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p style="font-family: 'opensans-bold', sans-serif; padding: 25px 0 5px;">Miscellaneous</p>
|
||||||
|
<ul>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/apart -</span> Opens particles GUI</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/cmi armorstand [last/near] -</span> Opens
|
||||||
|
Armorstand editor
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/ping -</span> Shows your ping</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/time -</span> Shows in-game time</li>
|
||||||
|
<li><span
|
||||||
|
style="font-family: 'opensans-bold', sans-serif;">/mail send <username> <message> -</span>
|
||||||
|
Sends a mail to the specified user
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/ignore <username> -</span> Ignores a
|
||||||
|
users messages to you and hides their public chat messages from you
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/r <message> -</span> Replies to the
|
||||||
|
last user to send you a message
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/c <message> -</span> Replies to the
|
||||||
|
last user you sent a message to
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/msg <username> <message> -</span>
|
||||||
|
Sends a message to the specified user
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/pvp [on/off] -</span> Toggles your pvp on/off
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/sethome <homename> -</span> Sets a home
|
||||||
|
with the specified name
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/delhome <homename> -</span> Shows your
|
||||||
|
homes (click to remove), or removes the specified home
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/spawn -</span> Teleports you to spawn</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/tpa <username> -</span> Sends a
|
||||||
|
teleport request to the specified user
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/tpaaccept [username] -</span> Accepts last
|
||||||
|
teleport request, or the teleport request of the specified user
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/tpahere <username> -</span> Sends a
|
||||||
|
teleport here request to the specified user
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/tpbypass -</span> Used to teleport to a
|
||||||
|
location the plugin marks as unsafe (such as void or lava)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/tpdeny [username] -</span> Denies last
|
||||||
|
teleport request, or the teleport request of the specified user
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/givepet <username> -</span> Gives
|
||||||
|
vanilla pet to specified user after you right click it
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/trapped -</span> Teleports you out of the
|
||||||
|
claim you are in if you do not have build permission there
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/colorsextra -</span> Displays useable RGB
|
||||||
|
colors (shift+lmb to paste in chat)
|
||||||
|
</li>
|
||||||
|
<li><span style="font-family: 'opensans-bold', sans-serif;">/dispose (/trash) -</span> Opens a GUI where
|
||||||
|
you can dispose of items
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
main ul {
|
||||||
|
font-family: 'opensans', sans-serif;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
main li {
|
||||||
|
margin-left: 30px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
color: var(--font-color);
|
||||||
|
transition: 0.5s ease;
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CommandlistComponent } from './commandlist.component';
|
||||||
|
|
||||||
|
describe('CommandlistComponent', () => {
|
||||||
|
let component: CommandlistComponent;
|
||||||
|
let fixture: ComponentFixture<CommandlistComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [CommandlistComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(CommandlistComponent);
|
||||||
|
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-commandlist',
|
||||||
|
imports: [
|
||||||
|
HeaderComponent
|
||||||
|
],
|
||||||
|
templateUrl: './commandlist.component.html',
|
||||||
|
styleUrl: './commandlist.component.scss'
|
||||||
|
})
|
||||||
|
export class CommandlistComponent {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export const ALTITUDE_VERSION = "1.21"
|
export const ALTITUDE_VERSION = "1.21"
|
||||||
|
|
||||||
export const enum THEME_MODE {
|
export const enum THEME_MODE {
|
||||||
LIGHT= 'theme-light',
|
LIGHT = 'theme-light',
|
||||||
DARK = 'theme-dark'
|
DARK = 'theme-dark'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<button type="button" class="button-outer" (click)="copyIpToClipboard()">
|
||||||
|
<span class="button-inner">{{ displayText }}</span>
|
||||||
|
</button>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CopyIpComponent } from './copy-ip.component';
|
||||||
|
|
||||||
|
describe('CopyIpComponent', () => {
|
||||||
|
let component: CopyIpComponent;
|
||||||
|
let fixture: ComponentFixture<CopyIpComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [CopyIpComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(CopyIpComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {CommonModule} from '@angular/common';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-copy-ip',
|
||||||
|
templateUrl: './copy-ip.component.html',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule
|
||||||
|
],
|
||||||
|
styleUrl: './copy-ip.component.scss'
|
||||||
|
})
|
||||||
|
export class CopyIpComponent {
|
||||||
|
public displayText: string = 'Copy IP'
|
||||||
|
|
||||||
|
public copyIpToClipboard(): void {
|
||||||
|
navigator.clipboard.writeText('play.alttd.com');
|
||||||
|
this.displayText = 'Copied!';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
<ng-container>
|
||||||
|
<app-header [current_page]="'skyblock'" height="460px" background_image="/public/img/backgrounds/path.jpg"
|
||||||
|
[overlay_gradient]="0.5">
|
||||||
|
<div class="title" header-content>
|
||||||
|
<h1>Custom Features</h1>
|
||||||
|
<h2>A list of custom features created specifically to enhance the survival experience on Altitude.</h2>
|
||||||
|
<h3
|
||||||
|
style="font-family: 'minecraft-text', sans-serif; font-size: 0.8rem; margin-top: 10px; color: #FFFFFF !important;">
|
||||||
|
Commands for the features are listed <a [routerLink]="['/commandlist']">here</a></h3>
|
||||||
|
</div>
|
||||||
|
</app-header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Blocks</h2>
|
||||||
|
<p style="padding-bottom: 0 !important;">Beacons can be activated by redstone</p>
|
||||||
|
<ul class="full-page-list">
|
||||||
|
<li>Powering the block above a beacon disables the beam</li>
|
||||||
|
</ul>
|
||||||
|
<p>Glow lichen can be silk touched</p>
|
||||||
|
<p>Vines can be silk touched and bonemealed</p>
|
||||||
|
<p>Coral and coral fans can be bonemealed to grow a new coral tree</p>
|
||||||
|
<p>Mushroom blocks can be stripped with an axe</p>
|
||||||
|
<p>Candles can be placed without a block beneath them</p>
|
||||||
|
<p>Crying obsidian can be used to create nether portals</p>
|
||||||
|
<p style="padding-bottom: 0 !important;">Gravel and sand are craftable in stonecutters</p>
|
||||||
|
<ul class="full-page-list">
|
||||||
|
<li>Cobblestone -> Gravel</li>
|
||||||
|
<li>Gravel -> Sand</li>
|
||||||
|
</ul>
|
||||||
|
<p>Sponges can remove lava - wet sponges remove more (Note: the sponge will be consumed)</p>
|
||||||
|
<p>Item frames can be made invisible using a diamond</p>
|
||||||
|
<p>Rotation of item frames can be locked using a slime ball</p>
|
||||||
|
<p>Copper can be oxidized using a cauldron of water</p>
|
||||||
|
<p>Pink petals can be placed on any surface</p>
|
||||||
|
<p>Wither roses can be bonemealed</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<section class="darkmodeSectionThree">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Containers</h2>
|
||||||
|
<p>Dispensers can interact with campfires</p>
|
||||||
|
<p>Dispensers can interact with cauldrons</p>
|
||||||
|
<p>Dispensers can place pumpkins</p>
|
||||||
|
<p>Dispensers can carve pumpkins</p>
|
||||||
|
<p>Barrels have 54 inventory slots</p>
|
||||||
|
<p>Furnaces have a built in item filter, this moves any item they can't process to the output</p>
|
||||||
|
<p>Naming a container or villager "public" makes them publically available</p>
|
||||||
|
<p>Natual loot chests (in dungeons, etc.) are not protected by claims (but can only be broken by the claim
|
||||||
|
owner)</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Compostable items</h2>
|
||||||
|
<ul class="full-page-list">
|
||||||
|
<li>leather: 0.10</li>
|
||||||
|
<li>wooden_shovel: 0.10</li>
|
||||||
|
<li>rotten_flesh: 0.15</li>
|
||||||
|
<li>salmon: 0.15</li>
|
||||||
|
<li>cod: 0.15</li>
|
||||||
|
<li>porkchop: 0.15</li>
|
||||||
|
<li>beef: 0.15</li>
|
||||||
|
<li>chicken: 0.15</li>
|
||||||
|
<li>rabbit: 0.15</li>
|
||||||
|
<li>mutton: 0.15</li>
|
||||||
|
<li>bamboo: 0.15</li>
|
||||||
|
<li>wooden_sword: 0.20</li>
|
||||||
|
<li>wooden_hoe: 0.20</li>
|
||||||
|
<li>wooden_pickaxe: 0.30</li>
|
||||||
|
<li>wooden_axe: 0.30</li>
|
||||||
|
<li>poisonous_potato: 0.35</li>
|
||||||
|
<li>leather_boots: 0.40</li>
|
||||||
|
<li>flowering_azalea_leaves: 0.50</li>
|
||||||
|
<li>leather_helmet: 0.50</li>
|
||||||
|
<li>leather_leggings: 0.70</li>
|
||||||
|
<li>leather_chestplate: 0.80</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<section class="darkmodeSectionThree">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Ender Dragon</h2>
|
||||||
|
<p>First kill of the month awards a dragon egg</p>
|
||||||
|
<p style="padding-bottom: 0 !important;">Can drop items when killed</p>
|
||||||
|
<ul class="full-page-list">
|
||||||
|
<li>elytra: 30%</li>
|
||||||
|
<li>dragon_head: 30%</li>
|
||||||
|
<li>dragon_breath: 40%</li>
|
||||||
|
<li>and seperately from the rest, a 0.1% chance for a dragon_egg</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Mobs</h2>
|
||||||
|
<p>Bats can spawn above sea level (bat spawning only enabled during halloween)</p>
|
||||||
|
<p style="padding-bottom: 0 !important;">Mobs can respawn in structures</p>
|
||||||
|
<ul class="full-page-list">
|
||||||
|
<li>Shulker -> End City</li>
|
||||||
|
<li>Piglin Brute, Piglin -> Bastion</li>
|
||||||
|
<li>Evoker, Vindicator, Allay -> Woodland Mansion</li>
|
||||||
|
<li>Allay -> Pillager Outpost</li>
|
||||||
|
<li>Camel -> Desert Village</li>
|
||||||
|
</ul>
|
||||||
|
<p>Naming a mob "muted", "silenced", "silent" or "silence me" silences a mob</p>
|
||||||
|
<p>Naming a mob "muted", "silenced", "silent", "silence me", "protected" or "protect me" prevents it from
|
||||||
|
being damaged by non-trusted players in a claim</p>
|
||||||
|
<p>Naming a mob "baby" prevents it from growing up</p>
|
||||||
|
<p>Wither health bar is removed when silenced</p>
|
||||||
|
<p>Turtles can hatch during daytime</p>
|
||||||
|
<p>Unclaim vanilla player pets (dogs, cats, parrots, horses) if the owner has been offline for 60+ days by
|
||||||
|
right clicking with a lead</p>
|
||||||
|
<p>Fisherman villagers can sell Bucket of Salmon</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<section class="darkmodeSectionThree">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Restrictions & disabled vanilla features</h2>
|
||||||
|
<p>Restricted iron golem spawning (10 golems spawning in a radius of 112 blocks in a time span of 1
|
||||||
|
minute)</p>
|
||||||
|
<p>Disabled stacking raids (having multiple raids going on at once)</p>
|
||||||
|
<p>Disabled TNT duping</p>
|
||||||
|
<p>Disabled bat spawning</p>
|
||||||
|
<p>Disabled patrol spawning</p>
|
||||||
|
<p>Zombies don't attack villagers</p>
|
||||||
|
<p>Disabled processing of most things around AFK players</p>
|
||||||
|
<p>Disabled AI for villagers in a 1x1 area</p>
|
||||||
|
<p>Shulkers shooting each other does not spawn a new shulker since they respawn naturally in End Cities</p>
|
||||||
|
<p>Pigmen cannot spawn from portals</p>
|
||||||
|
<p>Allay duplication is disabled since they respawn natually in Mansions and Outposts</p>
|
||||||
|
<p>Players cannot claim land within 100 blocks of another claim without the owner's permission</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<h2>Miscellaneous</h2>
|
||||||
|
<p>Grindstones can strip shulker box & beehive NBT data</p>
|
||||||
|
<p>Sneak click mending</p>
|
||||||
|
<p>/iwanttobreakthisblock, required to break some natural generated blocks</p>
|
||||||
|
<p>/sneakclickmending, allows you to mend your items by right clicking while sneaking</p>
|
||||||
|
<p>Swift Sneak enchantment can be found in villager trades</p>
|
||||||
|
<p>Nether portals can be as small as 1x2 (1x2 portal, 3x4 base)</p>
|
||||||
|
<p>Lootchests refill once for every player</p>
|
||||||
|
<p>Empty maps can be duplicated using paper in a cartography table</p>
|
||||||
|
<p>Eating glowberries gives you a glowing effect</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
main ul {
|
||||||
|
font-family: opensans, sans-serif;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
main li {
|
||||||
|
margin-left: 30px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user