This commit is contained in:
parent
1ec8248c6f
commit
c04a51de35
14
API/pom.xml
14
API/pom.xml
@ -70,6 +70,20 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-data-mongodb</artifactId>
|
<artifactId>spring-boot-starter-data-mongodb</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>io.lettuce</groupId>
|
||||||
|
<artifactId>lettuce-core</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>redis.clients</groupId>
|
||||||
|
<artifactId>jedis</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- Dependencies -->
|
<!-- Dependencies -->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
@ -6,6 +6,8 @@ import lombok.extern.log4j.Log4j2;
|
|||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;
|
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 org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -17,6 +19,8 @@ import java.util.Objects;
|
|||||||
* @author Fascinated (fascinated7)
|
* @author Fascinated (fascinated7)
|
||||||
*/
|
*/
|
||||||
@EnableScheduling
|
@EnableScheduling
|
||||||
|
@EnableMongoRepositories(basePackages = "cc.fascinated.repository.mongo")
|
||||||
|
@EnableRedisRepositories(basePackages = "cc.fascinated.repository.redis")
|
||||||
@SpringBootApplication(exclude = UserDetailsServiceAutoConfiguration.class)
|
@SpringBootApplication(exclude = UserDetailsServiceAutoConfiguration.class)
|
||||||
@Log4j2(topic = "Score Tracker")
|
@Log4j2(topic = "Score Tracker")
|
||||||
public class Main {
|
public class Main {
|
||||||
|
@ -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)
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
@ -3,9 +3,11 @@ package cc.fascinated.common;
|
|||||||
import kong.unirest.core.Headers;
|
import kong.unirest.core.Headers;
|
||||||
import kong.unirest.core.HttpResponse;
|
import kong.unirest.core.HttpResponse;
|
||||||
import kong.unirest.core.Unirest;
|
import kong.unirest.core.Unirest;
|
||||||
|
import kong.unirest.core.UnirestParsingException;
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Fascinated (fascinated7)
|
* @author Fascinated (fascinated7)
|
||||||
@ -49,6 +51,7 @@ public class Request {
|
|||||||
}
|
}
|
||||||
response = Unirest.get(url).asObject(clazz);
|
response = Unirest.get(url).asObject(clazz);
|
||||||
}
|
}
|
||||||
|
response.getParsingError().ifPresent(e -> log.error("Failed to parse response", e));
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,33 @@
|
|||||||
package cc.fascinated.common;/**
|
package cc.fascinated.common;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
|
||||||
|
/**
|
||||||
* @author Fascinated (fascinated7)
|
* @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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,73 @@
|
|||||||
package cc.fascinated.config;/**
|
package cc.fascinated.config;
|
||||||
* @author Fascinated (fascinated7)
|
|
||||||
*/public class RedisConfig {
|
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<String, Object> redisTemplate() {
|
||||||
|
RedisTemplate<String, Object> 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);
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
* @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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,4 +1,50 @@
|
|||||||
package cc.fascinated.log;/**
|
package cc.fascinated.log;
|
||||||
* @author Fascinated (fascinated7)
|
|
||||||
*/public class TransactionLogger {
|
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<Object> {
|
||||||
|
@Override
|
||||||
|
public boolean supports(@NonNull MethodParameter returnType, @NonNull Class<? extends HttpMessageConverter<?>> converterType) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object beforeBodyWrite(Object body, @NonNull MethodParameter returnType, @NonNull MediaType selectedContentType,
|
||||||
|
@NonNull Class<? extends HttpMessageConverter<?>> 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;
|
||||||
|
}
|
||||||
|
}
|
@ -4,33 +4,26 @@ import cc.fascinated.common.StringUtils;
|
|||||||
import cc.fascinated.model.token.steam.SteamAuthenticateUserTicketToken;
|
import cc.fascinated.model.token.steam.SteamAuthenticateUserTicketToken;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import org.springframework.data.annotation.Id;
|
||||||
|
import org.springframework.data.redis.core.RedisHash;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Fascinated (fascinated7)
|
* @author Fascinated (fascinated7)
|
||||||
*/
|
*/
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Getter
|
@Getter
|
||||||
public class SteamAuthToken {
|
@RedisHash(value = "AuthToken", timeToLive = 60 * 60 * 6) // 6 hours
|
||||||
/**
|
public class AuthToken {
|
||||||
* The steam id of the user.
|
|
||||||
*/
|
|
||||||
private final String steamId;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The auth token of the user.
|
* The auth token of the user.
|
||||||
*/
|
*/
|
||||||
|
@Id
|
||||||
private final String authToken;
|
private final String authToken;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the SteamProfile from an auth token.
|
* The id of the user.
|
||||||
*
|
|
||||||
* @param token The auth token.
|
|
||||||
* @return The SteamProfile.
|
|
||||||
*/
|
*/
|
||||||
public static SteamAuthToken getFromAuthToken(SteamAuthenticateUserTicketToken token) {
|
private final UUID userId;
|
||||||
return new SteamAuthToken(
|
|
||||||
token.getResponse().getParams().getSteamId(),
|
|
||||||
StringUtils.randomString(32)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,14 @@
|
|||||||
package cc.fascinated.model.auth;/**
|
package cc.fascinated.model.auth;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
* @author Fascinated (fascinated7)
|
* @author Fascinated (fascinated7)
|
||||||
*/public class LoginRequest {
|
*/
|
||||||
|
@Getter
|
||||||
|
public class LoginRequest {
|
||||||
|
/**
|
||||||
|
* The ticket to authenticate the user.
|
||||||
|
*/
|
||||||
|
private String ticket;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package cc.fascinated.model.leaderboard;
|
package cc.fascinated.model.leaderboard;
|
||||||
|
|
||||||
import cc.fascinated.common.ScoreSaberUtils;
|
import cc.fascinated.common.ScoreSaberUtils;
|
||||||
import cc.fascinated.model.token.ScoreSaberLeaderboardToken;
|
import cc.fascinated.model.token.scoresaber.ScoreSaberLeaderboardToken;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package cc.fascinated.model.token;
|
package cc.fascinated.model.token.scoresaber;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package cc.fascinated.model.token;
|
package cc.fascinated.model.token.scoresaber;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package cc.fascinated.model.token;
|
package cc.fascinated.model.token.scoresaber;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package cc.fascinated.model.token;
|
package cc.fascinated.model.token.scoresaber;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package cc.fascinated.model.token;
|
package cc.fascinated.model.token.scoresaber;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package cc.fascinated.model.token;
|
package cc.fascinated.model.token.scoresaber;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package cc.fascinated.model.token;
|
package cc.fascinated.model.token.scoresaber;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package cc.fascinated.model.token;
|
package cc.fascinated.model.token.scoresaber;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package cc.fascinated.model.token.steam.token;
|
package cc.fascinated.model.token.steam;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package cc.fascinated.model.user;
|
package cc.fascinated.model.user;
|
||||||
|
|
||||||
import cc.fascinated.common.DateUtils;
|
import cc.fascinated.common.DateUtils;
|
||||||
import cc.fascinated.model.token.ScoreSaberAccountToken;
|
import cc.fascinated.model.token.scoresaber.ScoreSaberAccountToken;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@ -3,9 +3,9 @@ package cc.fascinated.platform.impl;
|
|||||||
import cc.fascinated.common.DateUtils;
|
import cc.fascinated.common.DateUtils;
|
||||||
import cc.fascinated.common.MathUtils;
|
import cc.fascinated.common.MathUtils;
|
||||||
import cc.fascinated.model.score.Score;
|
import cc.fascinated.model.score.Score;
|
||||||
import cc.fascinated.model.token.ScoreSaberAccountToken;
|
import cc.fascinated.model.token.scoresaber.ScoreSaberAccountToken;
|
||||||
import cc.fascinated.model.token.ScoreSaberLeaderboardPageToken;
|
import cc.fascinated.model.token.scoresaber.ScoreSaberLeaderboardPageToken;
|
||||||
import cc.fascinated.model.token.ScoreSaberLeaderboardToken;
|
import cc.fascinated.model.token.scoresaber.ScoreSaberLeaderboardToken;
|
||||||
import cc.fascinated.model.user.User;
|
import cc.fascinated.model.user.User;
|
||||||
import cc.fascinated.model.user.history.HistoryPoint;
|
import cc.fascinated.model.user.history.HistoryPoint;
|
||||||
import cc.fascinated.platform.CurvePoint;
|
import cc.fascinated.platform.CurvePoint;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package cc.fascinated.repository;
|
package cc.fascinated.repository.mongo;
|
||||||
|
|
||||||
import cc.fascinated.model.Counter;
|
import cc.fascinated.model.Counter;
|
||||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package cc.fascinated.repository;
|
package cc.fascinated.repository.mongo;
|
||||||
|
|
||||||
import cc.fascinated.model.score.Score;
|
import cc.fascinated.model.score.Score;
|
||||||
import cc.fascinated.platform.Platform;
|
import cc.fascinated.platform.Platform;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package cc.fascinated.repository;
|
package cc.fascinated.repository.mongo;
|
||||||
|
|
||||||
import cc.fascinated.model.token.scoresaber.ScoreSaberLeaderboardToken;
|
import cc.fascinated.model.token.scoresaber.ScoreSaberLeaderboardToken;
|
||||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package cc.fascinated.repository;
|
package cc.fascinated.repository.mongo;
|
||||||
|
|
||||||
import cc.fascinated.model.user.User;
|
import cc.fascinated.model.user.User;
|
||||||
import org.springframework.data.mongodb.repository.MongoRepository;
|
import org.springframework.data.mongodb.repository.MongoRepository;
|
||||||
|
@ -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)
|
* @author Fascinated (fascinated7)
|
||||||
*/public class AuthTokenRepository {
|
*/
|
||||||
}
|
public interface AuthTokenRepository extends CrudRepository<AuthToken, String> {}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package cc.fascinated.services;
|
package cc.fascinated.services;
|
||||||
|
|
||||||
import cc.fascinated.model.Counter;
|
import cc.fascinated.model.Counter;
|
||||||
import cc.fascinated.repository.CounterRepository;
|
import cc.fascinated.repository.mongo.CounterRepository;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
@ -2,11 +2,11 @@ package cc.fascinated.services;
|
|||||||
|
|
||||||
import cc.fascinated.common.Request;
|
import cc.fascinated.common.Request;
|
||||||
import cc.fascinated.exception.impl.BadRequestException;
|
import cc.fascinated.exception.impl.BadRequestException;
|
||||||
import cc.fascinated.model.token.ScoreSaberAccountToken;
|
import cc.fascinated.model.token.scoresaber.ScoreSaberAccountToken;
|
||||||
import cc.fascinated.model.token.ScoreSaberLeaderboardPageToken;
|
import cc.fascinated.model.token.scoresaber.ScoreSaberLeaderboardPageToken;
|
||||||
import cc.fascinated.model.token.ScoreSaberLeaderboardToken;
|
import cc.fascinated.model.token.scoresaber.ScoreSaberLeaderboardToken;
|
||||||
import cc.fascinated.model.user.User;
|
import cc.fascinated.model.user.User;
|
||||||
import cc.fascinated.repository.ScoreSaberLeaderboardRepository;
|
import cc.fascinated.repository.mongo.ScoreSaberLeaderboardRepository;
|
||||||
import kong.unirest.core.HttpResponse;
|
import kong.unirest.core.HttpResponse;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
|
@ -10,16 +10,16 @@ import cc.fascinated.model.score.Score;
|
|||||||
import cc.fascinated.model.score.TotalScoresResponse;
|
import cc.fascinated.model.score.TotalScoresResponse;
|
||||||
import cc.fascinated.model.score.impl.scoresaber.ScoreSaberScore;
|
import cc.fascinated.model.score.impl.scoresaber.ScoreSaberScore;
|
||||||
import cc.fascinated.model.score.impl.scoresaber.ScoreSaberScoreResponse;
|
import cc.fascinated.model.score.impl.scoresaber.ScoreSaberScoreResponse;
|
||||||
import cc.fascinated.model.token.ScoreSaberLeaderboardToken;
|
import cc.fascinated.model.token.scoresaber.ScoreSaberLeaderboardToken;
|
||||||
import cc.fascinated.model.token.ScoreSaberPlayerScoreToken;
|
import cc.fascinated.model.token.scoresaber.ScoreSaberPlayerScoreToken;
|
||||||
import cc.fascinated.model.token.ScoreSaberScoreToken;
|
import cc.fascinated.model.token.scoresaber.ScoreSaberScoreToken;
|
||||||
import cc.fascinated.model.user.User;
|
import cc.fascinated.model.user.User;
|
||||||
import cc.fascinated.model.user.UserDTO;
|
import cc.fascinated.model.user.UserDTO;
|
||||||
import cc.fascinated.model.user.history.HistoryPoint;
|
import cc.fascinated.model.user.history.HistoryPoint;
|
||||||
import cc.fascinated.model.user.hmd.DeviceController;
|
import cc.fascinated.model.user.hmd.DeviceController;
|
||||||
import cc.fascinated.model.user.hmd.DeviceHeadset;
|
import cc.fascinated.model.user.hmd.DeviceHeadset;
|
||||||
import cc.fascinated.platform.Platform;
|
import cc.fascinated.platform.Platform;
|
||||||
import cc.fascinated.repository.ScoreRepository;
|
import cc.fascinated.repository.mongo.ScoreRepository;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
@ -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)
|
* @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<SteamAuthenticateUserTicketToken> 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
package cc.fascinated.services;
|
package cc.fascinated.services;
|
||||||
|
|
||||||
|
import cc.fascinated.common.StringUtils;
|
||||||
import cc.fascinated.common.TimeUtils;
|
import cc.fascinated.common.TimeUtils;
|
||||||
import cc.fascinated.exception.impl.BadRequestException;
|
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.ScoreSaberAccount;
|
||||||
import cc.fascinated.model.user.User;
|
import cc.fascinated.model.user.User;
|
||||||
import cc.fascinated.model.user.history.HistoryPoint;
|
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.NonNull;
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
import net.jodah.expiringmap.ExpirationPolicy;
|
import net.jodah.expiringmap.ExpirationPolicy;
|
||||||
@ -34,12 +38,24 @@ public class UserService {
|
|||||||
@NonNull
|
@NonNull
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The auth token repository to use
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
private final AuthTokenRepository authTokenRepository;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The ScoreSaber service to use
|
* The ScoreSaber service to use
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
private final ScoreSaberService scoreSaberService;
|
private final ScoreSaberService scoreSaberService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Steam service to use
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
private final SteamService steamService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The user cache to use
|
* The user cache to use
|
||||||
*/
|
*/
|
||||||
@ -50,9 +66,12 @@ public class UserService {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
@Autowired
|
@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.userRepository = userRepository;
|
||||||
|
this.authTokenRepository = authTokenRepository;
|
||||||
this.scoreSaberService = scoreSaberService;
|
this.scoreSaberService = scoreSaberService;
|
||||||
|
this.steamService = steamService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -110,6 +129,36 @@ public class UserService {
|
|||||||
return user;
|
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
|
* Saves a user to the database
|
||||||
*
|
*
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
package cc.fascinated.websocket.impl;
|
package cc.fascinated.websocket.impl;
|
||||||
|
|
||||||
import cc.fascinated.model.token.ScoreSaberPlayerScoreToken;
|
import cc.fascinated.model.token.scoresaber.ScoreSaberPlayerScoreToken;
|
||||||
import cc.fascinated.model.token.ScoreSaberScoreToken;
|
import cc.fascinated.model.token.scoresaber.ScoreSaberScoreToken;
|
||||||
import cc.fascinated.model.token.ScoreSaberWebsocketDataToken;
|
import cc.fascinated.model.token.scoresaber.ScoreSaberWebsocketDataToken;
|
||||||
import cc.fascinated.services.ScoreService;
|
import cc.fascinated.services.ScoreService;
|
||||||
import cc.fascinated.services.UserService;
|
import cc.fascinated.services.UserService;
|
||||||
import cc.fascinated.websocket.Websocket;
|
import cc.fascinated.websocket.Websocket;
|
||||||
|
@ -3,36 +3,31 @@ server:
|
|||||||
address: 0.0.0.0
|
address: 0.0.0.0
|
||||||
port: 7500
|
port: 7500
|
||||||
|
|
||||||
|
# ScoreTracker Configuration
|
||||||
|
scoretracker:
|
||||||
|
steam:
|
||||||
|
api-key: "xxx"
|
||||||
|
|
||||||
# Spring Configuration
|
# Spring Configuration
|
||||||
spring:
|
spring:
|
||||||
data:
|
data:
|
||||||
|
# Redis Configuration
|
||||||
|
redis:
|
||||||
|
host: localhost
|
||||||
|
port: 6379
|
||||||
|
database: 0
|
||||||
|
auth: ""
|
||||||
|
|
||||||
# MongoDB Configuration
|
# MongoDB Configuration
|
||||||
mongodb:
|
mongodb:
|
||||||
uri: "mongodb://bs-tracker:p4$$w0rd@localhost:27017"
|
uri: "mongodb://bs-tracker:p4$$w0rd@localhost:27017"
|
||||||
database: "bs-tracker"
|
database: "bs-tracker"
|
||||||
auto-index-creation: true # Automatically create collection indexes
|
auto-index-creation: true # Automatically create collection indexes
|
||||||
|
|
||||||
datasource:
|
|
||||||
url: jdbc:postgresql://localhost:5432/<YOUR_DATABASE_NAME>
|
|
||||||
username: <YOUR_USERNAME>
|
|
||||||
password: <YOUR_PASSWORD>
|
|
||||||
jpa:
|
|
||||||
hibernate:
|
|
||||||
ddl-auto: <create | create-drop | update | validate | none>
|
|
||||||
properties:
|
|
||||||
hibernate:
|
|
||||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
|
||||||
|
|
||||||
# Don't serialize null values by default with Jackson
|
# Don't serialize null values by default with Jackson
|
||||||
jackson:
|
jackson:
|
||||||
default-property-inclusion: non_null
|
default-property-inclusion: non_null
|
||||||
|
|
||||||
# QuestDB Configuration
|
|
||||||
questdb:
|
|
||||||
host: localhost:9000
|
|
||||||
username: admin
|
|
||||||
password: quest
|
|
||||||
|
|
||||||
# DO NOT TOUCH BELOW
|
# DO NOT TOUCH BELOW
|
||||||
management:
|
management:
|
||||||
# Disable all actuator endpoints
|
# Disable all actuator endpoints
|
||||||
|
107
Mod/API/Authentication.cs
Normal file
107
Mod/API/Authentication.cs
Normal file
@ -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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Are we signed in?
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsSignedIn()
|
||||||
|
{
|
||||||
|
return _signedIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the steam ticket and user info
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>the steam ticket</returns>
|
||||||
|
private static async Task<string> GetSteamTicket()
|
||||||
|
{
|
||||||
|
Plugin.Log.Info("Getting steam ticket...");
|
||||||
|
return (await new SteamPlatformUserModel().GetUserAuthToken()).token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Login the user
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="onSuccess">callback for successful login, returns the token</param>
|
||||||
|
/// <param name="onFail">callback for failed login</param>
|
||||||
|
/// <returns>an IEnumerator</returns>
|
||||||
|
public static async Task LoginUser(Action<string> onSuccess, Action<string> 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<object, object> {
|
||||||
|
{ "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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates the auth token and logs out if it's invalid
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>whether the token is valid</returns>
|
||||||
|
public static async Task<bool> 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<object, object> {
|
||||||
|
{ "token", _authToken }
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
if (request.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_signedIn = false;
|
||||||
|
_authToken = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
82
Mod/API/Request.cs
Normal file
82
Mod/API/Request.cs
Normal file
@ -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<string, string>
|
||||||
|
{
|
||||||
|
{ "Authorization", $"Bearer {token}" }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
reason =>
|
||||||
|
{
|
||||||
|
FailReason = reason; // Store the reason for failure
|
||||||
|
client.DefaultRequestHeaders.Clear(); // Clear headers
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Persist the given headers for all future requests
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="headers">the headers to persist</param>
|
||||||
|
public static void PersistHeaders(Dictionary<string, string> headers)
|
||||||
|
{
|
||||||
|
client.DefaultRequestHeaders.Clear(); // Clear existing headers
|
||||||
|
foreach (var header in headers)
|
||||||
|
{
|
||||||
|
client.DefaultRequestHeaders.Add(header.Key, header.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a POST request to the given URL with the given data
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">the url to post to</param>
|
||||||
|
/// <param name="data">the data to post</param>
|
||||||
|
/// <param name="checkAuth">whether to check for authentication</param>
|
||||||
|
/// <returns>the task</returns>
|
||||||
|
public static async Task<HttpResponseMessage> PostJsonAsync(string url, Dictionary<object, object> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +0,0 @@
|
|||||||
using IPALogger = IPA.Logging.Logger;
|
|
||||||
|
|
||||||
namespace ScoreTracker.Common
|
|
||||||
{
|
|
||||||
internal class Logger
|
|
||||||
{
|
|
||||||
public static IPALogger Log { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
7
Mod/Consts.cs
Normal file
7
Mod/Consts.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace ScoreTracker
|
||||||
|
{
|
||||||
|
internal class Consts
|
||||||
|
{
|
||||||
|
public static string ApiUrl = "http://localhost:7500";
|
||||||
|
}
|
||||||
|
}
|
@ -4,5 +4,6 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<ImportBSMTTargets>True</ImportBSMTTargets>
|
<ImportBSMTTargets>True</ImportBSMTTargets>
|
||||||
<BSMTProjectType>BSIPA</BSMTProjectType>
|
<BSMTProjectType>BSIPA</BSMTProjectType>
|
||||||
|
<LangVersion>8.0</LangVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
13
Mod/Installers/AppInstaller.cs
Normal file
13
Mod/Installers/AppInstaller.cs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
using Zenject;
|
||||||
|
|
||||||
|
namespace ScoreTracker.Core
|
||||||
|
{
|
||||||
|
internal class AppInstaller : Installer
|
||||||
|
{
|
||||||
|
|
||||||
|
public override void InstallBindings()
|
||||||
|
{
|
||||||
|
Plugin.Container = Container;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,12 @@
|
|||||||
using IPA;
|
using IPA;
|
||||||
using ScoreTracker.Common;
|
|
||||||
using IPALogger = IPA.Logging.Logger;
|
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
|
namespace ScoreTracker
|
||||||
{
|
{
|
||||||
@ -8,24 +14,36 @@ namespace ScoreTracker
|
|||||||
public class Plugin
|
public class Plugin
|
||||||
{
|
{
|
||||||
internal static Plugin Instance { get; private set; }
|
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]
|
[Init]
|
||||||
public Plugin(IPALogger logger)
|
public Plugin(IPALogger logger, PluginMetadata metadata, Zenjector zenjector)
|
||||||
{
|
{
|
||||||
Instance = this;
|
Instance = this;
|
||||||
Logger.Log = logger; // Setup the logger
|
Log = logger; // Setup the logger
|
||||||
|
|
||||||
|
// Install our Zenject bindings
|
||||||
|
zenjector.Install<AppInstaller>(Location.App);
|
||||||
}
|
}
|
||||||
|
|
||||||
[OnStart]
|
[OnStart]
|
||||||
public void OnApplicationStart()
|
public void OnApplicationStart()
|
||||||
{
|
{
|
||||||
Logger.Log.Info("OnApplicationStart");
|
Log.Info("OnApplicationStart");
|
||||||
|
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await Request.PostJsonAsync("http://localhost:7500/boobies", new Dictionary<object, object> {
|
||||||
|
{ "boobies", "yes" }
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[OnExit]
|
[OnExit]
|
||||||
public void OnApplicationQuit()
|
public void OnApplicationQuit()
|
||||||
{
|
{
|
||||||
Logger.Log.Info("OnApplicationQuit");
|
Log.Info("OnApplicationQuit");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -40,6 +40,17 @@
|
|||||||
<DisableZipRelease>True</DisableZipRelease>
|
<DisableZipRelease>True</DisableZipRelease>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Reference Include="BGNet, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" />
|
||||||
|
<Reference Include="BS_Utils, Version=1.12.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||||
|
<Private>False</Private>
|
||||||
|
<HintPath>$(BeatSaberDir)\Plugins\BS_Utils.dll</HintPath>
|
||||||
|
<SpecificVersion>False</SpecificVersion>
|
||||||
|
</Reference>
|
||||||
|
<Reference Include="SiraUtil, Version=3.1.2.0, Culture=neutral, processorArchitecture=MSIL">
|
||||||
|
<Private>False</Private>
|
||||||
|
<HintPath>$(BeatSaberDir)\Plugins\SiraUtil.dll</HintPath>
|
||||||
|
<SpecificVersion>False</SpecificVersion>
|
||||||
|
</Reference>
|
||||||
<Reference Include="System" />
|
<Reference Include="System" />
|
||||||
<Reference Include="System.Core" />
|
<Reference Include="System.Core" />
|
||||||
<Reference Include="System.Xml.Linq" />
|
<Reference Include="System.Xml.Linq" />
|
||||||
@ -86,13 +97,26 @@
|
|||||||
<HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIModule.dll</HintPath>
|
<HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIModule.dll</HintPath>
|
||||||
<Private>False</Private>
|
<Private>False</Private>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
<Reference Include="UnityEngine.UnityWebRequestModule, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||||
|
<Private>False</Private>
|
||||||
|
<HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UnityWebRequestModule.dll</HintPath>
|
||||||
|
<SpecificVersion>False</SpecificVersion>
|
||||||
|
</Reference>
|
||||||
<Reference Include="UnityEngine.VRModule">
|
<Reference Include="UnityEngine.VRModule">
|
||||||
<HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.VRModule.dll</HintPath>
|
<HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.VRModule.dll</HintPath>
|
||||||
<Private>False</Private>
|
<Private>False</Private>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
<Reference Include="Zenject, Version=0.0.0.0, Culture=neutral, processorArchitecture=MSIL">
|
||||||
|
<Private>False</Private>
|
||||||
|
<HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\Zenject.dll</HintPath>
|
||||||
|
<SpecificVersion>False</SpecificVersion>
|
||||||
|
</Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Common\Logger.cs" />
|
<Compile Include="API\Authentication.cs" />
|
||||||
|
<Compile Include="API\Request.cs" />
|
||||||
|
<Compile Include="Consts.cs" />
|
||||||
|
<Compile Include="Installers\AppInstaller.cs" />
|
||||||
<Compile Include="Plugin.cs" />
|
<Compile Include="Plugin.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@ -109,6 +133,9 @@
|
|||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="Newtonsoft.Json">
|
||||||
|
<Version>13.0.3</Version>
|
||||||
|
</PackageReference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup />
|
<ItemGroup />
|
||||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||||
|
2
Mod/ScoreTracker.csproj.DotSettings
Normal file
2
Mod/ScoreTracker.csproj.DotSettings
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:String x:Key="/Default/CodeInspection/CSharpLanguageProject/LanguageLevel/@EntryValue">CSharp80</s:String></wpf:ResourceDictionary>
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/nike4613/ModSaber-MetadataFileSchema/master/Schema.json",
|
"$schema": "https://raw.githubusercontent.com/nike4613/ModSaber-MetadataFileSchema/master/Schema.json",
|
||||||
"id": "score-tracker-mod",
|
"id": "ScoreTracker",
|
||||||
"name": "ScoreTracker",
|
"name": "ScoreTracker",
|
||||||
"author": "fascinated7",
|
"author": "fascinated7",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
@ -9,7 +9,7 @@
|
|||||||
"dependsOn": {
|
"dependsOn": {
|
||||||
"BSIPA": "^4.2.2",
|
"BSIPA": "^4.2.2",
|
||||||
"BS Utils": "^1.12.0",
|
"BS Utils": "^1.12.0",
|
||||||
"BeatSaberMarkupLanguage": "^1.6.3"
|
"SiraUtil": "^3.1.0"
|
||||||
},
|
},
|
||||||
"loadAfter": [ "BS Utils", "BeatSaberMarkupLanguage" ]
|
"loadAfter": [ "BS Utils", "SiraUtil" ]
|
||||||
}
|
}
|
Reference in New Issue
Block a user