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 { + @Override + public boolean supports(@NonNull MethodParameter returnType, @NonNull Class> converterType) { + return true; + } + + @Override + public Object beforeBodyWrite(Object body, @NonNull MethodParameter returnType, @NonNull MediaType selectedContentType, + @NonNull Class> selectedConverterType, + @NonNull ServerHttpRequest rawRequest, @NonNull ServerHttpResponse rawResponse) { + HttpServletRequest request = ((ServletServerHttpRequest) rawRequest).getServletRequest(); + HttpServletResponse response = ((ServletServerHttpResponse) rawResponse).getServletResponse(); + + // Get the request ip ip + String ip = IPUtils.getRealIp(request); + + log.info("[Request] %s - %s %s %s".formatted( + ip, request.getMethod(), request.getRequestURI(), response.getStatus() + )); + return body; + } +} \ No newline at end of file diff --git a/API/src/main/java/cc/fascinated/model/auth/AuthToken.java b/API/src/main/java/cc/fascinated/model/auth/AuthToken.java index 350b303..91682a2 100644 --- a/API/src/main/java/cc/fascinated/model/auth/AuthToken.java +++ b/API/src/main/java/cc/fascinated/model/auth/AuthToken.java @@ -4,33 +4,26 @@ import cc.fascinated.common.StringUtils; import cc.fascinated.model.token.steam.SteamAuthenticateUserTicketToken; import lombok.AllArgsConstructor; import lombok.Getter; +import org.springframework.data.annotation.Id; +import org.springframework.data.redis.core.RedisHash; + +import java.util.UUID; /** * @author Fascinated (fascinated7) */ @AllArgsConstructor @Getter -public class SteamAuthToken { - /** - * The steam id of the user. - */ - private final String steamId; - +@RedisHash(value = "AuthToken", timeToLive = 60 * 60 * 6) // 6 hours +public class AuthToken { /** * The auth token of the user. */ + @Id private final String authToken; /** - * Gets the SteamProfile from an auth token. - * - * @param token The auth token. - * @return The SteamProfile. + * The id of the user. */ - public static SteamAuthToken getFromAuthToken(SteamAuthenticateUserTicketToken token) { - return new SteamAuthToken( - token.getResponse().getParams().getSteamId(), - StringUtils.randomString(32) - ); - } + private final UUID userId; } diff --git a/API/src/main/java/cc/fascinated/model/auth/LoginRequest.java b/API/src/main/java/cc/fascinated/model/auth/LoginRequest.java index c933ecd..bb9cfb0 100644 --- a/API/src/main/java/cc/fascinated/model/auth/LoginRequest.java +++ b/API/src/main/java/cc/fascinated/model/auth/LoginRequest.java @@ -1,4 +1,14 @@ -package cc.fascinated.model.auth;/** +package cc.fascinated.model.auth; + +import lombok.Getter; + +/** * @author Fascinated (fascinated7) - */public class LoginRequest { + */ +@Getter +public class LoginRequest { + /** + * The ticket to authenticate the user. + */ + private String ticket; } diff --git a/API/src/main/java/cc/fascinated/model/leaderboard/Leaderboard.java b/API/src/main/java/cc/fascinated/model/leaderboard/Leaderboard.java index e913c4b..d5e5402 100644 --- a/API/src/main/java/cc/fascinated/model/leaderboard/Leaderboard.java +++ b/API/src/main/java/cc/fascinated/model/leaderboard/Leaderboard.java @@ -1,7 +1,7 @@ package cc.fascinated.model.leaderboard; import cc.fascinated.common.ScoreSaberUtils; -import cc.fascinated.model.token.ScoreSaberLeaderboardToken; +import cc.fascinated.model.token.scoresaber.ScoreSaberLeaderboardToken; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberAccountToken.java b/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberAccountToken.java index ef72fc3..dddae31 100644 --- a/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberAccountToken.java +++ b/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberAccountToken.java @@ -1,4 +1,4 @@ -package cc.fascinated.model.token; +package cc.fascinated.model.token.scoresaber; import lombok.Getter; diff --git a/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberLeaderboardPageToken.java b/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberLeaderboardPageToken.java index b305dbe..4721a6a 100644 --- a/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberLeaderboardPageToken.java +++ b/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberLeaderboardPageToken.java @@ -1,4 +1,4 @@ -package cc.fascinated.model.token; +package cc.fascinated.model.token.scoresaber; import lombok.Getter; import lombok.Setter; diff --git a/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberLeaderboardToken.java b/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberLeaderboardToken.java index 2542645..f054820 100644 --- a/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberLeaderboardToken.java +++ b/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberLeaderboardToken.java @@ -1,4 +1,4 @@ -package cc.fascinated.model.token; +package cc.fascinated.model.token.scoresaber; import lombok.Getter; import lombok.ToString; diff --git a/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberPageMetadataToken.java b/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberPageMetadataToken.java index 945e478..672695d 100644 --- a/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberPageMetadataToken.java +++ b/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberPageMetadataToken.java @@ -1,4 +1,4 @@ -package cc.fascinated.model.token; +package cc.fascinated.model.token.scoresaber; import lombok.Getter; import lombok.ToString; diff --git a/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberPlayerScoreToken.java b/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberPlayerScoreToken.java index fc4ff9b..fc9b9d4 100644 --- a/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberPlayerScoreToken.java +++ b/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberPlayerScoreToken.java @@ -1,4 +1,4 @@ -package cc.fascinated.model.token; +package cc.fascinated.model.token.scoresaber; import lombok.Getter; import lombok.ToString; diff --git a/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberScoreToken.java b/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberScoreToken.java index df9af6c..4de6f99 100644 --- a/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberScoreToken.java +++ b/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberScoreToken.java @@ -1,4 +1,4 @@ -package cc.fascinated.model.token; +package cc.fascinated.model.token.scoresaber; import lombok.Getter; import lombok.ToString; diff --git a/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberScoresPageToken.java b/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberScoresPageToken.java index 432c9e0..915cd30 100644 --- a/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberScoresPageToken.java +++ b/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberScoresPageToken.java @@ -1,4 +1,4 @@ -package cc.fascinated.model.token; +package cc.fascinated.model.token.scoresaber; import lombok.Getter; import lombok.Setter; diff --git a/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberWebsocketDataToken.java b/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberWebsocketDataToken.java index e58d942..07f42e4 100644 --- a/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberWebsocketDataToken.java +++ b/API/src/main/java/cc/fascinated/model/token/scoresaber/ScoreSaberWebsocketDataToken.java @@ -1,4 +1,4 @@ -package cc.fascinated.model.token; +package cc.fascinated.model.token.scoresaber; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.AllArgsConstructor; diff --git a/API/src/main/java/cc/fascinated/model/token/steam/SteamAuthenticateUserTicketToken.java b/API/src/main/java/cc/fascinated/model/token/steam/SteamAuthenticateUserTicketToken.java index a48817b..fb999b3 100644 --- a/API/src/main/java/cc/fascinated/model/token/steam/SteamAuthenticateUserTicketToken.java +++ b/API/src/main/java/cc/fascinated/model/token/steam/SteamAuthenticateUserTicketToken.java @@ -1,4 +1,4 @@ -package cc.fascinated.model.token.steam.token; +package cc.fascinated.model.token.steam; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; diff --git a/API/src/main/java/cc/fascinated/model/user/ScoreSaberAccount.java b/API/src/main/java/cc/fascinated/model/user/ScoreSaberAccount.java index 1d4025d..45aab1a 100644 --- a/API/src/main/java/cc/fascinated/model/user/ScoreSaberAccount.java +++ b/API/src/main/java/cc/fascinated/model/user/ScoreSaberAccount.java @@ -1,7 +1,7 @@ package cc.fascinated.model.user; import cc.fascinated.common.DateUtils; -import cc.fascinated.model.token.ScoreSaberAccountToken; +import cc.fascinated.model.token.scoresaber.ScoreSaberAccountToken; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/API/src/main/java/cc/fascinated/platform/impl/ScoreSaberPlatform.java b/API/src/main/java/cc/fascinated/platform/impl/ScoreSaberPlatform.java index de08558..5bb3f22 100644 --- a/API/src/main/java/cc/fascinated/platform/impl/ScoreSaberPlatform.java +++ b/API/src/main/java/cc/fascinated/platform/impl/ScoreSaberPlatform.java @@ -3,9 +3,9 @@ package cc.fascinated.platform.impl; import cc.fascinated.common.DateUtils; import cc.fascinated.common.MathUtils; import cc.fascinated.model.score.Score; -import cc.fascinated.model.token.ScoreSaberAccountToken; -import cc.fascinated.model.token.ScoreSaberLeaderboardPageToken; -import cc.fascinated.model.token.ScoreSaberLeaderboardToken; +import cc.fascinated.model.token.scoresaber.ScoreSaberAccountToken; +import cc.fascinated.model.token.scoresaber.ScoreSaberLeaderboardPageToken; +import cc.fascinated.model.token.scoresaber.ScoreSaberLeaderboardToken; import cc.fascinated.model.user.User; import cc.fascinated.model.user.history.HistoryPoint; import cc.fascinated.platform.CurvePoint; diff --git a/API/src/main/java/cc/fascinated/repository/mongo/CounterRepository.java b/API/src/main/java/cc/fascinated/repository/mongo/CounterRepository.java index 2ea8b5a..7888f40 100644 --- a/API/src/main/java/cc/fascinated/repository/mongo/CounterRepository.java +++ b/API/src/main/java/cc/fascinated/repository/mongo/CounterRepository.java @@ -1,4 +1,4 @@ -package cc.fascinated.repository; +package cc.fascinated.repository.mongo; import cc.fascinated.model.Counter; import org.springframework.data.mongodb.repository.MongoRepository; diff --git a/API/src/main/java/cc/fascinated/repository/mongo/ScoreRepository.java b/API/src/main/java/cc/fascinated/repository/mongo/ScoreRepository.java index d771613..020ab93 100644 --- a/API/src/main/java/cc/fascinated/repository/mongo/ScoreRepository.java +++ b/API/src/main/java/cc/fascinated/repository/mongo/ScoreRepository.java @@ -1,4 +1,4 @@ -package cc.fascinated.repository; +package cc.fascinated.repository.mongo; import cc.fascinated.model.score.Score; import cc.fascinated.platform.Platform; diff --git a/API/src/main/java/cc/fascinated/repository/mongo/ScoreSaberLeaderboardRepository.java b/API/src/main/java/cc/fascinated/repository/mongo/ScoreSaberLeaderboardRepository.java index bbe20ba..5a0f07c 100644 --- a/API/src/main/java/cc/fascinated/repository/mongo/ScoreSaberLeaderboardRepository.java +++ b/API/src/main/java/cc/fascinated/repository/mongo/ScoreSaberLeaderboardRepository.java @@ -1,4 +1,4 @@ -package cc.fascinated.repository; +package cc.fascinated.repository.mongo; import cc.fascinated.model.token.scoresaber.ScoreSaberLeaderboardToken; import org.springframework.data.mongodb.repository.MongoRepository; diff --git a/API/src/main/java/cc/fascinated/repository/mongo/UserRepository.java b/API/src/main/java/cc/fascinated/repository/mongo/UserRepository.java index e2f17a9..cece120 100644 --- a/API/src/main/java/cc/fascinated/repository/mongo/UserRepository.java +++ b/API/src/main/java/cc/fascinated/repository/mongo/UserRepository.java @@ -1,4 +1,4 @@ -package cc.fascinated.repository; +package cc.fascinated.repository.mongo; import cc.fascinated.model.user.User; import org.springframework.data.mongodb.repository.MongoRepository; diff --git a/API/src/main/java/cc/fascinated/repository/redis/AuthTokenRepository.java b/API/src/main/java/cc/fascinated/repository/redis/AuthTokenRepository.java index 868f3f9..ad2e28c 100644 --- a/API/src/main/java/cc/fascinated/repository/redis/AuthTokenRepository.java +++ b/API/src/main/java/cc/fascinated/repository/redis/AuthTokenRepository.java @@ -1,4 +1,11 @@ -package cc.fascinated.repository.mongo.redis;/** +package cc.fascinated.repository.redis; + +import cc.fascinated.model.auth.AuthToken; +import org.springframework.data.repository.CrudRepository; + +import java.util.UUID; + +/** * @author Fascinated (fascinated7) - */public class AuthTokenRepository { -} + */ +public interface AuthTokenRepository extends CrudRepository {} diff --git a/API/src/main/java/cc/fascinated/services/CounterService.java b/API/src/main/java/cc/fascinated/services/CounterService.java index 897bf95..2019939 100644 --- a/API/src/main/java/cc/fascinated/services/CounterService.java +++ b/API/src/main/java/cc/fascinated/services/CounterService.java @@ -1,7 +1,7 @@ package cc.fascinated.services; import cc.fascinated.model.Counter; -import cc.fascinated.repository.CounterRepository; +import cc.fascinated.repository.mongo.CounterRepository; import lombok.AllArgsConstructor; import lombok.Getter; import org.springframework.beans.factory.annotation.Autowired; diff --git a/API/src/main/java/cc/fascinated/services/ScoreSaberService.java b/API/src/main/java/cc/fascinated/services/ScoreSaberService.java index bed23f5..1a26bc4 100644 --- a/API/src/main/java/cc/fascinated/services/ScoreSaberService.java +++ b/API/src/main/java/cc/fascinated/services/ScoreSaberService.java @@ -2,11 +2,11 @@ package cc.fascinated.services; import cc.fascinated.common.Request; import cc.fascinated.exception.impl.BadRequestException; -import cc.fascinated.model.token.ScoreSaberAccountToken; -import cc.fascinated.model.token.ScoreSaberLeaderboardPageToken; -import cc.fascinated.model.token.ScoreSaberLeaderboardToken; +import cc.fascinated.model.token.scoresaber.ScoreSaberAccountToken; +import cc.fascinated.model.token.scoresaber.ScoreSaberLeaderboardPageToken; +import cc.fascinated.model.token.scoresaber.ScoreSaberLeaderboardToken; import cc.fascinated.model.user.User; -import cc.fascinated.repository.ScoreSaberLeaderboardRepository; +import cc.fascinated.repository.mongo.ScoreSaberLeaderboardRepository; import kong.unirest.core.HttpResponse; import lombok.NonNull; import lombok.extern.log4j.Log4j2; diff --git a/API/src/main/java/cc/fascinated/services/ScoreService.java b/API/src/main/java/cc/fascinated/services/ScoreService.java index 04a8ac5..fe1f778 100644 --- a/API/src/main/java/cc/fascinated/services/ScoreService.java +++ b/API/src/main/java/cc/fascinated/services/ScoreService.java @@ -10,16 +10,16 @@ import cc.fascinated.model.score.Score; import cc.fascinated.model.score.TotalScoresResponse; import cc.fascinated.model.score.impl.scoresaber.ScoreSaberScore; import cc.fascinated.model.score.impl.scoresaber.ScoreSaberScoreResponse; -import cc.fascinated.model.token.ScoreSaberLeaderboardToken; -import cc.fascinated.model.token.ScoreSaberPlayerScoreToken; -import cc.fascinated.model.token.ScoreSaberScoreToken; +import cc.fascinated.model.token.scoresaber.ScoreSaberLeaderboardToken; +import cc.fascinated.model.token.scoresaber.ScoreSaberPlayerScoreToken; +import cc.fascinated.model.token.scoresaber.ScoreSaberScoreToken; import cc.fascinated.model.user.User; import cc.fascinated.model.user.UserDTO; import cc.fascinated.model.user.history.HistoryPoint; import cc.fascinated.model.user.hmd.DeviceController; import cc.fascinated.model.user.hmd.DeviceHeadset; import cc.fascinated.platform.Platform; -import cc.fascinated.repository.ScoreRepository; +import cc.fascinated.repository.mongo.ScoreRepository; import lombok.NonNull; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; diff --git a/API/src/main/java/cc/fascinated/services/SteamService.java b/API/src/main/java/cc/fascinated/services/SteamService.java index 2dc2dd4..292e286 100644 --- a/API/src/main/java/cc/fascinated/services/SteamService.java +++ b/API/src/main/java/cc/fascinated/services/SteamService.java @@ -1,4 +1,51 @@ -package cc.fascinated.services;/** +package cc.fascinated.services; + +import cc.fascinated.common.Request; +import cc.fascinated.model.token.steam.SteamAuthenticateUserTicketToken; +import kong.unirest.core.HttpResponse; +import lombok.extern.log4j.Log4j2; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +/** * @author Fascinated (fascinated7) - */public class SteamService { + */ +@Service +@Log4j2 +public class SteamService { + /** + * Steam API endpoints. + */ + private static final String STEAM_API_URL = "https://api.steampowered.com/"; + private static final String USER_AUTH_TICKET = STEAM_API_URL + "ISteamUserAuth/AuthenticateUserTicket/v1/?key=%s&appid=620980&ticket=%s"; + + /** + * The key to use for authentication + * with the Steam API. + */ + private final String steamKey; + + @Autowired + public SteamService(@Value("${scoretracker.steam.api-key}") String steamKey) { + this.steamKey = steamKey; + } + + /** + * Gets the steam ID from a user's ticket. + * + * @param ticket the ticket to get the steam ID from + * @return the steam ID from the ticket + */ + public SteamAuthenticateUserTicketToken getSteamUserFromTicket(String ticket) { + HttpResponse response = Request.get( + USER_AUTH_TICKET.formatted(steamKey, ticket), + SteamAuthenticateUserTicketToken.class + ); + if (response.getStatus() != 200) { + log.error("Failed to get steam ID from ticket: %s".formatted(response.getStatus())); + return null; + } + return response.getBody(); + } } diff --git a/API/src/main/java/cc/fascinated/services/UserService.java b/API/src/main/java/cc/fascinated/services/UserService.java index af103c9..bf53112 100644 --- a/API/src/main/java/cc/fascinated/services/UserService.java +++ b/API/src/main/java/cc/fascinated/services/UserService.java @@ -1,12 +1,16 @@ package cc.fascinated.services; +import cc.fascinated.common.StringUtils; import cc.fascinated.common.TimeUtils; import cc.fascinated.exception.impl.BadRequestException; -import cc.fascinated.model.token.ScoreSaberAccountToken; +import cc.fascinated.model.auth.AuthToken; +import cc.fascinated.model.token.scoresaber.ScoreSaberAccountToken; +import cc.fascinated.model.token.steam.SteamAuthenticateUserTicketToken; import cc.fascinated.model.user.ScoreSaberAccount; import cc.fascinated.model.user.User; import cc.fascinated.model.user.history.HistoryPoint; -import cc.fascinated.repository.UserRepository; +import cc.fascinated.repository.mongo.UserRepository; +import cc.fascinated.repository.redis.AuthTokenRepository; import lombok.NonNull; import lombok.extern.log4j.Log4j2; import net.jodah.expiringmap.ExpirationPolicy; @@ -34,12 +38,24 @@ public class UserService { @NonNull private final UserRepository userRepository; + /** + * The auth token repository to use + */ + @NonNull + private final AuthTokenRepository authTokenRepository; + /** * The ScoreSaber service to use */ @NonNull private final ScoreSaberService scoreSaberService; + /** + * The Steam service to use + */ + @NonNull + private final SteamService steamService; + /** * The user cache to use */ @@ -50,9 +66,12 @@ public class UserService { .build(); @Autowired - public UserService(@NonNull UserRepository userRepository, @NonNull ScoreSaberService scoreSaberService) { + public UserService(@NonNull UserRepository userRepository, @NonNull AuthTokenRepository authTokenRepository, + @NonNull ScoreSaberService scoreSaberService, @NonNull SteamService steamService) { this.userRepository = userRepository; + this.authTokenRepository = authTokenRepository; this.scoreSaberService = scoreSaberService; + this.steamService = steamService; } /** @@ -110,6 +129,36 @@ public class UserService { return user; } + /** + * Creates a new auth token using a steam ticket + * + * @param ticket the ticket to get the auth token from + * @return the auth token + * @throws BadRequestException if the ticket is invalid + */ + public AuthToken getAuthToken(String ticket) { + SteamAuthenticateUserTicketToken steamUser = this.steamService.getSteamUserFromTicket(ticket); + assert steamUser != null; + User user = this.getUser(steamUser.getResponse().getParams().getSteamId()); + if (user == null) { + throw new BadRequestException("Failed to get user from steam id"); + } + return this.authTokenRepository.save(new AuthToken( + StringUtils.randomString(32), + user.getId() + )); + } + + /** + * Validates an auth token + * + * @param authToken the auth token to validate + * @return true if the auth token is valid, false otherwise + */ + public boolean isValidAuthToken(String authToken) { + return this.authTokenRepository.existsById(authToken); + } + /** * Saves a user to the database * diff --git a/API/src/main/java/cc/fascinated/websocket/impl/ScoreSaberWebsocket.java b/API/src/main/java/cc/fascinated/websocket/impl/ScoreSaberWebsocket.java index f23697e..464a43f 100644 --- a/API/src/main/java/cc/fascinated/websocket/impl/ScoreSaberWebsocket.java +++ b/API/src/main/java/cc/fascinated/websocket/impl/ScoreSaberWebsocket.java @@ -1,8 +1,8 @@ package cc.fascinated.websocket.impl; -import cc.fascinated.model.token.ScoreSaberPlayerScoreToken; -import cc.fascinated.model.token.ScoreSaberScoreToken; -import cc.fascinated.model.token.ScoreSaberWebsocketDataToken; +import cc.fascinated.model.token.scoresaber.ScoreSaberPlayerScoreToken; +import cc.fascinated.model.token.scoresaber.ScoreSaberScoreToken; +import cc.fascinated.model.token.scoresaber.ScoreSaberWebsocketDataToken; import cc.fascinated.services.ScoreService; import cc.fascinated.services.UserService; import cc.fascinated.websocket.Websocket; diff --git a/API/src/main/resources/application.yml b/API/src/main/resources/application.yml index 9e9b47d..0976303 100644 --- a/API/src/main/resources/application.yml +++ b/API/src/main/resources/application.yml @@ -3,36 +3,31 @@ server: address: 0.0.0.0 port: 7500 +# ScoreTracker Configuration +scoretracker: + steam: + api-key: "xxx" + # Spring Configuration spring: data: + # Redis Configuration + redis: + host: localhost + port: 6379 + database: 0 + auth: "" + # MongoDB Configuration mongodb: uri: "mongodb://bs-tracker:p4$$w0rd@localhost:27017" database: "bs-tracker" auto-index-creation: true # Automatically create collection indexes - datasource: - url: jdbc:postgresql://localhost:5432/ - username: - password: - jpa: - hibernate: - ddl-auto: - properties: - hibernate: - dialect: org.hibernate.dialect.PostgreSQLDialect - # Don't serialize null values by default with Jackson jackson: default-property-inclusion: non_null -# QuestDB Configuration -questdb: - host: localhost:9000 - username: admin - password: quest - # DO NOT TOUCH BELOW management: # Disable all actuator endpoints diff --git a/Mod/API/Authentication.cs b/Mod/API/Authentication.cs new file mode 100644 index 0000000..c1460f3 --- /dev/null +++ b/Mod/API/Authentication.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace ScoreTracker.API +{ + internal class Authentication + { + private static bool _signedIn = false; + private static string _authToken; + + /// + /// Are we signed in? + /// + public static bool IsSignedIn() + { + return _signedIn; + } + + /// + /// Get the steam ticket and user info + /// + /// the steam ticket + private static async Task GetSteamTicket() + { + Plugin.Log.Info("Getting steam ticket..."); + return (await new SteamPlatformUserModel().GetUserAuthToken()).token; + } + + /// + /// Login the user + /// + /// callback for successful login, returns the token + /// callback for failed login + /// an IEnumerator + public static async Task LoginUser(Action onSuccess, Action onFail) + { + if (_signedIn && !string.IsNullOrEmpty(_authToken)) + { + onSuccess(_authToken); + return; + } + + var ticketTask = GetSteamTicket(); + await Task.Run(() => ticketTask.Wait()); + + var ticket = ticketTask.Result; + if (string.IsNullOrEmpty(ticket)) + { + Plugin.Log.Error("Login failed :( no steam auth token"); + onFail("No Steam Auth Token"); + return; + } + + Plugin.Log.Info("Logging in..."); + var request = await Request.PostJsonAsync($"{Consts.ApiUrl}/auth/login", new Dictionary { + { "ticket", ticket } + }, false); + if (request.IsSuccessStatusCode) + { + var authToken = request.Headers.GetValues("Authorization").First(); + Plugin.Log.Info($"Login successful! auth token: {authToken}"); + + onSuccess(authToken); + _signedIn = true; + _authToken = authToken; + } + else + { + Plugin.Log.Error($"Login failed! body: {request.StatusCode}"); + onFail($"Login failed: {request.StatusCode}"); + + _signedIn = false; + _authToken = null; + } + } + + + /// + /// Validates the auth token and logs out if it's invalid + /// + /// whether the token is valid + public static async Task ValidateAuthToken() + { + if (!_signedIn || string.IsNullOrEmpty(_authToken)) // If we're not signed in, return false + { + return false; + } + + var request = await Request.PostJsonAsync($"{Consts.ApiUrl}/auth/validate", new Dictionary { + { "token", _authToken } + }, false); + + if (request.IsSuccessStatusCode) + { + return true; + } + else + { + _signedIn = false; + _authToken = null; + return false; + } + } + } +} diff --git a/Mod/API/Request.cs b/Mod/API/Request.cs new file mode 100644 index 0000000..c1844fd --- /dev/null +++ b/Mod/API/Request.cs @@ -0,0 +1,82 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; + +namespace ScoreTracker.API +{ + internal class Request + { + private static readonly HttpClient client = new HttpClient(); + + private class AuthHelper + { + public bool IsLoggedIn; + public string FailReason = ""; + + public async Task EnsureLoggedIn() + { + if (Authentication.IsSignedIn() && await Authentication.ValidateAuthToken()) + { + return; // Already logged in with a valid token + } + + await Authentication.LoginUser( + token => { + IsLoggedIn = true; + PersistHeaders(new Dictionary + { + { "Authorization", $"Bearer {token}" } + }); + }, + reason => + { + FailReason = reason; // Store the reason for failure + client.DefaultRequestHeaders.Clear(); // Clear headers + } + ); + } + } + + /// + /// Persist the given headers for all future requests + /// + /// the headers to persist + public static void PersistHeaders(Dictionary headers) + { + client.DefaultRequestHeaders.Clear(); // Clear existing headers + foreach (var header in headers) + { + client.DefaultRequestHeaders.Add(header.Key, header.Value); + } + } + + /// + /// Create a POST request to the given URL with the given data + /// + /// the url to post to + /// the data to post + /// whether to check for authentication + /// the task + public static async Task PostJsonAsync(string url, Dictionary json, bool checkAuth = true) + { + if (checkAuth) + { + var authHelper = new AuthHelper(); + await authHelper.EnsureLoggedIn(); + if (!authHelper.IsLoggedIn) + { + throw new Exception($"Failed to log in: {authHelper.FailReason}"); + } + } + var jsonString = JsonConvert.SerializeObject(json, Formatting.None); + var content = new StringContent(jsonString, Encoding.UTF8, "application/json"); + + // Send the POST request + var response = await client.PostAsync(url, content); + return response; + } + } +} diff --git a/Mod/Common/Logger.cs b/Mod/Common/Logger.cs deleted file mode 100644 index 44fb531..0000000 --- a/Mod/Common/Logger.cs +++ /dev/null @@ -1,9 +0,0 @@ -using IPALogger = IPA.Logging.Logger; - -namespace ScoreTracker.Common -{ - internal class Logger - { - public static IPALogger Log { get; set; } - } -} diff --git a/Mod/Consts.cs b/Mod/Consts.cs new file mode 100644 index 0000000..73866e4 --- /dev/null +++ b/Mod/Consts.cs @@ -0,0 +1,7 @@ +namespace ScoreTracker +{ + internal class Consts + { + public static string ApiUrl = "http://localhost:7500"; + } +} diff --git a/Mod/Directory.Build.props b/Mod/Directory.Build.props index ce46b94..d5d87b2 100644 --- a/Mod/Directory.Build.props +++ b/Mod/Directory.Build.props @@ -4,5 +4,6 @@ True BSIPA + 8.0 \ No newline at end of file diff --git a/Mod/Installers/AppInstaller.cs b/Mod/Installers/AppInstaller.cs new file mode 100644 index 0000000..47e20ad --- /dev/null +++ b/Mod/Installers/AppInstaller.cs @@ -0,0 +1,13 @@ +using Zenject; + +namespace ScoreTracker.Core +{ + internal class AppInstaller : Installer + { + + public override void InstallBindings() + { + Plugin.Container = Container; + } + } +} \ No newline at end of file diff --git a/Mod/Plugin.cs b/Mod/Plugin.cs index 53a877d..9646091 100644 --- a/Mod/Plugin.cs +++ b/Mod/Plugin.cs @@ -1,6 +1,12 @@ using IPA; -using ScoreTracker.Common; using IPALogger = IPA.Logging.Logger; +using SiraUtil.Zenject; +using IPA.Loader; +using Zenject; +using ScoreTracker.Core; +using System.Threading.Tasks; +using ScoreTracker.API; +using System.Collections.Generic; namespace ScoreTracker { @@ -8,24 +14,36 @@ namespace ScoreTracker public class Plugin { internal static Plugin Instance { get; private set; } + internal static IPALogger Log { get; private set; } + internal static DiContainer Container; // Workaround to access the Zenject container in SceneLoaded [Init] - public Plugin(IPALogger logger) + public Plugin(IPALogger logger, PluginMetadata metadata, Zenjector zenjector) { Instance = this; - Logger.Log = logger; // Setup the logger + Log = logger; // Setup the logger + + // Install our Zenject bindings + zenjector.Install(Location.App); } [OnStart] public void OnApplicationStart() { - Logger.Log.Info("OnApplicationStart"); + Log.Info("OnApplicationStart"); + + Task.Run(async () => + { + await Request.PostJsonAsync("http://localhost:7500/boobies", new Dictionary { + { "boobies", "yes" } + }); + }); } [OnExit] public void OnApplicationQuit() { - Logger.Log.Info("OnApplicationQuit"); + Log.Info("OnApplicationQuit"); } } diff --git a/Mod/ScoreTracker.csproj b/Mod/ScoreTracker.csproj index 8ab4338..d937f02 100644 --- a/Mod/ScoreTracker.csproj +++ b/Mod/ScoreTracker.csproj @@ -40,6 +40,17 @@ True + + + False + $(BeatSaberDir)\Plugins\BS_Utils.dll + False + + + False + $(BeatSaberDir)\Plugins\SiraUtil.dll + False + @@ -86,13 +97,26 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIModule.dll False + + False + $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UnityWebRequestModule.dll + False + $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.VRModule.dll False + + False + $(BeatSaberDir)\Beat Saber_Data\Managed\Zenject.dll + False + - + + + + @@ -109,6 +133,9 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all + + 13.0.3 + diff --git a/Mod/ScoreTracker.csproj.DotSettings b/Mod/ScoreTracker.csproj.DotSettings new file mode 100644 index 0000000..b9fd6ee --- /dev/null +++ b/Mod/ScoreTracker.csproj.DotSettings @@ -0,0 +1,2 @@ + + CSharp80 \ No newline at end of file diff --git a/Mod/manifest.json b/Mod/manifest.json index 09ce89d..d1b9b56 100644 --- a/Mod/manifest.json +++ b/Mod/manifest.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/nike4613/ModSaber-MetadataFileSchema/master/Schema.json", - "id": "score-tracker-mod", + "id": "ScoreTracker", "name": "ScoreTracker", "author": "fascinated7", "version": "0.0.1", @@ -9,7 +9,7 @@ "dependsOn": { "BSIPA": "^4.2.2", "BS Utils": "^1.12.0", - "BeatSaberMarkupLanguage": "^1.6.3" + "SiraUtil": "^3.1.0" }, - "loadAfter": [ "BS Utils", "BeatSaberMarkupLanguage" ] + "loadAfter": [ "BS Utils", "SiraUtil" ] } \ No newline at end of file