Compare commits
78
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5013b9a204 | ||
|
|
fcb64db137 | ||
|
|
d2e064e2b4 | ||
|
|
f50f2dc6c2 | ||
|
|
c277306c2c | ||
|
|
1f03a4bdc3 | ||
|
|
7f1c59d102 | ||
|
|
f968a64dd4 | ||
|
|
c25364caf7 | ||
|
|
15c3cc7f26 | ||
|
|
2b96957876 | ||
|
|
b16fab26e7 | ||
|
|
28fd05a656 | ||
|
|
ff1b09be92 | ||
|
|
8a839ac922 | ||
|
|
3f76a98409 | ||
|
|
871615702b | ||
|
|
291c9df5c6 | ||
|
|
4150324d75 | ||
|
|
4267c782a7 | ||
|
|
343964eda8 | ||
|
|
1ce2088cae | ||
|
|
0b952e07f7 | ||
|
|
c2b9a8a574 | ||
|
|
d3ef296784 | ||
|
|
5a792463cc | ||
|
|
974d50d7cd | ||
|
|
62f837914c | ||
|
|
ace969ba3b | ||
|
|
2fc6ba53f6 | ||
|
|
4c38b070ea | ||
|
|
db394beda6 | ||
|
|
76cb3cd89c | ||
|
|
5d8ab2deef | ||
|
|
aef32a8982 | ||
|
|
42f0961f13 | ||
|
|
04310e1cce | ||
|
|
54e747118c | ||
|
|
43430cfbef | ||
|
|
cce83a08de | ||
|
|
f0faa63ca7 | ||
|
|
dfea91d8ca | ||
|
|
73916f0aae | ||
|
|
ebe66c87c0 | ||
|
|
c42fc38b2c | ||
|
|
213f9987d9 | ||
|
|
48cac607de | ||
|
|
6ed2e15017 | ||
|
|
7fc25f46f3 | ||
|
|
c72703ea32 | ||
|
|
e837a9216d | ||
|
|
d4363b3a8a | ||
|
|
1e5862bae6 | ||
|
|
daf88ea437 | ||
|
|
9abd570b87 | ||
|
|
5284d498f3 | ||
|
|
c3a7be82e9 | ||
|
|
fdb57289f8 | ||
|
|
60c1329163 | ||
|
|
0e71c0f581 | ||
|
|
eb67a33331 | ||
|
|
39f20796ce | ||
|
|
e00165c56f | ||
|
|
02c6497700 | ||
|
|
0efd476676 | ||
|
|
237518638c | ||
|
|
fea1a98cea | ||
|
|
ecd9b3d824 | ||
|
|
9808b5d63d | ||
|
|
c13b7077a7 | ||
|
|
023ae809ef | ||
|
|
3a6f137c9a | ||
|
|
cb8447a096 | ||
|
|
3e98e1a498 | ||
|
|
4c31a91bb4 | ||
|
|
d6faaba01c | ||
|
|
56175e62d6 | ||
|
|
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.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.oauth2.jwt.JwtDecoder;
|
||||
import org.springframework.security.oauth2.jwt.JwtEncoder;
|
||||
@@ -31,20 +32,37 @@ import java.security.interfaces.RSAPublicKey;
|
||||
public class SecurityConfig {
|
||||
|
||||
private final KeyPairService keyPairService;
|
||||
private final SecurityAuthFailureHandler securityAuthFailureHandler;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
return http
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/login/userLogin/**", "/login/requestNewUserLogin/**").permitAll()
|
||||
.requestMatchers("/team/**", "/history/**").permitAll()
|
||||
.requestMatchers("/form/**").hasAuthority(PermissionClaimDto.USER.getValue())
|
||||
.requestMatchers("/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()))
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.build();
|
||||
.authorizeHttpRequests(
|
||||
auth -> auth
|
||||
.requestMatchers("/api/form/**").hasAuthority(PermissionClaimDto.USER.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()
|
||||
)
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.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();
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
package com.alttd.altitudeweb.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.resource.PathResourceResolver;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Configuration
|
||||
@Slf4j @Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry.addResourceHandler("/**")
|
||||
.addResourceLocations("classpath:/static/")
|
||||
.addResourceLocations("classpath:/static/browser")
|
||||
.resourceChain(true)
|
||||
.addResolver(new PathResourceResolver() {
|
||||
@Override
|
||||
@@ -23,11 +26,23 @@ public class WebConfig implements WebMvcConfigurer {
|
||||
Resource requestedResource = location.createRelative(resourcePath);
|
||||
|
||||
if (requestedResource.exists() && requestedResource.isReadable()) {
|
||||
log.debug("Serving resource {} from {}", resourcePath, location);
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+22
-15
@@ -36,6 +36,9 @@ public class LoginController implements LoginApi {
|
||||
@Value("${login.secret:#{null}}")
|
||||
private String loginSecret;
|
||||
|
||||
@Value("${my-server.address:#{null}}")
|
||||
private String serverAddress;
|
||||
|
||||
private record CacheEntry(UUID uuid, Instant expiry) {}
|
||||
|
||||
private static final ConcurrentMap<String, CacheEntry> cache = new ConcurrentHashMap<>();
|
||||
@@ -83,21 +86,23 @@ public class LoginController implements LoginApi {
|
||||
@RateLimit(limit = 5, timeValue = 1, timeUnit = TimeUnit.MINUTES, key = "login")
|
||||
@Override
|
||||
public ResponseEntity<String> login(String code) {
|
||||
CacheEntry cacheEntry1 = new CacheEntry(UUID.fromString("55e46bc3-2a29-4c53-850f-dbd944dc5c5f"), Instant.now().plusSeconds(TimeUnit.DAYS.toSeconds(1)));
|
||||
cache.put("23232323", cacheEntry1);
|
||||
if (code == null) {
|
||||
log.warn("Received null login code");
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
CacheEntry cacheEntry = cache.get(code);
|
||||
if (cacheEntry == null || cacheEntry.expiry().isBefore(Instant.now())) {
|
||||
log.warn("Received invalid login code {}", code);
|
||||
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||
}
|
||||
|
||||
String token = generateToken(cacheEntry.uuid);
|
||||
log.debug("Generated token for user {} with token {}", cacheEntry.uuid, token);
|
||||
|
||||
cache.remove(code);
|
||||
|
||||
log.debug("Generated token for user {}", cacheEntry.uuid);
|
||||
return ResponseEntity.ok(token);
|
||||
}
|
||||
|
||||
@@ -134,12 +139,13 @@ public class LoginController implements LoginApi {
|
||||
Instant now = Instant.now();
|
||||
//TODO make a JWT for renewing and one for storing permissions for a session (expiry 1 hour)
|
||||
Instant expiryTime = now.plusSeconds(TimeUnit.DAYS.toSeconds(30));
|
||||
CompletableFuture<PrivilegedUser> privilegedUserCompletableFuture = new CompletableFuture<>();
|
||||
CompletableFuture<Optional<PrivilegedUser>> privilegedUserCompletableFuture = new CompletableFuture<>();
|
||||
List<PermissionClaimDto> claimList = new ArrayList<>();
|
||||
Connection.getConnection(Databases.DEFAULT)
|
||||
.runQuery(sqlSession -> {
|
||||
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());
|
||||
|
||||
privilegedUserCompletableFuture.complete(privilegedUser);
|
||||
@@ -148,19 +154,20 @@ public class LoginController implements LoginApi {
|
||||
privilegedUserCompletableFuture.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
PrivilegedUser privilegedUser = privilegedUserCompletableFuture.join();
|
||||
Optional<PrivilegedUser> privilegedUser = privilegedUserCompletableFuture.join();
|
||||
claimList.add(PermissionClaimDto.USER);
|
||||
if (privilegedUser != null) {
|
||||
privilegedUser.getPermissions().forEach(permission -> {
|
||||
try {
|
||||
claimList.add(PermissionClaimDto.valueOf(permission));
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Received invalid permission claim: {}", permission);
|
||||
}
|
||||
});
|
||||
}
|
||||
privilegedUser.ifPresent(user -> user.getPermissions().forEach(permission -> {
|
||||
try {
|
||||
claimList.add(PermissionClaimDto.fromValue(permission));
|
||||
log.debug("Added permission claim {}", permission);
|
||||
} catch (IllegalArgumentException e) {
|
||||
log.warn("Received invalid permission claim: {}", permission);
|
||||
}
|
||||
}));
|
||||
log.debug("Generated token for user {} with claims {}", uuid.toString(),
|
||||
claimList.stream().map(PermissionClaimDto::getValue).toList());
|
||||
JwtClaimsSet claims = JwtClaimsSet.builder()
|
||||
.issuer("altitudeweb")
|
||||
.issuer(serverAddress)
|
||||
.claim("authorities", claimList.stream().map(PermissionClaimDto::getValue).toList())
|
||||
.issuedAt(now)
|
||||
.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.password=${DB_PASSWORD:root}
|
||||
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.password=${DB_PASSWORD:root}
|
||||
cors.allowed-origins=${CORS:http://localhost:4200}
|
||||
my-server.address=${SERVER_ADDRESS:http://localhost}
|
||||
logging.level.com.alttd.altitudeweb=DEBUG
|
||||
|
||||
@@ -6,4 +6,7 @@ database.user=${DB_USER:root}
|
||||
database.password=${DB_PASSWORD:root}
|
||||
cors.allowed-origins=${CORS:https://alttd.com}
|
||||
login.secret=${LOGIN_SECRET:SET_TOKEN}
|
||||
particles.file_path=${user.home}/.altitudeweb/particles
|
||||
notification.server.url=${SERVER_IP:10.0.0.107}:${SERVER_PORT:8080}
|
||||
my-server.address=${SERVER_ADDRESS:https://alttd.com}
|
||||
logging.level.com.alttd.altitudeweb=INFO
|
||||
|
||||
+8
-7
@@ -1,29 +1,30 @@
|
||||
package com.alttd.altitudeweb.database.web_db;
|
||||
|
||||
import org.apache.ibatis.annotations.*;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface PrivilegedUserMapper {
|
||||
|
||||
/**
|
||||
* Retrieves a user by their UUID along with their permissions
|
||||
* @param uuid The UUID of the user to retrieve
|
||||
* @return The PrivilegedUser with their permissions, or null if not found
|
||||
* @return The optional PrivilegedUser with their permissions
|
||||
*/
|
||||
@Select("""
|
||||
SELECT privileged_users.id, privileged_users.uuid, privileges.privileges as permission
|
||||
FROM privileged_users
|
||||
LEFT JOIN privileges ON privileged_users.id = privileges.user_id
|
||||
WHERE privileged_users.uuid = #{uuid}
|
||||
""")
|
||||
SELECT id, uuid
|
||||
FROM privileged_users
|
||||
WHERE uuid = #{uuid}
|
||||
""")
|
||||
@Results({
|
||||
@Result(property = "id", column = "id"),
|
||||
@Result(property = "uuid", column = "uuid"),
|
||||
@Result(property = "permissions", column = "id", javaType = List.class,
|
||||
many = @Many(select = "getPermissionsForUser"))
|
||||
})
|
||||
PrivilegedUser getUserByUuid(@Param("uuid") String uuid);
|
||||
Optional<PrivilegedUser> getUserByUuid(@Param("uuid") String uuid);
|
||||
|
||||
/**
|
||||
* Retrieves all privileged users with their permissions
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.alttd.altitudeweb.setup;
|
||||
|
||||
import com.alttd.altitudeweb.database.Databases;
|
||||
import com.alttd.altitudeweb.database.web_db.KeyPairMapper;
|
||||
import com.alttd.altitudeweb.database.web_db.PrivilegedUserMapper;
|
||||
import com.alttd.altitudeweb.database.web_db.SettingsMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.session.SqlSession;
|
||||
@@ -18,6 +19,7 @@ public class InitializeWebDb {
|
||||
Connection.getConnection(Databases.DEFAULT, (configuration) -> {
|
||||
configuration.addMapper(SettingsMapper.class);
|
||||
configuration.addMapper(KeyPairMapper.class);
|
||||
configuration.addMapper(PrivilegedUserMapper.class);
|
||||
}).join()
|
||||
.runQuery(SqlSession -> {
|
||||
createSettingsTable(SqlSession);
|
||||
|
||||
+37
-13
@@ -15,11 +15,12 @@
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"builder": "@angular/build:application",
|
||||
"options": {
|
||||
"outputPath": "dist",
|
||||
"outputPath": {
|
||||
"base": "dist"
|
||||
},
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": [
|
||||
"zone.js"
|
||||
],
|
||||
@@ -34,7 +35,8 @@
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
"scripts": [],
|
||||
"browser": "src/main.ts"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
@@ -47,9 +49,7 @@
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true
|
||||
"namedChunks": false
|
||||
},
|
||||
"development": {
|
||||
"sourceMap": true,
|
||||
@@ -71,14 +71,12 @@
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"sourceMap": false,
|
||||
"namedChunks": false,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true
|
||||
"namedChunks": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"builder": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "frontend:build:production"
|
||||
@@ -90,10 +88,10 @@
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n"
|
||||
"builder": "@angular/build:extract-i18n"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"builder": "@angular/build:karma",
|
||||
"options": {
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
@@ -119,5 +117,31 @@
|
||||
},
|
||||
"cli": {
|
||||
"analytics": false
|
||||
},
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"type": "component"
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"type": "directive"
|
||||
},
|
||||
"@schematics/angular:service": {
|
||||
"type": "service"
|
||||
},
|
||||
"@schematics/angular:guard": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:interceptor": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:module": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:pipe": {
|
||||
"typeSeparator": "."
|
||||
},
|
||||
"@schematics/angular:resolver": {
|
||||
"typeSeparator": "."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+27
-27
@@ -8,8 +8,9 @@ plugins {
|
||||
|
||||
node {
|
||||
download.set(true)
|
||||
version.set("22.14.0")
|
||||
npmVersion.set("10.9.2")
|
||||
// Update to the version that's compatible with your environment requirements
|
||||
version.set("20.19.0")
|
||||
npmVersion.set("10.2.3") // A compatible npm version for Node.js 20.19.0
|
||||
workDir.set(file("${project.projectDir}/node"))
|
||||
npmWorkDir.set(file("${project.projectDir}/node"))
|
||||
}
|
||||
@@ -21,38 +22,37 @@ tasks.register<Delete>("cleanDist") {
|
||||
}
|
||||
|
||||
// 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'"
|
||||
group = "build"
|
||||
|
||||
doLast {
|
||||
// Use nodeCommand directly from the plugin
|
||||
project.exec {
|
||||
workingDir(project.projectDir)
|
||||
|
||||
// Use node's npm to ensure it works on all environments
|
||||
val nodeDir = "${project.projectDir}/node"
|
||||
val isWindows = System.getProperty("os.name").lowercase().contains("windows")
|
||||
|
||||
if (isWindows) {
|
||||
val npmCmd = file(nodeDir).listFiles()?.find { it.name.startsWith("npm") && it.isDirectory }?.let {
|
||||
"${it.absolutePath}/npm.cmd"
|
||||
} ?: "$nodeDir/node_modules/npm/bin/npm.cmd"
|
||||
|
||||
commandLine(npmCmd, "run", "build:dev")
|
||||
} else {
|
||||
val npmExecutable = file(nodeDir).listFiles()?.find { it.name.startsWith("npm") && it.isDirectory }?.let {
|
||||
"${it.absolutePath}/bin/npm"
|
||||
} ?: "$nodeDir/node_modules/npm/bin/npm"
|
||||
|
||||
commandLine(npmExecutable, "run", "build:beta")
|
||||
}
|
||||
}
|
||||
}
|
||||
// Determine which build script to run based on the OS
|
||||
val isWindows = System.getProperty("os.name").lowercase().contains("windows")
|
||||
npmCommand.set(listOf("run", if (isWindows) "build:dev" else "build:beta"))
|
||||
|
||||
dependsOn("npmInstall")
|
||||
}
|
||||
|
||||
// Add a new task to check Node.js and npm versions
|
||||
tasks.register<com.github.gradle.node.task.NodeTask>("nodeVersionCheck") {
|
||||
description = "Check Node.js and npm versions"
|
||||
script.set(file("${projectDir}/node-version-check.js"))
|
||||
|
||||
doFirst {
|
||||
// Create a temporary script to check versions
|
||||
file("${projectDir}/node-version-check.js").writeText("""
|
||||
console.log('Node.js version:', process.version);
|
||||
console.log('npm version:', require('npm/package.json').version);
|
||||
console.log('Build command that would be used:', process.platform === 'win32' ? 'build:dev' : 'build:beta');
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
doLast {
|
||||
// Clean up the temporary script
|
||||
delete("${projectDir}/node-version-check.js")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named("assemble") {
|
||||
dependsOn("npmBuild")
|
||||
}
|
||||
|
||||
+15
-14
@@ -13,26 +13,27 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/cdk": "^19.2.18",
|
||||
"@angular/common": "^19.2.0",
|
||||
"@angular/compiler": "^19.2.0",
|
||||
"@angular/core": "^19.2.0",
|
||||
"@angular/forms": "^19.2.0",
|
||||
"@angular/material": "^19.2.18",
|
||||
"@angular/platform-browser": "^19.2.0",
|
||||
"@angular/platform-browser-dynamic": "^19.2.0",
|
||||
"@angular/router": "^19.2.0",
|
||||
"@angular/cdk": "^20.1.3",
|
||||
"@angular/common": "^20.1.0",
|
||||
"@angular/compiler": "^20.1.0",
|
||||
"@angular/core": "^20.1.0",
|
||||
"@angular/forms": "^20.1.0",
|
||||
"@angular/material": "^20.1.3",
|
||||
"@angular/platform-browser": "^20.1.0",
|
||||
"@angular/platform-browser-dynamic": "^20.1.0",
|
||||
"@angular/router": "^20.1.0",
|
||||
"@auth0/angular-jwt": "^5.2.0",
|
||||
"@types/three": "^0.177.0",
|
||||
"ngx-cookie-service": "^19.1.2",
|
||||
"ngx-cookie-service": "^20.0.1",
|
||||
"rxjs": "~7.8.0",
|
||||
"three": "^0.177.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^19.2.5",
|
||||
"@angular/cli": "^19.2.5",
|
||||
"@angular/compiler-cli": "^19.2.0",
|
||||
"@angular/build": "^20.1.0",
|
||||
"@angular/cli": "^20.1.0",
|
||||
"@angular/compiler-cli": "^20.1.0",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"jasmine-core": "~5.6.0",
|
||||
"karma": "~6.4.0",
|
||||
@@ -40,6 +41,6 @@
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.7.2"
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 KiB |
@@ -1,21 +0,0 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {ScrollService} from '../scroll/scroll.service';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {HeaderComponent} from '../header/header.component';
|
||||
import {RemoveTrailingPeriodPipe} from "../util/RemoveTrailingPeriodPipe";
|
||||
|
||||
@Component({
|
||||
selector: 'app-about',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
HeaderComponent,
|
||||
RemoveTrailingPeriodPipe
|
||||
],
|
||||
templateUrl: './about.component.html',
|
||||
styleUrl: './about.component.scss'
|
||||
})
|
||||
export class AboutComponent {
|
||||
constructor(public scrollService: ScrollService) {
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {Meta, Title} from '@angular/platform-browser';
|
||||
import {ALTITUDE_VERSION} from './constant';
|
||||
import {ALTITUDE_VERSION} from '@custom-types/constant';
|
||||
import {Router, RouterOutlet} from '@angular/router';
|
||||
import {FooterComponent} from './footer/footer.component';
|
||||
import {FooterComponent} from '@pages/footer/footer/footer.component';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -10,8 +10,8 @@ import {FooterComponent} from './footer/footer.component';
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.scss',
|
||||
imports: [
|
||||
FooterComponent,
|
||||
RouterOutlet
|
||||
RouterOutlet,
|
||||
FooterComponent
|
||||
]
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import {ApplicationConfig, provideZoneChangeDetection} from '@angular/core';
|
||||
import {provideRouter} from '@angular/router';
|
||||
import {CookieService} from 'ngx-cookie-service';
|
||||
|
||||
import {routes} from './app.routes';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [provideZoneChangeDetection({eventCoalescing: true}), provideRouter(routes)]
|
||||
providers: [
|
||||
provideZoneChangeDetection({eventCoalescing: true}),
|
||||
provideRouter(routes),
|
||||
CookieService,
|
||||
]
|
||||
};
|
||||
|
||||
@@ -1,116 +1,133 @@
|
||||
import {Routes} from '@angular/router';
|
||||
import {AuthGuard} from './guards/auth.guard';
|
||||
|
||||
export const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent)
|
||||
loadComponent: () => import('./pages/home/home.component').then(m => m.HomeComponent)
|
||||
},
|
||||
{
|
||||
path: 'particles',
|
||||
loadComponent: () => import('./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',
|
||||
loadComponent: () => import('./map/map.component').then(m => m.MapComponent)
|
||||
loadComponent: () => import('./pages/features/map/map.component').then(m => m.MapComponent)
|
||||
},
|
||||
{
|
||||
path: 'rules',
|
||||
loadComponent: () => import('./rules/rules.component').then(m => m.RulesComponent)
|
||||
loadComponent: () => import('./pages/reference/rules/rules.component').then(m => m.RulesComponent)
|
||||
},
|
||||
{
|
||||
path: 'vote',
|
||||
loadComponent: () => import('./vote/vote.component').then(m => m.VoteComponent)
|
||||
loadComponent: () => import('./pages/vote/vote.component').then(m => m.VoteComponent)
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
loadComponent: () => import('./about/about.component').then(m => m.AboutComponent)
|
||||
loadComponent: () => import('./pages/altitude/about/about.component').then(m => m.AboutComponent)
|
||||
},
|
||||
{
|
||||
path: 'socials',
|
||||
loadComponent: () => import('./socials/socials.component').then(m => m.SocialsComponent)
|
||||
loadComponent: () => import('./pages/altitude/socials/socials.component').then(m => m.SocialsComponent)
|
||||
},
|
||||
{
|
||||
path: 'team',
|
||||
loadComponent: () => import('./team/team.component').then(m => m.TeamComponent)
|
||||
loadComponent: () => import('./pages/altitude/team/team.component').then(m => m.TeamComponent)
|
||||
},
|
||||
{
|
||||
path: 'birthdays',
|
||||
loadComponent: () => import('./birthdays/birthdays.component').then(m => m.BirthdaysComponent)
|
||||
loadComponent: () => import('./pages/altitude/birthdays/birthdays.component').then(m => m.BirthdaysComponent)
|
||||
},
|
||||
{
|
||||
path: 'terms',
|
||||
loadComponent: () => import('./terms/terms.component').then(m => m.TermsComponent)
|
||||
loadComponent: () => import('./pages/footer/terms/terms.component').then(m => m.TermsComponent)
|
||||
},
|
||||
{
|
||||
path: 'privacy',
|
||||
loadComponent: () => import('./privacy/privacy.component').then(m => m.PrivacyComponent)
|
||||
loadComponent: () => import('./pages/footer/privacy/privacy.component').then(m => m.PrivacyComponent)
|
||||
},
|
||||
{
|
||||
path: 'bans',
|
||||
loadComponent: () => import('./bans/bans.component').then(m => m.BansComponent)
|
||||
loadComponent: () => import('./pages/reference/bans/bans.component').then(m => m.BansComponent)
|
||||
},
|
||||
{
|
||||
path: 'bans/:type/:id',
|
||||
loadComponent: () => import('./bans/details/details.component').then(m => m.DetailsComponent)
|
||||
loadComponent: () => import('./pages/reference/bans/details/details.component').then(m => m.DetailsComponent)
|
||||
},
|
||||
{
|
||||
path: 'economy',
|
||||
loadComponent: () => import('./economy/economy.component').then(m => m.EconomyComponent)
|
||||
loadComponent: () => import('./pages/features/economy/economy.component').then(m => m.EconomyComponent)
|
||||
},
|
||||
{
|
||||
path: 'claiming',
|
||||
loadComponent: () => import('./claiming/claiming.component').then(m => m.ClaimingComponent)
|
||||
loadComponent: () => import('./pages/features/claiming/claiming.component').then(m => m.ClaimingComponent)
|
||||
},
|
||||
{
|
||||
path: 'mypet',
|
||||
loadComponent: () => import('./mypet/mypet.component').then(m => m.MypetComponent)
|
||||
loadComponent: () => import('./pages/features/mypet/mypet.component').then(m => m.MypetComponent)
|
||||
},
|
||||
{
|
||||
path: 'warps',
|
||||
loadComponent: () => import('./warps/warps.component').then(m => m.WarpsComponent)
|
||||
loadComponent: () => import('./pages/features/warps/warps.component').then(m => m.WarpsComponent)
|
||||
},
|
||||
{
|
||||
path: 'skyblock',
|
||||
loadComponent: () => import('./skyblock/skyblock.component').then(m => m.SkyblockComponent)
|
||||
loadComponent: () => import('./pages/features/skyblock/skyblock.component').then(m => m.SkyblockComponent)
|
||||
},
|
||||
{
|
||||
path: 'customfeatures',
|
||||
loadComponent: () => import('./customfeatures/customfeatures.component').then(m => m.CustomfeaturesComponent)
|
||||
loadComponent: () => import('./pages/features/customfeatures/customfeatures.component').then(m => m.CustomfeaturesComponent)
|
||||
},
|
||||
{
|
||||
path: 'guide',
|
||||
loadComponent: () => import('./guide/guide.component').then(m => m.GuideComponent)
|
||||
loadComponent: () => import('./pages/reference/guide/guide.component').then(m => m.GuideComponent)
|
||||
},
|
||||
{
|
||||
path: 'ranks',
|
||||
loadComponent: () => import('./ranks/ranks.component').then(m => m.RanksComponent)
|
||||
loadComponent: () => import('./pages/reference/ranks/ranks.component').then(m => m.RanksComponent)
|
||||
},
|
||||
{
|
||||
path: 'commandlist',
|
||||
loadComponent: () => import('./commandlist/commandlist.component').then(m => m.CommandlistComponent)
|
||||
loadComponent: () => import('./pages/reference/commandlist/commandlist.component').then(m => m.CommandlistComponent)
|
||||
},
|
||||
{
|
||||
path: 'mapart',
|
||||
loadComponent: () => import('./mapart/mapart.component').then(m => m.MapartComponent)
|
||||
loadComponent: () => import('./pages/reference/mapart/mapart.component').then(m => m.MapartComponent)
|
||||
},
|
||||
{
|
||||
path: 'lag',
|
||||
loadComponent: () => import('./lag/lag.component').then(m => m.LagComponent)
|
||||
loadComponent: () => import('./pages/reference/lag/lag.component').then(m => m.LagComponent)
|
||||
},
|
||||
{
|
||||
path: 'staffpowers',
|
||||
loadComponent: () => import('./staffpowers/staffpowers.component').then(m => m.StaffpowersComponent)
|
||||
loadComponent: () => import('./pages/reference/staffpowers/staffpowers.component').then(m => m.StaffpowersComponent)
|
||||
},
|
||||
{
|
||||
path: 'forms/:form',
|
||||
loadComponent: () => import('./forms/forms.component').then(m => m.FormsComponent)
|
||||
path: 'forms/appeal',
|
||||
loadComponent: () => import('./pages/forms/appeal/appeal.component').then(m => m.AppealComponent),
|
||||
canActivate: [AuthGuard],
|
||||
data: {
|
||||
requiredAuthorizations: ['SCOPE_user']
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'forms',
|
||||
loadComponent: () => import('./forms/forms.component').then(m => m.FormsComponent)
|
||||
loadComponent: () => import('./pages/forms/forms.component').then(m => m.FormsComponent)
|
||||
},
|
||||
{
|
||||
path: 'particles',
|
||||
loadComponent: () => import('./particles/particles.component').then(m => m.ParticlesComponent)
|
||||
path: 'community',
|
||||
loadComponent: () => import('./pages/altitude/community/community.component').then(m => m.CommunityComponent)
|
||||
},
|
||||
{
|
||||
path: 'nicknames',
|
||||
loadComponent: () => import('./pages/reference/nicknames/nicknames.component').then(m => m.NicknamesComponent)
|
||||
},
|
||||
{
|
||||
path: 'nickgenerator',
|
||||
loadComponent: () => import('./pages/reference/nickgenerator/nickgenerator.component').then(m => m.NickgeneratorComponent)
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="'bans'" height="200px" background_image="/public/img/backgrounds/staff.png"
|
||||
[overlay_gradient]="0.5">>
|
||||
<div class="title" header-content>
|
||||
<h1>Minecraft Punishments</h1>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<main>
|
||||
<section class="darkmodeSection">
|
||||
<div class="container">
|
||||
<div class="columnSection">
|
||||
<div class="historyButtonContainer">
|
||||
<div [id]="getCurrentButtonId('all')" class="button-outer" (click)="changeHistoryPunishment('all')">
|
||||
<span class="button-inner"
|
||||
[ngClass]="active">All</span>
|
||||
</div>
|
||||
<div [id]="getCurrentButtonId('ban')" class="button-outer" (click)="changeHistoryPunishment('ban')">
|
||||
<span class="button-inner"
|
||||
[ngClass]="active">Bans</span>
|
||||
</div>
|
||||
<div [id]="getCurrentButtonId('mute')" class="button-outer" (click)="changeHistoryPunishment('mute')">
|
||||
<span class="button-inner"
|
||||
[ngClass]="active">Mutes</span>
|
||||
</div>
|
||||
<div [id]="getCurrentButtonId('warn')" class="button-outer" (click)="changeHistoryPunishment('warn')">
|
||||
<span class="button-inner"
|
||||
[ngClass]="active">Warnings</span>
|
||||
</div>
|
||||
<div [id]="getCurrentUserTypeButtonId('player')" class="button-outer" (click)="changeUserType('player')"
|
||||
style="margin-left: 120px;">
|
||||
<span class="button-inner"
|
||||
[ngClass]="active">Player</span>
|
||||
</div>
|
||||
<div [id]="getCurrentUserTypeButtonId('staff')" class="button-outer" (click)="changeUserType('staff')">
|
||||
<span class="button-inner"
|
||||
[ngClass]="active">Staff</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="historySearchContainer">
|
||||
<input class="historySearch"
|
||||
type="search"
|
||||
placeholder="Search.."
|
||||
[(ngModel)]="searchTerm"
|
||||
(input)="filterNames()"
|
||||
(keyup.enter)="search()"
|
||||
>
|
||||
<div class="dropdown-results" *ngIf="filteredNames.length > 0 && searchTerm">
|
||||
<div
|
||||
class="dropdown-item"
|
||||
*ngFor="let name of filteredNames"
|
||||
(mousedown)="selectName(name)">
|
||||
{{ name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="historyTable">
|
||||
<app-history [userType]="userType" [punishmentType]="punishmentType"
|
||||
[page]="page" [searchTerm]="finalSearchTerm" (pageChange)="updatePageSize($event)"
|
||||
(selectItem)="setSearch($event)">
|
||||
</app-history>
|
||||
</div>
|
||||
<div class="changePageButtons">
|
||||
<button [ngClass]="{'active': buttonActive(0), 'disabled': !buttonActive(0)}"
|
||||
[disabled]="!buttonActive(0)"
|
||||
(click)="setPage(0)" class="historyPageButton">
|
||||
First page
|
||||
</button>
|
||||
<button [ngClass]="{'active': buttonActive(0), 'disabled': !buttonActive(0)}"
|
||||
[disabled]="!buttonActive(0)"
|
||||
(click)="previousPage()" class="historyPageButton">
|
||||
Previous page
|
||||
</button>
|
||||
<span class="pageNumber">{{ this.page }} / {{ getMaxPage() }}</span>
|
||||
<button [ngClass]="{'active': buttonActive(getMaxPage()), 'disabled': !buttonActive(getMaxPage())}"
|
||||
[disabled]="!buttonActive(getMaxPage())"
|
||||
(click)="nextPage()" class="historyPageButton">
|
||||
Next page
|
||||
</button>
|
||||
<button [ngClass]="{'active': buttonActive(getMaxPage()), 'disabled': !buttonActive(getMaxPage())}"
|
||||
[disabled]="!buttonActive(getMaxPage())"
|
||||
(click)="setPage(getMaxPage())" class="historyPageButton">
|
||||
Last page
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</ng-container>
|
||||
@@ -1,109 +0,0 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="'bans'" height="200px" background_image="/public/img/backgrounds/staff.png"
|
||||
[overlay_gradient]="0.5">>
|
||||
<div class="title" header-content>
|
||||
<h1>Minecraft Punishments</h1>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<main>
|
||||
<section class="darkmodeSection">
|
||||
<section class="columnSection">
|
||||
<div class="detailsBackButton">
|
||||
<ng-container *ngIf="punishment === undefined">
|
||||
<p>Loading...</p>
|
||||
</ng-container>
|
||||
|
||||
<a [routerLink]="['/bans']">< Back</a>
|
||||
</div>
|
||||
</section>
|
||||
<section class="columnSection center">
|
||||
<ng-container *ngIf="punishment">
|
||||
<div>
|
||||
<span class="tag tagInfo"
|
||||
[ngClass]="{
|
||||
'tagPermanent': this.historyFormat.isPermanent(punishment),
|
||||
'tagExpired': !this.historyFormat.isPermanent(punishment)
|
||||
}">
|
||||
{{ this.historyFormat.getType(punishment) }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
class="tag tagInfo"
|
||||
[ngClass]="{
|
||||
'tagActive': this.historyFormat.isActive(punishment),
|
||||
'tagInactive': !this.historyFormat.isActive(punishment)
|
||||
}">
|
||||
{{ this.historyFormat.isActive(punishment) ? 'Active' : 'Inactive' }}
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</section>
|
||||
<section class="columnSection">
|
||||
<div class="columnContainer">
|
||||
<div class="columnParagraph">
|
||||
<ng-container *ngIf="punishment">
|
||||
<div class="playerContainer">
|
||||
<h2>Player</h2>
|
||||
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(punishment.uuid, '150')"
|
||||
width="150"
|
||||
height="150"
|
||||
alt="{{punishment.username}}'s Minecraft skin"
|
||||
>
|
||||
<h3 class="detailsUsername">{{ punishment.username }}</h3>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columnContainer">
|
||||
<div class="columnParagraph">
|
||||
<ng-container *ngIf="punishment">
|
||||
<div class="playerContainer">
|
||||
<h2>Moderator</h2>
|
||||
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(punishment.punishedByUuid, '150')"
|
||||
width="150"
|
||||
height="150"
|
||||
alt="{{punishment.punishedBy}}'s Minecraft skin"
|
||||
>
|
||||
<h3 class="detailsUsername">{{ punishment.punishedBy }}</h3>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columnContainer">
|
||||
<div class="columnParagraph">
|
||||
<ng-container *ngIf="punishment">
|
||||
<div class="detailsInfo">
|
||||
<h2>Reason</h2>
|
||||
<p>{{ punishment.reason | removeTrailingPeriod }}</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columnContainer">
|
||||
<div class="columnParagraph">
|
||||
<ng-container *ngIf="punishment">
|
||||
<div class="detailsInfo">
|
||||
<h2>Date</h2>
|
||||
<p>{{ this.historyFormat.getPunishmentTime(punishment) }}</p>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<section class="columnSection">
|
||||
<ng-container *ngIf="punishment">
|
||||
<span>Expires</span>
|
||||
<span>{{ this.historyFormat.getExpiredTime(punishment) }}</span>
|
||||
<ng-container *ngIf="punishment.removedBy !== undefined && punishment.removedBy.length > 0">
|
||||
<span>Un{{ this.historyFormat.getType(punishment).toLocaleLowerCase() }} reason</span>
|
||||
<span>{{ punishment.removedReason == null ? 'No reason specified' : punishment.removedReason }}</span>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</section>
|
||||
@@ -1,53 +0,0 @@
|
||||
<ng-container *ngIf="history.length === 0">
|
||||
<p>No history found</p>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="history.length > 0">
|
||||
<table [cellSpacing]="0">
|
||||
<div class="historyTableHead">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="historyType">Type</th>
|
||||
<th class="historyPlayer">Player</th>
|
||||
<th class="historyPlayer">Banned By</th>
|
||||
<th class="historyReason">Reason</th>
|
||||
<th class="historyDate">Date</th>
|
||||
<th class="historyDate">Expires</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</div>
|
||||
<div>
|
||||
<tbody>
|
||||
<tr class="historyPlayerRow" *ngFor="let entry of history">
|
||||
<td class="historyType" (click)="showDetailedPunishment(entry)">
|
||||
{{ this.historyFormat.getType(entry) }}
|
||||
</td>
|
||||
<td class="historyPlayer" (click)="setSearch(entry.username, 'player')">
|
||||
<div class="playerContainer">
|
||||
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(entry.uuid)" width="25" height="25"
|
||||
alt="{{entry.username}}'s Minecraft skin">
|
||||
<span class="username">{{ entry.username }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="historyPlayer" (click)="setSearch(entry.punishedBy, 'staff')">
|
||||
<div class="playerContainer">
|
||||
<img class="avatar" [ngSrc]="this.historyFormat.getAvatarUrl(entry.punishedByUuid)" width="25" height="25"
|
||||
alt="{{entry.punishedBy}}'s Minecraft skin">
|
||||
<span>{{ entry.punishedBy }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="historyReason" (click)="showDetailedPunishment(entry)">
|
||||
{{ entry.reason | removeTrailingPeriod }}
|
||||
</td>
|
||||
<td class="historyDate" (click)="showDetailedPunishment(entry)">
|
||||
{{ this.historyFormat.getPunishmentTime(entry) }}
|
||||
</td>
|
||||
<td class="historyDate" (click)="showDetailedPunishment(entry)">
|
||||
{{ this.historyFormat.getExpiredTime(entry) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</div>
|
||||
</table>
|
||||
</ng-container>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<app-forms [currentPage]="'appeal'" [formTitle]="'Minecraft Appeal'">
|
||||
<div form-content>
|
||||
|
||||
</div>
|
||||
</app-forms>
|
||||
@@ -1,69 +0,0 @@
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {FormsComponent} from '../forms.component';
|
||||
import {FormControl, FormGroup, Validators} from '@angular/forms';
|
||||
import {AppealsService, MinecraftAppeal} from '../../../api';
|
||||
|
||||
@Component({
|
||||
selector: 'app-appeal',
|
||||
imports: [
|
||||
FormsComponent
|
||||
],
|
||||
templateUrl: './appeal.component.html',
|
||||
styleUrl: './appeal.component.scss'
|
||||
})
|
||||
export class AppealComponent implements OnInit {
|
||||
|
||||
public form: FormGroup<Appeal>;
|
||||
|
||||
constructor(private appealApi: AppealsService) {
|
||||
this.form = new FormGroup({
|
||||
username: new FormControl('', {nonNullable: true, validators: [Validators.required]}),
|
||||
punishmentId: new FormControl('', {nonNullable: true, validators: [Validators.required]}),
|
||||
email: new FormControl('', {nonNullable: true, validators: [Validators.required, Validators.email]}),
|
||||
appeal: new FormControl('', {nonNullable: true, validators: [Validators.required, Validators.minLength(10)]})
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
public onSubmit() {
|
||||
if (this.form === undefined) {
|
||||
console.error('Form is undefined');
|
||||
return
|
||||
}
|
||||
if (this.form.valid) {
|
||||
this.sendForm()
|
||||
} else {
|
||||
// Mark all fields as touched to trigger validation display
|
||||
Object.keys(this.form.controls).forEach(field => {
|
||||
const control = this.form!.get(field);
|
||||
if (!(control instanceof FormGroup)) {
|
||||
console.error('Control [' + control + '] is not a FormGroup');
|
||||
return;
|
||||
}
|
||||
control.markAsTouched({onlySelf: true});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private sendForm() {
|
||||
const rawValue = this.form.getRawValue();
|
||||
const appeal: MinecraftAppeal = {
|
||||
appeal: rawValue.appeal,
|
||||
email: rawValue.email,
|
||||
punishmentId: parseInt(rawValue.punishmentId),
|
||||
username: rawValue.username,
|
||||
uuid: ''//TODO
|
||||
}
|
||||
this.appealApi.submitMinecraftAppeal(appeal).subscribe()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
interface Appeal {
|
||||
username: FormControl<string>;
|
||||
punishmentId: FormControl<string>;
|
||||
email: FormControl<string>;
|
||||
appeal: FormControl<string>;
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="currentPage" height="200px" background_image="/public/img/backgrounds/staff.png"
|
||||
[overlay_gradient]="0.5">
|
||||
<div class="title" header-content>
|
||||
<h1>{{ formTitle }}</h1>
|
||||
</div>
|
||||
</app-header>
|
||||
<ng-container *ngIf="!type">
|
||||
<ng-container *ngFor="let formType of FormType | keyvalue">
|
||||
<button mat-raised-button (click)="setFormType(formType.value)">
|
||||
{{ formType }}
|
||||
</button>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<div>
|
||||
<ng-content select="[form-content]"></ng-content>
|
||||
</div>
|
||||
</ng-container>
|
||||
@@ -1,68 +0,0 @@
|
||||
import {Component, Input, OnInit} from '@angular/core';
|
||||
import {HeaderComponent} from '../header/header.component';
|
||||
import {MatDialog} from '@angular/material/dialog';
|
||||
import {ActivatedRoute} from '@angular/router';
|
||||
import {LoginDialogComponent} from '../login/login.component';
|
||||
import {KeyValuePipe, NgForOf, NgIf} from '@angular/common';
|
||||
import {FormType} from './form_type';
|
||||
import {MatButton} from '@angular/material/button';
|
||||
import {AuthService} from '../services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-forms',
|
||||
imports: [
|
||||
HeaderComponent,
|
||||
NgIf,
|
||||
NgForOf,
|
||||
MatButton,
|
||||
KeyValuePipe
|
||||
],
|
||||
templateUrl: './forms.component.html',
|
||||
styleUrl: './forms.component.scss'
|
||||
})
|
||||
export class FormsComponent implements OnInit {
|
||||
@Input() formTitle: string = 'Form';
|
||||
@Input() currentPage: string = 'forms';
|
||||
|
||||
public type: FormType | undefined;
|
||||
|
||||
constructor(private authService: AuthService,
|
||||
private dialog: MatDialog,
|
||||
private route: ActivatedRoute,
|
||||
) {
|
||||
this.route.paramMap.subscribe(async params => {
|
||||
const code = params.get('code');
|
||||
|
||||
if (code) {
|
||||
this.authService.login(code).subscribe();
|
||||
} else if (!this.authService.checkAuthStatus()) {
|
||||
const dialogRef = this.dialog.open(LoginDialogComponent, {
|
||||
width: '400px',
|
||||
disableClose: true
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.route.paramMap.subscribe(params => {
|
||||
switch (params.get('form')) {
|
||||
case FormType.APPEAL:
|
||||
this.type = FormType.APPEAL;
|
||||
this.currentPage = 'appeal';
|
||||
break;
|
||||
default:
|
||||
throw new Error("Invalid type");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected readonly FormType = FormType;
|
||||
protected readonly Object = Object;
|
||||
|
||||
public setFormType(formType: FormType) {
|
||||
this.type = formType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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>
|
||||
@@ -0,0 +1,18 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {ScrollService} from '@services/scroll.service';
|
||||
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-about',
|
||||
standalone: true,
|
||||
imports: [
|
||||
HeaderComponent
|
||||
],
|
||||
templateUrl: './about.component.html',
|
||||
styleUrl: './about.component.scss'
|
||||
})
|
||||
export class AboutComponent {
|
||||
constructor(public scrollService: ScrollService) {
|
||||
}
|
||||
}
|
||||
+4
-5
@@ -1,15 +1,14 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {ScrollService} from '../scroll/scroll.service';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {HeaderComponent} from '../header/header.component';
|
||||
import {ScrollService} from '@services/scroll.service';
|
||||
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-birthdays',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
HeaderComponent
|
||||
],
|
||||
],
|
||||
templateUrl: './birthdays.component.html',
|
||||
styleUrl: './birthdays.component.scss'
|
||||
})
|
||||
@@ -0,0 +1,79 @@
|
||||
<ng-container>
|
||||
<app-header [current_page]="'community'" height="460px" background_image="/public/img/backgrounds/community.jpg"
|
||||
[overlay_gradient]="0.5">
|
||||
<div class="title" header-content>
|
||||
<h1>Community</h1>
|
||||
<h2>Talented people who help Altitude in more than one way.</h2>
|
||||
</div>
|
||||
</app-header>
|
||||
|
||||
<main>
|
||||
<section class="darkmodeSection">
|
||||
<div class="customContainer">
|
||||
<h2>Current Nitro Boosters</h2>
|
||||
</div>
|
||||
</section>
|
||||
<section id="social" class="darkmodeSectionThree">
|
||||
<div class="container" style="padding: 50px 0 0 0; justify-content: center;">
|
||||
<h2 class="sectionTitle">Social Media</h2>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: center; padding-bottom: 30px;">
|
||||
<p style="text-align: center;">We're currently not looking for more people to help manage our socials.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section id="crateTeam" class="darkmodeSection">
|
||||
<div class="container" style="padding: 50px 0 0 0; justify-content: center;">
|
||||
<h2 class="sectionTitle">Crate Team</h2>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSectionThree">
|
||||
<div class="container" style="padding: 50px 0 0 0; justify-content: center;">
|
||||
<h2 class="sectionTitle">Event Leaders</h2>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: center; padding-bottom: 30px;">
|
||||
<p style="text-align: center;">We're currently not looking for more Event Leaders.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSection">
|
||||
<div class="container" style="padding: 50px 0 0 0; justify-content: center;">
|
||||
<h2 class="sectionTitle">Event Team</h2>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: center; padding-bottom: 30px;">
|
||||
<div style="flex-direction: column;">
|
||||
<p style="text-align: center;">We occasionally open applications for the event team.</p>
|
||||
<p style="text-align: center;">If you're interested in joining you simply need to keep an eye on the Discord
|
||||
announcements.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="darkmodeSectionThree">
|
||||
<div class="container" style="padding: 50px 0 0 0; justify-content: center;">
|
||||
<h2 class="sectionTitle">YouTubers & Streamers</h2>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: center; padding-bottom: 30px;">
|
||||
<div style="flex-direction: column;">
|
||||
<p style="text-align: center;"><a style="cursor: pointer;" id="reqButton">Show Requirements...</a></p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="req" class="hide" style="display: flex; justify-content: center; padding-bottom: 30px;">
|
||||
<div style="flex-direction: column; justify-content: center; max-width: 800px;">
|
||||
<p style="text-align: center;"><span style="font-family: 'opensans-bold', sans-serif;">Requirements:</span>
|
||||
</p>
|
||||
<p style="text-align: center;">You need to have at least one recent stream/video on Altitude which we can use
|
||||
to gauge if your audience enjoys your content on Altitude.</p>
|
||||
<br>
|
||||
<p style="text-align: center;">Twitch: You need to be affiliate and get at least 5 viewers on average while
|
||||
streaming on Altitude.</p>
|
||||
<p style="text-align: center;">YouTube videos: You need at least 500 subs and have at least 200 views per
|
||||
video within a week on average for Altitude content.</p>
|
||||
<p style="text-align: center;">YouTube streamers: You need at least 500 subs and have at least 5 viewers on
|
||||
average while streaming on Altitude.</p>
|
||||
<br>
|
||||
<p style="text-align: center;"><span style="font-family: 'opensans-bold', sans-serif;">Note:</span> Before
|
||||
accepting or denying you we will watch your latest video/stream on Altitude (so keep your broadcasts public
|
||||
on twitch).</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,12 @@
|
||||
.customContainer {
|
||||
width: 80%;
|
||||
max-width: 1020px;
|
||||
margin: auto;
|
||||
padding: 80px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CommunityComponent } from './community.component';
|
||||
|
||||
describe('CommunityComponent', () => {
|
||||
let component: CommunityComponent;
|
||||
let fixture: ComponentFixture<CommunityComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [CommunityComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CommunityComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {HeaderComponent} from "@header/header.component";
|
||||
|
||||
@Component({
|
||||
selector: 'app-community',
|
||||
imports: [
|
||||
HeaderComponent
|
||||
],
|
||||
templateUrl: './community.component.html',
|
||||
styleUrl: './community.component.scss'
|
||||
})
|
||||
export class CommunityComponent {
|
||||
|
||||
}
|
||||
+4
-5
@@ -1,16 +1,15 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {ScrollService} from '../scroll/scroll.service';
|
||||
import {CommonModule, NgOptimizedImage} from '@angular/common';
|
||||
import {HeaderComponent} from '../header/header.component';
|
||||
import {ScrollService} from '@services/scroll.service';
|
||||
import { NgOptimizedImage } from '@angular/common';
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-socials',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
HeaderComponent,
|
||||
NgOptimizedImage
|
||||
],
|
||||
],
|
||||
templateUrl: './socials.component.html',
|
||||
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 {ScrollService} from '../scroll/scroll.service';
|
||||
import {BASE_PATH, Player, TeamService} from '../../api';
|
||||
import {CommonModule, NgOptimizedImage} from '@angular/common';
|
||||
import {HeaderComponent} from '../header/header.component';
|
||||
import {ScrollService} from '@services/scroll.service';
|
||||
import {BASE_PATH, Player, TeamService} from '@api';
|
||||
import { CommonModule, NgOptimizedImage } from '@angular/common';
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
import {CookieService} from 'ngx-cookie-service';
|
||||
import {map, Observable, shareReplay} from 'rxjs';
|
||||
import {environment} from '../../environments/environment';
|
||||
import {environment} from '@environment';
|
||||
|
||||
@Component({
|
||||
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
|
||||
yourself or do <b>/claim</b> to receive them for free. The shovel is used for modifying claims and the
|
||||
stick is used for viewing claim information.</p>
|
||||
<img ngSrc="/public/img/items/shovel.png" alt="Minecraft golden shovel" style="width: 25%;" height="114"
|
||||
width="114">
|
||||
<img ngSrc="/public/img/items/shovel.png" alt="Minecraft golden shovel"
|
||||
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
|
||||
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
|
||||
+7
@@ -7,3 +7,10 @@ main li {
|
||||
margin-left: 30px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 690px) {
|
||||
.shovelClaiming {
|
||||
height: 100px;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {HeaderComponent} from '../header/header.component';
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
import {NgOptimizedImage} from '@angular/common';
|
||||
import {RouterLink} from '@angular/router';
|
||||
|
||||
+1
-2
@@ -163,14 +163,13 @@
|
||||
<div class="columnParagraph">
|
||||
<h2>Miscellaneous</h2>
|
||||
<p>Grindstones can strip shulker box & beehive NBT data</p>
|
||||
<p>Sneak click mending</p>
|
||||
<p>/iwanttobreakthisblock, required to break some natural generated blocks</p>
|
||||
<p>/sneakclickmending, allows you to mend your items by right clicking while sneaking</p>
|
||||
<p>Swift Sneak enchantment can be found in villager trades</p>
|
||||
<p>Nether portals can be as small as 1x2 (1x2 portal, 3x4 base)</p>
|
||||
<p>Lootchests refill once for every player</p>
|
||||
<p>Empty maps can be duplicated using paper in a cartography table</p>
|
||||
<p>Eating glowberries gives you a glowing effect</p>
|
||||
<p>Eating glow berries gives you a glowing effect</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
+6
@@ -7,3 +7,9 @@ main li {
|
||||
margin-left: 30px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 690px) {
|
||||
.columnContainer {
|
||||
text-align: left !important;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {HeaderComponent} from "../header/header.component";
|
||||
import {HeaderComponent} from "@header/header.component";
|
||||
import {RouterLink} from '@angular/router';
|
||||
|
||||
@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
|
||||
points here:</p>
|
||||
<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">
|
||||
</div>
|
||||
<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 {HeaderComponent} from '../header/header.component';
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
import {NgOptimizedImage} from '@angular/common';
|
||||
|
||||
@Component({
|
||||
+10
@@ -11,3 +11,13 @@
|
||||
padding-bottom: 25px;
|
||||
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 {CommonModule} from '@angular/common';
|
||||
import {HeaderComponent} from '../header/header.component';
|
||||
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
HeaderComponent
|
||||
],
|
||||
],
|
||||
selector: 'app-map',
|
||||
templateUrl: './map.component.html',
|
||||
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
|
||||
active MyPet by doing <span style="font-family: 'opensans-bold', sans-serif;">/petswitch</span> and
|
||||
selecting the one you want to use.</p>
|
||||
<img ngSrc="/public/img/items/lead.png" alt="Minecraft lead/leash" style="width: 20%;" height="96"
|
||||
width="96">
|
||||
<img ngSrc="/public/img/items/lead.png" alt="Minecraft lead/leash"
|
||||
class="leadMyPet" style="width: 20%;" height="96" width="96">
|
||||
</div>
|
||||
<div class="columnParagraph">
|
||||
<h2>Skilltrees and Levels</h2>
|
||||
+6
@@ -13,3 +13,9 @@ main li {
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 690px) {
|
||||
.leadMyPet {
|
||||
height: 70px;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {HeaderComponent} from '../header/header.component';
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
import {NgOptimizedImage} from '@angular/common';
|
||||
|
||||
@Component({
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {HeaderComponent} from '../header/header.component';
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
|
||||
@Component({
|
||||
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
|
||||
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>
|
||||
<img ngSrc="/public/img/random/warpgui.png"
|
||||
alt="In-game warp GUI"
|
||||
style="width: 80%; padding-bottom: 15px;"
|
||||
height="232" width="356">
|
||||
<img ngSrc="/public/img/random/warpgui.png" alt="In-game warp GUI"
|
||||
class="guiWarp" style="width: 80%; padding-bottom: 15px;" height="232" width="356">
|
||||
</div>
|
||||
</div>
|
||||
<div class="columnContainer">
|
||||
@@ -45,7 +43,7 @@
|
||||
</div>
|
||||
</section>
|
||||
<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>
|
||||
<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
|
||||
@@ -207,10 +205,8 @@
|
||||
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
|
||||
corner.</p>
|
||||
<img ngSrc="/public/img/random/editwarpgui.png"
|
||||
alt="In-game warp edit GUI"
|
||||
style="width: 80%;"
|
||||
width="384" height="170">
|
||||
<img ngSrc="/public/img/random/editwarpgui.png" alt="In-game warp edit GUI"
|
||||
class="editWarp" 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,
|
||||
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
|
||||
+10
@@ -12,3 +12,13 @@ main li {
|
||||
color: var(--font-color);
|
||||
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 {HeaderComponent} from '../header/header.component';
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
import {NgOptimizedImage} from '@angular/common';
|
||||
import {RouterLink} from '@angular/router';
|
||||
|
||||
+2
-2
@@ -3,12 +3,12 @@
|
||||
<div class="footerInner">
|
||||
<div class="footerText">
|
||||
<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
|
||||
suggested by our community!</p>
|
||||
<div class="followUs" style="height: 35px; display: flex; align-items: flex-end;">
|
||||
<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 target="_blank" rel="noopener" href="https://twitter.com/alttdmc">
|
||||
<img ngSrc="/public/img/logos/twitter.png" alt="Twitter Button" height="32" width="32">
|
||||
+37
@@ -67,3 +67,40 @@ footer ul, footer p {
|
||||
.copyright {
|
||||
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 {ALTITUDE_VERSION} from '../constant';
|
||||
import {CommonModule, NgOptimizedImage} from '@angular/common';
|
||||
import {ALTITUDE_VERSION} from '@custom-types/constant';
|
||||
import { NgOptimizedImage } from '@angular/common';
|
||||
import {RouterLink} from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-footer',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterLink,
|
||||
NgOptimizedImage
|
||||
],
|
||||
],
|
||||
templateUrl: './footer.component.html',
|
||||
styleUrl: './footer.component.scss'
|
||||
})
|
||||
+4
-5
@@ -1,17 +1,16 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {ScrollService} from '../scroll/scroll.service';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {HeaderComponent} from '../header/header.component';
|
||||
import {ScrollService} from '@services/scroll.service';
|
||||
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
import {RouterLink} from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-privacy',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
HeaderComponent,
|
||||
RouterLink
|
||||
],
|
||||
],
|
||||
templateUrl: './privacy.component.html',
|
||||
styleUrl: './privacy.component.scss'
|
||||
})
|
||||
+4
-5
@@ -1,7 +1,7 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {ScrollService} from '../scroll/scroll.service';
|
||||
import {HeaderComponent} from '../header/header.component';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {ScrollService} from '@services/scroll.service';
|
||||
import {HeaderComponent} from '@header/header.component';
|
||||
|
||||
import {RouterLink} from '@angular/router';
|
||||
|
||||
@Component({
|
||||
@@ -9,10 +9,9 @@ import {RouterLink} from '@angular/router';
|
||||
standalone: true,
|
||||
templateUrl: './terms.component.html',
|
||||
imports: [
|
||||
CommonModule,
|
||||
HeaderComponent,
|
||||
RouterLink
|
||||
],
|
||||
],
|
||||
styleUrl: './terms.component.scss'
|
||||
})
|
||||
export class TermsComponent {
|
||||
@@ -0,0 +1,58 @@
|
||||
<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) {
|
||||
<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>
|
||||
</section>
|
||||
}
|
||||
|
||||
<!-- Page 2 -->
|
||||
@if (currentPageIndex === 1) {
|
||||
<section class="formPage">
|
||||
<h1>Page 2</h1>
|
||||
<p>This is the second page of the form.</p>
|
||||
</section>
|
||||
}
|
||||
|
||||
<!-- Page 3 -->
|
||||
@if (currentPageIndex === 2) {
|
||||
<section class="formPage">
|
||||
<h1>Page 3</h1>
|
||||
<p>This is the third page of the form.</p>
|
||||
</section>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Navigation dots -->
|
||||
<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,86 @@
|
||||
: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;
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import {AfterViewInit, Component, ElementRef, OnInit, Renderer2} from '@angular/core';
|
||||
import {FormControl, FormGroup, Validators} from '@angular/forms';
|
||||
import {AppealsService, MinecraftAppeal} 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';
|
||||
|
||||
@Component({
|
||||
selector: 'app-appeal',
|
||||
imports: [
|
||||
HeaderComponent,
|
||||
NgOptimizedImage,
|
||||
MatButtonModule,
|
||||
MatIconModule
|
||||
],
|
||||
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;
|
||||
|
||||
constructor(
|
||||
private appealApi: AppealsService,
|
||||
private elementRef: ElementRef,
|
||||
private renderer: Renderer2
|
||||
) {
|
||||
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() {
|
||||
}
|
||||
|
||||
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 appeal: MinecraftAppeal = {
|
||||
appeal: rawValue.appeal,
|
||||
email: rawValue.email,
|
||||
punishmentId: parseInt(rawValue.punishmentId),
|
||||
username: rawValue.username,
|
||||
uuid: ''//TODO
|
||||
}
|
||||
this.appealApi.submitMinecraftAppeal(appeal).subscribe()
|
||||
}
|
||||
|
||||
public currentPageIndex: number = 0;
|
||||
public totalPages: number[] = [0, 1, 2];
|
||||
|
||||
public goToPage(pageIndex: number): void {
|
||||
if (pageIndex >= 0 && pageIndex < this.totalPages.length) {
|
||||
this.currentPageIndex = pageIndex;
|
||||
}
|
||||
}
|
||||
|
||||
public previousPage() {
|
||||
this.goToPage(this.currentPageIndex - 1);
|
||||
}
|
||||
|
||||
public nextPage() {
|
||||
this.goToPage(this.currentPageIndex + 1);
|
||||
}
|
||||
|
||||
public isFirstPage(): boolean {
|
||||
return this.currentPageIndex === 0;
|
||||
}
|
||||
|
||||
public isLastPage(): boolean {
|
||||
return this.currentPageIndex === this.totalPages.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
interface Appeal {
|
||||
username: FormControl<string>;
|
||||
punishmentId: FormControl<string>;
|
||||
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';
|
||||
}
|
||||
+31
-1
@@ -62,12 +62,19 @@
|
||||
<li><a href="https://alttd.com/blog/">Blog</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@if (!isAuthenticated) {
|
||||
<li>
|
||||
<a (click)="openLoginDialog()">
|
||||
Login
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/"><img ngSrc="/public/img/logos/logo.png" priority alt="Altitude Server Logo" height="319"
|
||||
width="550"></a>
|
||||
<ul>
|
||||
<ul id="nav_list">
|
||||
<li class="nav_li"><a [id]="getCurrentPageId(['home'])" class="nav_link" href="/" [ngClass]="active">Home</a>
|
||||
</li>
|
||||
<li class="nav_li">
|
||||
@@ -130,7 +137,30 @@
|
||||
<li class="nav_li"><a class="nav_link2" target="_blank" href="https://alttd.com/blog/">Blog</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
@if (isAuthenticated) {
|
||||
<li class="nav_li">
|
||||
<a [id]="getCurrentPageId(['particles'])"
|
||||
class="nav_link fake_link" [ngClass]="active">Special</a>
|
||||
<ul class="dropdown">
|
||||
<li class="nav_li"><a class="nav_link2" [routerLink]="['/particles']">Particles</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
}
|
||||
@if (!isAuthenticated) {
|
||||
<li class="nav_li login-button">
|
||||
<a class="nav_link fake_link" (click)="openLoginDialog()">
|
||||
Login
|
||||
</a>
|
||||
</li>
|
||||
} @else {
|
||||
<li class="nav_li login-button">
|
||||
<a class="nav_link fake_link" (click)="logout()">
|
||||
Logout
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
<app-theme></app-theme>
|
||||
</div>
|
||||
</nav>
|
||||
+4
-14
@@ -259,6 +259,10 @@ nav img {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
@-moz-document url-prefix() {
|
||||
.dropdown2 {
|
||||
top: 12px;
|
||||
@@ -271,12 +275,6 @@ nav img {
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1000px) {
|
||||
#scroll-button {
|
||||
padding-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1150px) {
|
||||
.dropdown {
|
||||
top: -10px;
|
||||
@@ -305,10 +303,6 @@ nav img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.switch-div {
|
||||
top: 65px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 690px) {
|
||||
@@ -340,10 +334,6 @@ nav img {
|
||||
background: none;
|
||||
border-bottom: 3px solid var(--pureblack);
|
||||
}
|
||||
|
||||
.switch-div {
|
||||
top: 33px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 690px) {
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user