Compare commits
72
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb72ce14cc | ||
|
|
d1ba89acc8 | ||
|
|
d28b4a2b62 | ||
|
|
56f4ccf40e | ||
|
|
d73f057596 | ||
|
|
e825d83124 | ||
|
|
238c5d9644 | ||
|
|
4222df87a3 | ||
|
|
16cc57d774 | ||
|
|
c536bfbf30 | ||
|
|
f67cb50f41 | ||
|
|
bdb38e5011 | ||
|
|
ae1e972438 | ||
|
|
737b26a6c7 | ||
|
|
5013b9a204 | ||
|
|
fcb64db137 | ||
|
|
d2e064e2b4 | ||
|
|
f50f2dc6c2 | ||
|
|
c277306c2c | ||
|
|
1f03a4bdc3 | ||
|
|
7f1c59d102 | ||
|
|
f968a64dd4 | ||
|
|
c25364caf7 | ||
|
|
15c3cc7f26 | ||
|
|
2b96957876 | ||
|
|
b16fab26e7 | ||
|
|
28fd05a656 | ||
|
|
ff1b09be92 | ||
|
|
8a839ac922 | ||
|
|
3f76a98409 | ||
|
|
871615702b | ||
|
|
291c9df5c6 | ||
|
|
4150324d75 | ||
|
|
4267c782a7 | ||
|
|
343964eda8 | ||
|
|
1ce2088cae | ||
|
|
0b952e07f7 | ||
|
|
c2b9a8a574 | ||
|
|
d3ef296784 | ||
|
|
5a792463cc | ||
|
|
974d50d7cd | ||
|
|
62f837914c | ||
|
|
ace969ba3b | ||
|
|
2fc6ba53f6 | ||
|
|
4c38b070ea | ||
|
|
db394beda6 | ||
|
|
76cb3cd89c | ||
|
|
5d8ab2deef | ||
|
|
aef32a8982 | ||
|
|
42f0961f13 | ||
|
|
04310e1cce | ||
|
|
54e747118c | ||
|
|
43430cfbef | ||
|
|
cce83a08de | ||
|
|
f0faa63ca7 | ||
|
|
dfea91d8ca | ||
|
|
73916f0aae | ||
|
|
ebe66c87c0 | ||
|
|
c42fc38b2c | ||
|
|
213f9987d9 | ||
|
|
48cac607de | ||
|
|
6ed2e15017 | ||
|
|
7fc25f46f3 | ||
|
|
c72703ea32 | ||
|
|
e837a9216d | ||
|
|
d4363b3a8a | ||
|
|
1e5862bae6 | ||
|
|
daf88ea437 | ||
|
|
9abd570b87 | ||
|
|
5284d498f3 | ||
|
|
c3a7be82e9 | ||
|
|
174ed834ca |
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
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,19 +32,37 @@ 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("/api/form/**").hasAuthority(PermissionClaimDto.USER.getValue())
|
||||||
.requestMatchers("/form/**").hasAuthority(PermissionClaimDto.USER.getValue())
|
.requestMatchers("/api/login/userLogin").hasAuthority(PermissionClaimDto.USER.getValue())
|
||||||
.requestMatchers("/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
.requestMatchers("/api/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||||
|
.requestMatchers("/api/particles/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||||
|
.requestMatchers("/api/files/save/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||||
.anyRequest().permitAll()
|
.anyRequest().permitAll()
|
||||||
)
|
)
|
||||||
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.anonymous(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();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package com.alttd.altitudeweb.controllers.application;
|
package com.alttd.altitudeweb.controllers.forms;
|
||||||
|
|
||||||
import com.alttd.altitudeweb.api.AppealsApi;
|
import com.alttd.altitudeweb.api.AppealsApi;
|
||||||
import com.alttd.altitudeweb.services.limits.RateLimit;
|
import com.alttd.altitudeweb.services.limits.RateLimit;
|
||||||
+69
-11
@@ -1,18 +1,26 @@
|
|||||||
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.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,6 +44,9 @@ public class LoginController implements LoginApi {
|
|||||||
@Value("${login.secret:#{null}}")
|
@Value("${login.secret:#{null}}")
|
||||||
private String loginSecret;
|
private String loginSecret;
|
||||||
|
|
||||||
|
@Value("${my-server.address:#{null}}")
|
||||||
|
private String serverAddress;
|
||||||
|
|
||||||
private record CacheEntry(UUID uuid, Instant expiry) {}
|
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<>();
|
||||||
@@ -80,24 +91,69 @@ 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");
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
log.debug("Loaded authentication for logged in user {}", authentication);
|
||||||
|
if (authentication == null || !(authentication.getPrincipal() instanceof Jwt jwt)) {
|
||||||
|
log.debug("Loaded authentication for logged in user is null or not a jwt");
|
||||||
|
return ResponseEntity.status(401).build();
|
||||||
|
}
|
||||||
|
String stringUuid = jwt.getSubject();
|
||||||
|
UUID uuid;
|
||||||
|
try {
|
||||||
|
uuid = UUID.fromString(stringUuid);
|
||||||
|
log.debug("Loaded username for logged in user {}", uuid);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return ResponseEntity.badRequest().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
UsernameDto usernameDto = new UsernameDto();
|
||||||
|
usernameDto.setUsername(getUsername(uuid));
|
||||||
|
log.debug("Loaded username for logged in user {}", usernameDto.getUsername());
|
||||||
|
return ResponseEntity.ok(usernameDto);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
@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)));
|
|
||||||
cache.put("23232323", cacheEntry1);
|
|
||||||
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 (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();
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,12 +190,13 @@ 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());
|
||||||
|
Optional<PrivilegedUser> privilegedUser = sqlSession.getMapper(PrivilegedUserMapper.class)
|
||||||
.getUserByUuid(uuid.toString());
|
.getUserByUuid(uuid.toString());
|
||||||
|
|
||||||
privilegedUserCompletableFuture.complete(privilegedUser);
|
privilegedUserCompletableFuture.complete(privilegedUser);
|
||||||
@@ -148,19 +205,20 @@ public class LoginController implements LoginApi {
|
|||||||
privilegedUserCompletableFuture.completeExceptionally(e);
|
privilegedUserCompletableFuture.completeExceptionally(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
PrivilegedUser privilegedUser = privilegedUserCompletableFuture.join();
|
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.valueOf(permission));
|
claimList.add(PermissionClaimDto.fromValue(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", claimList.stream().map(PermissionClaimDto::getValue).toList())
|
||||||
.issuedAt(now)
|
.issuedAt(now)
|
||||||
.expiresAt(expiryTime)
|
.expiresAt(expiryTime)
|
||||||
|
|||||||
+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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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:http://localhost:4200}
|
cors.allowed-origins=${CORS:http://localhost:4200}
|
||||||
|
my-server.address=${SERVER_ADDRESS:http://localhost}
|
||||||
logging.level.com.alttd.altitudeweb=DEBUG
|
logging.level.com.alttd.altitudeweb=DEBUG
|
||||||
|
|||||||
@@ -6,4 +6,7 @@ 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
|
||||||
|
|||||||
@@ -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("""
|
||||||
|
|||||||
+6
-5
@@ -1,21 +1,22 @@
|
|||||||
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;
|
||||||
|
|
||||||
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"),
|
||||||
@@ -23,7 +24,7 @@ public interface PrivilegedUserMapper {
|
|||||||
@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") String uuid);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieves all privileged users with their permissions
|
* Retrieves all privileged users with their permissions
|
||||||
|
|||||||
@@ -97,10 +97,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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,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.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 lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.ibatis.session.SqlSession;
|
import org.apache.ibatis.session.SqlSession;
|
||||||
@@ -18,6 +19,7 @@ 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);
|
||||||
}).join()
|
}).join()
|
||||||
.runQuery(SqlSession -> {
|
.runQuery(SqlSession -> {
|
||||||
createSettingsTable(SqlSession);
|
createSettingsTable(SqlSession);
|
||||||
|
|||||||
+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": "."
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-26
@@ -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
|
|
||||||
project.exec {
|
|
||||||
workingDir(project.projectDir)
|
|
||||||
|
|
||||||
// Use node's npm to ensure it works on all environments
|
|
||||||
val nodeDir = "${project.projectDir}/node"
|
|
||||||
val isWindows = System.getProperty("os.name").lowercase().contains("windows")
|
val isWindows = System.getProperty("os.name").lowercase().contains("windows")
|
||||||
|
npmCommand.set(listOf("run", if (isWindows) "build:dev" else "build:beta"))
|
||||||
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")
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-14
@@ -13,26 +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",
|
||||||
|
"@auth0/angular-jwt": "^5.2.0",
|
||||||
"@types/three": "^0.177.0",
|
"@types/three": "^0.177.0",
|
||||||
"ngx-cookie-service": "^19.1.2",
|
"ngx-cookie-service": "^20.0.1",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"three": "^0.177.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",
|
||||||
@@ -40,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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,116 +1,133 @@
|
|||||||
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',
|
path: 'particles',
|
||||||
loadComponent: () => import('./particles/particles.component').then(m => m.ParticlesComponent)
|
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',
|
path: 'forms/appeal',
|
||||||
loadComponent: () => import('./forms/forms.component').then(m => m.FormsComponent)
|
loadComponent: () => import('./pages/forms/appeal/appeal.component').then(m => m.AppealComponent),
|
||||||
|
canActivate: [AuthGuard],
|
||||||
|
data: {
|
||||||
|
requiredAuthorizations: ['SCOPE_user']
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'forms',
|
path: 'forms',
|
||||||
loadComponent: () => import('./forms/forms.component').then(m => m.FormsComponent)
|
loadComponent: () => import('./pages/forms/forms.component').then(m => m.FormsComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'particles',
|
path: 'community',
|
||||||
loadComponent: () => import('./particles/particles.component').then(m => m.ParticlesComponent)
|
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,44 @@
|
|||||||
|
import {Injectable} from '@angular/core';
|
||||||
|
import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree} from '@angular/router';
|
||||||
|
import {Observable} from 'rxjs';
|
||||||
|
import {AuthService} from '@services/auth.service';
|
||||||
|
import {environment} from '@environment';
|
||||||
|
|
||||||
|
@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 (environment.defaultAuthStatus) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
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>
|
|
||||||
+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-about',
|
selector: 'app-about',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
|
||||||
HeaderComponent
|
HeaderComponent
|
||||||
],
|
],
|
||||||
templateUrl: './about.component.html',
|
templateUrl: './about.component.html',
|
||||||
styleUrl: './about.component.scss'
|
styleUrl: './about.component.scss'
|
||||||
})
|
})
|
||||||
+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'
|
||||||
})
|
})
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<ng-container>
|
||||||
|
<app-header [current_page]="'community'" height="460px" background_image="/public/img/backgrounds/community.jpg"
|
||||||
|
[overlay_gradient]="0.5">
|
||||||
|
<div class="title" header-content>
|
||||||
|
<h1>Community</h1>
|
||||||
|
<h2>Talented people who help Altitude in more than one way.</h2>
|
||||||
|
</div>
|
||||||
|
</app-header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<div class="customContainer">
|
||||||
|
<h2>Current Nitro Boosters</h2>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section id="social" class="darkmodeSectionThree">
|
||||||
|
<div class="container" style="padding: 50px 0 0 0; justify-content: center;">
|
||||||
|
<h2 class="sectionTitle">Social Media</h2>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: center; padding-bottom: 30px;">
|
||||||
|
<p style="text-align: center;">We're currently not looking for more people to help manage our socials.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section id="crateTeam" class="darkmodeSection">
|
||||||
|
<div class="container" style="padding: 50px 0 0 0; justify-content: center;">
|
||||||
|
<h2 class="sectionTitle">Crate Team</h2>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="darkmodeSectionThree">
|
||||||
|
<div class="container" style="padding: 50px 0 0 0; justify-content: center;">
|
||||||
|
<h2 class="sectionTitle">Event Leaders</h2>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: center; padding-bottom: 30px;">
|
||||||
|
<p style="text-align: center;">We're currently not looking for more Event Leaders.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<div class="container" style="padding: 50px 0 0 0; justify-content: center;">
|
||||||
|
<h2 class="sectionTitle">Event Team</h2>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: center; padding-bottom: 30px;">
|
||||||
|
<div style="flex-direction: column;">
|
||||||
|
<p style="text-align: center;">We occasionally open applications for the event team.</p>
|
||||||
|
<p style="text-align: center;">If you're interested in joining you simply need to keep an eye on the Discord
|
||||||
|
announcements.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="darkmodeSectionThree">
|
||||||
|
<div class="container" style="padding: 50px 0 0 0; justify-content: center;">
|
||||||
|
<h2 class="sectionTitle">YouTubers & Streamers</h2>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: center; padding-bottom: 30px;">
|
||||||
|
<div style="flex-direction: column;">
|
||||||
|
<p style="text-align: center;"><a style="cursor: pointer;" id="reqButton">Show Requirements...</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="req" class="hide" style="display: flex; justify-content: center; padding-bottom: 30px;">
|
||||||
|
<div style="flex-direction: column; justify-content: center; max-width: 800px;">
|
||||||
|
<p style="text-align: center;"><span style="font-family: 'opensans-bold', sans-serif;">Requirements:</span>
|
||||||
|
</p>
|
||||||
|
<p style="text-align: center;">You need to have at least one recent stream/video on Altitude which we can use
|
||||||
|
to gauge if your audience enjoys your content on Altitude.</p>
|
||||||
|
<br>
|
||||||
|
<p style="text-align: center;">Twitch: You need to be affiliate and get at least 5 viewers on average while
|
||||||
|
streaming on Altitude.</p>
|
||||||
|
<p style="text-align: center;">YouTube videos: You need at least 500 subs and have at least 200 views per
|
||||||
|
video within a week on average for Altitude content.</p>
|
||||||
|
<p style="text-align: center;">YouTube streamers: You need at least 500 subs and have at least 5 viewers on
|
||||||
|
average while streaming on Altitude.</p>
|
||||||
|
<br>
|
||||||
|
<p style="text-align: center;"><span style="font-family: 'opensans-bold', sans-serif;">Note:</span> Before
|
||||||
|
accepting or denying you we will watch your latest video/stream on Altitude (so keep your broadcasts public
|
||||||
|
on twitch).</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
.customContainer {
|
||||||
|
width: 80%;
|
||||||
|
max-width: 1020px;
|
||||||
|
margin: auto;
|
||||||
|
padding: 80px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hide {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CommunityComponent } from './community.component';
|
||||||
|
|
||||||
|
describe('CommunityComponent', () => {
|
||||||
|
let component: CommunityComponent;
|
||||||
|
let fixture: ComponentFixture<CommunityComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [CommunityComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(CommunityComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import {Component} from '@angular/core';
|
||||||
|
import {HeaderComponent} from "@header/header.component";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-community',
|
||||||
|
imports: [
|
||||||
|
HeaderComponent
|
||||||
|
],
|
||||||
|
templateUrl: './community.component.html',
|
||||||
|
styleUrl: './community.component.scss'
|
||||||
|
})
|
||||||
|
export class CommunityComponent {
|
||||||
|
|
||||||
|
}
|
||||||
+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({
|
||||||
+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-skyblock',
|
selector: 'app-skyblock',
|
||||||
+5
-9
@@ -19,10 +19,8 @@
|
|||||||
name, description, and icon) on their own! This lets you easily promote your town, shop, farm, or just
|
name, description, and icon) on their own! This lets you easily promote your town, shop, farm, or just
|
||||||
about anything else.</p>
|
about anything else.</p>
|
||||||
<p>All warps are separated into categories so it's easy to find the category of warp you're looking for.</p>
|
<p>All warps are separated into categories so it's easy to find the category of warp you're looking for.</p>
|
||||||
<img ngSrc="/public/img/random/warpgui.png"
|
<img ngSrc="/public/img/random/warpgui.png" alt="In-game warp GUI"
|
||||||
alt="In-game warp GUI"
|
class="guiWarp" style="width: 80%; padding-bottom: 15px;" height="232" width="356">
|
||||||
style="width: 80%; padding-bottom: 15px;"
|
|
||||||
height="232" width="356">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="columnContainer">
|
<div class="columnContainer">
|
||||||
@@ -45,7 +43,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section class="columnSection" style="padding-top: 0;">
|
<section class="columnSection" style="padding-top: 0;">
|
||||||
<div class="columnParagraph" style="padding-left: 15px;">
|
<div class="columnParagraph" style="padding-left: 15px; text-align: center;">
|
||||||
<h2>Warp Requirements</h2>
|
<h2>Warp Requirements</h2>
|
||||||
<p>You need to be the owner of the claim your warp is placed in. It should look good, and be as finished as
|
<p>You need to be the owner of the claim your warp is placed in. It should look good, and be as finished as
|
||||||
your warp type allows you to have it. Safety is an important aspect as well, visitors should not be
|
your warp type allows you to have it. Safety is an important aspect as well, visitors should not be
|
||||||
@@ -207,10 +205,8 @@
|
|||||||
warp, just do <span style="font-family: 'opensans-bold', sans-serif;">/warps</span> and click on the chest
|
warp, just do <span style="font-family: 'opensans-bold', sans-serif;">/warps</span> and click on the chest
|
||||||
labeled "<span style="font-family: 'opensans-bold', sans-serif;">My Warps</span>" in the bottom left
|
labeled "<span style="font-family: 'opensans-bold', sans-serif;">My Warps</span>" in the bottom left
|
||||||
corner.</p>
|
corner.</p>
|
||||||
<img ngSrc="/public/img/random/editwarpgui.png"
|
<img ngSrc="/public/img/random/editwarpgui.png" alt="In-game warp edit GUI"
|
||||||
alt="In-game warp edit GUI"
|
class="editWarp" style="width: 80%;" width="384" height="170">
|
||||||
style="width: 80%;"
|
|
||||||
width="384" height="170">
|
|
||||||
<p>Maintaining a warp also involves making sure it looks nice, shops are well stocked, and, if it's a town,
|
<p>Maintaining a warp also involves making sure it looks nice, shops are well stocked, and, if it's a town,
|
||||||
open plots are always available for residents to move in. Make sure you keep up on maintaining your warp
|
open plots are always available for residents to move in. Make sure you keep up on maintaining your warp
|
||||||
or it could be deleted! If a warp is deleted by a staff member you will not receive a refund for the
|
or it could be deleted! If a warp is deleted by a staff member you will not receive a refund for the
|
||||||
+10
@@ -12,3 +12,13 @@ main li {
|
|||||||
color: var(--font-color);
|
color: var(--font-color);
|
||||||
transition: 0.5s ease;
|
transition: 0.5s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 690px) {
|
||||||
|
.guiWarp {
|
||||||
|
height: 190px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editWarp {
|
||||||
|
height: 130px;
|
||||||
|
}
|
||||||
|
}
|
||||||
+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';
|
||||||
|
|
||||||
+2
-2
@@ -3,12 +3,12 @@
|
|||||||
<div class="footerInner">
|
<div class="footerInner">
|
||||||
<div class="footerText">
|
<div class="footerText">
|
||||||
<h2>ABOUT US</h2>
|
<h2>ABOUT US</h2>
|
||||||
<p>Altitude is a community-centered {{ ALTITUDE_VERSION }} survival server. We're one of those servers you come
|
<p>Altitude is a community-centered {{ ALTITUDE_VERSION }} survival server. We're one of those servers you come
|
||||||
to call "home". We are your place to get together with friends and play survival, with a few extra features
|
to call "home". We are your place to get together with friends and play survival, with a few extra features
|
||||||
suggested by our community!</p>
|
suggested by our community!</p>
|
||||||
<div class="followUs" style="height: 35px; display: flex; align-items: flex-end;">
|
<div class="followUs" style="height: 35px; display: flex; align-items: flex-end;">
|
||||||
<a target="_blank" rel="noopener" href="https://discordapp.com/invite/TGqpzCJ">
|
<a target="_blank" rel="noopener" href="https://discordapp.com/invite/TGqpzCJ">
|
||||||
<img ngSrc="/public/img/logos/discord.png" alt="Discord Button" height="32" width="32">
|
<img priority ngSrc="/public/img/logos/discord.png" alt="Discord Button" height="32" width="32">
|
||||||
</a>
|
</a>
|
||||||
<a target="_blank" rel="noopener" href="https://twitter.com/alttdmc">
|
<a target="_blank" rel="noopener" href="https://twitter.com/alttdmc">
|
||||||
<img ngSrc="/public/img/logos/twitter.png" alt="Twitter Button" height="32" width="32">
|
<img ngSrc="/public/img/logos/twitter.png" alt="Twitter Button" height="32" width="32">
|
||||||
+37
@@ -67,3 +67,40 @@ footer ul, footer p {
|
|||||||
.copyright {
|
.copyright {
|
||||||
margin-top: 50px;
|
margin-top: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1150px) {
|
||||||
|
.footerInner {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerText {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerNav {
|
||||||
|
border-left: none;
|
||||||
|
padding-left: 0px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.followUs {
|
||||||
|
width: 100px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footerNav {
|
||||||
|
border-left: none;
|
||||||
|
padding-left: 0px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copyright {
|
||||||
|
margin-top: 30px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-4
@@ -1,16 +1,15 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {ALTITUDE_VERSION} from '../constant';
|
import {ALTITUDE_VERSION} from '@custom-types/constant';
|
||||||
import {CommonModule, NgOptimizedImage} from '@angular/common';
|
import { NgOptimizedImage } from '@angular/common';
|
||||||
import {RouterLink} from '@angular/router';
|
import {RouterLink} from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-footer',
|
selector: 'app-footer',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
|
||||||
RouterLink,
|
RouterLink,
|
||||||
NgOptimizedImage
|
NgOptimizedImage
|
||||||
],
|
],
|
||||||
templateUrl: './footer.component.html',
|
templateUrl: './footer.component.html',
|
||||||
styleUrl: './footer.component.scss'
|
styleUrl: './footer.component.scss'
|
||||||
})
|
})
|
||||||
+4
-5
@@ -1,17 +1,16 @@
|
|||||||
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';
|
||||||
import {RouterLink} from '@angular/router';
|
import {RouterLink} from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-privacy',
|
selector: 'app-privacy',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
|
||||||
HeaderComponent,
|
HeaderComponent,
|
||||||
RouterLink
|
RouterLink
|
||||||
],
|
],
|
||||||
templateUrl: './privacy.component.html',
|
templateUrl: './privacy.component.html',
|
||||||
styleUrl: './privacy.component.scss'
|
styleUrl: './privacy.component.scss'
|
||||||
})
|
})
|
||||||
+4
-5
@@ -1,7 +1,7 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {ScrollService} from '../scroll/scroll.service';
|
import {ScrollService} from '@services/scroll.service';
|
||||||
import {HeaderComponent} from '../header/header.component';
|
import {HeaderComponent} from '@header/header.component';
|
||||||
import {CommonModule} from '@angular/common';
|
|
||||||
import {RouterLink} from '@angular/router';
|
import {RouterLink} from '@angular/router';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -9,10 +9,9 @@ import {RouterLink} from '@angular/router';
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
templateUrl: './terms.component.html',
|
templateUrl: './terms.component.html',
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
|
||||||
HeaderComponent,
|
HeaderComponent,
|
||||||
RouterLink
|
RouterLink
|
||||||
],
|
],
|
||||||
styleUrl: './terms.component.scss'
|
styleUrl: './terms.component.scss'
|
||||||
})
|
})
|
||||||
export class TermsComponent {
|
export class TermsComponent {
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
<div>
|
||||||
|
<app-header [current_page]="'appeal'" height="200px" background_image="/public/img/backgrounds/staff.png"
|
||||||
|
[overlay_gradient]="0.5">
|
||||||
|
<div class="title" header-content>
|
||||||
|
<h1>Appeal</h1>
|
||||||
|
</div>
|
||||||
|
</app-header>
|
||||||
|
<main>
|
||||||
|
<section class="darkmodeSection appeal-container">
|
||||||
|
<div class="form-container">
|
||||||
|
<div class="pages">
|
||||||
|
@if (currentPageIndex === 0) {
|
||||||
|
@if (history()?.length === 0) {
|
||||||
|
<section class="formPage">
|
||||||
|
<img ngSrc="/public/img/logos/logo.png" alt="Discord" height="319" width="550"/>
|
||||||
|
<h1>Punishment Appeal</h1>
|
||||||
|
<p>You have no punishments to appeal.</p>
|
||||||
|
</section>
|
||||||
|
} @else {
|
||||||
|
<section class="formPage">
|
||||||
|
<img ngSrc="/public/img/logos/logo.png" alt="Discord" height="319" width="550"/>
|
||||||
|
<h1>Punishment Appeal</h1>
|
||||||
|
<p>We aim to respond within 48 hours.</p>
|
||||||
|
<button mat-raised-button (click)="nextPage()" [disabled]="history() == null">
|
||||||
|
@if (history() == null) {
|
||||||
|
<mat-spinner></mat-spinner>
|
||||||
|
} @else {
|
||||||
|
Next
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (currentPageIndex === 1) {
|
||||||
|
<section class="formPage">
|
||||||
|
<div class="description">
|
||||||
|
<p>You are logged in as <strong>{{ authService.username() }}</strong>. If this is the correct account
|
||||||
|
please continue</p>
|
||||||
|
<br>
|
||||||
|
<p><strong>Notice: </strong> Submitting an appeal is <strong>not</strong> an instant process.
|
||||||
|
We will investigate the punishment you are appealing and respond within 48 hours.</p>
|
||||||
|
<p style="font-style: italic;">Appeals that seem to have been made with
|
||||||
|
little to no effort will be automatically denied.</p>
|
||||||
|
</div>
|
||||||
|
<button mat-raised-button (click)="nextPage()" [disabled]="authService.username() == null">
|
||||||
|
I, {{ authService.username() }}, understand and agree
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (currentPageIndex === 2) {
|
||||||
|
<section class="formPage">
|
||||||
|
<div class="description">
|
||||||
|
<h2>Please select the punishment you want to appeal</h2>
|
||||||
|
</div>
|
||||||
|
<mat-form-field>
|
||||||
|
<mat-label>Punishment</mat-label>
|
||||||
|
<mat-select (valueChange)="onPunishmentSelected($event)">
|
||||||
|
@for (punishment of history(); track punishment) {
|
||||||
|
<mat-option [value]="punishment">{{ punishment.type }} - {{ punishment.reason }}</mat-option>
|
||||||
|
}
|
||||||
|
</mat-select>
|
||||||
|
</mat-form-field>
|
||||||
|
@if (selectedPunishment() != null) {
|
||||||
|
<button mat-raised-button (click)="nextPage()" [disabled]="selectedPunishment() == null">
|
||||||
|
Appeal {{ selectedPunishment()!.type }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
|
<form [formGroup]="form">
|
||||||
|
@if (currentPageIndex === 3) {
|
||||||
|
<section class="formPage">
|
||||||
|
<div class="description">
|
||||||
|
<h2>Please enter your email.</h2>
|
||||||
|
<p style="font-style: italic">It does not have to be your minecraft email.</p>
|
||||||
|
<mat-form-field appearance="fill" style="width: 100%;">
|
||||||
|
<mat-label>Email</mat-label>
|
||||||
|
<input matInput formControlName="email" placeholder="Email">
|
||||||
|
@if (form.controls.email.invalid && form.controls.email.touched) {
|
||||||
|
<mat-error>
|
||||||
|
@if (form.controls.email.errors?.['required']) {
|
||||||
|
Email is required
|
||||||
|
} @else if (form.controls.email.errors?.['email']) {
|
||||||
|
Please enter a valid email address
|
||||||
|
}
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<button mat-raised-button (click)="nextPage()" [disabled]="form.controls.email.invalid">
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (currentPageIndex === 4) {
|
||||||
|
<section class="formPage">
|
||||||
|
<div class="description">
|
||||||
|
<h2>Why should your {{ selectedPunishment()?.type }} be reduced or removed?</h2>
|
||||||
|
<p style="font-style: italic">Please take your time writing this, we're more likely to accept an
|
||||||
|
appeal if effort was put into it.</p>
|
||||||
|
<mat-form-field appearance="fill" style="width: 100%;">
|
||||||
|
<mat-label>Reason</mat-label>
|
||||||
|
<textarea matInput formControlName="appeal" placeholder="Reason" rows="6"></textarea>
|
||||||
|
@if (form.controls.appeal.invalid && form.controls.appeal.touched) {
|
||||||
|
<mat-error>
|
||||||
|
@if (form.controls.appeal.errors?.['required']) {
|
||||||
|
Reason is required
|
||||||
|
} @else if (form.controls.appeal.errors?.['minlength']) {
|
||||||
|
Reason must be at least 10 characters
|
||||||
|
}
|
||||||
|
</mat-error>
|
||||||
|
}
|
||||||
|
</mat-form-field>
|
||||||
|
</div>
|
||||||
|
<button mat-raised-button (click)="onSubmit()" [disabled]="form.invalid">
|
||||||
|
Submit Appeal
|
||||||
|
</button>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Navigation dots -->
|
||||||
|
@if (totalPages.length > 1) {
|
||||||
|
<div class="form-navigation">
|
||||||
|
<button mat-icon-button class="nav-button" (click)="previousPage()" [disabled]="isFirstPage()">
|
||||||
|
<mat-icon>navigate_before</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
@for (i of totalPages; track i) {
|
||||||
|
<div
|
||||||
|
class="nav-dot"
|
||||||
|
[class.active]="i === currentPageIndex"
|
||||||
|
(click)="goToPage(i)">
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<button mat-icon-button class="nav-button" (click)="nextPage()" [disabled]="isLastPage()">
|
||||||
|
<mat-icon>navigate_next</mat-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.appeal-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 80vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formPage {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
animation: fadeIn 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-navigation {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.nav-dot {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: rgba(255, 255, 255, 0.3);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-button {
|
||||||
|
color: #1f9bde;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pages {
|
||||||
|
margin-top: auto;
|
||||||
|
margin-bottom: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
max-width: 75ch;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
import {AfterViewInit, Component, ElementRef, OnInit, Renderer2, signal} from '@angular/core';
|
||||||
|
import {FormControl, FormGroup, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||||
|
import {AppealsService, HistoryService, MinecraftAppeal, PunishmentHistory} from '@api';
|
||||||
|
import {HeaderComponent} from '@header/header.component';
|
||||||
|
import {NgOptimizedImage} from '@angular/common';
|
||||||
|
import {MatButtonModule} from '@angular/material/button';
|
||||||
|
import {MatIconModule} from '@angular/material/icon';
|
||||||
|
import {AuthService} from '@services/auth.service';
|
||||||
|
import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
|
||||||
|
import {MatFormFieldModule} from '@angular/material/form-field';
|
||||||
|
import {MatSelectModule} from '@angular/material/select';
|
||||||
|
import {MatInputModule} from '@angular/material/input';
|
||||||
|
import {HistoryFormatService} from '@pages/reference/bans/history-format.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-appeal',
|
||||||
|
imports: [
|
||||||
|
HeaderComponent,
|
||||||
|
NgOptimizedImage,
|
||||||
|
MatButtonModule,
|
||||||
|
MatIconModule,
|
||||||
|
MatProgressSpinnerModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatSelectModule,
|
||||||
|
MatInputModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
],
|
||||||
|
templateUrl: './appeal.component.html',
|
||||||
|
styleUrl: './appeal.component.scss'
|
||||||
|
})
|
||||||
|
export class AppealComponent implements OnInit, AfterViewInit {
|
||||||
|
|
||||||
|
public form: FormGroup<Appeal>;
|
||||||
|
private resizeObserver: ResizeObserver | null = null;
|
||||||
|
private boundHandleResize: any;
|
||||||
|
protected history = signal<PunishmentHistory[] | null>(null);
|
||||||
|
protected selectedPunishment = signal<PunishmentHistory | null>(null);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private historyFormatService: HistoryFormatService,
|
||||||
|
private appealApi: AppealsService,
|
||||||
|
private historyApi: HistoryService,
|
||||||
|
protected authService: AuthService,
|
||||||
|
private elementRef: ElementRef,
|
||||||
|
private renderer: Renderer2
|
||||||
|
) {
|
||||||
|
this.form = new FormGroup({
|
||||||
|
email: new FormControl('', {nonNullable: true, validators: [Validators.required, Validators.email]}),
|
||||||
|
appeal: new FormControl('', {nonNullable: true, validators: [Validators.required, Validators.minLength(10)]})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
const uuid = this.authService.getUuid();
|
||||||
|
if (uuid === null) {
|
||||||
|
throw new Error('JWT subject is null, are you logged in?');
|
||||||
|
}
|
||||||
|
this.historyApi.getAllHistoryForUUID(uuid).subscribe(history => {
|
||||||
|
this.history.set(history.filter(item => this.historyFormatService.isActive(item)));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
ngAfterViewInit() {
|
||||||
|
this.setupResizeObserver();
|
||||||
|
this.updateContainerHeight();
|
||||||
|
|
||||||
|
this.boundHandleResize = this.handleResize.bind(this);
|
||||||
|
window.addEventListener('resize', this.boundHandleResize);
|
||||||
|
|
||||||
|
setTimeout(() => this.updateContainerHeight(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
if (this.resizeObserver) {
|
||||||
|
this.resizeObserver.disconnect();
|
||||||
|
this.resizeObserver = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.boundHandleResize) {
|
||||||
|
window.removeEventListener('resize', this.boundHandleResize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleResize() {
|
||||||
|
this.updateContainerHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupResizeObserver() {
|
||||||
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
|
this.updateContainerHeight();
|
||||||
|
});
|
||||||
|
|
||||||
|
const headerElement = document.querySelector('app-header');
|
||||||
|
if (headerElement) {
|
||||||
|
this.resizeObserver.observe(headerElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
const footerElement = document.querySelector('footer');
|
||||||
|
if (footerElement) {
|
||||||
|
this.resizeObserver.observe(footerElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateContainerHeight() {
|
||||||
|
const headerElement = document.querySelector('app-header');
|
||||||
|
const footerElement = document.querySelector('footer');
|
||||||
|
|
||||||
|
const container = this.elementRef.nativeElement.querySelector('.appeal-container');
|
||||||
|
|
||||||
|
if (headerElement && footerElement && container) {
|
||||||
|
const headerHeight = headerElement.getBoundingClientRect().height;
|
||||||
|
const footerHeight = footerElement.getBoundingClientRect().height;
|
||||||
|
|
||||||
|
const calculatedHeight = `calc(100vh - ${headerHeight}px - ${footerHeight}px)`;
|
||||||
|
this.renderer.setStyle(container, 'min-height', calculatedHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onSubmit() {
|
||||||
|
if (this.form === undefined) {
|
||||||
|
console.error('Form is undefined');
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.form.valid) {
|
||||||
|
this.sendForm()
|
||||||
|
} else {
|
||||||
|
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 uuid = this.authService.getUuid();
|
||||||
|
if (uuid === null) {
|
||||||
|
throw new Error('JWT subject is null, are you logged in?');
|
||||||
|
}
|
||||||
|
const appeal: MinecraftAppeal = {
|
||||||
|
appeal: rawValue.appeal,
|
||||||
|
email: rawValue.email,
|
||||||
|
punishmentId: this.selectedPunishment()!.id,
|
||||||
|
username: this.authService.username()!,
|
||||||
|
uuid: uuid
|
||||||
|
}
|
||||||
|
this.appealApi.submitMinecraftAppeal(appeal).subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
public currentPageIndex: number = 0;
|
||||||
|
public totalPages: number[] = [0];
|
||||||
|
|
||||||
|
public goToPage(pageIndex: number): void {
|
||||||
|
if (pageIndex >= 0 && pageIndex < this.totalPages.length) {
|
||||||
|
this.currentPageIndex = pageIndex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public previousPage() {
|
||||||
|
this.goToPage(this.currentPageIndex - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public nextPage() {
|
||||||
|
if (this.currentPageIndex === this.totalPages.length - 1) {
|
||||||
|
this.totalPages.push(this.currentPageIndex + 1);
|
||||||
|
}
|
||||||
|
this.goToPage(this.currentPageIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isFirstPage(): boolean {
|
||||||
|
return this.currentPageIndex === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isLastPage(): boolean {
|
||||||
|
return this.currentPageIndex === this.totalPages.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected readonly length = length;
|
||||||
|
|
||||||
|
onPunishmentSelected($event: PunishmentHistory) {
|
||||||
|
this.selectedPunishment.set($event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Appeal {
|
||||||
|
email: FormControl<string>;
|
||||||
|
appeal: FormControl<string>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<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>
|
||||||
|
<main>
|
||||||
|
<section class="darkmodeSection">
|
||||||
|
<section class="columnSection">
|
||||||
|
<div class="columnContainer">
|
||||||
|
<div class="columnParagraph">
|
||||||
|
<a [routerLink]="['/forms/appeal']">
|
||||||
|
<h2>Appeal</h2>
|
||||||
|
<p>
|
||||||
|
If you feel your punishment was unjust, click here to appeal.
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</ng-container>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import {Component, Input} from '@angular/core';
|
||||||
|
import {HeaderComponent} from '@header/header.component';
|
||||||
|
import {RouterLink} from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-forms',
|
||||||
|
imports: [
|
||||||
|
HeaderComponent,
|
||||||
|
RouterLink
|
||||||
|
],
|
||||||
|
templateUrl: './forms.component.html',
|
||||||
|
styleUrl: './forms.component.scss'
|
||||||
|
})
|
||||||
|
export class FormsComponent {
|
||||||
|
@Input() formTitle: string = 'Form';
|
||||||
|
@Input() currentPage: string = 'forms';
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user