Compare commits
113
Commits
bans
...
eab1c9322b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eab1c9322b | ||
|
|
ffddffa8dc | ||
|
|
0b4c1ccebf | ||
|
|
2e89fcec66 | ||
|
|
42b11eecf1 | ||
|
|
d1da1296bb | ||
|
|
523bf3d43f | ||
|
|
4ccce7e190 | ||
|
|
641083732d | ||
|
|
da17cf9696 | ||
|
|
de1876c90c | ||
|
|
c3f3b20807 | ||
|
|
37fb49cda1 | ||
|
|
db642103ed | ||
|
|
f026f24263 | ||
|
|
eaee31ab2b | ||
|
|
24d7cfe913 | ||
|
|
770a2e0d14 | ||
|
|
101794d8f2 | ||
|
|
eb72ce14cc | ||
|
|
d1ba89acc8 | ||
|
|
d28b4a2b62 | ||
|
|
56f4ccf40e | ||
|
|
d73f057596 | ||
|
|
e825d83124 | ||
|
|
238c5d9644 | ||
|
|
4222df87a3 | ||
|
|
16cc57d774 | ||
|
|
c536bfbf30 | ||
|
|
f67cb50f41 | ||
|
|
bdb38e5011 | ||
|
|
ae1e972438 | ||
|
|
737b26a6c7 | ||
|
|
5013b9a204 | ||
|
|
fcb64db137 | ||
|
|
d2e064e2b4 | ||
|
|
f50f2dc6c2 | ||
|
|
c277306c2c | ||
|
|
1f03a4bdc3 | ||
|
|
7f1c59d102 | ||
|
|
f968a64dd4 | ||
|
|
c25364caf7 | ||
|
|
15c3cc7f26 | ||
|
|
2b96957876 | ||
|
|
b16fab26e7 | ||
|
|
28fd05a656 | ||
|
|
ff1b09be92 | ||
|
|
8a839ac922 | ||
|
|
3f76a98409 | ||
|
|
871615702b | ||
|
|
291c9df5c6 | ||
|
|
4150324d75 | ||
|
|
4267c782a7 | ||
|
|
343964eda8 | ||
|
|
1ce2088cae | ||
|
|
0b952e07f7 | ||
|
|
c2b9a8a574 | ||
|
|
d3ef296784 | ||
|
|
5a792463cc | ||
|
|
974d50d7cd | ||
|
|
62f837914c | ||
|
|
ace969ba3b | ||
|
|
2fc6ba53f6 | ||
|
|
4c38b070ea | ||
|
|
db394beda6 | ||
|
|
76cb3cd89c | ||
|
|
5d8ab2deef | ||
|
|
aef32a8982 | ||
|
|
42f0961f13 | ||
|
|
04310e1cce | ||
|
|
54e747118c | ||
|
|
cce83a08de | ||
|
|
f0faa63ca7 | ||
|
|
dfea91d8ca | ||
|
|
73916f0aae | ||
|
|
ebe66c87c0 | ||
|
|
c42fc38b2c | ||
|
|
213f9987d9 | ||
|
|
48cac607de | ||
|
|
6ed2e15017 | ||
|
|
7fc25f46f3 | ||
|
|
c72703ea32 | ||
|
|
e837a9216d | ||
|
|
d4363b3a8a | ||
|
|
1e5862bae6 | ||
|
|
daf88ea437 | ||
|
|
9abd570b87 | ||
|
|
5284d498f3 | ||
|
|
c3a7be82e9 | ||
|
|
fdb57289f8 | ||
|
|
60c1329163 | ||
|
|
0e71c0f581 | ||
|
|
eb67a33331 | ||
|
|
39f20796ce | ||
|
|
e00165c56f | ||
|
|
02c6497700 | ||
|
|
0efd476676 | ||
|
|
237518638c | ||
|
|
fea1a98cea | ||
|
|
ecd9b3d824 | ||
|
|
9808b5d63d | ||
|
|
c13b7077a7 | ||
|
|
023ae809ef | ||
|
|
3a6f137c9a | ||
|
|
cb8447a096 | ||
|
|
3e98e1a498 | ||
|
|
4c31a91bb4 | ||
|
|
d6faaba01c | ||
|
|
56175e62d6 | ||
|
|
283838f444 | ||
|
|
49fe335c73 | ||
|
|
1875f050c6 | ||
|
|
52d8658be3 |
@@ -27,6 +27,7 @@ dependencies {
|
|||||||
implementation(project(":open_api"))
|
implementation(project(":open_api"))
|
||||||
implementation(project(":database"))
|
implementation(project(":database"))
|
||||||
implementation(project(":frontend"))
|
implementation(project(":frontend"))
|
||||||
|
implementation(project(":discord"))
|
||||||
annotationProcessor("org.projectlombok:lombok")
|
annotationProcessor("org.projectlombok:lombok")
|
||||||
implementation("com.mysql:mysql-connector-j:8.0.32")
|
implementation("com.mysql:mysql-connector-j:8.0.32")
|
||||||
implementation("org.mybatis:mybatis:3.5.13")
|
implementation("org.mybatis:mybatis:3.5.13")
|
||||||
@@ -36,6 +37,8 @@ dependencies {
|
|||||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||||
implementation("org.springframework.security:spring-security-oauth2-resource-server")
|
implementation("org.springframework.security:spring-security-oauth2-resource-server")
|
||||||
implementation("org.springframework.security:spring-security-oauth2-jose")
|
implementation("org.springframework.security:spring-security-oauth2-jose")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-mail:3.1.5")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
|
||||||
|
|
||||||
//AOP
|
//AOP
|
||||||
implementation("org.aspectj:aspectjrt:1.9.19")
|
implementation("org.aspectj:aspectjrt:1.9.19")
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import org.springframework.boot.SpringApplication;
|
|||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication(scanBasePackages = {"com.alttd.altitudeweb"})
|
||||||
@EnableAspectJAutoProxy
|
@EnableAspectJAutoProxy
|
||||||
public class AltitudeWebApplication {
|
public class AltitudeWebApplication {
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.alttd.altitudeweb.config;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||||
|
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
public class SecurityAuthFailureHandler implements AccessDeniedHandler, AuthenticationEntryPoint {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(HttpServletRequest request, HttpServletResponse response,
|
||||||
|
AccessDeniedException accessDeniedException) throws IOException {
|
||||||
|
log.warn("Access denied: User '{}' attempted to access '{}' without proper permissions",
|
||||||
|
request.getUserPrincipal() != null ? request.getUserPrincipal().getName() : "unknown",
|
||||||
|
request.getRequestURI());
|
||||||
|
|
||||||
|
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access Denied");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void commence(HttpServletRequest request, HttpServletResponse response,
|
||||||
|
AuthenticationException authException) throws IOException {
|
||||||
|
log.warn("Authentication failure: Unauthenticated user attempted to access secured endpoint '{}'",
|
||||||
|
request.getRequestURI());
|
||||||
|
|
||||||
|
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Authentication Required");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,9 +11,11 @@ import com.nimbusds.jose.proc.SecurityContext;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.security.config.Customizer;
|
import org.springframework.security.config.Customizer;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||||
@@ -31,20 +33,40 @@ import java.security.interfaces.RSAPublicKey;
|
|||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
|
||||||
private final KeyPairService keyPairService;
|
private final KeyPairService keyPairService;
|
||||||
|
private final SecurityAuthFailureHandler securityAuthFailureHandler;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
return http
|
return http
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(
|
||||||
.requestMatchers("/login/userLogin/**", "/login/requestNewUserLogin/**").permitAll()
|
auth -> auth
|
||||||
.requestMatchers("/team/**", "/history/**").permitAll()
|
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
||||||
.requestMatchers("/form/**").hasAuthority(PermissionClaimDto.USER.getValue())
|
.requestMatchers("/api/form/**").authenticated()
|
||||||
.requestMatchers("/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
.requestMatchers("/api/login/getUsername").authenticated()
|
||||||
.anyRequest().authenticated()
|
.requestMatchers("/api/mail/**").authenticated()
|
||||||
)
|
.requestMatchers("/api/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||||
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
|
.requestMatchers("/api/particles/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.requestMatchers("/api/files/save/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||||
.build();
|
.requestMatchers("/api/login/userLogin/**").permitAll()
|
||||||
|
.anyRequest().permitAll()
|
||||||
|
)
|
||||||
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
|
.oauth2ResourceServer(
|
||||||
|
oauth2 -> oauth2
|
||||||
|
.jwt(Customizer.withDefaults())
|
||||||
|
.authenticationEntryPoint(securityAuthFailureHandler)
|
||||||
|
.accessDeniedHandler(securityAuthFailureHandler)
|
||||||
|
)
|
||||||
|
.exceptionHandling(
|
||||||
|
ex -> ex
|
||||||
|
.authenticationEntryPoint(securityAuthFailureHandler)
|
||||||
|
.accessDeniedHandler(securityAuthFailureHandler)
|
||||||
|
)
|
||||||
|
.sessionManagement(
|
||||||
|
session -> session
|
||||||
|
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||||
|
)
|
||||||
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
package com.alttd.altitudeweb.config;
|
package com.alttd.altitudeweb.config;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.core.io.ClassPathResource;
|
import org.springframework.core.io.ClassPathResource;
|
||||||
import org.springframework.core.io.Resource;
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.stereotype.Controller;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||||
import org.springframework.web.servlet.resource.PathResourceResolver;
|
import org.springframework.web.servlet.resource.PathResourceResolver;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
@Configuration
|
@Slf4j @Configuration
|
||||||
public class WebConfig implements WebMvcConfigurer {
|
public class WebConfig implements WebMvcConfigurer {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||||
registry.addResourceHandler("/**")
|
registry.addResourceHandler("/**")
|
||||||
.addResourceLocations("classpath:/static/")
|
.addResourceLocations("classpath:/static/browser")
|
||||||
.resourceChain(true)
|
.resourceChain(true)
|
||||||
.addResolver(new PathResourceResolver() {
|
.addResolver(new PathResourceResolver() {
|
||||||
@Override
|
@Override
|
||||||
@@ -23,11 +26,23 @@ public class WebConfig implements WebMvcConfigurer {
|
|||||||
Resource requestedResource = location.createRelative(resourcePath);
|
Resource requestedResource = location.createRelative(resourcePath);
|
||||||
|
|
||||||
if (requestedResource.exists() && requestedResource.isReadable()) {
|
if (requestedResource.exists() && requestedResource.isReadable()) {
|
||||||
|
log.debug("Serving resource {} from {}", resourcePath, location);
|
||||||
return requestedResource;
|
return requestedResource;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ClassPathResource("/static/index.html");
|
log.debug("Resource {} not found in {}, serving index.html", resourcePath, location);
|
||||||
|
|
||||||
|
return new ClassPathResource("/static/browser/index.html");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Controller
|
||||||
|
public static class HomeController {
|
||||||
|
@GetMapping("/")
|
||||||
|
public String index() {
|
||||||
|
return "forward:/index.html";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
-36
@@ -1,36 +0,0 @@
|
|||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
package com.alttd.altitudeweb.controllers.data_from_auth;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public class AuthenticatedUuid {
|
||||||
|
/**
|
||||||
|
* Extracts and validates the authenticated user's UUID from the JWT token.
|
||||||
|
*
|
||||||
|
* @return The UUID of the authenticated user
|
||||||
|
* @throws ResponseStatusException with 401 status if authentication is invalid
|
||||||
|
*/
|
||||||
|
public static UUID getAuthenticatedUserUuid() {
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
|
if (authentication == null || !(authentication.getPrincipal() instanceof Jwt jwt)) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Authentication required");
|
||||||
|
}
|
||||||
|
|
||||||
|
String stringUuid = jwt.getSubject();
|
||||||
|
|
||||||
|
try {
|
||||||
|
return UUID.fromString(stringUuid);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid UUID format");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package com.alttd.altitudeweb.controllers.forms;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.api.AppealsApi;
|
||||||
|
import com.alttd.altitudeweb.database.Databases;
|
||||||
|
import com.alttd.altitudeweb.database.litebans.HistoryRecord;
|
||||||
|
import com.alttd.altitudeweb.database.litebans.HistoryType;
|
||||||
|
import com.alttd.altitudeweb.database.litebans.IdHistoryMapper;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.forms.Appeal;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.forms.AppealMapper;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.mail.EmailVerification;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.mail.EmailVerificationMapper;
|
||||||
|
import com.alttd.altitudeweb.mappers.AppealDataMapper;
|
||||||
|
import com.alttd.altitudeweb.model.AppealResponseDto;
|
||||||
|
import com.alttd.altitudeweb.model.DiscordAppealDto;
|
||||||
|
import com.alttd.altitudeweb.model.MinecraftAppealDto;
|
||||||
|
import com.alttd.altitudeweb.model.UpdateMailDto;
|
||||||
|
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||||
|
import com.alttd.altitudeweb.services.mail.AppealMail;
|
||||||
|
import com.alttd.altitudeweb.setup.Connection;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpStatusCode;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@AllArgsConstructor
|
||||||
|
@RateLimit(limit = 30, timeValue = 1, timeUnit = TimeUnit.HOURS)
|
||||||
|
public class AppealController implements AppealsApi {
|
||||||
|
|
||||||
|
private final AppealDataMapper mapper;
|
||||||
|
private final AppealMail appealMail;
|
||||||
|
private final com.alttd.altitudeweb.services.discord.AppealDiscord appealDiscord;
|
||||||
|
|
||||||
|
@RateLimit(limit = 3, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "discordAppeal")
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<AppealResponseDto> 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) {
|
||||||
|
CompletableFuture<Appeal> appealCompletableFuture = new CompletableFuture<>();
|
||||||
|
|
||||||
|
Connection.getConnection(Databases.DEFAULT)
|
||||||
|
.runQuery(sqlSession -> {
|
||||||
|
log.debug("Loading history by id");
|
||||||
|
try {
|
||||||
|
Appeal appeal = mapper.minecraftAppealDtoToAppeal(minecraftAppealDto);
|
||||||
|
sqlSession.getMapper(AppealMapper.class).createAppeal(appeal);
|
||||||
|
appealCompletableFuture.complete(appeal);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to load history count", e);
|
||||||
|
appealCompletableFuture.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Appeal appeal = appealCompletableFuture.join();
|
||||||
|
HistoryRecord history = getHistory(appeal.historyType(), appeal.historyId());
|
||||||
|
if (history == null) {
|
||||||
|
throw new ResponseStatusException(HttpStatusCode.valueOf(404), "History not found");
|
||||||
|
}
|
||||||
|
// Send to Discord channels
|
||||||
|
try {
|
||||||
|
appealDiscord.sendAppealToDiscord(appeal, history);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to send appeal {} to Discord", appeal.id(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
appealMail.sendAppealNotification(appeal, history);
|
||||||
|
|
||||||
|
CompletableFuture<Optional<EmailVerification>> emailVerificationCompletableFuture = new CompletableFuture<>();
|
||||||
|
Connection.getConnection(Databases.DEFAULT)
|
||||||
|
.runQuery(sqlSession -> {
|
||||||
|
log.debug("Retrieving mail by uuid and address");
|
||||||
|
|
||||||
|
EmailVerification verifiedMail = sqlSession.getMapper(EmailVerificationMapper.class)
|
||||||
|
.findByUserAndEmail(appeal.uuid(), appeal.email().toLowerCase());
|
||||||
|
emailVerificationCompletableFuture.complete(Optional.ofNullable(verifiedMail));
|
||||||
|
});
|
||||||
|
Optional<EmailVerification> optionalEmailVerification = emailVerificationCompletableFuture.join();
|
||||||
|
|
||||||
|
if (optionalEmailVerification.isEmpty()) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
EmailVerification emailVerification = optionalEmailVerification.get();
|
||||||
|
if (!emailVerification.verified()) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
AppealResponseDto appealResponseDto = new AppealResponseDto(
|
||||||
|
appeal.id().toString(),
|
||||||
|
"Your appeal has been submitted. You will be notified when it has been reviewed.",
|
||||||
|
true);
|
||||||
|
return ResponseEntity.ok().body(appealResponseDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<AppealResponseDto> updateMail(UpdateMailDto updateMailDto) {
|
||||||
|
throw new ResponseStatusException(HttpStatusCode.valueOf(501), "Updating mail is not yet supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
private HistoryRecord getHistory(String type, int id) {
|
||||||
|
HistoryType historyTypeEnum = HistoryType.getHistoryType(type);
|
||||||
|
CompletableFuture<HistoryRecord> historyRecordCompletableFuture = new CompletableFuture<>();
|
||||||
|
|
||||||
|
Connection.getConnection(Databases.LITE_BANS)
|
||||||
|
.runQuery(sqlSession -> {
|
||||||
|
log.debug("Loading history by id");
|
||||||
|
try {
|
||||||
|
HistoryRecord punishment = sqlSession.getMapper(IdHistoryMapper.class)
|
||||||
|
.getRecentHistory(historyTypeEnum, id);
|
||||||
|
historyRecordCompletableFuture.complete(punishment);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to load history count", e);
|
||||||
|
historyRecordCompletableFuture.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return historyRecordCompletableFuture.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package com.alttd.altitudeweb.controllers.forms;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.api.MailApi;
|
||||||
|
import com.alttd.altitudeweb.controllers.data_from_auth.AuthenticatedUuid;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.mail.EmailVerification;
|
||||||
|
import com.alttd.altitudeweb.model.MailResponseDto;
|
||||||
|
import com.alttd.altitudeweb.model.SubmitEmailDto;
|
||||||
|
import com.alttd.altitudeweb.model.VerifyCodeDto;
|
||||||
|
import com.alttd.altitudeweb.model.EmailEntryDto;
|
||||||
|
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||||
|
import com.alttd.altitudeweb.services.mail.MailVerificationService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RateLimit(limit = 60, timeValue = 1, timeUnit = TimeUnit.HOURS)
|
||||||
|
public class MailController implements MailApi {
|
||||||
|
|
||||||
|
private final MailVerificationService mailVerificationService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@RateLimit(limit = 5, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "mailSubmit")
|
||||||
|
public ResponseEntity<MailResponseDto> submitEmailForVerification(SubmitEmailDto submitEmailDto) {
|
||||||
|
UUID uuid = AuthenticatedUuid.getAuthenticatedUserUuid();
|
||||||
|
boolean emailAlreadyVerified = mailVerificationService.listAll(uuid).stream()
|
||||||
|
.filter(EmailVerification::verified)
|
||||||
|
.map(EmailVerification::email)
|
||||||
|
.anyMatch(mail -> mail.equalsIgnoreCase(submitEmailDto.getEmail()));
|
||||||
|
if (emailAlreadyVerified) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Email already verified for user");
|
||||||
|
}
|
||||||
|
|
||||||
|
EmailVerification saved = mailVerificationService.submitEmail(uuid, submitEmailDto.getEmail());
|
||||||
|
MailResponseDto response = new MailResponseDto()
|
||||||
|
.email(saved.email())
|
||||||
|
.message("Verification email sent")
|
||||||
|
.verified(false);
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@RateLimit(limit = 20, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "mailVerify")
|
||||||
|
public ResponseEntity<MailResponseDto> verifyEmailCode(VerifyCodeDto verifyCodeDto) {
|
||||||
|
UUID uuid = AuthenticatedUuid.getAuthenticatedUserUuid();
|
||||||
|
Optional<EmailVerification> optionalEmailVerification = mailVerificationService.verifyCode(uuid, verifyCodeDto.getCode());
|
||||||
|
if (optionalEmailVerification.isEmpty()) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid verification code");
|
||||||
|
}
|
||||||
|
EmailVerification emailVerification = optionalEmailVerification.get();
|
||||||
|
MailResponseDto response = new MailResponseDto()
|
||||||
|
.email(emailVerification.email())
|
||||||
|
.message("Email verified successfully")
|
||||||
|
.verified(true);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@RateLimit(limit = 5, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "mailResend")
|
||||||
|
public ResponseEntity<MailResponseDto> resendVerificationEmail(SubmitEmailDto submitEmailDto) {
|
||||||
|
UUID uuid = AuthenticatedUuid.getAuthenticatedUserUuid();
|
||||||
|
EmailVerification updated = mailVerificationService.resend(uuid, submitEmailDto.getEmail());
|
||||||
|
if (updated == null) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Email not found for user");
|
||||||
|
}
|
||||||
|
MailResponseDto response = new MailResponseDto()
|
||||||
|
.email(updated.email())
|
||||||
|
.message("Verification email resent")
|
||||||
|
.verified(false);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@RateLimit(limit = 10, timeValue = 1, timeUnit = TimeUnit.HOURS, key = "mailDelete")
|
||||||
|
public ResponseEntity<Void> deleteEmail(SubmitEmailDto submitEmailDto) {
|
||||||
|
UUID uuid = AuthenticatedUuid.getAuthenticatedUserUuid();
|
||||||
|
boolean deleted = mailVerificationService.delete(uuid, submitEmailDto.getEmail());
|
||||||
|
if (!deleted) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Email not found for user");
|
||||||
|
}
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<List<EmailEntryDto>> getUserEmails() {
|
||||||
|
UUID uuid = AuthenticatedUuid.getAuthenticatedUserUuid();
|
||||||
|
List<EmailVerification> emails = mailVerificationService.listAll(uuid);
|
||||||
|
List<EmailEntryDto> result = emails.stream()
|
||||||
|
.map(ev -> new EmailEntryDto().email(ev.email()).verified(ev.verified()))
|
||||||
|
.toList();
|
||||||
|
return ResponseEntity.ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
+113
-35
@@ -1,18 +1,27 @@
|
|||||||
package com.alttd.altitudeweb.controllers.login;
|
package com.alttd.altitudeweb.controllers.login;
|
||||||
|
|
||||||
import com.alttd.altitudeweb.api.LoginApi;
|
import com.alttd.altitudeweb.api.LoginApi;
|
||||||
|
import com.alttd.altitudeweb.controllers.data_from_auth.AuthenticatedUuid;
|
||||||
|
import com.alttd.altitudeweb.database.litebans.HistoryRecord;
|
||||||
|
import com.alttd.altitudeweb.database.litebans.RecentNamesMapper;
|
||||||
|
import com.alttd.altitudeweb.database.litebans.UUIDHistoryMapper;
|
||||||
import com.alttd.altitudeweb.model.PermissionClaimDto;
|
import com.alttd.altitudeweb.model.PermissionClaimDto;
|
||||||
import com.alttd.altitudeweb.database.Databases;
|
import com.alttd.altitudeweb.database.Databases;
|
||||||
import com.alttd.altitudeweb.database.web_db.PrivilegedUser;
|
import com.alttd.altitudeweb.database.web_db.PrivilegedUser;
|
||||||
import com.alttd.altitudeweb.database.web_db.PrivilegedUserMapper;
|
import com.alttd.altitudeweb.database.web_db.PrivilegedUserMapper;
|
||||||
|
import com.alttd.altitudeweb.model.UsernameDto;
|
||||||
import com.alttd.altitudeweb.setup.Connection;
|
import com.alttd.altitudeweb.setup.Connection;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.autoconfigure.security.oauth2.resource.OAuth2ResourceServerProperties;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import com.alttd.altitudeweb.services.limits.RateLimit;
|
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.oauth2.jwt.Jwt;
|
||||||
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
|
||||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||||
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
import org.springframework.security.oauth2.jwt.JwtEncoderParameters;
|
||||||
@@ -36,7 +45,11 @@ public class LoginController implements LoginApi {
|
|||||||
@Value("${login.secret:#{null}}")
|
@Value("${login.secret:#{null}}")
|
||||||
private String loginSecret;
|
private String loginSecret;
|
||||||
|
|
||||||
private record CacheEntry(UUID uuid, Instant expiry) {}
|
@Value("${my-server.address:#{null}}")
|
||||||
|
private String serverAddress;
|
||||||
|
|
||||||
|
private record CacheEntry(UUID uuid, Instant expiry) {
|
||||||
|
}
|
||||||
|
|
||||||
private static final ConcurrentMap<String, CacheEntry> cache = new ConcurrentHashMap<>();
|
private static final ConcurrentMap<String, CacheEntry> cache = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
@@ -68,9 +81,9 @@ public class LoginController implements LoginApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Optional<String> key = cache.entrySet().stream()
|
Optional<String> key = cache.entrySet().stream()
|
||||||
.filter(entry -> entry.getValue().uuid.equals(uuidFromString))
|
.filter(entry -> entry.getValue().uuid.equals(uuidFromString))
|
||||||
.map(Map.Entry::getKey)
|
.map(Map.Entry::getKey)
|
||||||
.findFirst();
|
.findFirst();
|
||||||
|
|
||||||
if (key.isPresent()) {
|
if (key.isPresent()) {
|
||||||
return ResponseEntity.ok(key.get());
|
return ResponseEntity.ok(key.get());
|
||||||
@@ -80,24 +93,79 @@ public class LoginController implements LoginApi {
|
|||||||
return ResponseEntity.ok(loginCode);
|
return ResponseEntity.ok(loginCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<UsernameDto> getUsername() {
|
||||||
|
log.debug("Loading username for logged in user");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get authenticated UUID using the utility method
|
||||||
|
UUID uuid = AuthenticatedUuid.getAuthenticatedUserUuid();
|
||||||
|
log.debug("Loaded username for logged in user {}", uuid);
|
||||||
|
|
||||||
|
// Create response with username
|
||||||
|
UsernameDto usernameDto = new UsernameDto();
|
||||||
|
usernameDto.setUsername(getUsername(uuid));
|
||||||
|
log.debug("Loaded username for logged in user {}", usernameDto.getUsername());
|
||||||
|
|
||||||
|
return ResponseEntity.ok(usernameDto);
|
||||||
|
|
||||||
|
} catch (ResponseStatusException e) {
|
||||||
|
// The utility method already throws proper exceptions, we just need to convert them to ResponseEntity
|
||||||
|
return ResponseEntity.status(e.getStatusCode()).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getUsername(UUID uuid) {
|
||||||
|
CompletableFuture<String> username = new CompletableFuture<>();
|
||||||
|
|
||||||
|
Connection.getConnection(Databases.LITE_BANS)
|
||||||
|
.runQuery(sqlSession -> {
|
||||||
|
log.debug("Loading all history through logged in uuid");
|
||||||
|
try {
|
||||||
|
String temp = sqlSession
|
||||||
|
.getMapper(RecentNamesMapper.class)
|
||||||
|
.getUsername(uuid.toString());
|
||||||
|
username.complete(temp);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to find username for uuid {}", uuid, e);
|
||||||
|
username.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return username.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Value("${UNSECURED:#{false}}")
|
||||||
|
private boolean unsecured;
|
||||||
|
|
||||||
@RateLimit(limit = 5, timeValue = 1, timeUnit = TimeUnit.MINUTES, key = "login")
|
@RateLimit(limit = 5, timeValue = 1, timeUnit = TimeUnit.MINUTES, key = "login")
|
||||||
@Override
|
@Override
|
||||||
public ResponseEntity<String> login(String code) {
|
public ResponseEntity<String> login(String code) {
|
||||||
CacheEntry cacheEntry1 = new CacheEntry(UUID.fromString("55e46bc3-2a29-4c53-850f-dbd944dc5c5f"), Instant.now().plusSeconds(TimeUnit.DAYS.toSeconds(1)));
|
if (unsecured) {
|
||||||
cache.put("23232323", cacheEntry1);
|
log.warn("Unsecured login is enabled, skipping login validation!");
|
||||||
|
} else {
|
||||||
|
log.info("Received login request with code {}", code);
|
||||||
|
}
|
||||||
if (code == null) {
|
if (code == null) {
|
||||||
|
log.warn("Received null login code");
|
||||||
return ResponseEntity.badRequest().build();
|
return ResponseEntity.badRequest().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
CacheEntry cacheEntry = cache.get(code);
|
CacheEntry cacheEntry = cache.get(code);
|
||||||
if (cacheEntry == null || cacheEntry.expiry().isBefore(Instant.now())) {
|
if (!unsecured && (cacheEntry == null || cacheEntry.expiry().isBefore(Instant.now()))) {
|
||||||
|
log.warn("Received invalid login code {}", code);
|
||||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (unsecured && cacheEntry == null) {
|
||||||
|
cacheEntry = new CacheEntry(UUID.fromString("55e46bc3-2a29-4c53-850f-dbd944dc5c5f"), Instant.now().plusSeconds(TimeUnit.DAYS.toSeconds(1)));
|
||||||
|
}
|
||||||
|
|
||||||
String token = generateToken(cacheEntry.uuid);
|
String token = generateToken(cacheEntry.uuid);
|
||||||
|
log.debug("Generated token for user {} with token {}", cacheEntry.uuid, token);
|
||||||
|
|
||||||
cache.remove(code);
|
cache.remove(code);
|
||||||
|
|
||||||
|
log.debug("Generated token for user {}", cacheEntry.uuid);
|
||||||
return ResponseEntity.ok(token);
|
return ResponseEntity.ok(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +177,7 @@ public class LoginController implements LoginApi {
|
|||||||
loginCode.append(characters.charAt(index));
|
loginCode.append(characters.charAt(index));
|
||||||
}
|
}
|
||||||
CacheEntry cacheEntry = new CacheEntry(uuid,
|
CacheEntry cacheEntry = new CacheEntry(uuid,
|
||||||
Instant.now().plusSeconds(TimeUnit.MINUTES.toSeconds(15)));
|
Instant.now().plusSeconds(TimeUnit.MINUTES.toSeconds(15)));
|
||||||
cache.put(loginCode.toString(), cacheEntry);
|
cache.put(loginCode.toString(), cacheEntry);
|
||||||
return loginCode.toString();
|
return loginCode.toString();
|
||||||
}
|
}
|
||||||
@@ -134,38 +202,48 @@ public class LoginController implements LoginApi {
|
|||||||
Instant now = Instant.now();
|
Instant now = Instant.now();
|
||||||
//TODO make a JWT for renewing and one for storing permissions for a session (expiry 1 hour)
|
//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));
|
Instant expiryTime = now.plusSeconds(TimeUnit.DAYS.toSeconds(30));
|
||||||
CompletableFuture<PrivilegedUser> privilegedUserCompletableFuture = new CompletableFuture<>();
|
CompletableFuture<Optional<PrivilegedUser>> privilegedUserCompletableFuture = new CompletableFuture<>();
|
||||||
List<PermissionClaimDto> claimList = new ArrayList<>();
|
List<PermissionClaimDto> claimList = new ArrayList<>();
|
||||||
Connection.getConnection(Databases.DEFAULT)
|
Connection.getConnection(Databases.DEFAULT)
|
||||||
.runQuery(sqlSession -> {
|
.runQuery(sqlSession -> {
|
||||||
try {
|
try {
|
||||||
PrivilegedUser privilegedUser = sqlSession.getMapper(PrivilegedUserMapper.class)
|
log.debug("Loading user by uuid {}", uuid.toString());
|
||||||
.getUserByUuid(uuid.toString());
|
PrivilegedUserMapper mapper = sqlSession.getMapper(PrivilegedUserMapper.class);
|
||||||
|
Optional<PrivilegedUser> privilegedUser = mapper
|
||||||
|
.getUserByUuid(uuid);
|
||||||
|
|
||||||
privilegedUserCompletableFuture.complete(privilegedUser);
|
if (privilegedUser.isEmpty()) {
|
||||||
} catch (Exception e) {
|
int privilegedUserId = mapper.createPrivilegedUser(uuid);
|
||||||
log.error("Failed to load user by uuid", e);
|
privilegedUserCompletableFuture.complete(
|
||||||
privilegedUserCompletableFuture.completeExceptionally(e);
|
Optional.of(new PrivilegedUser(privilegedUserId, uuid, List.of())));
|
||||||
}
|
} else {
|
||||||
});
|
privilegedUserCompletableFuture.complete(privilegedUser);
|
||||||
PrivilegedUser privilegedUser = privilegedUserCompletableFuture.join();
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to load user by uuid", e);
|
||||||
|
privilegedUserCompletableFuture.completeExceptionally(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Optional<PrivilegedUser> privilegedUser = privilegedUserCompletableFuture.join();
|
||||||
claimList.add(PermissionClaimDto.USER);
|
claimList.add(PermissionClaimDto.USER);
|
||||||
if (privilegedUser != null) {
|
privilegedUser.ifPresent(user -> user.getPermissions().forEach(permission -> {
|
||||||
privilegedUser.getPermissions().forEach(permission -> {
|
try {
|
||||||
try {
|
claimList.add(PermissionClaimDto.fromValue(permission));
|
||||||
claimList.add(PermissionClaimDto.valueOf(permission));
|
log.debug("Added permission claim {}", permission);
|
||||||
} catch (IllegalArgumentException e) {
|
} catch (IllegalArgumentException e) {
|
||||||
log.warn("Received invalid permission claim: {}", permission);
|
log.warn("Received invalid permission claim: {}", permission);
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
}
|
log.debug("Generated token for user {} with claims {}", uuid.toString(),
|
||||||
|
claimList.stream().map(PermissionClaimDto::getValue).toList());
|
||||||
JwtClaimsSet claims = JwtClaimsSet.builder()
|
JwtClaimsSet claims = JwtClaimsSet.builder()
|
||||||
.issuer("altitudeweb")
|
.issuer(serverAddress)
|
||||||
.claim("authorities", claimList.stream().map(PermissionClaimDto::getValue).toList())
|
.claim("authorities",
|
||||||
.issuedAt(now)
|
claimList.stream().map(PermissionClaimDto::getValue).toList())
|
||||||
.expiresAt(expiryTime)
|
.issuedAt(now)
|
||||||
.subject(uuid.toString())
|
.expiresAt(expiryTime)
|
||||||
.build();
|
.subject(uuid.toString())
|
||||||
|
.build();
|
||||||
|
|
||||||
return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
|
return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
|
||||||
}
|
}
|
||||||
|
|||||||
+176
@@ -0,0 +1,176 @@
|
|||||||
|
package com.alttd.altitudeweb.controllers.particles;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.api.ParticlesApi;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.core.io.ByteArrayResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RestController
|
||||||
|
public class ParticleController implements ParticlesApi {
|
||||||
|
|
||||||
|
@Value("${login.secret:#{null}}")
|
||||||
|
private String loginSecret;
|
||||||
|
|
||||||
|
@Value("${particles.file_path}")
|
||||||
|
private String particlesFilePath;
|
||||||
|
|
||||||
|
@Value("${notification.server.url:http://localhost:8080}")
|
||||||
|
private String notificationServerUrl;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<Resource> downloadFile(String authorization, String filename) throws Exception {
|
||||||
|
if (authorization == null || !authorization.equals(loginSecret)) {
|
||||||
|
return ResponseEntity.status(401).build();
|
||||||
|
}
|
||||||
|
File file = new File(particlesFilePath);
|
||||||
|
if (!file.exists() || !file.isDirectory()) {
|
||||||
|
log.error("Particles file path {} is not a directory, not downloading particles file", particlesFilePath);
|
||||||
|
return ResponseEntity.status(404).build();
|
||||||
|
}
|
||||||
|
File targetFile = new File(file, filename);
|
||||||
|
return getFileForDownload(targetFile, filename);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<Resource> downloadFileForUser(String authorization, String uuid, String filename) throws Exception {
|
||||||
|
if (authorization == null || !authorization.equals(loginSecret)) {
|
||||||
|
return ResponseEntity.status(401).build();
|
||||||
|
}
|
||||||
|
File file = new File(particlesFilePath);
|
||||||
|
if (!file.exists() || !file.isDirectory()) {
|
||||||
|
log.error("Particles file path {} is not a directory, not downloading particles user file", particlesFilePath);
|
||||||
|
return ResponseEntity.status(404).build();
|
||||||
|
}
|
||||||
|
File targetDir = new File(file, uuid);
|
||||||
|
if (targetDir.exists()) {
|
||||||
|
return getFileForDownload(targetDir, filename);
|
||||||
|
} else {
|
||||||
|
log.warn("User {} does not have a directory for particles files", uuid);
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseEntity<Resource> getFileForDownload(File file, String filename) {
|
||||||
|
File targetFile = new File(file, filename);
|
||||||
|
if (!targetFile.exists()) {
|
||||||
|
log.warn("Particles file {} does not exist", targetFile.getAbsolutePath());
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetFile.isFile()) {
|
||||||
|
log.warn("Particles file {} is not a file", targetFile.getAbsolutePath());
|
||||||
|
return ResponseEntity.status(404).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Path path = targetFile.toPath();
|
||||||
|
ByteArrayResource resource = new ByteArrayResource(Files.readAllBytes(path));
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.contentLength(targetFile.length())
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
|
||||||
|
.body(resource);
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Failed to read particles file {}: {}", targetFile.getAbsolutePath(), e.getMessage(), e);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<Void> saveFile(String filename, MultipartFile content) throws Exception {
|
||||||
|
File file = new File(particlesFilePath);
|
||||||
|
if (!file.exists() || !file.isDirectory()) {
|
||||||
|
log.error("Particles file path {} is not a directory, not saving particles file", particlesFilePath);
|
||||||
|
return ResponseEntity.status(404).build();
|
||||||
|
}
|
||||||
|
ResponseEntity<Void> voidResponseEntity = writeContentToFile(file, filename, content);
|
||||||
|
notifyServerOfFileUpload(filename);
|
||||||
|
return voidResponseEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResponseEntity<Void> saveFileForUser(String uuid, String filename, MultipartFile content) throws Exception {
|
||||||
|
File file = new File(particlesFilePath);
|
||||||
|
if (!file.exists() || !file.isDirectory()) {
|
||||||
|
log.error("Particles file path {} is not a directory, not saving particles user file", particlesFilePath);
|
||||||
|
return ResponseEntity.status(404).build();
|
||||||
|
}
|
||||||
|
File targetDir = new File(file, uuid);
|
||||||
|
if (!file.exists()) {
|
||||||
|
log.debug("Creating particles directory {}", targetDir.getAbsolutePath());
|
||||||
|
if (targetDir.mkdirs()) {
|
||||||
|
log.info("Created particles user directory {}", targetDir.getAbsolutePath());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ResponseEntity<Void> voidResponseEntity = writeContentToFile(file, filename, content);
|
||||||
|
notifyServerOfFileUpload(uuid, filename);
|
||||||
|
return voidResponseEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyServerOfFileUpload(String filename) {
|
||||||
|
String notificationUrl = String.format("%s/notify/%s.json", notificationServerUrl, filename);
|
||||||
|
sendNotification(notificationUrl, String.format("file upload: %s", filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyServerOfFileUpload(String uuid, String filename) {
|
||||||
|
String notificationUrl = String.format("%s/notify/%s/%s.json", notificationServerUrl, uuid, filename);
|
||||||
|
sendNotification(notificationUrl, String.format("file upload for user %s: %s", uuid, filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendNotification(String notificationUrl, String logDescription) {
|
||||||
|
try {
|
||||||
|
RestTemplate restTemplate = new RestTemplate();
|
||||||
|
ResponseEntity<String> response = restTemplate.getForEntity(notificationUrl, String.class);
|
||||||
|
|
||||||
|
if (response.getStatusCode().is2xxSuccessful()) {
|
||||||
|
log.info("Successfully notified server of {}", logDescription);
|
||||||
|
} else {
|
||||||
|
log.warn("Failed to notify server of {}, status: {}",
|
||||||
|
logDescription, response.getStatusCode());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error notifying server of {}", logDescription, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private ResponseEntity<Void> writeContentToFile(File dir, String filename, MultipartFile content) {
|
||||||
|
File targetFile = new File(dir, filename);
|
||||||
|
if (!Files.isWritable(targetFile.toPath())) {
|
||||||
|
log.error("Particles file {} is not writable", targetFile.getAbsolutePath());
|
||||||
|
return ResponseEntity.status(403).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetFile.exists()) {
|
||||||
|
log.warn("Overwriting existing particles file {}", targetFile.getAbsolutePath());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
content.transferTo(targetFile);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to write particles file {}", targetFile.getAbsolutePath(), e);
|
||||||
|
return ResponseEntity.status(500).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ResponseEntity.ok().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.alttd.altitudeweb.mappers;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.database.web_db.forms.Appeal;
|
||||||
|
import com.alttd.altitudeweb.model.MinecraftAppealDto;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AppealDataMapper {
|
||||||
|
public MinecraftAppealDto appealToMinecraftAppealDto(Appeal appeal) {
|
||||||
|
MinecraftAppealDto minecraftAppealDto = new MinecraftAppealDto();
|
||||||
|
minecraftAppealDto.setAppeal(appeal.reason());
|
||||||
|
minecraftAppealDto.setUsername(appeal.username());
|
||||||
|
minecraftAppealDto.setUuid(appeal.uuid());
|
||||||
|
minecraftAppealDto.setEmail(appeal.email());
|
||||||
|
return minecraftAppealDto;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Appeal minecraftAppealDtoToAppeal(MinecraftAppealDto minecraftAppealDto) {
|
||||||
|
return new Appeal(
|
||||||
|
UUID.randomUUID(),
|
||||||
|
minecraftAppealDto.getUuid(),
|
||||||
|
minecraftAppealDto.getPunishmentType().toString(),
|
||||||
|
minecraftAppealDto.getPunishmentId(),
|
||||||
|
minecraftAppealDto.getUsername(),
|
||||||
|
minecraftAppealDto.getAppeal(),
|
||||||
|
Instant.now(),
|
||||||
|
null,
|
||||||
|
minecraftAppealDto.getEmail(),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
package com.alttd.altitudeweb.services.discord;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.database.Databases;
|
||||||
|
import com.alttd.altitudeweb.database.discord.OutputChannel;
|
||||||
|
import com.alttd.altitudeweb.database.discord.OutputChannelMapper;
|
||||||
|
import com.alttd.altitudeweb.database.litebans.HistoryCountMapper;
|
||||||
|
import com.alttd.altitudeweb.database.litebans.HistoryRecord;
|
||||||
|
import com.alttd.altitudeweb.database.litebans.HistoryType;
|
||||||
|
import com.alttd.altitudeweb.database.litebans.UserType;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.forms.Appeal;
|
||||||
|
import com.alttd.altitudeweb.setup.Connection;
|
||||||
|
import com.alttd.webinterface.send_message.DiscordSender;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class AppealDiscord {
|
||||||
|
|
||||||
|
private static final String OUTPUT_TYPE = "APPEAL";
|
||||||
|
|
||||||
|
public void sendAppealToDiscord(Appeal appeal, HistoryRecord history) {
|
||||||
|
// Fetch channels
|
||||||
|
CompletableFuture<List<OutputChannel>> channelsFuture = new CompletableFuture<>();
|
||||||
|
Connection.getConnection(Databases.DISCORD).runQuery(sql -> {
|
||||||
|
try {
|
||||||
|
List<OutputChannel> channels = sql.getMapper(OutputChannelMapper.class)
|
||||||
|
.getChannelsWithOutputType(OUTPUT_TYPE);
|
||||||
|
channelsFuture.complete(channels);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to load output channels for {}", OUTPUT_TYPE, e);
|
||||||
|
channelsFuture.complete(new ArrayList<>());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch counts for user
|
||||||
|
CompletableFuture<Integer> bansF = getCountAsync(HistoryType.BAN, appeal.uuid());
|
||||||
|
CompletableFuture<Integer> mutesF = getCountAsync(HistoryType.MUTE, appeal.uuid());
|
||||||
|
CompletableFuture<Integer> warnsF = getCountAsync(HistoryType.WARN, appeal.uuid());
|
||||||
|
CompletableFuture<Integer> kicksF = getCountAsync(HistoryType.KICK, appeal.uuid());
|
||||||
|
|
||||||
|
List<OutputChannel> channels = channelsFuture.join();
|
||||||
|
int bans = bansF.join();
|
||||||
|
int mutes = mutesF.join();
|
||||||
|
int warns = warnsF.join();
|
||||||
|
int kicks = kicksF.join();
|
||||||
|
|
||||||
|
if (channels.isEmpty()) {
|
||||||
|
log.warn("No Discord output channels found for type {}. Skipping Discord send.", OUTPUT_TYPE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build embed
|
||||||
|
boolean active = history.getUntil() == null || history.getUntil() <= 0 || history.getUntil() > System.currentTimeMillis();
|
||||||
|
String createdAt = formatInstant(appeal.createdAt());
|
||||||
|
|
||||||
|
List<DiscordSender.EmbedField> fields = new ArrayList<>();
|
||||||
|
// Group: User
|
||||||
|
fields.add(new DiscordSender.EmbedField(
|
||||||
|
"User",
|
||||||
|
"Username: " + safe(appeal.username()) + "\n" +
|
||||||
|
"UUID: " + safe(String.valueOf(appeal.uuid())) + "\n" +
|
||||||
|
"Email: " + safe(appeal.email()) + "\n" +
|
||||||
|
"Submitted: " + createdAt,
|
||||||
|
false
|
||||||
|
));
|
||||||
|
// Group: Punishment
|
||||||
|
fields.add(new DiscordSender.EmbedField(
|
||||||
|
"Punishment",
|
||||||
|
"Type: " + safe(String.valueOf(appeal.historyType())) + "\n" +
|
||||||
|
"ID: " + safe(String.valueOf(appeal.historyId())) + "\n" +
|
||||||
|
"Reason: " + safe(history.getReason()) + "\n" +
|
||||||
|
"Active: " + active,
|
||||||
|
false
|
||||||
|
));
|
||||||
|
// Group: Previous punishments
|
||||||
|
fields.add(new DiscordSender.EmbedField(
|
||||||
|
"Previous punishments",
|
||||||
|
"Bans: " + bans + "\n" +
|
||||||
|
"Mutes: " + mutes + "\n" +
|
||||||
|
"Warnings: " + warns + "\n" +
|
||||||
|
"Kicks: " + kicks,
|
||||||
|
true
|
||||||
|
));
|
||||||
|
|
||||||
|
String description = safe(appeal.reason());
|
||||||
|
|
||||||
|
List<Long> channelIds = channels.stream()
|
||||||
|
.map(OutputChannel::channel)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// colorRgb = null (use default), timestamp = appeal.createdAt if available
|
||||||
|
Instant timestamp = appeal.createdAt() != null ? appeal.createdAt() : Instant.now();
|
||||||
|
DiscordSender.getInstance().sendEmbedToChannels(
|
||||||
|
channelIds,
|
||||||
|
"New Appeal Submitted",
|
||||||
|
description,
|
||||||
|
fields,
|
||||||
|
null,
|
||||||
|
timestamp,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CompletableFuture<Integer> getCountAsync(HistoryType type, java.util.UUID uuid) {
|
||||||
|
CompletableFuture<Integer> future = new CompletableFuture<>();
|
||||||
|
Connection.getConnection(Databases.LITE_BANS).runQuery(sql -> {
|
||||||
|
try {
|
||||||
|
Integer count = sql.getMapper(HistoryCountMapper.class)
|
||||||
|
.getUuidPunishmentCount(type, UserType.PLAYER, uuid);
|
||||||
|
future.complete(count == null ? 0 : count);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to load punishment count for {} ({})", type, uuid, e);
|
||||||
|
future.complete(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return future;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String safe(String s) {
|
||||||
|
return s == null ? "unknown" : s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatInstant(Instant instant) {
|
||||||
|
if (instant == null) return "unknown";
|
||||||
|
return instant.atZone(ZoneId.of("UTC"))
|
||||||
|
.format(DateTimeFormatter.ofPattern("yyyy MMMM dd hh:mm a '(UTC)'"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package com.alttd.altitudeweb.services.mail;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.database.litebans.HistoryRecord;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.forms.Appeal;
|
||||||
|
import jakarta.mail.MessagingException;
|
||||||
|
import jakarta.mail.internet.MimeMessage;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.mail.javamail.JavaMailSender;
|
||||||
|
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.thymeleaf.context.Context;
|
||||||
|
import org.thymeleaf.spring6.SpringTemplateEngine;
|
||||||
|
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class AppealMail {
|
||||||
|
|
||||||
|
private final JavaMailSender mailSender;
|
||||||
|
private final SpringTemplateEngine templateEngine;
|
||||||
|
|
||||||
|
@Value("${spring.mail.username}")
|
||||||
|
private String fromEmail;
|
||||||
|
|
||||||
|
private static final String APPEAL_EMAIL = "[email protected]";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an email notification about the appeal to both the user and the appeals team.
|
||||||
|
*
|
||||||
|
* @param appeal The appeal object containing all necessary information
|
||||||
|
*/
|
||||||
|
public void sendAppealNotification(Appeal appeal, HistoryRecord history) {
|
||||||
|
try {
|
||||||
|
sendEmailToAppealsTeam(appeal, history);
|
||||||
|
|
||||||
|
log.info("Appeal notification emails sent successfully for appeal ID: {}", appeal.id());
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to send appeal notification emails for appeal ID: {}", appeal.id(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendEmailToAppealsTeam(Appeal appeal, HistoryRecord history) throws MessagingException {
|
||||||
|
MimeMessage message = mailSender.createMimeMessage();
|
||||||
|
MimeMessageHelper helper = new MimeMessageHelper(message, true);
|
||||||
|
|
||||||
|
helper.setFrom(fromEmail);
|
||||||
|
helper.setTo(APPEAL_EMAIL);
|
||||||
|
helper.setReplyTo(appeal.email());
|
||||||
|
helper.setSubject("New Appeal Submitted - " + appeal.username());
|
||||||
|
|
||||||
|
Context context = new Context();
|
||||||
|
context.setVariable("appeal", appeal);
|
||||||
|
context.setVariable("history", history);
|
||||||
|
context.setVariable("createdAt", appeal.createdAt()
|
||||||
|
.atZone(ZoneId.of("UTC"))
|
||||||
|
.format(DateTimeFormatter.ofPattern("yyyy MMMM dd hh:mm a '(UTC)'")));
|
||||||
|
context.setVariable("active", history.getUntil() <= 0 || history.getUntil() > System.currentTimeMillis());
|
||||||
|
String content = templateEngine.process("appeal-email", context);
|
||||||
|
|
||||||
|
helper.setText(content, true);
|
||||||
|
mailSender.send(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+144
@@ -0,0 +1,144 @@
|
|||||||
|
package com.alttd.altitudeweb.services.mail;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.database.Databases;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.mail.EmailVerification;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.mail.EmailVerificationMapper;
|
||||||
|
import com.alttd.altitudeweb.setup.Connection;
|
||||||
|
import jakarta.mail.MessagingException;
|
||||||
|
import jakarta.mail.internet.MimeMessage;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.mail.javamail.JavaMailSender;
|
||||||
|
import org.springframework.mail.javamail.MimeMessageHelper;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MailVerificationService {
|
||||||
|
|
||||||
|
private final JavaMailSender mailSender;
|
||||||
|
|
||||||
|
@Value("${spring.mail.username}")
|
||||||
|
private String fromEmail;
|
||||||
|
|
||||||
|
public java.util.List<EmailVerification> listAll(UUID userUuid) {
|
||||||
|
java.util.concurrent.CompletableFuture<java.util.List<EmailVerification>> future = new java.util.concurrent.CompletableFuture<>();
|
||||||
|
Connection.getConnection(Databases.DEFAULT)
|
||||||
|
.runQuery(sql -> {
|
||||||
|
EmailVerificationMapper mapper = sql.getMapper(EmailVerificationMapper.class);
|
||||||
|
future.complete(mapper.findAllByUser(userUuid));
|
||||||
|
});
|
||||||
|
return future.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
public EmailVerification submitEmail(UUID userUuid, String email) {
|
||||||
|
String code = generateCode();
|
||||||
|
Instant now = Instant.now();
|
||||||
|
final String finalEmail = email.toLowerCase();
|
||||||
|
|
||||||
|
CompletableFuture<EmailVerification> future = new CompletableFuture<>();
|
||||||
|
Connection.getConnection(Databases.DEFAULT)
|
||||||
|
.runQuery(sql -> {
|
||||||
|
EmailVerificationMapper mapper = sql.getMapper(EmailVerificationMapper.class);
|
||||||
|
EmailVerification existing = mapper.findByUserAndEmail(userUuid, finalEmail);
|
||||||
|
EmailVerification toPersist;
|
||||||
|
if (existing == null) {
|
||||||
|
toPersist = new EmailVerification(UUID.randomUUID(), userUuid, finalEmail, code, false, now, null, now);
|
||||||
|
mapper.insert(toPersist);
|
||||||
|
} else {
|
||||||
|
mapper.updateCodeAndLastSent(existing.id(), code, now);
|
||||||
|
toPersist = new EmailVerification(existing.id(), userUuid, finalEmail, code, false, existing.createdAt(), null, now);
|
||||||
|
}
|
||||||
|
future.complete(toPersist);
|
||||||
|
});
|
||||||
|
EmailVerification saved = future.join();
|
||||||
|
sendVerificationEmail(saved);
|
||||||
|
return saved;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<EmailVerification> verifyCode(UUID userUuid, String code) {
|
||||||
|
CompletableFuture<Optional<EmailVerification>> future = new CompletableFuture<>();
|
||||||
|
Connection.getConnection(Databases.DEFAULT)
|
||||||
|
.runQuery(sql -> {
|
||||||
|
EmailVerificationMapper mapper = sql.getMapper(EmailVerificationMapper.class);
|
||||||
|
EmailVerification found = mapper.findByUserAndCode(userUuid, code);
|
||||||
|
if (found == null) {
|
||||||
|
future.complete(Optional.empty());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mapper.markVerified(found.id(), Instant.now());
|
||||||
|
future.complete(Optional.of(found));
|
||||||
|
});
|
||||||
|
return future.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
public EmailVerification resend(UUID userUuid, String email) {
|
||||||
|
String code = generateCode();
|
||||||
|
Instant now = Instant.now();
|
||||||
|
final String finalEmail = email.toLowerCase();
|
||||||
|
CompletableFuture<EmailVerification> future = new CompletableFuture<>();
|
||||||
|
Connection.getConnection(Databases.DEFAULT)
|
||||||
|
.runQuery(sql -> {
|
||||||
|
EmailVerificationMapper mapper = sql.getMapper(EmailVerificationMapper.class);
|
||||||
|
EmailVerification existing = mapper.findByUserAndEmail(userUuid, finalEmail);
|
||||||
|
if (existing != null) {
|
||||||
|
mapper.updateCodeAndLastSent(existing.id(), code, now);
|
||||||
|
future.complete(new EmailVerification(existing.id(), userUuid,
|
||||||
|
finalEmail, code, false, existing.createdAt(), null, now));
|
||||||
|
} else {
|
||||||
|
future.complete(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
EmailVerification updated = future.join();
|
||||||
|
if (updated != null) {
|
||||||
|
sendVerificationEmail(updated);
|
||||||
|
}
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean delete(UUID userUuid, String email) {
|
||||||
|
final String finalEmail = email.toLowerCase();
|
||||||
|
CompletableFuture<Boolean> future = new CompletableFuture<>();
|
||||||
|
Connection.getConnection(Databases.DEFAULT)
|
||||||
|
.runQuery(sql -> {
|
||||||
|
EmailVerificationMapper mapper = sql.getMapper(EmailVerificationMapper.class);
|
||||||
|
EmailVerification existing = mapper.findByUserAndEmail(userUuid, finalEmail);
|
||||||
|
if (existing != null) {
|
||||||
|
mapper.deleteByUserAndEmail(userUuid, finalEmail);
|
||||||
|
future.complete(true);
|
||||||
|
} else {
|
||||||
|
future.complete(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return future.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendVerificationEmail(EmailVerification emailVerification) {
|
||||||
|
try {
|
||||||
|
MimeMessage message = mailSender.createMimeMessage();
|
||||||
|
MimeMessageHelper helper = new MimeMessageHelper(message, true);
|
||||||
|
helper.setFrom(fromEmail);
|
||||||
|
helper.setTo(emailVerification.email().toLowerCase());
|
||||||
|
helper.setSubject("Your verification code");
|
||||||
|
helper.setText("Your verification code is: " + emailVerification.verificationCode(), false);
|
||||||
|
mailSender.send(message);
|
||||||
|
} catch (MessagingException e) {
|
||||||
|
log.error("Failed to send verification email to {}", emailVerification.email(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateCode() {
|
||||||
|
// 6-digit numeric code
|
||||||
|
Random random = new Random();
|
||||||
|
int num = 100000 + random.nextInt(899999);
|
||||||
|
return String.valueOf(num);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,4 +5,5 @@ database.host=${DB_HOST:localhost}
|
|||||||
database.user=${DB_USER:root}
|
database.user=${DB_USER:root}
|
||||||
database.password=${DB_PASSWORD:root}
|
database.password=${DB_PASSWORD:root}
|
||||||
cors.allowed-origins=${CORS:https://beta.alttd.com}
|
cors.allowed-origins=${CORS:https://beta.alttd.com}
|
||||||
logging.level.com.alttd.altitudeweb=INFO
|
my-server.address=${SERVER_ADDRESS:https://beta.alttd.com}
|
||||||
|
logging.level.com.alttd.altitudeweb=DEBUG
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
spring.application.name=AltitudeWeb
|
cors.allowed-origins=${CORS:http://localhost:4200,http://localhost:8080}
|
||||||
database.name=${DB_NAME:web_db}
|
my-server.address=${SERVER_ADDRESS:http://localhost:8080}
|
||||||
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
|
logging.level.com.alttd.altitudeweb=DEBUG
|
||||||
|
logging.level.org.springframework.security=DEBUG
|
||||||
|
|||||||
@@ -6,4 +6,15 @@ database.user=${DB_USER:root}
|
|||||||
database.password=${DB_PASSWORD:root}
|
database.password=${DB_PASSWORD:root}
|
||||||
cors.allowed-origins=${CORS:https://alttd.com}
|
cors.allowed-origins=${CORS:https://alttd.com}
|
||||||
login.secret=${LOGIN_SECRET:SET_TOKEN}
|
login.secret=${LOGIN_SECRET:SET_TOKEN}
|
||||||
|
particles.file_path=${user.home}/.altitudeweb/particles
|
||||||
|
notification.server.url=${SERVER_IP:10.0.0.107}:${SERVER_PORT:8080}
|
||||||
|
my-server.address=${SERVER_ADDRESS:https://alttd.com}
|
||||||
logging.level.com.alttd.altitudeweb=INFO
|
logging.level.com.alttd.altitudeweb=INFO
|
||||||
|
discord.token=${DISCORD_TOKEN}
|
||||||
|
spring.mail.host=${MAIL_HOST:smtp.zoho.com}
|
||||||
|
spring.mail.port=${MAIL_PORT:465}
|
||||||
|
spring.mail.username=${MAIL_USER}
|
||||||
|
spring.mail.password=${MAIL_PASSWORD}
|
||||||
|
spring.mail.properties.mail.smtp.auth=true
|
||||||
|
spring.mail.properties.mail.smtp.ssl.enable=true
|
||||||
|
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html xmlns:th="http://www.thymeleaf.org" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Appeal Notification</title>
|
||||||
|
<style>
|
||||||
|
ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h2 th:text="'Appeal by ' + ${appeal.username}">Appeal by Username</h2>
|
||||||
|
<p>Punishment information</p>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Username:</strong> <span th:text="${appeal.username}">username</span></li>
|
||||||
|
<li><strong>UUID:</strong> <span th:text="${appeal.uuid}">uuid</span></li>
|
||||||
|
<li><strong>Email:</strong> <span th:text="${appeal.email}">email</span></li>
|
||||||
|
<li><strong>Submitted at:</strong> <span th:text="${createdAt}">date</span></li>
|
||||||
|
<li><strong>Reason:</strong> <span th:text="${history.reason}">reason</span></li>
|
||||||
|
<li><strong>Active:</strong> <span th:text="${active}">unknown</span></li>
|
||||||
|
</ul>
|
||||||
|
<h3>Appeal:</h3>
|
||||||
|
<p th:text="${appeal.reason}">Reason text</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -6,7 +6,8 @@ import lombok.Getter;
|
|||||||
public enum Databases {
|
public enum Databases {
|
||||||
DEFAULT("web_db"),
|
DEFAULT("web_db"),
|
||||||
LUCK_PERMS("luckperms"),
|
LUCK_PERMS("luckperms"),
|
||||||
LITE_BANS("litebans");
|
LITE_BANS("litebans"),
|
||||||
|
DISCORD("discordLink");
|
||||||
|
|
||||||
private final String internalName;
|
private final String internalName;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
package com.alttd.altitudeweb.database.discord;
|
||||||
|
|
||||||
|
public record OutputChannel(long guild, String outputType, long channel, String channelType) {
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
package com.alttd.altitudeweb.database.discord;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface OutputChannelMapper {
|
||||||
|
@Select("""
|
||||||
|
SELECT guild, output_type, channel, channel_type
|
||||||
|
FROM output_channels
|
||||||
|
WHERE output_type = #{outputType}
|
||||||
|
""")
|
||||||
|
List<OutputChannel> getChannelsWithOutputType(@Param("outputType") String outputType);
|
||||||
|
}
|
||||||
@@ -7,6 +7,16 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public interface RecentNamesMapper {
|
public interface RecentNamesMapper {
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT DISTINCT name AS username
|
||||||
|
FROM litebans_history
|
||||||
|
WHERE uuid = #{uuid}
|
||||||
|
ORDER BY date DESC;
|
||||||
|
""")
|
||||||
|
String getUsername(@Param("uuid") String uuid);
|
||||||
|
|
||||||
|
|
||||||
@Select("""
|
@Select("""
|
||||||
SELECT DISTINCT user_lookup.name AS punished_name
|
SELECT DISTINCT user_lookup.name AS punished_name
|
||||||
FROM ${tableName} AS punishment
|
FROM ${tableName} AS punishment
|
||||||
|
|||||||
@@ -4,7 +4,12 @@ import org.apache.ibatis.annotations.*;
|
|||||||
|
|
||||||
public interface KeyPairMapper {
|
public interface KeyPairMapper {
|
||||||
|
|
||||||
@Select("SELECT * FROM key_pair ORDER BY id DESC LIMIT 1")
|
@Select("""
|
||||||
|
SELECT id, private_key AS privateKey, public_key AS publicKey, created_at AS createdAt
|
||||||
|
FROM key_pair
|
||||||
|
ORDER BY id
|
||||||
|
DESC LIMIT 1
|
||||||
|
""")
|
||||||
KeyPairEntity getKeyPair();
|
KeyPairEntity getKeyPair();
|
||||||
|
|
||||||
@Insert("""
|
@Insert("""
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ import lombok.Data;
|
|||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class PrivilegedUser {
|
public class PrivilegedUser {
|
||||||
private int id;
|
private Integer id;
|
||||||
private String uuid;
|
private UUID uuid;
|
||||||
private List<String> permissions;
|
private List<String> permissions;
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-7
@@ -1,29 +1,31 @@
|
|||||||
package com.alttd.altitudeweb.database.web_db;
|
package com.alttd.altitudeweb.database.web_db;
|
||||||
|
|
||||||
import org.apache.ibatis.annotations.*;
|
import org.apache.ibatis.annotations.*;
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
public interface PrivilegedUserMapper {
|
public interface PrivilegedUserMapper {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves a user by their UUID along with their permissions
|
* Retrieves a user by their UUID along with their permissions
|
||||||
* @param uuid The UUID of the user to retrieve
|
* @param uuid The UUID of the user to retrieve
|
||||||
* @return The PrivilegedUser with their permissions, or null if not found
|
* @return The optional PrivilegedUser with their permissions
|
||||||
*/
|
*/
|
||||||
@Select("""
|
@Select("""
|
||||||
SELECT privileged_users.id, privileged_users.uuid, privileges.privileges as permission
|
SELECT id, uuid
|
||||||
FROM privileged_users
|
FROM privileged_users
|
||||||
LEFT JOIN privileges ON privileged_users.id = privileges.user_id
|
WHERE uuid = #{uuid}
|
||||||
WHERE privileged_users.uuid = #{uuid}
|
""")
|
||||||
""")
|
|
||||||
@Results({
|
@Results({
|
||||||
@Result(property = "id", column = "id"),
|
@Result(property = "id", column = "id"),
|
||||||
@Result(property = "uuid", column = "uuid"),
|
@Result(property = "uuid", column = "uuid"),
|
||||||
@Result(property = "permissions", column = "id", javaType = List.class,
|
@Result(property = "permissions", column = "id", javaType = List.class,
|
||||||
many = @Many(select = "getPermissionsForUser"))
|
many = @Many(select = "getPermissionsForUser"))
|
||||||
})
|
})
|
||||||
PrivilegedUser getUserByUuid(@Param("uuid") String uuid);
|
Optional<PrivilegedUser> getUserByUuid(@Param("uuid") UUID uuid);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all privileged users with their permissions
|
* Retrieves all privileged users with their permissions
|
||||||
@@ -99,4 +101,11 @@ public interface PrivilegedUserMapper {
|
|||||||
WHERE user_id = #{userId} AND privileges = #{permission}
|
WHERE user_id = #{userId} AND privileges = #{permission}
|
||||||
""")
|
""")
|
||||||
int removePermissionFromUser(@Param("userId") int userId, @Param("permission") String permission);
|
int removePermissionFromUser(@Param("userId") int userId, @Param("permission") String permission);
|
||||||
|
|
||||||
|
@Insert("""
|
||||||
|
INSERT INTO privileged_users (uuid)
|
||||||
|
VALUES (#{uuid})
|
||||||
|
""")
|
||||||
|
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
|
||||||
|
int createPrivilegedUser(UUID uuid);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.alttd.altitudeweb.database.web_db.forms;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record Appeal(
|
||||||
|
UUID id,
|
||||||
|
UUID uuid,
|
||||||
|
String historyType,
|
||||||
|
Integer historyId,
|
||||||
|
String username,
|
||||||
|
String reason,
|
||||||
|
Instant createdAt,
|
||||||
|
Instant sendAt,
|
||||||
|
String email,
|
||||||
|
Long assignedTo
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.alttd.altitudeweb.database.web_db.forms;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.Insert;
|
||||||
|
import org.apache.ibatis.annotations.Select;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface AppealMapper {
|
||||||
|
|
||||||
|
@Insert("""
|
||||||
|
INSERT INTO appeals (uuid, username, historyType, historyId, reason, created_at, send_at, e_mail, assigned_to)
|
||||||
|
VALUES (#{uuid}, #{username}, #{historyType}, #{historyId}, #{reason}, #{createdAt}, #{sendAt}, #{email}, #{assignedTo})
|
||||||
|
""")
|
||||||
|
void createAppeal(Appeal appeal);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT id, uuid, historyType, historyId, reason, created_at AS createdAt, send_at AS sendAt, e_mail AS email, assigned_to AS assignedTo
|
||||||
|
FROM appeals
|
||||||
|
WHERE id = #{id}
|
||||||
|
""")
|
||||||
|
Appeal getAppealById(int id);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT id, uuid, historyType, historyId, reason, created_at AS createdAt, send_at AS sendAt, e_mail AS email, assigned_to AS assignedTo
|
||||||
|
FROM appeals
|
||||||
|
WHERE uuid = #{uuid}
|
||||||
|
""")
|
||||||
|
List<Appeal> getAppealsByUuid(String uuid);
|
||||||
|
}
|
||||||
+16
@@ -0,0 +1,16 @@
|
|||||||
|
package com.alttd.altitudeweb.database.web_db.mail;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record EmailVerification(
|
||||||
|
UUID id,
|
||||||
|
UUID userUuid,
|
||||||
|
String email,
|
||||||
|
String verificationCode,
|
||||||
|
boolean verified,
|
||||||
|
Instant createdAt,
|
||||||
|
Instant verifiedAt,
|
||||||
|
Instant lastSentAt
|
||||||
|
) {
|
||||||
|
}
|
||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
package com.alttd.altitudeweb.database.web_db.mail;
|
||||||
|
|
||||||
|
import org.apache.ibatis.annotations.*;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public interface EmailVerificationMapper {
|
||||||
|
|
||||||
|
@Insert("""
|
||||||
|
INSERT INTO user_emails (id, user_uuid, email, verification_code, verified, created_at, last_sent_at)
|
||||||
|
VALUES (#{id}, #{userUuid}, #{email}, #{verificationCode}, #{verified}, #{createdAt}, #{lastSentAt})
|
||||||
|
""")
|
||||||
|
void insert(EmailVerification emailVerification);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT id, user_uuid AS userUuid, email, verification_code AS verificationCode, verified,
|
||||||
|
created_at AS createdAt, verified_at AS verifiedAt, last_sent_at AS lastSentAt
|
||||||
|
FROM user_emails
|
||||||
|
WHERE user_uuid = #{userUuid} AND email = #{email}
|
||||||
|
""")
|
||||||
|
EmailVerification findByUserAndEmail(@Param("userUuid") UUID userUuid, @Param("email") String email);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT id, user_uuid AS userUuid, email, verification_code AS verificationCode, verified,
|
||||||
|
created_at AS createdAt, verified_at AS verifiedAt, last_sent_at AS lastSentAt
|
||||||
|
FROM user_emails
|
||||||
|
WHERE user_uuid = #{userUuid} AND verification_code = #{code}
|
||||||
|
ORDER BY created_at DESC LIMIT 1
|
||||||
|
""")
|
||||||
|
EmailVerification findByUserAndCode(@Param("userUuid") UUID userUuid, @Param("code") String code);
|
||||||
|
|
||||||
|
@Select("""
|
||||||
|
SELECT id, user_uuid AS userUuid, email, verification_code AS verificationCode, verified,
|
||||||
|
created_at AS createdAt, verified_at AS verifiedAt, last_sent_at AS lastSentAt
|
||||||
|
FROM user_emails
|
||||||
|
WHERE user_uuid = #{userUuid}
|
||||||
|
ORDER BY created_at ASC
|
||||||
|
""")
|
||||||
|
java.util.List<EmailVerification> findAllByUser(@Param("userUuid") UUID userUuid);
|
||||||
|
|
||||||
|
@Update("""
|
||||||
|
UPDATE user_emails SET verified = 1, verified_at = #{verifiedAt}
|
||||||
|
WHERE id = #{id}
|
||||||
|
""")
|
||||||
|
void markVerified(@Param("id") UUID id, @Param("verifiedAt") Instant verifiedAt);
|
||||||
|
|
||||||
|
@Update("""
|
||||||
|
UPDATE user_emails SET verification_code = #{code}, last_sent_at = #{lastSentAt}, verified = 0, verified_at = NULL
|
||||||
|
WHERE id = #{id}
|
||||||
|
""")
|
||||||
|
void updateCodeAndLastSent(@Param("id") UUID id, @Param("code") String code, @Param("lastSentAt") Instant lastSentAt);
|
||||||
|
|
||||||
|
@Delete("""
|
||||||
|
DELETE FROM user_emails WHERE user_uuid = #{userUuid} AND email = #{email}
|
||||||
|
""")
|
||||||
|
void deleteByUserAndEmail(@Param("userUuid") UUID userUuid, @Param("email") String email);
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package com.alttd.altitudeweb.setup;
|
|||||||
import com.alttd.altitudeweb.database.Databases;
|
import com.alttd.altitudeweb.database.Databases;
|
||||||
import com.alttd.altitudeweb.database.web_db.DatabaseSettings;
|
import com.alttd.altitudeweb.database.web_db.DatabaseSettings;
|
||||||
import com.alttd.altitudeweb.database.web_db.SettingsMapper;
|
import com.alttd.altitudeweb.database.web_db.SettingsMapper;
|
||||||
|
import com.alttd.altitudeweb.type_handler.UUIDTypeHandler;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.ibatis.datasource.pooled.PooledDataSource;
|
import org.apache.ibatis.datasource.pooled.PooledDataSource;
|
||||||
import org.apache.ibatis.mapping.Environment;
|
import org.apache.ibatis.mapping.Environment;
|
||||||
@@ -13,6 +14,7 @@ import org.apache.ibatis.session.SqlSessionFactoryBuilder;
|
|||||||
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
|
import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
|
||||||
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.UUID;
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@@ -33,6 +35,7 @@ public class Connection {
|
|||||||
InitializeWebDb.init();
|
InitializeWebDb.init();
|
||||||
InitializeLiteBans.init();
|
InitializeLiteBans.init();
|
||||||
InitializeLuckPerms.init();
|
InitializeLuckPerms.init();
|
||||||
|
InitializeDiscord.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
@FunctionalInterface
|
@FunctionalInterface
|
||||||
@@ -97,10 +100,20 @@ public class Connection {
|
|||||||
sqlSessionFactory = createSqlSessionFactory(settings, addMappers);
|
sqlSessionFactory = createSqlSessionFactory(settings, addMappers);
|
||||||
}
|
}
|
||||||
|
|
||||||
try (SqlSession session = sqlSessionFactory.openSession()) {
|
SqlSession session = null;
|
||||||
|
try {
|
||||||
|
session = sqlSessionFactory.openSession();
|
||||||
consumer.accept(session);
|
consumer.accept(session);
|
||||||
|
session.commit();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
if (session != null) {
|
||||||
|
session.rollback();
|
||||||
|
}
|
||||||
log.error("Failed to run query", e);
|
log.error("Failed to run query", e);
|
||||||
|
} finally {
|
||||||
|
if (session != null) {
|
||||||
|
session.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
@@ -114,6 +127,7 @@ public class Connection {
|
|||||||
dataSource.setPassword(settings.password());
|
dataSource.setPassword(settings.password());
|
||||||
Environment environment = new Environment("production", new JdbcTransactionFactory(), dataSource);
|
Environment environment = new Environment("production", new JdbcTransactionFactory(), dataSource);
|
||||||
Configuration configuration = new Configuration(environment);
|
Configuration configuration = new Configuration(environment);
|
||||||
|
configuration.getTypeHandlerRegistry().register(UUID.class, UUIDTypeHandler.class);
|
||||||
addMappers.apply(configuration);
|
addMappers.apply(configuration);
|
||||||
|
|
||||||
return new SqlSessionFactoryBuilder().build(configuration);
|
return new SqlSessionFactoryBuilder().build(configuration);
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package com.alttd.altitudeweb.setup;
|
||||||
|
|
||||||
|
import com.alttd.altitudeweb.database.Databases;
|
||||||
|
import com.alttd.altitudeweb.database.discord.OutputChannelMapper;
|
||||||
|
import com.alttd.altitudeweb.database.luckperms.TeamMemberMapper;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class InitializeDiscord {
|
||||||
|
|
||||||
|
protected static void init() {
|
||||||
|
log.info("Initializing Discord");
|
||||||
|
Connection.getConnection(Databases.DISCORD, (configuration) -> {
|
||||||
|
configuration.addMapper(OutputChannelMapper.class);
|
||||||
|
}).join();
|
||||||
|
log.debug("Initialized Discord");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,7 +2,9 @@ package com.alttd.altitudeweb.setup;
|
|||||||
|
|
||||||
import com.alttd.altitudeweb.database.Databases;
|
import com.alttd.altitudeweb.database.Databases;
|
||||||
import com.alttd.altitudeweb.database.web_db.KeyPairMapper;
|
import com.alttd.altitudeweb.database.web_db.KeyPairMapper;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.PrivilegedUserMapper;
|
||||||
import com.alttd.altitudeweb.database.web_db.SettingsMapper;
|
import com.alttd.altitudeweb.database.web_db.SettingsMapper;
|
||||||
|
import com.alttd.altitudeweb.database.web_db.forms.AppealMapper;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.ibatis.session.SqlSession;
|
import org.apache.ibatis.session.SqlSession;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
@@ -18,12 +20,17 @@ public class InitializeWebDb {
|
|||||||
Connection.getConnection(Databases.DEFAULT, (configuration) -> {
|
Connection.getConnection(Databases.DEFAULT, (configuration) -> {
|
||||||
configuration.addMapper(SettingsMapper.class);
|
configuration.addMapper(SettingsMapper.class);
|
||||||
configuration.addMapper(KeyPairMapper.class);
|
configuration.addMapper(KeyPairMapper.class);
|
||||||
|
configuration.addMapper(PrivilegedUserMapper.class);
|
||||||
|
configuration.addMapper(AppealMapper.class);
|
||||||
|
configuration.addMapper(com.alttd.altitudeweb.database.web_db.mail.EmailVerificationMapper.class);
|
||||||
}).join()
|
}).join()
|
||||||
.runQuery(SqlSession -> {
|
.runQuery(sqlSession -> {
|
||||||
createSettingsTable(SqlSession);
|
createSettingsTable(sqlSession);
|
||||||
createKeyTable(SqlSession);
|
createKeyTable(sqlSession);
|
||||||
createPrivilegedUsersTable(SqlSession);
|
createPrivilegedUsersTable(sqlSession);
|
||||||
createPrivilegesTable(SqlSession);
|
createPrivilegesTable(sqlSession);
|
||||||
|
createAppealTable(sqlSession);
|
||||||
|
createUserEmailsTable(sqlSession);
|
||||||
});
|
});
|
||||||
log.debug("Initialized WebDb");
|
log.debug("Initialized WebDb");
|
||||||
}
|
}
|
||||||
@@ -68,7 +75,7 @@ public class InitializeWebDb {
|
|||||||
String query = """
|
String query = """
|
||||||
CREATE TABLE IF NOT EXISTS privileged_users (
|
CREATE TABLE IF NOT EXISTS privileged_users (
|
||||||
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
uuid VARCHAR(36) NOT NULL
|
uuid UUID UNIQUE NOT NULL
|
||||||
);
|
);
|
||||||
""";
|
""";
|
||||||
try (Statement statement = sqlSession.getConnection().createStatement()) {
|
try (Statement statement = sqlSession.getConnection().createStatement()) {
|
||||||
@@ -97,4 +104,48 @@ public class InitializeWebDb {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void createUserEmailsTable(@NotNull SqlSession sqlSession) {
|
||||||
|
String query = """
|
||||||
|
CREATE TABLE IF NOT EXISTS user_emails (
|
||||||
|
id UUID NOT NULL DEFAULT (UUID()) PRIMARY KEY,
|
||||||
|
user_uuid UUID NOT NULL,
|
||||||
|
email VARCHAR(255) NOT NULL,
|
||||||
|
verification_code VARCHAR(16) NOT NULL,
|
||||||
|
verified BOOLEAN NOT NULL DEFAULT 0,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
verified_at TIMESTAMP NULL,
|
||||||
|
last_sent_at TIMESTAMP NULL,
|
||||||
|
FOREIGN KEY (user_uuid) REFERENCES privileged_users(uuid) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
""";
|
||||||
|
try (Statement statement = sqlSession.getConnection().createStatement()) {
|
||||||
|
statement.execute(query);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void createAppealTable(@NotNull SqlSession sqlSession) {
|
||||||
|
String query = """
|
||||||
|
CREATE TABLE IF NOT EXISTS appeals (
|
||||||
|
id UUID NOT NULL DEFAULT (UUID()) PRIMARY KEY,
|
||||||
|
uuid UUID NOT NULL,
|
||||||
|
historyType VARCHAR(16) NOT NULL,
|
||||||
|
historyId BIGINT UNSIGNED NOT NULL,
|
||||||
|
username VARCHAR(16) NOT NULL,
|
||||||
|
reason TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
send_at TIMESTAMP NULL,
|
||||||
|
e_mail TEXT NOT NULL,
|
||||||
|
assigned_to BIGINT UNSIGNED NULL,
|
||||||
|
FOREIGN KEY (uuid) REFERENCES privileged_users(uuid) ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
""";
|
||||||
|
try (Statement statement = sqlSession.getConnection().createStatement()) {
|
||||||
|
statement.execute(query);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
plugins {
|
||||||
|
id("java")
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "com.alttd.webinterface"
|
||||||
|
version = "unspecified"
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
testImplementation(platform("org.junit:junit-bom:5.10.0"))
|
||||||
|
testImplementation("org.junit.jupiter:junit-jupiter")
|
||||||
|
// JDA
|
||||||
|
implementation("net.dv8tion:JDA:6.0.0-rc.2") {
|
||||||
|
exclude("opus-java") // exclude audio
|
||||||
|
exclude("tink") // exclude audio
|
||||||
|
}
|
||||||
|
compileOnly("org.projectlombok:lombok:1.18.38")
|
||||||
|
annotationProcessor("org.projectlombok:lombok:1.18.38")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.test {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.alttd.webinterface;
|
||||||
|
|
||||||
|
import com.alttd.webinterface.bot.DiscordBotInstance;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class DiscordBot {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
String discordToken = System.getProperty("DISCORD_TOKEN");
|
||||||
|
if (discordToken == null) {
|
||||||
|
log.error("Discord token not found, put it in the DISCORD_TOKEN environment variable");
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
DiscordBotInstance discordBotInstance = new DiscordBotInstance();
|
||||||
|
discordBotInstance.start(discordToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.alttd.webinterface.bot;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.dv8tion.jda.api.JDA;
|
||||||
|
import net.dv8tion.jda.api.JDABuilder;
|
||||||
|
import net.dv8tion.jda.api.requests.GatewayIntent;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class DiscordBotInstance {
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private JDA jda;
|
||||||
|
private volatile boolean ready = false;
|
||||||
|
|
||||||
|
public synchronized void start(String token) {
|
||||||
|
if (jda != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
jda = JDABuilder.createDefault(token,
|
||||||
|
GatewayIntent.GUILD_MEMBERS,
|
||||||
|
GatewayIntent.GUILD_PRESENCES,
|
||||||
|
GatewayIntent.GUILD_MESSAGES,
|
||||||
|
GatewayIntent.MESSAGE_CONTENT)
|
||||||
|
.addEventListeners(new ReadyListener(() -> {
|
||||||
|
ready = true;
|
||||||
|
}))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isReady() {
|
||||||
|
return ready && jda != null && jda.getStatus() == JDA.Status.CONNECTED;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.alttd.webinterface.bot;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.dv8tion.jda.api.events.session.ReadyEvent;
|
||||||
|
import net.dv8tion.jda.api.hooks.ListenerAdapter;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class ReadyListener extends ListenerAdapter {
|
||||||
|
|
||||||
|
private final Runnable onReadyCallback;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onReady(@NotNull ReadyEvent event) {
|
||||||
|
log.info("JDA is ready. Guilds loaded: {}", event.getJDA().getGuilds().size());
|
||||||
|
if (onReadyCallback != null) {
|
||||||
|
try {
|
||||||
|
onReadyCallback.run();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Error running onReady callback", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
package com.alttd.webinterface.send_message;
|
||||||
|
|
||||||
|
import com.alttd.webinterface.bot.DiscordBotInstance;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import net.dv8tion.jda.api.EmbedBuilder;
|
||||||
|
import net.dv8tion.jda.api.entities.MessageEmbed;
|
||||||
|
import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
|
||||||
|
|
||||||
|
import java.awt.*;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
public class DiscordSender {
|
||||||
|
|
||||||
|
private static final DiscordSender INSTANCE = new DiscordSender();
|
||||||
|
|
||||||
|
private final DiscordBotInstance botInstance = new DiscordBotInstance();
|
||||||
|
|
||||||
|
private DiscordSender() {}
|
||||||
|
|
||||||
|
public static DiscordSender getInstance() {
|
||||||
|
return INSTANCE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureStarted() {
|
||||||
|
if (botInstance.getJda() != null) return;
|
||||||
|
String token = Optional.ofNullable(System.getenv("DISCORD_TOKEN"))
|
||||||
|
.orElse(System.getProperty("DISCORD_TOKEN"));
|
||||||
|
if (token == null || token.isBlank()) {
|
||||||
|
log.error("Discord token not found. Set DISCORD_TOKEN as an environment variable or system property.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
botInstance.start(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendMessageToChannels(List<Long> channelIds, String message) {
|
||||||
|
ensureStarted();
|
||||||
|
if (botInstance.getJda() == null) {
|
||||||
|
log.error("JDA not initialized; cannot send Discord message.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (!botInstance.isReady()) {
|
||||||
|
botInstance.getJda().awaitReady();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Error while waiting for JDA ready state", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
channelIds.stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.forEach(id -> {
|
||||||
|
TextChannel channel = botInstance.getJda().getChannelById(TextChannel.class, id);
|
||||||
|
if (channel == null) {
|
||||||
|
log.warn("TextChannel with id {} not found", id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
channel.sendMessage(message).queue(
|
||||||
|
success -> log.debug("Sent message to channel {}", id),
|
||||||
|
error -> log.error("Failed sending message to channel {}", id, error)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendEmbedToChannels(List<Long> channelIds, String title, String description, List<EmbedField> fields,
|
||||||
|
Integer colorRgb, Instant timestamp, String footer) {
|
||||||
|
ensureStarted();
|
||||||
|
if (botInstance.getJda() == null) {
|
||||||
|
log.error("JDA not initialized; cannot send Discord embed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (!botInstance.isReady()) {
|
||||||
|
botInstance.getJda().awaitReady();
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("Error while waiting for JDA ready state", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
EmbedBuilder eb = new EmbedBuilder();
|
||||||
|
if (title != null && !title.isBlank()) eb.setTitle(title);
|
||||||
|
if (description != null && !description.isBlank()) eb.setDescription(description);
|
||||||
|
if (colorRgb != null) eb.setColor(new Color(colorRgb)); else eb.setColor(new Color(0xFF8C00)); // default orange
|
||||||
|
eb.setTimestamp(timestamp != null ? timestamp : Instant.now());
|
||||||
|
if (footer != null && !footer.isBlank()) eb.setFooter(footer);
|
||||||
|
|
||||||
|
if (fields != null) {
|
||||||
|
for (EmbedField f : fields) {
|
||||||
|
if (f == null) continue;
|
||||||
|
String name = f.getName() == null ? "" : f.getName();
|
||||||
|
String value = f.getValue() == null ? "" : f.getValue();
|
||||||
|
// JDA field value max is 1024; truncate to be safe
|
||||||
|
if (value.length() > 1024) value = value.substring(0, 1021) + "...";
|
||||||
|
eb.addField(new MessageEmbed.Field(name, value, f.isInline()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageEmbed embed = eb.build();
|
||||||
|
|
||||||
|
channelIds.stream()
|
||||||
|
.filter(Objects::nonNull)
|
||||||
|
.forEach(id -> {
|
||||||
|
TextChannel channel = botInstance.getJda().getChannelById(TextChannel.class, id);
|
||||||
|
if (channel == null) {
|
||||||
|
log.warn("TextChannel with id {} not found", id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
channel.sendMessageEmbeds(embed).queue(
|
||||||
|
success -> log.debug("Sent embed to channel {}", id),
|
||||||
|
error -> log.error("Failed sending embed to channel {}", id, error)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class EmbedField {
|
||||||
|
private String name;
|
||||||
|
private String value;
|
||||||
|
private boolean inline;
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
-13
@@ -15,11 +15,12 @@
|
|||||||
"prefix": "app",
|
"prefix": "app",
|
||||||
"architect": {
|
"architect": {
|
||||||
"build": {
|
"build": {
|
||||||
"builder": "@angular-devkit/build-angular:browser",
|
"builder": "@angular/build:application",
|
||||||
"options": {
|
"options": {
|
||||||
"outputPath": "dist",
|
"outputPath": {
|
||||||
|
"base": "dist"
|
||||||
|
},
|
||||||
"index": "src/index.html",
|
"index": "src/index.html",
|
||||||
"main": "src/main.ts",
|
|
||||||
"polyfills": [
|
"polyfills": [
|
||||||
"zone.js"
|
"zone.js"
|
||||||
],
|
],
|
||||||
@@ -34,7 +35,8 @@
|
|||||||
"styles": [
|
"styles": [
|
||||||
"src/styles.scss"
|
"src/styles.scss"
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": [],
|
||||||
|
"browser": "src/main.ts"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
@@ -47,9 +49,7 @@
|
|||||||
"optimization": true,
|
"optimization": true,
|
||||||
"outputHashing": "all",
|
"outputHashing": "all",
|
||||||
"sourceMap": false,
|
"sourceMap": false,
|
||||||
"namedChunks": false,
|
"namedChunks": false
|
||||||
"vendorChunk": false,
|
|
||||||
"buildOptimizer": true
|
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
@@ -71,14 +71,12 @@
|
|||||||
"optimization": true,
|
"optimization": true,
|
||||||
"outputHashing": "all",
|
"outputHashing": "all",
|
||||||
"sourceMap": false,
|
"sourceMap": false,
|
||||||
"namedChunks": false,
|
"namedChunks": false
|
||||||
"vendorChunk": false,
|
|
||||||
"buildOptimizer": true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular-devkit/build-angular:dev-server",
|
"builder": "@angular/build:dev-server",
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"buildTarget": "frontend:build:production"
|
"buildTarget": "frontend:build:production"
|
||||||
@@ -90,10 +88,10 @@
|
|||||||
"defaultConfiguration": "development"
|
"defaultConfiguration": "development"
|
||||||
},
|
},
|
||||||
"extract-i18n": {
|
"extract-i18n": {
|
||||||
"builder": "@angular-devkit/build-angular:extract-i18n"
|
"builder": "@angular/build:extract-i18n"
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"builder": "@angular-devkit/build-angular:karma",
|
"builder": "@angular/build:karma",
|
||||||
"options": {
|
"options": {
|
||||||
"polyfills": [
|
"polyfills": [
|
||||||
"zone.js",
|
"zone.js",
|
||||||
@@ -119,5 +117,31 @@
|
|||||||
},
|
},
|
||||||
"cli": {
|
"cli": {
|
||||||
"analytics": false
|
"analytics": false
|
||||||
|
},
|
||||||
|
"schematics": {
|
||||||
|
"@schematics/angular:component": {
|
||||||
|
"type": "component"
|
||||||
|
},
|
||||||
|
"@schematics/angular:directive": {
|
||||||
|
"type": "directive"
|
||||||
|
},
|
||||||
|
"@schematics/angular:service": {
|
||||||
|
"type": "service"
|
||||||
|
},
|
||||||
|
"@schematics/angular:guard": {
|
||||||
|
"typeSeparator": "."
|
||||||
|
},
|
||||||
|
"@schematics/angular:interceptor": {
|
||||||
|
"typeSeparator": "."
|
||||||
|
},
|
||||||
|
"@schematics/angular:module": {
|
||||||
|
"typeSeparator": "."
|
||||||
|
},
|
||||||
|
"@schematics/angular:pipe": {
|
||||||
|
"typeSeparator": "."
|
||||||
|
},
|
||||||
|
"@schematics/angular:resolver": {
|
||||||
|
"typeSeparator": "."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-27
@@ -8,8 +8,9 @@ plugins {
|
|||||||
|
|
||||||
node {
|
node {
|
||||||
download.set(true)
|
download.set(true)
|
||||||
version.set("22.14.0")
|
// Update to the version that's compatible with your environment requirements
|
||||||
npmVersion.set("10.9.2")
|
version.set("20.19.0")
|
||||||
|
npmVersion.set("10.2.3") // A compatible npm version for Node.js 20.19.0
|
||||||
workDir.set(file("${project.projectDir}/node"))
|
workDir.set(file("${project.projectDir}/node"))
|
||||||
npmWorkDir.set(file("${project.projectDir}/node"))
|
npmWorkDir.set(file("${project.projectDir}/node"))
|
||||||
}
|
}
|
||||||
@@ -21,38 +22,37 @@ tasks.register<Delete>("cleanDist") {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create a task that will run npm build
|
// Create a task that will run npm build
|
||||||
tasks.register("npmBuild") {
|
tasks.register<com.github.gradle.node.npm.task.NpmTask>("npmBuild") {
|
||||||
description = "Run 'npm run build'"
|
description = "Run 'npm run build'"
|
||||||
group = "build"
|
group = "build"
|
||||||
|
|
||||||
doLast {
|
// Determine which build script to run based on the OS
|
||||||
// Use nodeCommand directly from the plugin
|
val isWindows = System.getProperty("os.name").lowercase().contains("windows")
|
||||||
project.exec {
|
npmCommand.set(listOf("run", if (isWindows) "build:dev" else "build:beta"))
|
||||||
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")
|
dependsOn("npmInstall")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a new task to check Node.js and npm versions
|
||||||
|
tasks.register<com.github.gradle.node.task.NodeTask>("nodeVersionCheck") {
|
||||||
|
description = "Check Node.js and npm versions"
|
||||||
|
script.set(file("${projectDir}/node-version-check.js"))
|
||||||
|
|
||||||
|
doFirst {
|
||||||
|
// Create a temporary script to check versions
|
||||||
|
file("${projectDir}/node-version-check.js").writeText("""
|
||||||
|
console.log('Node.js version:', process.version);
|
||||||
|
console.log('npm version:', require('npm/package.json').version);
|
||||||
|
console.log('Build command that would be used:', process.platform === 'win32' ? 'build:dev' : 'build:beta');
|
||||||
|
""".trimIndent())
|
||||||
|
}
|
||||||
|
|
||||||
|
doLast {
|
||||||
|
// Clean up the temporary script
|
||||||
|
delete("${projectDir}/node-version-check.js")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tasks.named("assemble") {
|
tasks.named("assemble") {
|
||||||
dependsOn("npmBuild")
|
dependsOn("npmBuild")
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-14
@@ -13,24 +13,27 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/cdk": "^19.2.18",
|
"@angular/cdk": "^20.1.3",
|
||||||
"@angular/common": "^19.2.0",
|
"@angular/common": "^20.1.0",
|
||||||
"@angular/compiler": "^19.2.0",
|
"@angular/compiler": "^20.1.0",
|
||||||
"@angular/core": "^19.2.0",
|
"@angular/core": "^20.1.0",
|
||||||
"@angular/forms": "^19.2.0",
|
"@angular/forms": "^20.1.0",
|
||||||
"@angular/material": "^19.2.18",
|
"@angular/material": "^20.1.3",
|
||||||
"@angular/platform-browser": "^19.2.0",
|
"@angular/platform-browser": "^20.1.0",
|
||||||
"@angular/platform-browser-dynamic": "^19.2.0",
|
"@angular/platform-browser-dynamic": "^20.1.0",
|
||||||
"@angular/router": "^19.2.0",
|
"@angular/router": "^20.1.0",
|
||||||
"ngx-cookie-service": "^19.1.2",
|
"@auth0/angular-jwt": "^5.2.0",
|
||||||
|
"@types/three": "^0.177.0",
|
||||||
|
"ngx-cookie-service": "^20.0.1",
|
||||||
"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"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-devkit/build-angular": "^19.2.5",
|
"@angular/build": "^20.1.0",
|
||||||
"@angular/cli": "^19.2.5",
|
"@angular/cli": "^20.1.0",
|
||||||
"@angular/compiler-cli": "^19.2.0",
|
"@angular/compiler-cli": "^20.1.0",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
"jasmine-core": "~5.6.0",
|
"jasmine-core": "~5.6.0",
|
||||||
"karma": "~6.4.0",
|
"karma": "~6.4.0",
|
||||||
@@ -38,6 +41,6 @@
|
|||||||
"karma-coverage": "~2.2.0",
|
"karma-coverage": "~2.2.0",
|
||||||
"karma-jasmine": "~5.1.0",
|
"karma-jasmine": "~5.1.0",
|
||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
"typescript": "~5.7.2"
|
"typescript": "^5.8.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
@@ -1,21 +0,0 @@
|
|||||||
import {Component} from '@angular/core';
|
|
||||||
import {ScrollService} from '../scroll/scroll.service';
|
|
||||||
import {CommonModule} from '@angular/common';
|
|
||||||
import {HeaderComponent} from '../header/header.component';
|
|
||||||
import {RemoveTrailingPeriodPipe} from "../util/RemoveTrailingPeriodPipe";
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-about',
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
HeaderComponent,
|
|
||||||
RemoveTrailingPeriodPipe
|
|
||||||
],
|
|
||||||
templateUrl: './about.component.html',
|
|
||||||
styleUrl: './about.component.scss'
|
|
||||||
})
|
|
||||||
export class AboutComponent {
|
|
||||||
constructor(public scrollService: ScrollService) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
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 '@custom-types/constant';
|
||||||
import {Router, RouterOutlet} from '@angular/router';
|
import {Router, RouterOutlet} from '@angular/router';
|
||||||
import {FooterComponent} from './footer/footer.component';
|
import {FooterComponent} from '@pages/footer/footer/footer.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -10,8 +10,8 @@ import {FooterComponent} from './footer/footer.component';
|
|||||||
templateUrl: './app.component.html',
|
templateUrl: './app.component.html',
|
||||||
styleUrl: './app.component.scss',
|
styleUrl: './app.component.scss',
|
||||||
imports: [
|
imports: [
|
||||||
FooterComponent,
|
RouterOutlet,
|
||||||
RouterOutlet
|
FooterComponent
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class AppComponent implements OnInit {
|
export class AppComponent implements OnInit {
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
import {ApplicationConfig, provideZoneChangeDetection} from '@angular/core';
|
|
||||||
import {provideRouter} from '@angular/router';
|
|
||||||
|
|
||||||
import {routes} from './app.routes';
|
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
|
||||||
providers: [provideZoneChangeDetection({eventCoalescing: true}), provideRouter(routes)]
|
|
||||||
};
|
|
||||||
@@ -1,110 +1,141 @@
|
|||||||
import {Routes} from '@angular/router';
|
import {Routes} from '@angular/router';
|
||||||
|
import {AuthGuard} from './guards/auth.guard';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: '',
|
path: '',
|
||||||
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
|
loadComponent: () => import('./pages/home/home.component').then(m => m.HomeComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'particles',
|
||||||
|
loadComponent: () => import('./pages/particles/particles.component').then(m => m.ParticlesComponent),
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
data: {
|
||||||
|
requiredAuthorizations: ['SCOPE_head_mod']
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'map',
|
path: 'map',
|
||||||
loadComponent: () => import('./map/map.component').then(m => m.MapComponent)
|
loadComponent: () => import('./pages/features/map/map.component').then(m => m.MapComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'rules',
|
path: 'rules',
|
||||||
loadComponent: () => import('./rules/rules.component').then(m => m.RulesComponent)
|
loadComponent: () => import('./pages/reference/rules/rules.component').then(m => m.RulesComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'vote',
|
path: 'vote',
|
||||||
loadComponent: () => import('./vote/vote.component').then(m => m.VoteComponent)
|
loadComponent: () => import('./pages/vote/vote.component').then(m => m.VoteComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'about',
|
path: 'about',
|
||||||
loadComponent: () => import('./about/about.component').then(m => m.AboutComponent)
|
loadComponent: () => import('./pages/altitude/about/about.component').then(m => m.AboutComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'socials',
|
path: 'socials',
|
||||||
loadComponent: () => import('./socials/socials.component').then(m => m.SocialsComponent)
|
loadComponent: () => import('./pages/altitude/socials/socials.component').then(m => m.SocialsComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'team',
|
path: 'team',
|
||||||
loadComponent: () => import('./team/team.component').then(m => m.TeamComponent)
|
loadComponent: () => import('./pages/altitude/team/team.component').then(m => m.TeamComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'birthdays',
|
path: 'birthdays',
|
||||||
loadComponent: () => import('./birthdays/birthdays.component').then(m => m.BirthdaysComponent)
|
loadComponent: () => import('./pages/altitude/birthdays/birthdays.component').then(m => m.BirthdaysComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'terms',
|
path: 'terms',
|
||||||
loadComponent: () => import('./terms/terms.component').then(m => m.TermsComponent)
|
loadComponent: () => import('./pages/footer/terms/terms.component').then(m => m.TermsComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'privacy',
|
path: 'privacy',
|
||||||
loadComponent: () => import('./privacy/privacy.component').then(m => m.PrivacyComponent)
|
loadComponent: () => import('./pages/footer/privacy/privacy.component').then(m => m.PrivacyComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'bans',
|
path: 'bans',
|
||||||
loadComponent: () => import('./bans/bans.component').then(m => m.BansComponent)
|
loadComponent: () => import('./pages/reference/bans/bans.component').then(m => m.BansComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'bans/:type/:id',
|
path: 'bans/:type/:id',
|
||||||
loadComponent: () => import('./bans/details/details.component').then(m => m.DetailsComponent)
|
loadComponent: () => import('./pages/reference/bans/details/details.component').then(m => m.DetailsComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'economy',
|
path: 'economy',
|
||||||
loadComponent: () => import('./economy/economy.component').then(m => m.EconomyComponent)
|
loadComponent: () => import('./pages/features/economy/economy.component').then(m => m.EconomyComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'claiming',
|
path: 'claiming',
|
||||||
loadComponent: () => import('./claiming/claiming.component').then(m => m.ClaimingComponent)
|
loadComponent: () => import('./pages/features/claiming/claiming.component').then(m => m.ClaimingComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'mypet',
|
path: 'mypet',
|
||||||
loadComponent: () => import('./mypet/mypet.component').then(m => m.MypetComponent)
|
loadComponent: () => import('./pages/features/mypet/mypet.component').then(m => m.MypetComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'warps',
|
path: 'warps',
|
||||||
loadComponent: () => import('./warps/warps.component').then(m => m.WarpsComponent)
|
loadComponent: () => import('./pages/features/warps/warps.component').then(m => m.WarpsComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'skyblock',
|
path: 'skyblock',
|
||||||
loadComponent: () => import('./skyblock/skyblock.component').then(m => m.SkyblockComponent)
|
loadComponent: () => import('./pages/features/skyblock/skyblock.component').then(m => m.SkyblockComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'customfeatures',
|
path: 'customfeatures',
|
||||||
loadComponent: () => import('./customfeatures/customfeatures.component').then(m => m.CustomfeaturesComponent)
|
loadComponent: () => import('./pages/features/customfeatures/customfeatures.component').then(m => m.CustomfeaturesComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'guide',
|
path: 'guide',
|
||||||
loadComponent: () => import('./guide/guide.component').then(m => m.GuideComponent)
|
loadComponent: () => import('./pages/reference/guide/guide.component').then(m => m.GuideComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'ranks',
|
path: 'ranks',
|
||||||
loadComponent: () => import('./ranks/ranks.component').then(m => m.RanksComponent)
|
loadComponent: () => import('./pages/reference/ranks/ranks.component').then(m => m.RanksComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'commandlist',
|
path: 'commandlist',
|
||||||
loadComponent: () => import('./commandlist/commandlist.component').then(m => m.CommandlistComponent)
|
loadComponent: () => import('./pages/reference/commandlist/commandlist.component').then(m => m.CommandlistComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'mapart',
|
path: 'mapart',
|
||||||
loadComponent: () => import('./mapart/mapart.component').then(m => m.MapartComponent)
|
loadComponent: () => import('./pages/reference/mapart/mapart.component').then(m => m.MapartComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'lag',
|
path: 'lag',
|
||||||
loadComponent: () => import('./lag/lag.component').then(m => m.LagComponent)
|
loadComponent: () => import('./pages/reference/lag/lag.component').then(m => m.LagComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'staffpowers',
|
path: 'staffpowers',
|
||||||
loadComponent: () => import('./staffpowers/staffpowers.component').then(m => m.StaffpowersComponent)
|
loadComponent: () => import('./pages/reference/staffpowers/staffpowers.component').then(m => m.StaffpowersComponent)
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'forms/:form',
|
|
||||||
loadComponent: () => import('./forms/forms.component').then(m => m.FormsComponent)
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'forms',
|
path: 'forms',
|
||||||
loadComponent: () => import('./forms/forms.component').then(m => m.FormsComponent)
|
loadComponent: () => import('./pages/forms/forms.component').then(m => m.FormsComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'forms/appeal',
|
||||||
|
loadComponent: () => import('./pages/forms/appeal/appeal.component').then(m => m.AppealComponent),
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
data: {
|
||||||
|
requiredAuthorizations: ['SCOPE_user']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'forms/sent',
|
||||||
|
loadComponent: () => import('./pages/forms/sent/sent.component').then(m => m.SentComponent),
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
data: {
|
||||||
|
requiredAuthorizations: ['SCOPE_user']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'community',
|
||||||
|
loadComponent: () => import('./pages/altitude/community/community.component').then(m => m.CommunityComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'nicknames',
|
||||||
|
loadComponent: () => import('./pages/reference/nicknames/nicknames.component').then(m => m.NicknamesComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'nickgenerator',
|
||||||
|
loadComponent: () => import('./pages/reference/nickgenerator/nickgenerator.component').then(m => m.NickgeneratorComponent)
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,90 +0,0 @@
|
|||||||
<ng-container>
|
|
||||||
<app-header [current_page]="'bans'" height="200px" background_image="/public/img/backgrounds/staff.png"
|
|
||||||
[overlay_gradient]="0.5">>
|
|
||||||
<div class="title" header-content>
|
|
||||||
<h1>Minecraft Punishments</h1>
|
|
||||||
</div>
|
|
||||||
</app-header>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<section class="darkmodeSection">
|
|
||||||
<div class="container">
|
|
||||||
<div class="columnSection">
|
|
||||||
<div class="historyButtonContainer">
|
|
||||||
<div [id]="getCurrentButtonId('all')" class="button-outer" (click)="changeHistoryPunishment('all')">
|
|
||||||
<span class="button-inner"
|
|
||||||
[ngClass]="active">All</span>
|
|
||||||
</div>
|
|
||||||
<div [id]="getCurrentButtonId('ban')" class="button-outer" (click)="changeHistoryPunishment('ban')">
|
|
||||||
<span class="button-inner"
|
|
||||||
[ngClass]="active">Bans</span>
|
|
||||||
</div>
|
|
||||||
<div [id]="getCurrentButtonId('mute')" class="button-outer" (click)="changeHistoryPunishment('mute')">
|
|
||||||
<span class="button-inner"
|
|
||||||
[ngClass]="active">Mutes</span>
|
|
||||||
</div>
|
|
||||||
<div [id]="getCurrentButtonId('warn')" class="button-outer" (click)="changeHistoryPunishment('warn')">
|
|
||||||
<span class="button-inner"
|
|
||||||
[ngClass]="active">Warnings</span>
|
|
||||||
</div>
|
|
||||||
<div [id]="getCurrentUserTypeButtonId('player')" class="button-outer" (click)="changeUserType('player')"
|
|
||||||
style="margin-left: 120px;">
|
|
||||||
<span class="button-inner"
|
|
||||||
[ngClass]="active">Player</span>
|
|
||||||
</div>
|
|
||||||
<div [id]="getCurrentUserTypeButtonId('staff')" class="button-outer" (click)="changeUserType('staff')">
|
|
||||||
<span class="button-inner"
|
|
||||||
[ngClass]="active">Staff</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="historySearchContainer">
|
|
||||||
<input class="historySearch"
|
|
||||||
type="search"
|
|
||||||
placeholder="Search.."
|
|
||||||
[(ngModel)]="searchTerm"
|
|
||||||
(input)="filterNames()"
|
|
||||||
(keyup.enter)="search()"
|
|
||||||
>
|
|
||||||
<div class="dropdown-results" *ngIf="filteredNames.length > 0 && searchTerm">
|
|
||||||
<div
|
|
||||||
class="dropdown-item"
|
|
||||||
*ngFor="let name of filteredNames"
|
|
||||||
(mousedown)="selectName(name)">
|
|
||||||
{{ name }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="historyTable">
|
|
||||||
<app-history [userType]="userType" [punishmentType]="punishmentType"
|
|
||||||
[page]="page" [searchTerm]="finalSearchTerm" (pageChange)="updatePageSize($event)"
|
|
||||||
(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>
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
<ng-container>
|
|
||||||
<app-header [current_page]="'bans'" height="200px" background_image="/public/img/backgrounds/staff.png"
|
|
||||||
[overlay_gradient]="0.5">>
|
|
||||||
<div class="title" header-content>
|
|
||||||
<h1>Minecraft Punishments</h1>
|
|
||||||
</div>
|
|
||||||
</app-header>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
<section class="darkmodeSection">
|
|
||||||
<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>
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<ng-container *ngIf="history.length === 0">
|
|
||||||
<p>No history found</p>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
<ng-container *ngIf="history.length > 0">
|
|
||||||
<table [cellSpacing]="0">
|
|
||||||
<div class="historyTableHead">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="historyType">Type</th>
|
|
||||||
<th class="historyPlayer">Player</th>
|
|
||||||
<th class="historyPlayer">Banned By</th>
|
|
||||||
<th class="historyReason">Reason</th>
|
|
||||||
<th class="historyDate">Date</th>
|
|
||||||
<th class="historyDate">Expires</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<tbody>
|
|
||||||
<tr class="historyPlayerRow" *ngFor="let entry of history">
|
|
||||||
<td class="historyType" (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>
|
|
||||||
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<app-forms [currentPage]="'appeal'" [formTitle]="'Minecraft Appeal'">
|
|
||||||
<div form-content>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</app-forms>
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import {Component, OnInit} from '@angular/core';
|
|
||||||
import {FormsComponent} from '../forms.component';
|
|
||||||
import {FormControl, FormGroup, Validators} from '@angular/forms';
|
|
||||||
import {AppealsService, MinecraftAppeal} from '../../../api';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-appeal',
|
|
||||||
imports: [
|
|
||||||
FormsComponent
|
|
||||||
],
|
|
||||||
templateUrl: './appeal.component.html',
|
|
||||||
styleUrl: './appeal.component.scss'
|
|
||||||
})
|
|
||||||
export class AppealComponent implements OnInit {
|
|
||||||
|
|
||||||
public form: FormGroup<Appeal>;
|
|
||||||
|
|
||||||
constructor(private appealApi: AppealsService) {
|
|
||||||
this.form = new FormGroup({
|
|
||||||
username: new FormControl('', {nonNullable: true, validators: [Validators.required]}),
|
|
||||||
punishmentId: new FormControl('', {nonNullable: true, validators: [Validators.required]}),
|
|
||||||
email: new FormControl('', {nonNullable: true, validators: [Validators.required, Validators.email]}),
|
|
||||||
appeal: new FormControl('', {nonNullable: true, validators: [Validators.required, Validators.minLength(10)]})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public onSubmit() {
|
|
||||||
if (this.form === undefined) {
|
|
||||||
console.error('Form is undefined');
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (this.form.valid) {
|
|
||||||
this.sendForm()
|
|
||||||
} else {
|
|
||||||
// Mark all fields as touched to trigger validation display
|
|
||||||
Object.keys(this.form.controls).forEach(field => {
|
|
||||||
const control = this.form!.get(field);
|
|
||||||
if (!(control instanceof FormGroup)) {
|
|
||||||
console.error('Control [' + control + '] is not a FormGroup');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
control.markAsTouched({onlySelf: true});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sendForm() {
|
|
||||||
const rawValue = this.form.getRawValue();
|
|
||||||
const appeal: MinecraftAppeal = {
|
|
||||||
appeal: rawValue.appeal,
|
|
||||||
email: rawValue.email,
|
|
||||||
punishmentId: parseInt(rawValue.punishmentId),
|
|
||||||
username: rawValue.username,
|
|
||||||
uuid: ''//TODO
|
|
||||||
}
|
|
||||||
this.appealApi.submitMinecraftAppeal(appeal).subscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Appeal {
|
|
||||||
username: FormControl<string>;
|
|
||||||
punishmentId: FormControl<string>;
|
|
||||||
email: FormControl<string>;
|
|
||||||
appeal: FormControl<string>;
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<ng-container>
|
|
||||||
<app-header [current_page]="currentPage" height="200px" background_image="/public/img/backgrounds/staff.png"
|
|
||||||
[overlay_gradient]="0.5">
|
|
||||||
<div class="title" header-content>
|
|
||||||
<h1>{{ formTitle }}</h1>
|
|
||||||
</div>
|
|
||||||
</app-header>
|
|
||||||
<ng-container *ngIf="!type">
|
|
||||||
<ng-container *ngFor="let formType of FormType | keyvalue">
|
|
||||||
<button mat-raised-button (click)="setFormType(formType.value)">
|
|
||||||
{{ formType }}
|
|
||||||
</button>
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
|
||||||
<div>
|
|
||||||
<ng-content select="[form-content]"></ng-content>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import {Component, Input, OnInit} from '@angular/core';
|
|
||||||
import {HeaderComponent} from '../header/header.component';
|
|
||||||
import {MatDialog} from '@angular/material/dialog';
|
|
||||||
import {ActivatedRoute} from '@angular/router';
|
|
||||||
import {LoginDialogComponent} from '../login/login.component';
|
|
||||||
import {KeyValuePipe, NgForOf, NgIf} from '@angular/common';
|
|
||||||
import {FormType} from './form_type';
|
|
||||||
import {MatButton} from '@angular/material/button';
|
|
||||||
import {AuthService} from '../services/auth.service';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-forms',
|
|
||||||
imports: [
|
|
||||||
HeaderComponent,
|
|
||||||
NgIf,
|
|
||||||
NgForOf,
|
|
||||||
MatButton,
|
|
||||||
KeyValuePipe
|
|
||||||
],
|
|
||||||
templateUrl: './forms.component.html',
|
|
||||||
styleUrl: './forms.component.scss'
|
|
||||||
})
|
|
||||||
export class FormsComponent implements OnInit {
|
|
||||||
@Input() formTitle: string = 'Form';
|
|
||||||
@Input() currentPage: string = 'forms';
|
|
||||||
|
|
||||||
public type: FormType | undefined;
|
|
||||||
|
|
||||||
constructor(private authService: AuthService,
|
|
||||||
private dialog: MatDialog,
|
|
||||||
private route: ActivatedRoute,
|
|
||||||
) {
|
|
||||||
this.route.paramMap.subscribe(async params => {
|
|
||||||
const code = params.get('code');
|
|
||||||
|
|
||||||
if (code) {
|
|
||||||
this.authService.login(code).subscribe();
|
|
||||||
} else if (!this.authService.checkAuthStatus()) {
|
|
||||||
const dialogRef = this.dialog.open(LoginDialogComponent, {
|
|
||||||
width: '400px',
|
|
||||||
disableClose: true
|
|
||||||
});
|
|
||||||
|
|
||||||
dialogRef.afterClosed().subscribe();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.route.paramMap.subscribe(params => {
|
|
||||||
switch (params.get('form')) {
|
|
||||||
case FormType.APPEAL:
|
|
||||||
this.type = FormType.APPEAL;
|
|
||||||
this.currentPage = 'appeal';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error("Invalid type");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected readonly FormType = FormType;
|
|
||||||
protected readonly Object = Object;
|
|
||||||
|
|
||||||
public setFormType(formType: FormType) {
|
|
||||||
this.type = formType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree} from '@angular/router';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
|
import {AuthService} from '@services/auth.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AuthGuard implements CanActivate {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private router: Router
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
canActivate(
|
||||||
|
route: ActivatedRouteSnapshot,
|
||||||
|
state: RouterStateSnapshot
|
||||||
|
): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
|
||||||
|
if (!this.authService.checkAuthStatus()) {
|
||||||
|
return this.router.createUrlTree(['/']);
|
||||||
|
}
|
||||||
|
|
||||||
|
const requiredAuthorizations = route.data['requiredAuthorizations'] as string[];
|
||||||
|
|
||||||
|
if (!requiredAuthorizations || requiredAuthorizations.length === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userAuthorizations = this.authService.getUserAuthorizations();
|
||||||
|
const hasAccess = requiredAuthorizations.some(auth => userAuthorizations.includes(auth));
|
||||||
|
|
||||||
|
if (!hasAccess) {
|
||||||
|
return this.router.createUrlTree(['/']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
<ng-container>
|
|
||||||
<app-header [current_page]="'home'" height="100vh"
|
|
||||||
[background_image]="'/public/img/backgrounds/120spawn-min.png'">
|
|
||||||
<div class="title" header-content>
|
|
||||||
<h1 style="display: none;">Altitude</h1>
|
|
||||||
<img id="header-img" ngSrc="/public/img/logos/logo.png" alt="The Altitude Minecraft Server" height="319"
|
|
||||||
width="550">
|
|
||||||
<h2 style="font-size: 2.5em;" id="homeh2">Altitude now on {{ ALTITUDE_VERSION }}!</h2>
|
|
||||||
<a id="scroll-button" (click)="scrollToSection()">
|
|
||||||
<span></span>
|
|
||||||
<p style="display: none;">Scroll Down</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</app-header>
|
|
||||||
<main>
|
|
||||||
<section id="scrollingpoint" style="background: #202020; text-align: center; padding: 80px 0;">
|
|
||||||
<!-- TODO load player count from old api or backend?-->
|
|
||||||
<h2 style="color: white;"><span class="player-count">Loading...</span></h2>
|
|
||||||
<h2 style="color: white;">Server IP: play.alttd.com</h2>
|
|
||||||
<div style="padding-top: 35px;">
|
|
||||||
<app-copy-ip></app-copy-ip>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="darkmodeSection">
|
|
||||||
<div class="container">
|
|
||||||
<div class="paragraph">
|
|
||||||
<h2>Adventure Begins</h2>
|
|
||||||
<p>You awake in a strange town, where are you? There are residents running about trading with each other and
|
|
||||||
stories of distant realms with more towns. It's time to write your story. Welcome to Altitude, the laid-back
|
|
||||||
community-oriented server that hosts your home for Minecraft.</p>
|
|
||||||
</div>
|
|
||||||
<img ngSrc="/public/img/items/bookquill.png" style="width: 150px; align-self: center; margin: 0 auto;"
|
|
||||||
alt="Alternative Altitude Server Logo"
|
|
||||||
height="150" width="150">
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<!--
|
|
||||||
<section id="section1">
|
|
||||||
<div class="container" id="video">
|
|
||||||
<h2 style="display: none;">YouTube Trailer</h2>
|
|
||||||
<div style="border-radius:5px;overflow:hidden;position:relative;width:100%">
|
|
||||||
<img style="width: 100%" src="https://img.youtube.com/vi/Nzbj9Dbv5Wk/maxresdefault.jpg" alt="Altitude YouTube Trailer">
|
|
||||||
<div id="youtube">
|
|
||||||
<div class="play" onclick="playVideo()"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
-->
|
|
||||||
<section class="darkmodeSection">
|
|
||||||
<div class="container" style="padding: 10px 0 80px 0">
|
|
||||||
<iframe id="discord-widget" src="https://discordapp.com/widget?id=141644560005595136&theme=dark"></iframe>
|
|
||||||
<div class="paragraph" id="discordp">
|
|
||||||
<h2>Meet the Community</h2>
|
|
||||||
<p>Altitude is home to players young and old from all around the globe, and here, everyone is family. Altitude
|
|
||||||
is your place to get together with friends and relax - and maybe enjoy some survival too. Altitude is
|
|
||||||
intended for older players, but all are welcome!</p>
|
|
||||||
<p>Don't have Discord? Keep up with news and announcements at the <a href="alttd.com/blog">blog</a>.</p>
|
|
||||||
<div style="display: flex; justify-content: center;">
|
|
||||||
<a target="_blank" rel="noopener" href="https://discordapp.com/invite/TGqpzCJ">
|
|
||||||
<div style="margin-top: 30px;" class="button-outer">
|
|
||||||
<span class="button-inner">Join Our Discord</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section id="section2">
|
|
||||||
<div class="customContainer">
|
|
||||||
<h2 style="color: white; padding-bottom: 35px; font-size: 2.8em;">Survival Shaped by You</h2>
|
|
||||||
<p style="color: white; padding-bottom: 35px; font-size: 1.1em; margin: auto;">Altitude is built by the
|
|
||||||
community, for the community. We've added features requested by our members and several custom plugins to
|
|
||||||
create our "perfect" survival experience.</p>
|
|
||||||
<div class="survivalShapedContainerPlugins">
|
|
||||||
<div class="pluginColumn">
|
|
||||||
<h2>McMMO & MyPet</h2>
|
|
||||||
<p>Two of the most requested plugins on Altitude, level up yourself and your pet with these MMO-based
|
|
||||||
plugins!</p>
|
|
||||||
<a [routerLink]="['/mypet']">
|
|
||||||
<div class="button-outer">
|
|
||||||
<span class="button-inner">MyPet Info</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="pluginColumn">
|
|
||||||
<h2>Dynamic map</h2>
|
|
||||||
<p>See the world and the players around it in real-time! The map shows the entire survival world with claims
|
|
||||||
and warps.</p>
|
|
||||||
<a [routerLink]="['/map']">
|
|
||||||
<div class="button-outer">
|
|
||||||
<span class="button-inner">Visit Map</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="pluginColumn">
|
|
||||||
<h2>Player Shops</h2>
|
|
||||||
<p>Our economy is built on player shops, encouraging player interaction and putting the control in your
|
|
||||||
hands.</p>
|
|
||||||
<a [routerLink]="['/economy']">
|
|
||||||
<div class="button-outer">
|
|
||||||
<span class="button-inner">Shop Guide</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="removemobile darkmodeSection">
|
|
||||||
<div class="customContainer">
|
|
||||||
<h2 style="padding-bottom: 35px; font-size: 2.8em;">Community Builds</h2>
|
|
||||||
<p style="padding-bottom: 35px; font-size: 1.1em; margin: auto;">Thank you to our brilliant players for sharing
|
|
||||||
their impressive builds. Take a look at some of them here!<br>
|
|
||||||
If you know of any builds that should be featured, please let us know!</p>
|
|
||||||
<div class="sliderWrapper">
|
|
||||||
<div class="sliderContent">
|
|
||||||
<div class="indexSlider">
|
|
||||||
<div id="go-left" (click)="previousSlide()" class="circleBehind goLeft"></div>
|
|
||||||
<div id="go-right" (click)="nextSlide()" class="circleBehind goRight"></div>
|
|
||||||
<div class="display">
|
|
||||||
<span class="slide"
|
|
||||||
[style.background-image]="'url(' + slide + ')'"
|
|
||||||
[style.opacity]="carouselOpacity"
|
|
||||||
[style.display]="'block'"></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="dots">
|
|
||||||
<ng-container *ngFor="let slide of getSlideIndices();">
|
|
||||||
<span class="dot" (click)="setSlide(slide)" [ngClass]="getDotClass(slide)"></span>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section style="background: #202020;">
|
|
||||||
<div class="customContainer">
|
|
||||||
<h2 id="quote" style="color: white; font-family: 'minecraft-text',sans-serif; line-height: 1.3em;">"Great
|
|
||||||
community, great people, great server all round . . . If it can bring back my love for minecraft, it could
|
|
||||||
work wonders for you."</h2>
|
|
||||||
<p style="color: white; margin-top:30px;">- /u/seanhanley1993</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="darkmodeSection">
|
|
||||||
<div class="customContainer">
|
|
||||||
<img ngSrc="/public/img/logos/log.png" alt="Alternative Altitude Server Logo" height="129"
|
|
||||||
width="120">
|
|
||||||
<h2 style="margin: 20px 0; font-size: 1.8em; padding-bottom: 0 !important;">play.alttd.com</h2>
|
|
||||||
<app-copy-ip></app-copy-ip>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<a (click)="this.scrollService.scrollToTop()" class="scroll-up-button, active">
|
|
||||||
<span></span>
|
|
||||||
<p style="display: none;">Scroll Down</p>
|
|
||||||
</a>
|
|
||||||
</main>
|
|
||||||
</ng-container>
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
<h2 mat-dialog-title>Login</h2>
|
|
||||||
<div mat-dialog-content>
|
|
||||||
<form [formGroup]="loginForm">
|
|
||||||
<mat-form-field appearance="fill" style="width: 100%">
|
|
||||||
<mat-label>Enter your code</mat-label>
|
|
||||||
<input matInput formControlName="code" type="text">
|
|
||||||
<mat-error *ngIf="formHasError()">
|
|
||||||
Code is required
|
|
||||||
</mat-error>
|
|
||||||
</mat-form-field>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div mat-dialog-actions align="end">
|
|
||||||
<button mat-button (click)="onCancel()">Cancel</button>
|
|
||||||
<button mat-flat-button color="primary" (click)="onSubmit()" [disabled]="!loginForm.valid">
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {ScrollService} from '@services/scroll.service';
|
||||||
|
|
||||||
|
import {HeaderComponent} from '@header/header.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-about',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
HeaderComponent
|
||||||
|
],
|
||||||
|
templateUrl: './about.component.html',
|
||||||
|
styleUrl: './about.component.scss'
|
||||||
|
})
|
||||||
|
export class AboutComponent {
|
||||||
|
constructor(public scrollService: ScrollService) {
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-5
@@ -1,15 +1,14 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {ScrollService} from '../scroll/scroll.service';
|
import {ScrollService} from '@services/scroll.service';
|
||||||
import {CommonModule} from '@angular/common';
|
|
||||||
import {HeaderComponent} from '../header/header.component';
|
import {HeaderComponent} from '@header/header.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-birthdays',
|
selector: 'app-birthdays',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
|
||||||
HeaderComponent
|
HeaderComponent
|
||||||
],
|
],
|
||||||
templateUrl: './birthdays.component.html',
|
templateUrl: './birthdays.component.html',
|
||||||
styleUrl: './birthdays.component.scss'
|
styleUrl: './birthdays.component.scss'
|
||||||
})
|
})
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {HeaderComponent} from "../header/header.component";
|
import {HeaderComponent} from "@header/header.component";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-community',
|
selector: 'app-community',
|
||||||
+4
-5
@@ -1,16 +1,15 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {ScrollService} from '../scroll/scroll.service';
|
import {ScrollService} from '@services/scroll.service';
|
||||||
import {CommonModule, NgOptimizedImage} from '@angular/common';
|
import { NgOptimizedImage } from '@angular/common';
|
||||||
import {HeaderComponent} from '../header/header.component';
|
import {HeaderComponent} from '@header/header.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-socials',
|
selector: 'app-socials',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
|
||||||
HeaderComponent,
|
HeaderComponent,
|
||||||
NgOptimizedImage
|
NgOptimizedImage
|
||||||
],
|
],
|
||||||
templateUrl: './socials.component.html',
|
templateUrl: './socials.component.html',
|
||||||
styleUrl: './socials.component.scss'
|
styleUrl: './socials.component.scss'
|
||||||
})
|
})
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<ng-container>
|
||||||
|
<app-header [current_page]="'team'" height="450px" background_image="/public/img/backgrounds/staff.png"
|
||||||
|
[overlay_gradient]="0.5">>
|
||||||
|
<div class="title" header-content>
|
||||||
|
<h1>Staffing Team</h1>
|
||||||
|
<h2>The team that makes Altitude happen. Your owners, admins, moderators, and trainees are all working together to
|
||||||
|
create Altitude, to create home. This is where the magic happens.</h2>
|
||||||
|
</div>
|
||||||
|
</app-header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<div class="container teamContainer">
|
||||||
|
<h2 class="sectionTitle">Management</h2>
|
||||||
|
@for (member of getTeamMembers('owner') | async; track member) {
|
||||||
|
<div class="member">
|
||||||
|
<img [ngSrc]="getAvatarUrl(member)" alt="{{member.name}}'s Minecraft skin"
|
||||||
|
height="160" width="160" style="width: 160px;">
|
||||||
|
<h2>{{ member.name }}</h2>
|
||||||
|
<p>Owner</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
@for (member of getTeamMembers('manager') | async; track member) {
|
||||||
|
<div class="member">
|
||||||
|
<img [ngSrc]="getAvatarUrl(member)" alt="{{member.name}}'s Minecraft skin"
|
||||||
|
height="160" width="160" style="width: 160px;">
|
||||||
|
<h2>{{ member.name }}</h2>
|
||||||
|
<p>Manager</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="darkmodeSectionThree">
|
||||||
|
<div class="container teamContainer">
|
||||||
|
<h2 class="sectionTitle">Admins</h2>
|
||||||
|
@for (member of getTeamMembers('admin') | async; track member) {
|
||||||
|
<div class="member">
|
||||||
|
<img [ngSrc]="getAvatarUrl(member)" alt="{{member.name}}'s Minecraft skin"
|
||||||
|
height="160" width="160" style="width: 160px;">
|
||||||
|
<h2>{{ member.name }}</h2>
|
||||||
|
<p>Admin</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<div class="container teamContainer">
|
||||||
|
<h2 class="sectionTitle">Head Moderators</h2>
|
||||||
|
@for (member of getTeamMembers('headmod') | async; track member) {
|
||||||
|
<div class="member">
|
||||||
|
<img [ngSrc]="getAvatarUrl(member)" alt="{{member.name}}'s Minecraft skin"
|
||||||
|
height="160" width="160" style="width: 160px;">
|
||||||
|
<h2>{{ member.name }}</h2>
|
||||||
|
<p>Head Mod</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="darkmodeSectionThree">
|
||||||
|
<div class="container teamContainer">
|
||||||
|
<h2 class="sectionTitle">Moderators</h2>
|
||||||
|
@for (member of getTeamMembers('moderator') | async; track member) {
|
||||||
|
<div class="member">
|
||||||
|
<img [ngSrc]="getAvatarUrl(member)" alt="{{member.name}}'s Minecraft skin"
|
||||||
|
height="160" width="160" style="width: 160px;">
|
||||||
|
<h2>{{ member.name }}</h2>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
@if ((getTeamMembers('trainee') | async)?.length ?? 0 > 0) {
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<div class="container teamContainer">
|
||||||
|
<h2 class="sectionTitle">Trainees</h2>
|
||||||
|
@for (member of getTeamMembers('trainee') | async; track member) {
|
||||||
|
<div class="member">
|
||||||
|
<img [ngSrc]="getAvatarUrl(member)" alt="{{member.name}}'s Minecraft skin"
|
||||||
|
height="160" width="160" style="width: 160px;">
|
||||||
|
<h2>{{ member.name }}</h2>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
</main>
|
||||||
|
</ng-container>
|
||||||
+5
-5
@@ -1,11 +1,11 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {ScrollService} from '../scroll/scroll.service';
|
import {ScrollService} from '@services/scroll.service';
|
||||||
import {BASE_PATH, Player, TeamService} from '../../api';
|
import {BASE_PATH, Player, TeamService} from '@api';
|
||||||
import {CommonModule, NgOptimizedImage} from '@angular/common';
|
import { CommonModule, NgOptimizedImage } from '@angular/common';
|
||||||
import {HeaderComponent} from '../header/header.component';
|
import {HeaderComponent} from '@header/header.component';
|
||||||
import {CookieService} from 'ngx-cookie-service';
|
import {CookieService} from 'ngx-cookie-service';
|
||||||
import {map, Observable, shareReplay} from 'rxjs';
|
import {map, Observable, shareReplay} from 'rxjs';
|
||||||
import {environment} from '../../environments/environment';
|
import {environment} from '@environment';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-team',
|
selector: 'app-team',
|
||||||
+2
-2
@@ -19,8 +19,8 @@
|
|||||||
<p>To claim land, you will need some basic tools: a golden shovel, and a stick. You can craft these items
|
<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
|
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>
|
stick is used for viewing claim information.</p>
|
||||||
<img ngSrc="/public/img/items/shovel.png" alt="Minecraft golden shovel" style="width: 25%;" height="114"
|
<img ngSrc="/public/img/items/shovel.png" alt="Minecraft golden shovel"
|
||||||
width="114">
|
class="shovelClaiming" style="width: 25%;" height="114" width="114">
|
||||||
<p>Players start with an allowance of 500 “claim blocks” and can purchase additional “claim blocks” with
|
<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
|
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
|
many claims you can create depends on the rank you have - the amount can be found on the <a
|
||||||
+7
@@ -7,3 +7,10 @@ main li {
|
|||||||
margin-left: 30px;
|
margin-left: 30px;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@media (max-width: 690px) {
|
||||||
|
.shovelClaiming {
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {HeaderComponent} from '../header/header.component';
|
import {HeaderComponent} from '@header/header.component';
|
||||||
import {NgOptimizedImage} from '@angular/common';
|
import {NgOptimizedImage} from '@angular/common';
|
||||||
import {RouterLink} from '@angular/router';
|
import {RouterLink} from '@angular/router';
|
||||||
|
|
||||||
+1
-2
@@ -163,14 +163,13 @@
|
|||||||
<div class="columnParagraph">
|
<div class="columnParagraph">
|
||||||
<h2>Miscellaneous</h2>
|
<h2>Miscellaneous</h2>
|
||||||
<p>Grindstones can strip shulker box & beehive NBT data</p>
|
<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>/iwanttobreakthisblock, required to break some natural generated blocks</p>
|
||||||
<p>/sneakclickmending, allows you to mend your items by right clicking while sneaking</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>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>Nether portals can be as small as 1x2 (1x2 portal, 3x4 base)</p>
|
||||||
<p>Lootchests refill once for every player</p>
|
<p>Lootchests refill once for every player</p>
|
||||||
<p>Empty maps can be duplicated using paper in a cartography table</p>
|
<p>Empty maps can be duplicated using paper in a cartography table</p>
|
||||||
<p>Eating glowberries gives you a glowing effect</p>
|
<p>Eating glow berries gives you a glowing effect</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
+6
@@ -7,3 +7,9 @@ main li {
|
|||||||
margin-left: 30px;
|
margin-left: 30px;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 690px) {
|
||||||
|
.columnContainer {
|
||||||
|
text-align: left !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {HeaderComponent} from "../header/header.component";
|
import {HeaderComponent} from "@header/header.component";
|
||||||
import {RouterLink} from '@angular/router';
|
import {RouterLink} from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
+1
-1
@@ -104,7 +104,7 @@
|
|||||||
prices will become more and more extreme. You can see where these thresholds are based on your current
|
prices will become more and more extreme. You can see where these thresholds are based on your current
|
||||||
points here:</p>
|
points here:</p>
|
||||||
<img ngSrc="/public/img/random/pointbracket.png" alt="Visualization of the point bracket."
|
<img ngSrc="/public/img/random/pointbracket.png" alt="Visualization of the point bracket."
|
||||||
style="width: 100%; padding: 0 0 15px 0;"
|
class="visEconomy" style="width: 100%; padding: 0 0 15px 0;"
|
||||||
height="100" width="800">
|
height="100" width="800">
|
||||||
</div>
|
</div>
|
||||||
<div class="columnParagraph">
|
<div class="columnParagraph">
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
main ul {
|
||||||
|
font-family: opensans, sans-serif;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
main li {
|
||||||
|
margin-left: 30px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 690px) {
|
||||||
|
.title {
|
||||||
|
height: calc(100% - 110px);
|
||||||
|
margin-top: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title h2 {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visEconomy {
|
||||||
|
height: 50px;
|
||||||
|
width: 357px;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {HeaderComponent} from '../header/header.component';
|
import {HeaderComponent} from '@header/header.component';
|
||||||
import {NgOptimizedImage} from '@angular/common';
|
import {NgOptimizedImage} from '@angular/common';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
+10
@@ -11,3 +11,13 @@
|
|||||||
padding-bottom: 25px;
|
padding-bottom: 25px;
|
||||||
font-weight: 100;
|
font-weight: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 670px) {
|
||||||
|
.title h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title h2 {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-4
@@ -1,13 +1,12 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {CommonModule} from '@angular/common';
|
|
||||||
import {HeaderComponent} from '../header/header.component';
|
import {HeaderComponent} from '@header/header.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
|
||||||
HeaderComponent
|
HeaderComponent
|
||||||
],
|
],
|
||||||
selector: 'app-map',
|
selector: 'app-map',
|
||||||
templateUrl: './map.component.html',
|
templateUrl: './map.component.html',
|
||||||
styleUrl: './map.component.scss'
|
styleUrl: './map.component.scss'
|
||||||
+2
-2
@@ -24,8 +24,8 @@
|
|||||||
your other pets while they are in storage. To interact with your MyPet you will need to make it your
|
your other pets while they are in storage. To interact with your MyPet you will need to make it your
|
||||||
active MyPet by doing <span style="font-family: 'opensans-bold', sans-serif;">/petswitch</span> and
|
active MyPet by doing <span style="font-family: 'opensans-bold', sans-serif;">/petswitch</span> and
|
||||||
selecting the one you want to use.</p>
|
selecting the one you want to use.</p>
|
||||||
<img ngSrc="/public/img/items/lead.png" alt="Minecraft lead/leash" style="width: 20%;" height="96"
|
<img ngSrc="/public/img/items/lead.png" alt="Minecraft lead/leash"
|
||||||
width="96">
|
class="leadMyPet" style="width: 20%;" height="96" width="96">
|
||||||
</div>
|
</div>
|
||||||
<div class="columnParagraph">
|
<div class="columnParagraph">
|
||||||
<h2>Skilltrees and Levels</h2>
|
<h2>Skilltrees and Levels</h2>
|
||||||
+6
@@ -13,3 +13,9 @@ main li {
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 690px) {
|
||||||
|
.leadMyPet {
|
||||||
|
height: 70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {HeaderComponent} from '../header/header.component';
|
import {HeaderComponent} from '@header/header.component';
|
||||||
import {NgOptimizedImage} from '@angular/common';
|
import {NgOptimizedImage} from '@angular/common';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user