diff --git a/API/pom.xml b/API/pom.xml
index 38c4c92..2ee38fe 100644
--- a/API/pom.xml
+++ b/API/pom.xml
@@ -70,6 +70,20 @@
org.springframework.boot
spring-boot-starter-data-mongodb
+
+ org.springframework.boot
+ spring-boot-starter-data-redis
+
+
+ io.lettuce
+ lettuce-core
+
+
+
+
+ redis.clients
+ jedis
+
diff --git a/API/src/main/java/cc/fascinated/Main.java b/API/src/main/java/cc/fascinated/Main.java
index 1cf0277..4ceeedf 100644
--- a/API/src/main/java/cc/fascinated/Main.java
+++ b/API/src/main/java/cc/fascinated/Main.java
@@ -6,6 +6,8 @@ import lombok.extern.log4j.Log4j2;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
+import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
+import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.scheduling.annotation.EnableScheduling;
import java.io.File;
@@ -17,6 +19,8 @@ import java.util.Objects;
* @author Fascinated (fascinated7)
*/
@EnableScheduling
+@EnableMongoRepositories(basePackages = "cc.fascinated.repository.mongo")
+@EnableRedisRepositories(basePackages = "cc.fascinated.repository.redis")
@SpringBootApplication(exclude = UserDetailsServiceAutoConfiguration.class)
@Log4j2(topic = "Score Tracker")
public class Main {
diff --git a/API/src/main/java/cc/fascinated/common/IPUtils.java b/API/src/main/java/cc/fascinated/common/IPUtils.java
index 002f2e7..0a975dd 100644
--- a/API/src/main/java/cc/fascinated/common/IPUtils.java
+++ b/API/src/main/java/cc/fascinated/common/IPUtils.java
@@ -1,4 +1,54 @@
-package cc.fascinated.common;/**
+package cc.fascinated.common;
+
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.NonNull;
+import lombok.experimental.UtilityClass;
+
+/**
* @author Fascinated (fascinated7)
- */public class IPUtils {
-}
+ */
+@UtilityClass
+public final class IPUtils {
+ /**
+ * The regex expression for validating IPv4 addresses.
+ */
+ public static final String IPV4_REGEX = "^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$";
+
+ /**
+ * The regex expression for validating IPv6 addresses.
+ */
+ public static final String IPV6_REGEX = "^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^(([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4})?::(([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4})?$";
+
+ private static final String[] IP_HEADERS = new String[] {
+ "CF-Connecting-IP",
+ "X-Forwarded-For"
+ };
+
+ /**
+ * Get the real IP from the given request.
+ *
+ * @param request the request
+ * @return the real IP
+ */
+ @NonNull
+ public static String getRealIp(@NonNull HttpServletRequest request) {
+ String ip = request.getRemoteAddr();
+ for (String headerName : IP_HEADERS) {
+ String header = request.getHeader(headerName);
+ if (header == null) {
+ continue;
+ }
+ if (!header.contains(",")) { // Handle single IP
+ ip = header;
+ break;
+ }
+ // Handle multiple IPs
+ String[] ips = header.split(",");
+ for (String ipHeader : ips) {
+ ip = ipHeader;
+ break;
+ }
+ }
+ return ip;
+ }
+}
\ No newline at end of file
diff --git a/API/src/main/java/cc/fascinated/common/Request.java b/API/src/main/java/cc/fascinated/common/Request.java
index 5e606fb..60c3d8d 100644
--- a/API/src/main/java/cc/fascinated/common/Request.java
+++ b/API/src/main/java/cc/fascinated/common/Request.java
@@ -3,9 +3,11 @@ package cc.fascinated.common;
import kong.unirest.core.Headers;
import kong.unirest.core.HttpResponse;
import kong.unirest.core.Unirest;
+import kong.unirest.core.UnirestParsingException;
import lombok.extern.log4j.Log4j2;
import java.util.List;
+import java.util.Optional;
/**
* @author Fascinated (fascinated7)
@@ -49,6 +51,7 @@ public class Request {
}
response = Unirest.get(url).asObject(clazz);
}
+ response.getParsingError().ifPresent(e -> log.error("Failed to parse response", e));
return response;
}
diff --git a/API/src/main/java/cc/fascinated/common/StringUtils.java b/API/src/main/java/cc/fascinated/common/StringUtils.java
index 81cf42d..0972fe1 100644
--- a/API/src/main/java/cc/fascinated/common/StringUtils.java
+++ b/API/src/main/java/cc/fascinated/common/StringUtils.java
@@ -1,4 +1,33 @@
-package cc.fascinated.common;/**
+package cc.fascinated.common;
+
+import java.math.BigInteger;
+
+/**
* @author Fascinated (fascinated7)
- */public class StringUtils {
+ */
+public class StringUtils {
+ /**
+ * Converts a string to a hexadecimal string.
+ *
+ * @param arg the string to convert
+ * @return the hexadecimal string
+ */
+ public static String toHex(String arg) {
+ return String.format("%040x", new BigInteger(1, arg.getBytes()));
+ }
+
+ /**
+ * Generates a random string.
+ *
+ * @param length the length of the string
+ * @return the random string
+ */
+ public static String randomString(int length) {
+ String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ StringBuilder stringBuilder = new StringBuilder();
+ for (int i = 0; i < length; i++) {
+ stringBuilder.append(chars.charAt((int) (Math.random() * chars.length())));
+ }
+ return stringBuilder.toString();
+ }
}
diff --git a/API/src/main/java/cc/fascinated/config/RedisConfig.java b/API/src/main/java/cc/fascinated/config/RedisConfig.java
index 6ee52fa..148a496 100644
--- a/API/src/main/java/cc/fascinated/config/RedisConfig.java
+++ b/API/src/main/java/cc/fascinated/config/RedisConfig.java
@@ -1,4 +1,73 @@
-package cc.fascinated.config;/**
- * @author Fascinated (fascinated7)
- */public class RedisConfig {
-}
+package cc.fascinated.config;
+
+import lombok.NonNull;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+
+/**
+ * @author Braydon
+ */
+@Configuration
+@Log4j2(topic = "Redis")
+public class RedisConfig {
+ /**
+ * The Redis server host.
+ */
+ @Value("${spring.data.redis.host}")
+ private String host;
+
+ /**
+ * The Redis server port.
+ */
+ @Value("${spring.data.redis.port}")
+ private int port;
+
+ /**
+ * The Redis database index.
+ */
+ @Value("${spring.data.redis.database}")
+ private int database;
+
+ /**
+ * The optional Redis password.
+ */
+ @Value("${spring.data.redis.auth}")
+ private String auth;
+
+ /**
+ * Build the config to use for Redis.
+ *
+ * @return the config
+ * @see RedisTemplate for config
+ */
+ @Bean @NonNull
+ public RedisTemplate redisTemplate() {
+ RedisTemplate template = new RedisTemplate<>();
+ template.setConnectionFactory(jedisConnectionFactory());
+ return template;
+ }
+
+ /**
+ * Build the connection factory to use
+ * when making connections to Redis.
+ *
+ * @return the built factory
+ * @see JedisConnectionFactory for factory
+ */
+ @Bean @NonNull
+ public JedisConnectionFactory jedisConnectionFactory() {
+ log.info("Connecting to Redis at {}:{}/{}", host, port, database);
+ RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, port);
+ config.setDatabase(database);
+ if (!auth.trim().isEmpty()) { // Auth with our provided password
+ log.info("Using auth...");
+ config.setPassword(auth);
+ }
+ return new JedisConnectionFactory(config);
+ }
+}
\ No newline at end of file
diff --git a/API/src/main/java/cc/fascinated/controller/AuthenticationController.java b/API/src/main/java/cc/fascinated/controller/AuthenticationController.java
index 529ea55..c0bfe81 100644
--- a/API/src/main/java/cc/fascinated/controller/AuthenticationController.java
+++ b/API/src/main/java/cc/fascinated/controller/AuthenticationController.java
@@ -1,4 +1,63 @@
-package cc.fascinated.controller;/**
+package cc.fascinated.controller;
+
+import cc.fascinated.model.auth.LoginRequest;
+import cc.fascinated.model.auth.AuthToken;
+import cc.fascinated.services.UserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+/**
* @author Fascinated (fascinated7)
- */public class AuthenticationController {
+ */
+@RestController
+@RequestMapping(value = "/auth", produces = MediaType.APPLICATION_JSON_VALUE)
+public class AuthenticationController {
+ /**
+ * The user service to use
+ */
+ private final UserService userService;
+
+ @Autowired
+ public AuthenticationController(UserService userService) {
+ this.userService = userService;
+ }
+
+ /**
+ * A POST request to get an auth token from a steam ticket.
+ */
+ @ResponseBody
+ @PostMapping(value = "/login", consumes = MediaType.APPLICATION_JSON_VALUE)
+ public ResponseEntity> getAuthToken(@RequestBody LoginRequest request) {
+ if (request == null || request.getTicket() == null) {
+ return ResponseEntity.badRequest().body(Map.of(
+ "error", "Invalid request or missing ticket"
+ ));
+ }
+ AuthToken authToken = this.userService.getAuthToken(request.getTicket());
+ return ResponseEntity.ok()
+ .header("Authorization", authToken.getAuthToken())
+ .build();
+ }
+
+ /**
+ * A POST request to validate an auth token.
+ */
+ @ResponseBody
+ @PostMapping(value = "/validate")
+ public ResponseEntity> validateAuthToken(@RequestHeader("Authorization") String authToken) {
+ String token = authToken == null ? null : authToken.replace("Bearer ", "");
+ if (token == null) {
+ return ResponseEntity.badRequest().body(Map.of(
+ "error", "Invalid request or missing token"
+ ));
+ }
+ return new ResponseEntity<>(this.userService.isValidAuthToken(token) ? HttpStatus.OK : HttpStatus.UNAUTHORIZED);
+ }
}
+
+
diff --git a/API/src/main/java/cc/fascinated/log/TransactionLogger.java b/API/src/main/java/cc/fascinated/log/TransactionLogger.java
index 45eb949..4503f13 100644
--- a/API/src/main/java/cc/fascinated/log/TransactionLogger.java
+++ b/API/src/main/java/cc/fascinated/log/TransactionLogger.java
@@ -1,4 +1,50 @@
-package cc.fascinated.log;/**
- * @author Fascinated (fascinated7)
- */public class TransactionLogger {
-}
+package cc.fascinated.log;
+
+import cc.fascinated.common.IPUtils;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.NonNull;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.MethodParameter;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.http.server.ServletServerHttpRequest;
+import org.springframework.http.server.ServletServerHttpResponse;
+import org.springframework.web.bind.annotation.ControllerAdvice;
+import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
+
+/**
+ * Responsible for logging request and
+ * response transactions to the terminal.
+ *
+ * @author Braydon
+ * @see HttpServletRequest for request
+ * @see HttpServletResponse for response
+ */
+@ControllerAdvice
+@Slf4j(topic = "Req/Res Transaction")
+public class TransactionLogger implements ResponseBodyAdvice