52 Commits
Author SHA1 Message Date
Peter 974d50d7cd Merge remote-tracking branch 'origin/master' 2025-07-06 21:33:27 +02:00
Peter 62f837914c Add new routes and improve theme and header responsiveness
Added routes for 'community', 'nicknames', and 'nickgenerator'. Enhanced theme switch positioning and responsive design for smaller screens. Moved header login button to a more consistent position and upgraded Angular Material to version 19.2.19 for better compatibility.
2025-07-06 21:33:14 +02:00
stijn ace969ba3b Remove Blob handling logic from AuthService.login and simplify JWT processing. 2025-07-06 21:00:52 +02:00
stijn 2fc6ba53f6 Handle Blob responses in AuthService.login and enhance JWT decoding logic. Add utility methods for Blob detection and conversion. 2025-07-06 20:53:13 +02:00
stijn 4c38b070ea Handle Blob responses in AuthService.login and enhance JWT decoding logic. Add utility methods for Blob detection and conversion. 2025-07-06 20:53:03 +02:00
stijn db394beda6 Add debug logs for JWT and its decoded value in AuthService. 2025-07-06 20:32:02 +02:00
stijn 76cb3cd89c Decode JWT before saving in AuthService. 2025-07-06 20:09:49 +02:00
stijn 5d8ab2deef Add debug log for generated token in LoginController. 2025-07-06 19:39:46 +02:00
stijn aef32a8982 Change log level to DEBUG in beta configuration and improve logging in LoginController. 2025-07-06 19:23:38 +02:00
stijn 42f0961f13 Directly initialize JwtHelperService in auth service 2025-07-06 19:14:06 +02:00
stijn 04310e1cce Switch to localStorage for JWT handling and simplify case transformation logic in LoginComponent. Update app.config.ts and related services to align with the new token management method. Mark JwtClaims interface as exported. 2025-07-06 19:10:17 +02:00
Peter 54e747118c Merge branch 'bans' 2025-07-06 18:15:09 +02:00
Peter 43430cfbef Merge remote-tracking branch 'origin/bans' into bans
# Conflicts:
#	frontend/src/app/app.routes.ts
2025-07-06 11:13:59 +02:00
stijn cce83a08de Replace fakeLogin() with actual login() method in AuthService and remove redundant fakeLogin() implementation. 2025-07-04 23:32:08 +02:00
stijn f0faa63ca7 Add JWT support for authentication handling
Integrate `@auth0/angular-jwt` for Token management. Update `app.config.ts` with `JwtModule` setup and token getter from cookies. Enhance `AuthService` to include token handling, fake login, and JWT validation using `JwtHelperService`. Introduce `JwtClaims` interface for structured token claims.
2025-07-04 22:31:41 +02:00
stijn dfea91d8ca Add PrivilegedUserMapper to InitializeWebDb setup 2025-07-04 21:37:11 +02:00
stijn 73916f0aae Add login button to header 2025-07-04 21:14:45 +02:00
stijn ebe66c87c0 Rework folder structure in frontend
Pages are now grouped per group they appear in on in the header (where possible)
Utilities used by multiple pages in the project are grouped in folders such as services/pipes/etc
2025-07-04 19:50:21 +02:00
stijn c42fc38b2c Add SecurityAuthFailureHandler for better handling of authentication and access failures; update SecurityConfig to integrate the new handler. 2025-07-04 19:49:04 +02:00
stijn 213f9987d9 Remove particle component and its associated routes and security controls. 2025-07-03 20:08:56 +02:00
stijn 48cac607de Add route for lazy-loaded Login component. 2025-07-03 20:02:07 +02:00
stijn 6ed2e15017 Parametrize notification server URL configuration for improved flexibility. 2025-06-29 03:17:25 +02:00
stijn 7fc25f46f3 Add endpoints, services, and security controls for particle file management, including save and download APIs. 2025-06-29 03:15:39 +02:00
stijn c72703ea32 Refactor user privilege handling to use Optional instead of null checks. Remove unused cache entries and update security configuration to refine access controls. 2025-06-23 21:34:54 +02:00
auto e837a9216d Fix slider sticking out of page 2025-06-23 00:25:31 +02:00
auto d4363b3a8a Add particle type selection, size control, and enhance particle property handling 2025-06-23 00:23:03 +02:00
auto 1e5862bae6 Add new particle types and enhance particle attributes handling 2025-06-23 00:04:30 +02:00
auto daf88ea437 Add opacity control for intersection plane 2025-06-22 23:24:06 +02:00
auto 9abd570b87 Add support for darkmode 2025-06-22 23:15:06 +02:00
auto 5284d498f3 Add a reset camera button and implement default camera reset functionality 2025-06-22 20:53:11 +02:00
auto c3a7be82e9 Add an option to highlight particles 2025-06-22 20:46:08 +02:00
auto fdb57289f8 Fix weird interactions between plane and player model 2025-06-22 20:20:40 +02:00
auto 60c1329163 Switch to steve 2025-06-22 20:17:02 +02:00
auto 0e71c0f581 Fix textures overlapping and glitching for legs 2025-06-22 20:15:35 +02:00
auto eb67a33331 Change default camera position 2025-06-22 20:12:25 +02:00
auto 39f20796ce Simplify UV mapping logic and remove unnecessary face rotation handling 2025-06-22 20:08:42 +02:00
auto e00165c56f Fix uv rotation 2025-06-22 20:04:26 +02:00
auto 02c6497700 Fix mapping 2025-06-22 20:01:11 +02:00
auto 0efd476676 Initial attempt at textures 2025-06-22 19:53:27 +02:00
auto 237518638c Increase particles-list height in FramesComponent to 550px for improved visibility. 2025-06-22 19:34:20 +02:00
auto fea1a98cea Adjust RendererService to use dynamic container height and update OrbitControls limits 2025-06-22 19:27:55 +02:00
auto ecd9b3d824 Modularize renderer and plane control functionality into RenderContainerComponent 2025-06-22 19:23:54 +02:00
auto 9808b5d63d Add manual plane orientation controls with lock/unlock functionality
Implemented a UI overlay in `ParticlesComponent` for manual plane orientation selection with buttons for different orientations. Added lock/unlock toggle to control automatic orientation adjustment. Refactored `IntersectionPlaneService` to support locked state and manual orientation updates. Updated styles and layout to integrate the new controls seamlessly.
2025-06-22 19:16:32 +02:00
auto c13b7077a7 Remove unused OnInit lifecycle hook from ParticlesComponent and cleanup redundant comments. 2025-06-22 19:08:28 +02:00
auto 023ae809ef Modularize ParticlesComponent by separating particle, frame, and property functionalities into dedicated components
Refactored `ParticlesComponent` to use `ParticleComponent`, `FramesComponent`, and `PropertiesComponent` for better organization and reusability. Updated layout, improved UI structure, and centralized particle management logic. Enhanced clipboard functionality with a new JSON copy feature.
2025-06-22 19:06:52 +02:00
auto 3a6f137c9a Update ParticlesComponent to allow dynamic plane position limits and improve slider configuration
Modified plane position logic to use dynamic min/max offsets based on orientation. Enhanced slider component with binding to these offsets and updated display text for clarity. Refactored `IntersectionPlaneService` to expose new methods (`getMaxOffset`, `getMinOffset`) and adjusted position defaults.
2025-06-22 17:59:29 +02:00
auto cb8447a096 Correct default plane locations 2025-06-22 17:38:28 +02:00
auto 3e98e1a498 add plane orientation for looking from above/below 2025-06-22 17:32:32 +02:00
auto 4c31a91bb4 Add modular services for Three.js integration in ParticlesComponent
Implemented dedicated Angular services (e.g., RendererService, InputHandlerService, PlayerModelService) to modularize and simplify Three.js integration within the `ParticlesComponent`. Refactored component logic to delegate rendering, input handling, and model creation to respective services. Updated particle data models and removed redundant logic from the component.
2025-06-22 17:26:10 +02:00
auto d6faaba01c Update security configuration to allow unrestricted access
Changed default security rule to permit all requests instead of requiring authentication. Adjusted `SecurityConfig` to ensure open access for all endpoints.
2025-06-22 01:15:45 +02:00
auto 56175e62d6 Remove RemoveTrailingPeriodPipe import from AboutComponent 2025-06-22 01:10:53 +02:00
Peter 174ed834ca Added pages and fitting content for community, nickgenerator annd nicknames 2025-05-30 23:07:42 +02:00
201 changed files with 3354 additions and 1010 deletions
@@ -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");
}
}
@@ -31,19 +31,33 @@ import java.security.interfaces.RSAPublicKey;
public class SecurityConfig { public class SecurityConfig {
private final KeyPairService keyPairService; private final KeyPairService keyPairService;
private final SecurityAuthFailureHandler securityAuthFailureHandler;
@Bean @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http return http
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(
.requestMatchers("/login/userLogin/**", "/login/requestNewUserLogin/**").permitAll() auth -> auth
.requestMatchers("/team/**", "/history/**").permitAll()
.requestMatchers("/form/**").hasAuthority(PermissionClaimDto.USER.getValue()) .requestMatchers("/form/**").hasAuthority(PermissionClaimDto.USER.getValue())
.requestMatchers("/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue()) .requestMatchers("/head_mod/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
.anyRequest().authenticated() .requestMatchers("/particles/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
.requestMatchers("/files/save/**").hasAuthority(PermissionClaimDto.HEAD_MOD.getValue())
.anyRequest().permitAll()
) )
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults())) .oauth2ResourceServer(
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) oauth2 -> oauth2
.jwt(Customizer.withDefaults())
.authenticationEntryPoint(securityAuthFailureHandler)
.accessDeniedHandler(securityAuthFailureHandler)
)
.exceptionHandling(
ex -> ex
.authenticationEntryPoint(securityAuthFailureHandler)
.accessDeniedHandler(securityAuthFailureHandler)
)
.sessionManagement(
session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.build(); .build();
} }
@@ -83,21 +83,23 @@ public class LoginController implements LoginApi {
@RateLimit(limit = 5, timeValue = 1, timeUnit = TimeUnit.MINUTES, key = "login") @RateLimit(limit = 5, timeValue = 1, timeUnit = TimeUnit.MINUTES, key = "login")
@Override @Override
public ResponseEntity<String> login(String code) { public ResponseEntity<String> login(String code) {
CacheEntry cacheEntry1 = new CacheEntry(UUID.fromString("55e46bc3-2a29-4c53-850f-dbd944dc5c5f"), Instant.now().plusSeconds(TimeUnit.DAYS.toSeconds(1)));
cache.put("23232323", cacheEntry1);
if (code == null) { if (code == null) {
log.warn("Received null login code");
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
} }
CacheEntry cacheEntry = cache.get(code); CacheEntry cacheEntry = cache.get(code);
if (cacheEntry == null || cacheEntry.expiry().isBefore(Instant.now())) { if (cacheEntry == null || cacheEntry.expiry().isBefore(Instant.now())) {
log.warn("Received invalid login code {}", code);
return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
} }
String token = generateToken(cacheEntry.uuid); String token = generateToken(cacheEntry.uuid);
log.debug("Generated token for user {} with token {}", cacheEntry.uuid, token);
cache.remove(code); cache.remove(code);
log.debug("Generated token for user {}", cacheEntry.uuid);
return ResponseEntity.ok(token); return ResponseEntity.ok(token);
} }
@@ -134,12 +136,12 @@ public class LoginController implements LoginApi {
Instant now = Instant.now(); Instant now = Instant.now();
//TODO make a JWT for renewing and one for storing permissions for a session (expiry 1 hour) //TODO make a JWT for renewing and one for storing permissions for a session (expiry 1 hour)
Instant expiryTime = now.plusSeconds(TimeUnit.DAYS.toSeconds(30)); Instant expiryTime = now.plusSeconds(TimeUnit.DAYS.toSeconds(30));
CompletableFuture<PrivilegedUser> privilegedUserCompletableFuture = new CompletableFuture<>(); CompletableFuture<Optional<PrivilegedUser>> privilegedUserCompletableFuture = new CompletableFuture<>();
List<PermissionClaimDto> claimList = new ArrayList<>(); List<PermissionClaimDto> claimList = new ArrayList<>();
Connection.getConnection(Databases.DEFAULT) Connection.getConnection(Databases.DEFAULT)
.runQuery(sqlSession -> { .runQuery(sqlSession -> {
try { try {
PrivilegedUser privilegedUser = sqlSession.getMapper(PrivilegedUserMapper.class) Optional<PrivilegedUser> privilegedUser = sqlSession.getMapper(PrivilegedUserMapper.class)
.getUserByUuid(uuid.toString()); .getUserByUuid(uuid.toString());
privilegedUserCompletableFuture.complete(privilegedUser); privilegedUserCompletableFuture.complete(privilegedUser);
@@ -148,17 +150,15 @@ public class LoginController implements LoginApi {
privilegedUserCompletableFuture.completeExceptionally(e); privilegedUserCompletableFuture.completeExceptionally(e);
} }
}); });
PrivilegedUser privilegedUser = privilegedUserCompletableFuture.join(); Optional<PrivilegedUser> privilegedUser = privilegedUserCompletableFuture.join();
claimList.add(PermissionClaimDto.USER); claimList.add(PermissionClaimDto.USER);
if (privilegedUser != null) { privilegedUser.ifPresent(user -> user.getPermissions().forEach(permission -> {
privilegedUser.getPermissions().forEach(permission -> {
try { try {
claimList.add(PermissionClaimDto.valueOf(permission)); claimList.add(PermissionClaimDto.valueOf(permission));
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
log.warn("Received invalid permission claim: {}", permission); log.warn("Received invalid permission claim: {}", permission);
} }
}); }));
}
JwtClaimsSet claims = JwtClaimsSet.builder() JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("altitudeweb") .issuer("altitudeweb")
.claim("authorities", claimList.stream().map(PermissionClaimDto::getValue).toList()) .claim("authorities", claimList.stream().map(PermissionClaimDto::getValue).toList())
@@ -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,4 @@ database.host=${DB_HOST:localhost}
database.user=${DB_USER:root} database.user=${DB_USER:root}
database.password=${DB_PASSWORD:root} database.password=${DB_PASSWORD:root}
cors.allowed-origins=${CORS:https://beta.alttd.com} cors.allowed-origins=${CORS:https://beta.alttd.com}
logging.level.com.alttd.altitudeweb=INFO logging.level.com.alttd.altitudeweb=DEBUG
@@ -6,4 +6,6 @@ database.user=${DB_USER:root}
database.password=${DB_PASSWORD:root} database.password=${DB_PASSWORD:root}
cors.allowed-origins=${CORS:https://alttd.com} cors.allowed-origins=${CORS:https://alttd.com}
login.secret=${LOGIN_SECRET:SET_TOKEN} login.secret=${LOGIN_SECRET:SET_TOKEN}
particles.file_path=${user.home}/.altitudeweb/particles
notification.server.url=${SERVER_IP:10.0.0.107}:${SERVER_PORT:8080}
logging.level.com.alttd.altitudeweb=INFO logging.level.com.alttd.altitudeweb=INFO
@@ -1,8 +1,10 @@
package com.alttd.altitudeweb.database.web_db; package com.alttd.altitudeweb.database.web_db;
import org.apache.ibatis.annotations.*; import org.apache.ibatis.annotations.*;
import org.jetbrains.annotations.Nullable;
import java.util.List; import java.util.List;
import java.util.Optional;
public interface PrivilegedUserMapper { public interface PrivilegedUserMapper {
@@ -23,7 +25,7 @@ public interface PrivilegedUserMapper {
@Result(property = "permissions", column = "id", javaType = List.class, @Result(property = "permissions", column = "id", javaType = List.class,
many = @Many(select = "getPermissionsForUser")) many = @Many(select = "getPermissionsForUser"))
}) })
PrivilegedUser getUserByUuid(@Param("uuid") String uuid); Optional<PrivilegedUser> getUserByUuid(@Param("uuid") String uuid);
/** /**
* Retrieves all privileged users with their permissions * Retrieves all privileged users with their permissions
@@ -2,6 +2,7 @@ package com.alttd.altitudeweb.setup;
import com.alttd.altitudeweb.database.Databases; import com.alttd.altitudeweb.database.Databases;
import com.alttd.altitudeweb.database.web_db.KeyPairMapper; import com.alttd.altitudeweb.database.web_db.KeyPairMapper;
import com.alttd.altitudeweb.database.web_db.PrivilegedUserMapper;
import com.alttd.altitudeweb.database.web_db.SettingsMapper; import com.alttd.altitudeweb.database.web_db.SettingsMapper;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSession;
@@ -18,6 +19,7 @@ public class InitializeWebDb {
Connection.getConnection(Databases.DEFAULT, (configuration) -> { Connection.getConnection(Databases.DEFAULT, (configuration) -> {
configuration.addMapper(SettingsMapper.class); configuration.addMapper(SettingsMapper.class);
configuration.addMapper(KeyPairMapper.class); configuration.addMapper(KeyPairMapper.class);
configuration.addMapper(PrivilegedUserMapper.class);
}).join() }).join()
.runQuery(SqlSession -> { .runQuery(SqlSession -> {
createSettingsTable(SqlSession); createSettingsTable(SqlSession);
+2 -1
View File
@@ -18,10 +18,11 @@
"@angular/compiler": "^19.2.0", "@angular/compiler": "^19.2.0",
"@angular/core": "^19.2.0", "@angular/core": "^19.2.0",
"@angular/forms": "^19.2.0", "@angular/forms": "^19.2.0",
"@angular/material": "^19.2.18", "@angular/material": "^19.2.19",
"@angular/platform-browser": "^19.2.0", "@angular/platform-browser": "^19.2.0",
"@angular/platform-browser-dynamic": "^19.2.0", "@angular/platform-browser-dynamic": "^19.2.0",
"@angular/router": "^19.2.0", "@angular/router": "^19.2.0",
"@auth0/angular-jwt": "^5.2.0",
"@types/three": "^0.177.0", "@types/three": "^0.177.0",
"ngx-cookie-service": "^19.1.2", "ngx-cookie-service": "^19.1.2",
"rxjs": "~7.8.0", "rxjs": "~7.8.0",
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

+4 -4
View File
@@ -1,8 +1,8 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {Meta, Title} from '@angular/platform-browser'; import {Meta, Title} from '@angular/platform-browser';
import {ALTITUDE_VERSION} from './constant'; import {ALTITUDE_VERSION} from '@custom-types/constant';
import {Router, RouterOutlet} from '@angular/router'; import {Router, RouterOutlet} from '@angular/router';
import {FooterComponent} from './footer/footer.component'; import {FooterComponent} from '@pages/footer/footer/footer.component';
@Component({ @Component({
standalone: true, standalone: true,
@@ -10,8 +10,8 @@ import {FooterComponent} from './footer/footer.component';
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.scss', styleUrl: './app.component.scss',
imports: [ imports: [
FooterComponent, RouterOutlet,
RouterOutlet FooterComponent
] ]
}) })
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
+6 -1
View File
@@ -1,8 +1,13 @@
import {ApplicationConfig, provideZoneChangeDetection} from '@angular/core'; import {ApplicationConfig, provideZoneChangeDetection} from '@angular/core';
import {provideRouter} from '@angular/router'; import {provideRouter} from '@angular/router';
import {CookieService} from 'ngx-cookie-service';
import {routes} from './app.routes'; import {routes} from './app.routes';
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [provideZoneChangeDetection({eventCoalescing: true}), provideRouter(routes)] providers: [
provideZoneChangeDetection({eventCoalescing: true}),
provideRouter(routes),
CookieService,
]
}; };
+37 -29
View File
@@ -3,114 +3,122 @@ import {Routes} from '@angular/router';
export const routes: Routes = [ export const routes: Routes = [
{ {
path: '', path: '',
loadComponent: () => import('./home/home.component').then(m => m.HomeComponent) loadComponent: () => import('./pages/home/home.component').then(m => m.HomeComponent)
}, },
{ {
path: 'particles', path: 'particles',
loadComponent: () => import('./particles/particles.component').then(m => m.ParticlesComponent) loadComponent: () => import('./pages/particles/particles.component').then(m => m.ParticlesComponent)
}, },
{ {
path: 'map', path: 'map',
loadComponent: () => import('./map/map.component').then(m => m.MapComponent) loadComponent: () => import('./pages/features/map/map.component').then(m => m.MapComponent)
}, },
{ {
path: 'rules', path: 'rules',
loadComponent: () => import('./rules/rules.component').then(m => m.RulesComponent) loadComponent: () => import('./pages/reference/rules/rules.component').then(m => m.RulesComponent)
}, },
{ {
path: 'vote', path: 'vote',
loadComponent: () => import('./vote/vote.component').then(m => m.VoteComponent) loadComponent: () => import('./pages/vote/vote.component').then(m => m.VoteComponent)
}, },
{ {
path: 'about', path: 'about',
loadComponent: () => import('./about/about.component').then(m => m.AboutComponent) loadComponent: () => import('./pages/altitude/about/about.component').then(m => m.AboutComponent)
}, },
{ {
path: 'socials', path: 'socials',
loadComponent: () => import('./socials/socials.component').then(m => m.SocialsComponent) loadComponent: () => import('./pages/altitude/socials/socials.component').then(m => m.SocialsComponent)
}, },
{ {
path: 'team', path: 'team',
loadComponent: () => import('./team/team.component').then(m => m.TeamComponent) loadComponent: () => import('./pages/altitude/team/team.component').then(m => m.TeamComponent)
}, },
{ {
path: 'birthdays', path: 'birthdays',
loadComponent: () => import('./birthdays/birthdays.component').then(m => m.BirthdaysComponent) loadComponent: () => import('./pages/altitude/birthdays/birthdays.component').then(m => m.BirthdaysComponent)
}, },
{ {
path: 'terms', path: 'terms',
loadComponent: () => import('./terms/terms.component').then(m => m.TermsComponent) loadComponent: () => import('./pages/footer/terms/terms.component').then(m => m.TermsComponent)
}, },
{ {
path: 'privacy', path: 'privacy',
loadComponent: () => import('./privacy/privacy.component').then(m => m.PrivacyComponent) loadComponent: () => import('./pages/footer/privacy/privacy.component').then(m => m.PrivacyComponent)
}, },
{ {
path: 'bans', path: 'bans',
loadComponent: () => import('./bans/bans.component').then(m => m.BansComponent) loadComponent: () => import('./pages/reference/bans/bans.component').then(m => m.BansComponent)
}, },
{ {
path: 'bans/:type/:id', path: 'bans/:type/:id',
loadComponent: () => import('./bans/details/details.component').then(m => m.DetailsComponent) loadComponent: () => import('./pages/reference/bans/details/details.component').then(m => m.DetailsComponent)
}, },
{ {
path: 'economy', path: 'economy',
loadComponent: () => import('./economy/economy.component').then(m => m.EconomyComponent) loadComponent: () => import('./pages/features/economy/economy.component').then(m => m.EconomyComponent)
}, },
{ {
path: 'claiming', path: 'claiming',
loadComponent: () => import('./claiming/claiming.component').then(m => m.ClaimingComponent) loadComponent: () => import('./pages/features/claiming/claiming.component').then(m => m.ClaimingComponent)
}, },
{ {
path: 'mypet', path: 'mypet',
loadComponent: () => import('./mypet/mypet.component').then(m => m.MypetComponent) loadComponent: () => import('./pages/features/mypet/mypet.component').then(m => m.MypetComponent)
}, },
{ {
path: 'warps', path: 'warps',
loadComponent: () => import('./warps/warps.component').then(m => m.WarpsComponent) loadComponent: () => import('./pages/features/warps/warps.component').then(m => m.WarpsComponent)
}, },
{ {
path: 'skyblock', path: 'skyblock',
loadComponent: () => import('./skyblock/skyblock.component').then(m => m.SkyblockComponent) loadComponent: () => import('./pages/features/skyblock/skyblock.component').then(m => m.SkyblockComponent)
}, },
{ {
path: 'customfeatures', path: 'customfeatures',
loadComponent: () => import('./customfeatures/customfeatures.component').then(m => m.CustomfeaturesComponent) loadComponent: () => import('./pages/features/customfeatures/customfeatures.component').then(m => m.CustomfeaturesComponent)
}, },
{ {
path: 'guide', path: 'guide',
loadComponent: () => import('./guide/guide.component').then(m => m.GuideComponent) loadComponent: () => import('./pages/reference/guide/guide.component').then(m => m.GuideComponent)
}, },
{ {
path: 'ranks', path: 'ranks',
loadComponent: () => import('./ranks/ranks.component').then(m => m.RanksComponent) loadComponent: () => import('./pages/reference/ranks/ranks.component').then(m => m.RanksComponent)
}, },
{ {
path: 'commandlist', path: 'commandlist',
loadComponent: () => import('./commandlist/commandlist.component').then(m => m.CommandlistComponent) loadComponent: () => import('./pages/reference/commandlist/commandlist.component').then(m => m.CommandlistComponent)
}, },
{ {
path: 'mapart', path: 'mapart',
loadComponent: () => import('./mapart/mapart.component').then(m => m.MapartComponent) loadComponent: () => import('./pages/reference/mapart/mapart.component').then(m => m.MapartComponent)
}, },
{ {
path: 'lag', path: 'lag',
loadComponent: () => import('./lag/lag.component').then(m => m.LagComponent) loadComponent: () => import('./pages/reference/lag/lag.component').then(m => m.LagComponent)
}, },
{ {
path: 'staffpowers', path: 'staffpowers',
loadComponent: () => import('./staffpowers/staffpowers.component').then(m => m.StaffpowersComponent) loadComponent: () => import('./pages/reference/staffpowers/staffpowers.component').then(m => m.StaffpowersComponent)
}, },
{ {
path: 'forms/:form', path: 'forms/:form',
loadComponent: () => import('./forms/forms.component').then(m => m.FormsComponent) loadComponent: () => import('./pages/forms/forms.component').then(m => m.FormsComponent)
}, },
{ {
path: 'forms', path: 'forms',
loadComponent: () => import('./forms/forms.component').then(m => m.FormsComponent) loadComponent: () => import('./pages/forms/forms.component').then(m => m.FormsComponent)
}, },
{ {
path: 'particles', path: 'community',
loadComponent: () => import('./particles/particles.component').then(m => m.ParticlesComponent) loadComponent: () => import('./pages/altitude/community/community.component').then(m => m.CommunityComponent)
},
{
path: 'nicknames',
loadComponent: () => import('./pages/reference/nicknames/nicknames.component').then(m => m.NicknamesComponent)
},
{
path: 'nickgenerator',
loadComponent: () => import('./pages/reference/nickgenerator/nickgenerator.component').then(m => m.NickgeneratorComponent)
}, },
]; ];
@@ -1,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>
@@ -1,16 +1,14 @@
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {ScrollService} from '../scroll/scroll.service'; import {ScrollService} from '@services/scroll.service';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {HeaderComponent} from '../header/header.component'; import {HeaderComponent} from '@header/header.component';
import {RemoveTrailingPeriodPipe} from "../util/RemoveTrailingPeriodPipe";
@Component({ @Component({
selector: 'app-about', selector: 'app-about',
standalone: true, standalone: true,
imports: [ imports: [
CommonModule, CommonModule,
HeaderComponent, HeaderComponent
RemoveTrailingPeriodPipe
], ],
templateUrl: './about.component.html', templateUrl: './about.component.html',
styleUrl: './about.component.scss' styleUrl: './about.component.scss'
@@ -1,7 +1,7 @@
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {ScrollService} from '../scroll/scroll.service'; import {ScrollService} from '@services/scroll.service';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {HeaderComponent} from '../header/header.component'; import {HeaderComponent} from '@header/header.component';
@Component({ @Component({
selector: 'app-birthdays', selector: 'app-birthdays',
@@ -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 {
}
@@ -1,7 +1,7 @@
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {ScrollService} from '../scroll/scroll.service'; import {ScrollService} from '@services/scroll.service';
import {CommonModule, NgOptimizedImage} from '@angular/common'; import {CommonModule, NgOptimizedImage} from '@angular/common';
import {HeaderComponent} from '../header/header.component'; import {HeaderComponent} from '@header/header.component';
@Component({ @Component({
selector: 'app-socials', selector: 'app-socials',
@@ -1,11 +1,11 @@
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {ScrollService} from '../scroll/scroll.service'; import {ScrollService} from '@services/scroll.service';
import {BASE_PATH, Player, TeamService} from '../../api'; import {BASE_PATH, Player, TeamService} from '@api';
import {CommonModule, NgOptimizedImage} from '@angular/common'; import {CommonModule, NgOptimizedImage} from '@angular/common';
import {HeaderComponent} from '../header/header.component'; import {HeaderComponent} from '@header/header.component';
import {CookieService} from 'ngx-cookie-service'; import {CookieService} from 'ngx-cookie-service';
import {map, Observable, shareReplay} from 'rxjs'; import {map, Observable, shareReplay} from 'rxjs';
import {environment} from '../../environments/environment'; import {environment} from '@environment';
@Component({ @Component({
selector: 'app-team', selector: 'app-team',
@@ -1,5 +1,5 @@
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {HeaderComponent} from '../header/header.component'; import {HeaderComponent} from '@header/header.component';
import {NgOptimizedImage} from '@angular/common'; import {NgOptimizedImage} from '@angular/common';
import {RouterLink} from '@angular/router'; import {RouterLink} from '@angular/router';
@@ -1,5 +1,5 @@
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {HeaderComponent} from "../header/header.component"; import {HeaderComponent} from "@header/header.component";
import {RouterLink} from '@angular/router'; import {RouterLink} from '@angular/router';
@Component({ @Component({
@@ -1,5 +1,5 @@
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {HeaderComponent} from '../header/header.component'; import {HeaderComponent} from '@header/header.component';
import {NgOptimizedImage} from '@angular/common'; import {NgOptimizedImage} from '@angular/common';
@Component({ @Component({
@@ -1,6 +1,6 @@
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {HeaderComponent} from '../header/header.component'; import {HeaderComponent} from '@header/header.component';
@Component({ @Component({
standalone: true, standalone: true,
@@ -1,5 +1,5 @@
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {HeaderComponent} from '../header/header.component'; import {HeaderComponent} from '@header/header.component';
import {NgOptimizedImage} from '@angular/common'; import {NgOptimizedImage} from '@angular/common';
@Component({ @Component({
@@ -1,5 +1,5 @@
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {HeaderComponent} from '../header/header.component'; import {HeaderComponent} from '@header/header.component';
@Component({ @Component({
selector: 'app-skyblock', selector: 'app-skyblock',
@@ -1,5 +1,5 @@
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {HeaderComponent} from '../header/header.component'; import {HeaderComponent} from '@header/header.component';
import {NgOptimizedImage} from '@angular/common'; import {NgOptimizedImage} from '@angular/common';
import {RouterLink} from '@angular/router'; import {RouterLink} from '@angular/router';
@@ -1,5 +1,5 @@
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {ALTITUDE_VERSION} from '../constant'; import {ALTITUDE_VERSION} from '@custom-types/constant';
import {CommonModule, NgOptimizedImage} from '@angular/common'; import {CommonModule, NgOptimizedImage} from '@angular/common';
import {RouterLink} from '@angular/router'; import {RouterLink} from '@angular/router';
@@ -1,7 +1,7 @@
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {ScrollService} from '../scroll/scroll.service'; import {ScrollService} from '@services/scroll.service';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {HeaderComponent} from '../header/header.component'; import {HeaderComponent} from '@header/header.component';
import {RouterLink} from '@angular/router'; import {RouterLink} from '@angular/router';
@Component({ @Component({
@@ -1,6 +1,6 @@
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {ScrollService} from '../scroll/scroll.service'; import {ScrollService} from '@services/scroll.service';
import {HeaderComponent} from '../header/header.component'; import {HeaderComponent} from '@header/header.component';
import {CommonModule} from '@angular/common'; import {CommonModule} from '@angular/common';
import {RouterLink} from '@angular/router'; import {RouterLink} from '@angular/router';
@@ -1,7 +1,7 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {FormsComponent} from '../forms.component'; import {FormsComponent} from '../forms.component';
import {FormControl, FormGroup, Validators} from '@angular/forms'; import {FormControl, FormGroup, Validators} from '@angular/forms';
import {AppealsService, MinecraftAppeal} from '../../../api'; import {AppealsService, MinecraftAppeal} from '@api';
@Component({ @Component({
selector: 'app-appeal', selector: 'app-appeal',
@@ -1,12 +1,12 @@
import {Component, Input, OnInit} from '@angular/core'; import {Component, Input, OnInit} from '@angular/core';
import {HeaderComponent} from '../header/header.component'; import {HeaderComponent} from '@header/header.component';
import {MatDialog} from '@angular/material/dialog'; import {MatDialog} from '@angular/material/dialog';
import {ActivatedRoute} from '@angular/router'; import {ActivatedRoute} from '@angular/router';
import {LoginDialogComponent} from '../login/login.component'; import {LoginDialogComponent} from '@shared-components/login/login.component';
import {KeyValuePipe, NgForOf, NgIf} from '@angular/common'; import {KeyValuePipe, NgForOf, NgIf} from '@angular/common';
import {FormType} from './form_type'; import {FormType} from './form_type';
import {MatButton} from '@angular/material/button'; import {MatButton} from '@angular/material/button';
import {AuthService} from '../services/auth.service'; import {AuthService} from '@services/auth.service';
@Component({ @Component({
selector: 'app-forms', selector: 'app-forms',
@@ -62,12 +62,17 @@
<li><a href="https://alttd.com/blog/">Blog</a></li> <li><a href="https://alttd.com/blog/">Blog</a></li>
</ul> </ul>
</li> </li>
<li *ngIf="!isAuthenticated">
<a (click)="openLoginDialog()">
Login
</a>
</li>
</ul> </ul>
</div> </div>
</div> </div>
<a href="/"><img ngSrc="/public/img/logos/logo.png" priority alt="Altitude Server Logo" height="319" <a href="/"><img ngSrc="/public/img/logos/logo.png" priority alt="Altitude Server Logo" height="319"
width="550"></a> 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 class="nav_li"><a [id]="getCurrentPageId(['home'])" class="nav_link" href="/" [ngClass]="active">Home</a>
</li> </li>
<li class="nav_li"> <li class="nav_li">
@@ -130,7 +135,20 @@
<li class="nav_li"><a class="nav_link2" target="_blank" href="https://alttd.com/blog/">Blog</a></li> <li class="nav_li"><a class="nav_link2" target="_blank" href="https://alttd.com/blog/">Blog</a></li>
</ul> </ul>
</li> </li>
<li class="nav_li" *ngIf="isAuthenticated">
<a [id]="getCurrentPageId(['particles'])"
class="nav_link fake_link" [ngClass]="active">Special</a>
<ul class="dropdown" *ngIf="hasAccess(['HEAD_MOD'])">
<li class="nav_li"><a class="nav_link2" [routerLink]="['/particles']">Particles</a></li>
</ul> </ul>
</li>
<li class="nav_li login-button" *ngIf="!isAuthenticated">
<a class="nav_link fake_link" (click)="openLoginDialog()">
Login
</a>
</li>
</ul>
<app-theme></app-theme> <app-theme></app-theme>
</div> </div>
</nav> </nav>
@@ -259,6 +259,10 @@ nav img {
display: none; display: none;
} }
.login-button {
margin-left: 40px;
}
@-moz-document url-prefix() { @-moz-document url-prefix() {
.dropdown2 { .dropdown2 {
top: 12px; top: 12px;
@@ -271,12 +275,6 @@ nav img {
} }
} }
@media (max-width: 1000px) {
#scroll-button {
padding-top: 5px;
}
}
@media (max-width: 1150px) { @media (max-width: 1150px) {
.dropdown { .dropdown {
top: -10px; top: -10px;
@@ -305,10 +303,6 @@ nav img {
display: block; display: block;
width: 100%; width: 100%;
} }
.switch-div {
top: 65px;
}
} }
@media (max-width: 690px) { @media (max-width: 690px) {
@@ -340,10 +334,6 @@ nav img {
background: none; background: none;
border-bottom: 3px solid var(--pureblack); border-bottom: 3px solid var(--pureblack);
} }
.switch-div {
top: 33px;
}
} }
@media (min-width: 690px) { @media (min-width: 690px) {
@@ -1,7 +1,12 @@
import {Component, HostListener, Input} from '@angular/core'; import {Component, HostListener, Input, OnDestroy, OnInit} from '@angular/core';
import {CommonModule, NgOptimizedImage} from '@angular/common'; import {CommonModule, NgOptimizedImage} from '@angular/common';
import {ThemeComponent} from '../theme/theme.component'; import {ThemeComponent} from '@shared-components/theme/theme.component';
import {RouterLink} from '@angular/router'; import {RouterLink} from '@angular/router';
import {AuthService} from '@services/auth.service';
import {Subscription} from 'rxjs';
import {LoginDialogComponent} from '@shared-components/login/login.component';
import {MatButton} from '@angular/material/button';
import {MatDialog} from '@angular/material/dialog';
@Component({ @Component({
standalone: true, standalone: true,
@@ -9,13 +14,14 @@ import {RouterLink} from '@angular/router';
CommonModule, CommonModule,
ThemeComponent, ThemeComponent,
RouterLink, RouterLink,
NgOptimizedImage NgOptimizedImage,
MatButton
], ],
selector: 'app-header', selector: 'app-header',
templateUrl: './header.component.html', templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'] styleUrls: ['./header.component.scss']
}) })
export class HeaderComponent { export class HeaderComponent implements OnInit, OnDestroy {
@Input() current_page: string = ''; @Input() current_page: string = '';
@Input() background_image: string = ''; @Input() background_image: string = '';
@Input() height: string = ''; @Input() height: string = '';
@@ -23,6 +29,24 @@ export class HeaderComponent {
public active: string = ''; public active: string = '';
public inverseYPos: number = 0; public inverseYPos: number = 0;
private subscription: Subscription | undefined;
public isAuthenticated: boolean = false;
constructor(protected authService: AuthService,
private dialog: MatDialog) {
}
ngOnInit(): void {
this.subscription = this.authService.isAuthenticated$.subscribe(isAuthenticated => {
this.isAuthenticated = isAuthenticated;
}
);
}
ngOnDestroy(): void {
this.subscription?.unsubscribe();
}
@HostListener('window:scroll', []) @HostListener('window:scroll', [])
onWindowScroll(): void { onWindowScroll(): void {
@@ -59,4 +83,17 @@ export class HeaderComponent {
return ''; return '';
} }
} }
public openLoginDialog() {
const dialogRef = this.dialog.open(LoginDialogComponent, {
width: '400px',
})
dialogRef.afterClosed().subscribe(result => {
console.log(result);
});
}
public hasAccess(claims: string[]): boolean {
return this.authService.hasAccess(claims)
}
} }
@@ -322,6 +322,7 @@ main .container {
#header-img { #header-img {
width: 65%; width: 65%;
height: 18%;
} }
.title { .title {
@@ -1,11 +1,12 @@
import {Component, OnInit} from '@angular/core'; import {Component, OnInit} from '@angular/core';
import {Title} from '@angular/platform-browser'; import {Title} from '@angular/platform-browser';
import {ALTITUDE_VERSION} from '../constant'; import {ALTITUDE_VERSION} from '@custom-types/constant';
import {ScrollService} from '../scroll/scroll.service'; import {ScrollService} from '@services/scroll.service';
import {CommonModule, NgOptimizedImage} from '@angular/common'; import {CommonModule, NgOptimizedImage} from '@angular/common';
import {HeaderComponent} from '../header/header.component'; import {HeaderComponent} from '@header/header.component';
import {CopyIpComponent} from '../copy-ip/copy-ip.component'; import {CopyIpComponent} from '@shared-components/copy-ip/copy-ip.component';
import {RouterLink} from '@angular/router'; import {RouterLink} from '@angular/router';
import {JwtHelperService} from '@auth0/angular-jwt';
@Component({ @Component({
standalone: true, standalone: true,
@@ -16,6 +17,9 @@ import {RouterLink} from '@angular/router';
RouterLink, RouterLink,
NgOptimizedImage NgOptimizedImage
], ],
providers: [
{provide: JwtHelperService, useFactory: () => new JwtHelperService()}
],
selector: 'app-home', selector: 'app-home',
templateUrl: './home.component.html', templateUrl: './home.component.html',
styleUrl: './home.component.scss' styleUrl: './home.component.scss'
@@ -0,0 +1,48 @@
<div class="card-div">
<mat-card>
<mat-card-header>
<mat-card-title>Frames</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="frames-container">
<mat-tab-group [selectedIndex]="frames.indexOf(currentFrame)"
(selectedIndexChange)="switchFrame(frames[$event])">
<mat-tab *ngFor="let frameId of frames" [label]="frameId">
<div class="frame-content">
<h3>Particles in {{ frameId }}</h3>
<div class="particles-list">
<div *ngFor="let particle of particleData.frames[frameId]; let i = index" class="particle-item">
<span class="particle-item-text">
Particle {{ i + 1 }}: ({{ particle.x.toFixed(2) }}, {{ particle.y.toFixed(2) }}
, {{ particle.z.toFixed(2) }})
</span>
<button mat-icon-button (click)="removeParticle(frameId, i)">
<mat-icon>delete</mat-icon>
</button>
<button mat-icon-button (click)="highlightParticle(frameId, i)">
<mat-icon>lightbulb</mat-icon>
</button>
</div>
<div *ngIf="!particleData.frames[frameId] || particleData.frames[frameId].length === 0"
class="no-particles">
No particles in this frame. Click on the plane to add particles.
</div>
</div>
<div class="frame-actions">
<button mat-raised-button color="warn" (click)="removeFrame(frameId)"
[disabled]="frames.length <= 1">
Remove Frame
</button>
</div>
</div>
</mat-tab>
</mat-tab-group>
<div class="add-frame">
<button mat-raised-button color="primary" (click)="addFrame()">
Add New Frame
</button>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
@@ -0,0 +1,61 @@
.frames-container {
margin-top: 10px;
}
.frame-content {
padding: 15px;
}
.particles-list {
height: 550px;
overflow-y: auto;
border: 1px solid #eee;
border-radius: 4px;
padding: 10px;
margin-bottom: 15px;
}
.particle-item {
display: flex;
align-items: center;
padding: 8px;
border-bottom: 1px solid #eee;
gap: 10px;
}
.particle-item-text {
flex-grow: 1;
}
.particle-item:last-child {
border-bottom: none;
}
.no-particles {
padding: 20px;
text-align: center;
color: var(--color-primairy);
}
.frame-actions {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
.add-frame {
margin-top: 15px;
display: flex;
justify-content: center;
}
.card-div {
mat-card {
background-color: var(--color-primary);
color: var(--font-color);
}
}
span {
color: var(--font-color);
}
@@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FramesComponent } from './frames.component';
describe('FramesComponent', () => {
let component: FramesComponent;
let fixture: ComponentFixture<FramesComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FramesComponent]
})
.compileComponents();
fixture = TestBed.createComponent(FramesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,89 @@
import {Component} from '@angular/core';
import {MatButton, MatIconButton} from "@angular/material/button";
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from "@angular/material/card";
import {MatTab, MatTabGroup} from "@angular/material/tabs";
import {NgForOf, NgIf} from "@angular/common";
import {ParticleData} from '../../models/particle.model';
import {MatIcon} from '@angular/material/icon';
import {ParticleManagerService} from '../../services/particle-manager.service';
import {FrameManagerService} from '../../services/frame-manager.service';
@Component({
selector: 'app-frames',
imports: [
MatButton,
MatCard,
MatCardContent,
MatCardHeader,
MatCardTitle,
MatIcon,
MatIconButton,
MatTab,
MatTabGroup,
NgForOf,
NgIf
],
templateUrl: './frames.component.html',
styleUrl: './frames.component.scss'
})
export class FramesComponent {
constructor(
private particleManagerService: ParticleManagerService,
private frameManagerService: FrameManagerService) {
}
/**
* Get the particle data
*/
public get particleData(): ParticleData {
return this.particleManagerService.getParticleData();
}
/**
* Get the current frame
*/
public get currentFrame(): string {
return this.particleManagerService.getCurrentFrame();
}
/**
* Get all frames
*/
public get frames(): string[] {
return this.particleManagerService.getFrames();
}
/**
* Add a new frame
*/
public addFrame(): void {
this.frameManagerService.addFrame();
}
/**
* Switch to a different frame
*/
public switchFrame(frameId: string): void {
this.frameManagerService.switchFrame(frameId);
}
/**
* Remove a particle
*/
public removeParticle(frameId: string, index: number): void {
this.particleManagerService.removeParticle(frameId, index);
}
/**
* Remove a frame
*/
public removeFrame(frameId: string): void {
this.frameManagerService.removeFrame(frameId);
}
public highlightParticle(frameId: string, i: number) {
this.particleManagerService.highlightParticle(frameId, i);
}
}
@@ -0,0 +1,37 @@
<div class="card-div">
<mat-card class="particle-card">
<mat-card-header>
<mat-card-title>Particle Properties</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="particle-properties">
<div class="property-row">
<div class="color-picker">
<input type="color" [(ngModel)]="selectedColor">
<span>Current color: {{ selectedColor }}</span>
</div>
<mat-form-field appearance="fill" class="type-field">
<mat-label>Select Particle Type</mat-label>
<input type="text"
placeholder="Search for a particle type"
matInput
[formControl]="particleTypeControl"
[matAutocomplete]="auto">
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn">
<mat-option *ngFor="let type of filteredParticleTypes | async" [value]="type">
{{ type }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
</div>
<div class="size-slider">
<mat-slider min="0.1" max="4" step="0.1" class="full-width">
<input matSliderThumb [(ngModel)]="selectedSize">
</mat-slider>
<span>Size: {{ selectedSize }}</span>
</div>
</div>
</mat-card-content>
</mat-card>
</div>
@@ -0,0 +1,61 @@
.particle-card {
margin-top: 20px;
margin-bottom: 20px;
}
.particle-properties {
display: flex;
flex-direction: column;
gap: 20px;
}
.property-row {
display: flex;
align-items: center;
gap: 15px;
width: 100%;
}
.type-field {
flex: 1;
min-width: 20ch;
max-width: 40ch;
}
.color-picker {
display: flex;
flex: 1;
align-items: center;
gap: 10px;
}
.color-picker input[type="color"] {
width: 40px;
height: 40px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.card-div {
mat-card {
background-color: var(--color-primary);
color: var(--font-color);
}
}
.full-width {
width: 100%;
}
.size-slider {
width: 95%;
display: flex;
flex-direction: column;
margin-top: 5px;
}
span {
color: var(--font-color);
font-size: 0.9rem;
}
@@ -0,0 +1,23 @@
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {ParticleComponent} from './particle.component';
describe('ParticleComponent', () => {
let component: ParticleComponent;
let fixture: ComponentFixture<ParticleComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ParticleComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ParticleComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -0,0 +1,115 @@
import {Component, OnInit} from '@angular/core';
import {MatCard, MatCardContent, MatCardHeader, MatCardTitle} from '@angular/material/card';
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {ParticleManagerService} from '../../services/particle-manager.service';
import {Particle} from '../../models/particle.model';
import {MatSliderModule} from '@angular/material/slider';
import {MatSelectModule} from '@angular/material/select';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatInputModule} from '@angular/material/input';
import {MatAutocompleteModule} from '@angular/material/autocomplete';
import {Observable} from 'rxjs';
import {map, startWith} from 'rxjs/operators';
import {AsyncPipe, NgForOf} from '@angular/common';
@Component({
selector: 'app-particle',
imports: [
MatCard,
MatCardContent,
MatCardHeader,
MatCardTitle,
ReactiveFormsModule,
FormsModule,
MatSliderModule,
MatSelectModule,
MatFormFieldModule,
MatInputModule,
MatAutocompleteModule,
NgForOf,
AsyncPipe
],
templateUrl: './particle.component.html',
styleUrl: './particle.component.scss'
})
export class ParticleComponent implements OnInit {
// Available particle types from the enum
particleTypes = Object.values(Particle);
// Form control for the particle type dropdown with filtering
particleTypeControl = new FormControl();
filteredParticleTypes: Observable<string[]>;
constructor(
private particleManagerService: ParticleManagerService,
) {
this.filteredParticleTypes = this.particleTypeControl.valueChanges.pipe(
startWith(''),
map(value => this._filterParticleTypes(value || ''))
);
}
ngOnInit() {
// Initialize the particle type control with the current value
this.particleTypeControl.setValue(this.selectedParticle);
// Update the selected particle when the control value changes
this.particleTypeControl.valueChanges.subscribe(value => {
if (value && Object.values(Particle).includes(value)) {
this.selectedParticle = value;
}
});
}
private _filterParticleTypes(value: string): string[] {
const filterValue = value.toLowerCase();
return this.particleTypes.filter(type => type.toLowerCase().includes(filterValue));
}
// Display function for the autocomplete
displayFn(particle: string): string {
return particle ? particle : '';
}
/**
* Get the selected color
*/
public get selectedColor(): string {
return this.particleManagerService.getSelectedColor();
}
/**
* Set the selected color
*/
public set selectedColor(color: string) {
this.particleManagerService.setSelectedColor(color);
}
/**
* Get the selected particle type
*/
public get selectedParticle(): Particle {
return this.particleManagerService.particle;
}
/**
* Set the selected particle type
*/
public set selectedParticle(particle: Particle) {
this.particleManagerService.particle = particle;
}
/**
* Get the selected particle size
*/
public get selectedSize(): number {
return this.particleManagerService.size;
}
/**
* Set the selected particle size
*/
public set selectedSize(size: number) {
this.particleManagerService.size = size;
}
}
@@ -0,0 +1,93 @@
<div class="card-div">
<mat-card>
<mat-card-header>
<mat-card-title>Particle Properties</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Particle Name</mat-label>
<input matInput [(ngModel)]="particleData.particle_name" placeholder="Enter particle name">
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Display Name</mat-label>
<input matInput [(ngModel)]="particleData.display_name" placeholder="Enter display name">
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Particle Type</mat-label>
<mat-select [(ngModel)]="particleData.particle_type">
<mat-option *ngFor="let type of particleTypes" [value]="type">{{ type }}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Lore</mat-label>
<textarea matInput [(ngModel)]="particleData.lore" placeholder="Enter lore"></textarea>
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Display Item</mat-label>
<input matInput [(ngModel)]="particleData.display_item" placeholder="Enter display item">
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Permission</mat-label>
<input matInput [(ngModel)]="particleData.permission" placeholder="Enter permission">
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Package Permission</mat-label>
<input matInput [(ngModel)]="particleData.package_permission" placeholder="Enter package permission">
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Frame Delay</mat-label>
<input matInput type="number" [(ngModel)]="particleData.frame_delay" placeholder="Enter frame delay">
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Repeat</mat-label>
<input matInput type="number" [(ngModel)]="particleData.repeat" placeholder="Enter repeat count">
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Repeat Delay</mat-label>
<input matInput type="number" [(ngModel)]="particleData.repeat_delay"
placeholder="Enter repeat delay">
</mat-form-field>
</div>
<div class="form-row">
<mat-form-field appearance="outline">
<mat-label>Random Offset</mat-label>
<input matInput type="number" [(ngModel)]="particleData.random_offset"
placeholder="Enter random offset">
</mat-form-field>
</div>
<div class="form-row">
<mat-checkbox [(ngModel)]="particleData.stationary"><span>Stationary</span></mat-checkbox>
</div>
</mat-card-content>
</mat-card>
</div>

Some files were not shown because too many files have changed in this diff Show More