diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e540f5b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +tmp/* \ No newline at end of file diff --git a/backend/.gitignore b/backend/.gitignore index 16c407e..99a4769 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -23,8 +23,5 @@ src/main/resources/application-local.properties # Server Keystore src/main/resources/achievements-ssl-key.p12 -# Api Keys -apikeys/ - # Program Data -images/ \ No newline at end of file +storage/ \ No newline at end of file diff --git a/backend/src/main/java/achievements/Application.java b/backend/src/main/java/achievements/Application.java index add0fab..81dd0b3 100644 --- a/backend/src/main/java/achievements/Application.java +++ b/backend/src/main/java/achievements/Application.java @@ -4,11 +4,15 @@ import achievements.misc.DbConnection; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.web.client.RestTemplate; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.time.Duration; + @SpringBootApplication(exclude = SecurityAutoConfiguration.class) @EnableScheduling public class Application { @@ -34,4 +38,9 @@ public class Application { } }; } + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } } \ No newline at end of file diff --git a/backend/src/main/java/achievements/apis/PlatformAPI.java b/backend/src/main/java/achievements/apis/PlatformAPI.java new file mode 100644 index 0000000..f241a60 --- /dev/null +++ b/backend/src/main/java/achievements/apis/PlatformAPI.java @@ -0,0 +1,17 @@ +package achievements.apis; + +import achievements.data.APIResponse; +import org.springframework.web.client.RestTemplate; + +public abstract class PlatformAPI { + + protected int id; + protected RestTemplate rest; + + protected PlatformAPI(int id, RestTemplate rest) { + this.id = id; + this.rest = rest; + } + + public abstract APIResponse get(String userId); +} diff --git a/backend/src/main/java/achievements/apis/SteamAPI.java b/backend/src/main/java/achievements/apis/SteamAPI.java new file mode 100644 index 0000000..d03b307 --- /dev/null +++ b/backend/src/main/java/achievements/apis/SteamAPI.java @@ -0,0 +1,102 @@ +package achievements.apis; + +import achievements.apis.steam.GetOwnedGameBody; +import achievements.apis.steam.GetPlayerAchievementsBody; +import achievements.apis.steam.GetSchemaForGameBody; +import achievements.data.APIResponse; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Properties; + +public class SteamAPI extends PlatformAPI { + + private String apiKey; + + public SteamAPI(int id, RestTemplate rest) { + super(id, rest); + try { + var file = new FileInputStream("storage/apis/" + id + ".properties"); + var properties = new Properties(); + properties.load(file); + + apiKey = properties.getProperty("api-key"); + file.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public APIResponse get(String userId) { + var headers = new HttpHeaders(); + headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON)); + var entity = new HttpEntity(headers); + var ownedGamesUrl = UriComponentsBuilder.fromHttpUrl("http://api.steampowered.com/IPlayerService/GetOwnedGames/v1/") + .queryParam("key", apiKey) + .queryParam("steamid", userId) + .queryParam("include_appinfo", true) + .queryParam("include_played_free_games", true) + .toUriString(); + + var gameSchemaBaseUrl = UriComponentsBuilder.fromHttpUrl("https://api.steampowered.com/ISteamUserStats/GetSchemaForGame/v2/") + .queryParam("key", apiKey); + var playerAchievementsBaseUrl = UriComponentsBuilder.fromHttpUrl("https://api.steampowered.com/ISteamUserStats/GetPlayerAchievements/v1/") + .queryParam("key", apiKey) + .queryParam("steamid", userId); + + var games = new ArrayList(); + var ownedResponse = rest.exchange(ownedGamesUrl, HttpMethod.GET, entity, GetOwnedGameBody.class).getBody(); + for (var game : ownedResponse.getResponse().getGames()) { + var newGame = new APIResponse.Game(); + newGame.setPlatformGameId(Integer.toString(game.getAppid())); + newGame.setName(game.getName()); + newGame.setThumbnail("https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/apps/" + game.getAppid() + "/" + game.getImg_logo_url() + ".jpg"); + newGame.setPlayed(game.getPlaytime_forever() > 0); + + var achievements = new HashMap(); + + var gameSchemaUrl = gameSchemaBaseUrl.cloneBuilder() + .queryParam("appid", game.getAppid()) + .toUriString(); + var playerAchievementsUrl = playerAchievementsBaseUrl.cloneBuilder() + .queryParam("appid", game.getAppid()) + .toUriString(); + + + var schemaResponse = rest.exchange(gameSchemaUrl, HttpMethod.GET, entity, GetSchemaForGameBody.class).getBody().getGame().getAvailableGameStats(); + if (schemaResponse != null && schemaResponse.getAchievements() != null) { + for (var schema : schemaResponse.getAchievements()) { + var achievement = new APIResponse.Game.Achievement(); + achievement.setName(schema.getDisplayName()); + achievement.setDescription(schema.getDescription()); + achievement.setStages(1); + achievement.setThumbnail(schema.getIcon()); + achievements.put(schema.getName(), achievement); + } + + var playerAchievementsResponse = rest.exchange(playerAchievementsUrl, HttpMethod.GET, entity, GetPlayerAchievementsBody.class).getBody().getPlayerstats().getAchievements(); + for (var achievement : playerAchievementsResponse) { + achievements.get(achievement.getApiname()).setProgress(achievement.getAchieved()); + } + + newGame.setAchievements(new ArrayList<>(achievements.values())); + if (newGame.getAchievements().size() > 0) { + games.add(newGame); + } + } + } + var response = new APIResponse(); + response.setGames(games); + return response; + } +} diff --git a/backend/src/main/java/achievements/apis/steam/GetOwnedGameBody.java b/backend/src/main/java/achievements/apis/steam/GetOwnedGameBody.java new file mode 100644 index 0000000..e34f7ed --- /dev/null +++ b/backend/src/main/java/achievements/apis/steam/GetOwnedGameBody.java @@ -0,0 +1,96 @@ +package achievements.apis.steam; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class GetOwnedGameBody { + + public static class Response { + + public static class Game { + @JsonProperty("appid") + private int appid; + @JsonProperty("name") + private String name; + @JsonProperty("playtime_forever") + private int playtime_forever; + @JsonProperty("img_icon_url") + private String img_icon_url; + @JsonProperty("img_logo_url") + private String img_logo_url; + + public int getAppid() { + return appid; + } + + public void setAppid(int appid) { + this.appid = appid; + } + + public int getPlaytime_forever() { + return playtime_forever; + } + + public void setPlaytime_forever(int playtime_forever) { + this.playtime_forever = playtime_forever; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getImg_icon_url() { + return img_icon_url; + } + + public void setImg_icon_url(String img_icon_url) { + this.img_icon_url = img_icon_url; + } + + public String getImg_logo_url() { + return img_logo_url; + } + + public void setImg_logo_url(String img_logo_url) { + this.img_logo_url = img_logo_url; + } + } + + @JsonProperty("game_count") + private int game_count; + @JsonProperty("games") + private List games; + + public int getGame_count() { + return game_count; + } + + public void setGame_count(int game_count) { + this.game_count = game_count; + } + + public List getGames() { + return games; + } + + public void setGames(List games) { + this.games = games; + } + } + + @JsonProperty("response") + private Response response; + + public Response getResponse() { + return response; + } + + public void setResponse(Response response) { + this.response = response; + } +} diff --git a/backend/src/main/java/achievements/apis/steam/GetPlayerAchievementsBody.java b/backend/src/main/java/achievements/apis/steam/GetPlayerAchievementsBody.java new file mode 100644 index 0000000..c6c899b --- /dev/null +++ b/backend/src/main/java/achievements/apis/steam/GetPlayerAchievementsBody.java @@ -0,0 +1,54 @@ +package achievements.apis.steam; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class GetPlayerAchievementsBody { + public static class PlayerStats { + public static class Achievement { + @JsonProperty("apiname") + private String apiname; + @JsonProperty("achieved") + private int achieved; + + public String getApiname() { + return apiname; + } + + public void setApiname(String apiname) { + this.apiname = apiname; + } + + public int getAchieved() { + return achieved; + } + + public void setAchieved(int achieved) { + this.achieved = achieved; + } + } + + @JsonProperty("achievements") + private List achievements; + + public List getAchievements() { + return achievements; + } + + public void setAchievements(List achievements) { + this.achievements = achievements; + } + } + + @JsonProperty("playerstats") + private PlayerStats playerstats; + + public PlayerStats getPlayerstats() { + return playerstats; + } + + public void setPlayerstats(PlayerStats playerstats) { + this.playerstats = playerstats; + } +} diff --git a/backend/src/main/java/achievements/apis/steam/GetSchemaForGameBody.java b/backend/src/main/java/achievements/apis/steam/GetSchemaForGameBody.java new file mode 100644 index 0000000..a4f1a4d --- /dev/null +++ b/backend/src/main/java/achievements/apis/steam/GetSchemaForGameBody.java @@ -0,0 +1,87 @@ +package achievements.apis.steam; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class GetSchemaForGameBody { + public static class Game { + public static class GameStats { + public static class Achievement { + @JsonProperty("name") + private String name; + @JsonProperty("displayName") + private String displayName; + @JsonProperty("description") + private String description; + @JsonProperty("icon") + private String icon; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getIcon() { + return icon; + } + + public void setIcon(String icon) { + this.icon = icon; + } + } + + @JsonProperty("achievements") + private List achievements; + + public List getAchievements() { + return achievements; + } + + public void setAchievements(List achievements) { + this.achievements = achievements; + } + } + + @JsonProperty("availableGameStats") + private GameStats availableGameStats; + + public GameStats getAvailableGameStats() { + return availableGameStats; + } + + public void setAvailableGameStats(GameStats availableGameStats) { + this.availableGameStats = availableGameStats; + } + } + + @JsonProperty("game") + private Game game; + + public Game getGame() { + return game; + } + + public void setGame(Game game) { + this.game = game; + } +} diff --git a/backend/src/main/java/achievements/controllers/AchievementController.java b/backend/src/main/java/achievements/controllers/AchievementController.java new file mode 100644 index 0000000..6e72521 --- /dev/null +++ b/backend/src/main/java/achievements/controllers/AchievementController.java @@ -0,0 +1,27 @@ +package achievements.controllers; + +import achievements.services.ImageService; +import achievements.services.AchievementService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; + +@RestController +@RequestMapping("/achievement") +public class AchievementController { + + @Autowired + private AchievementService achievementService; + @Autowired + private ImageService imageService; + + @GetMapping(value = "/{achievement}/image") + public void getProfilePicture(@PathVariable("achievement") int achievement, HttpServletResponse response) { + var icon = achievementService.getIcon(achievement); + imageService.send(icon, "achievement", response); + } +} diff --git a/backend/src/main/java/achievements/controllers/LoginController.java b/backend/src/main/java/achievements/controllers/AuthController.java similarity index 98% rename from backend/src/main/java/achievements/controllers/LoginController.java rename to backend/src/main/java/achievements/controllers/AuthController.java index 42bfa51..6897ffa 100644 --- a/backend/src/main/java/achievements/controllers/LoginController.java +++ b/backend/src/main/java/achievements/controllers/AuthController.java @@ -14,7 +14,7 @@ import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/auth") -public class LoginController { +public class AuthController { @Autowired private AuthenticationService authService; diff --git a/backend/src/main/java/achievements/controllers/GameController.java b/backend/src/main/java/achievements/controllers/GameController.java new file mode 100644 index 0000000..e8e7662 --- /dev/null +++ b/backend/src/main/java/achievements/controllers/GameController.java @@ -0,0 +1,27 @@ +package achievements.controllers; + +import achievements.services.ImageService; +import achievements.services.GameService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; + +@RestController +@RequestMapping("/game") +public class GameController { + + @Autowired + private GameService gameService; + @Autowired + private ImageService imageService; + + @GetMapping(value = "/{game}/image") + public void getProfilePicture(@PathVariable("game") int game, HttpServletResponse response) { + var icon = gameService.getIcon(game); + imageService.send(icon, "game", response); + } +} diff --git a/backend/src/main/java/achievements/controllers/PlatformController.java b/backend/src/main/java/achievements/controllers/PlatformController.java index 4da5ecf..f37a5c4 100644 --- a/backend/src/main/java/achievements/controllers/PlatformController.java +++ b/backend/src/main/java/achievements/controllers/PlatformController.java @@ -1,42 +1,27 @@ package achievements.controllers; +import achievements.services.ImageService; import achievements.services.PlatformService; -import org.apache.tomcat.util.http.fileupload.IOUtils; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletResponse; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; @RestController +@RequestMapping("/platform") public class PlatformController { @Autowired - private PlatformService platforms; + private ImageService imageService; + @Autowired + private PlatformService platformService; - @GetMapping(value = "/platform/image/{id}", produces = "application/json") - public void getPlatformImage(@PathVariable("id") int id, HttpServletResponse response) { - try { - var file = new File("images/platform/" + id + ".png"); - if (file.exists()) { - var stream = new FileInputStream(file); - IOUtils.copy(stream, response.getOutputStream()); - - response.setContentType("image/png"); - response.setStatus(200); - response.flushBuffer(); - stream.close(); - } else { - response.setStatus(HttpStatus.BAD_REQUEST.value()); - } - } catch (IOException e) { - e.printStackTrace(); - - } + @GetMapping(value = "/{platform}/image") + public void getIcon(@PathVariable("platform") int platform, HttpServletResponse response) { + var icon = platformService.getIcon(platform); + imageService.send(icon, "platform", response); } } diff --git a/backend/src/main/java/achievements/controllers/SearchController.java b/backend/src/main/java/achievements/controllers/SearchController.java new file mode 100644 index 0000000..bc15808 --- /dev/null +++ b/backend/src/main/java/achievements/controllers/SearchController.java @@ -0,0 +1,26 @@ +package achievements.controllers; + +import achievements.data.query.SearchAchievements; +import achievements.services.SearchService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class SearchController { + + @Autowired + private SearchService searchService; + + @PostMapping(value = "/achievements", consumes = "application/json", produces = "application/json") + public ResponseEntity searchAchievements(@RequestBody SearchAchievements searchAchievements) { + var achievements = searchService.searchAchievements(searchAchievements); + if (achievements != null) { + return ResponseEntity.ok(achievements); + } else { + return ResponseEntity.badRequest().body("[]"); + } + } +} diff --git a/backend/src/main/java/achievements/controllers/UserController.java b/backend/src/main/java/achievements/controllers/UserController.java index cca7fd6..2118053 100644 --- a/backend/src/main/java/achievements/controllers/UserController.java +++ b/backend/src/main/java/achievements/controllers/UserController.java @@ -2,20 +2,18 @@ package achievements.controllers; import achievements.data.APError; import achievements.data.APPostRequest; -import achievements.data.query.AddPlatformRequest; -import achievements.data.query.RemovePlatformRequest; +import achievements.data.query.AddPlatform; +import achievements.data.query.RemovePlatform; import achievements.data.query.SetUsername; +import achievements.services.ImageService; import achievements.services.UserService; -import org.apache.tomcat.util.http.fileupload.IOUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.util.FileCopyUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import javax.servlet.http.HttpServletResponse; -import java.io.*; @RestController @RequestMapping("/user") @@ -24,6 +22,9 @@ public class UserController { @Autowired private UserService userService; + @Autowired + private ImageService imageService; + @GetMapping(value = "/{user}", produces = "application/json") public ResponseEntity getProfile(@PathVariable("user") int user) { var profile = userService.getProfile(user); @@ -45,38 +46,21 @@ public class UserController { @GetMapping(value = "/{user}/image") public void getProfilePicture(@PathVariable("user") int user, HttpServletResponse response) { - var pfp = userService.getProfileImageType(user); - if (pfp == null) { - - } else { - var file = new File("images/user/" + pfp[0] + "." + pfp[1]); - response.setContentType("image/" + pfp[2]); - try { - var stream = new FileInputStream(file); - IOUtils.copy(stream, response.getOutputStream()); - - response.flushBuffer(); - stream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } + var profileImage = userService.getProfileImage(user); + imageService.send(profileImage, "user", response); } @PostMapping(value = "/{user}/image", consumes = "multipart/form-data", produces = "application/json") public ResponseEntity setProfilePicture(@PathVariable("user") int user, @RequestPart APPostRequest session, @RequestPart MultipartFile file) { try { - var type = userService.setProfileImageType(user, session.getKey(), file.getContentType()); + var type = userService.setProfileImage(user, session.getKey(), file); if ("not_an_image".equals(type)) { return ResponseEntity.badRequest().body("{ \"code\": 1, \"message\": \"Not an image type\" }"); } else if ("unsupported_type".equals(type)) { return ResponseEntity.badRequest().body("{ \"code\": 1, \"message\": \"Unsupported file type\" }"); } else if ("forbidden".equals(type)) { return ResponseEntity.status(HttpStatus.FORBIDDEN).body("{ \"code\": 2, \"message\": \"Invalid credentials\" }"); - } else if (!"unknown".equals(type)) { - var pfp = new FileOutputStream("images/user/" + user + "." + type); - FileCopyUtils.copy(file.getInputStream(), pfp); - pfp.close(); + } else if ("success".equals(type)) { return ResponseEntity.status(HttpStatus.CREATED).body("{ \"code\": 0, \"message\": \"Success\" }"); } @@ -87,7 +71,7 @@ public class UserController { } @PostMapping(value = "/{user}/platforms/add", consumes = "application/json", produces = "application/json") - public ResponseEntity addPlatformForUser(@PathVariable("user") int userId, @RequestBody AddPlatformRequest request) { + public ResponseEntity addPlatformForUser(@PathVariable("user") int userId, @RequestBody AddPlatform request) { var result = userService.addPlatform(userId, request); if (result == 0) { return ResponseEntity.status(HttpStatus.CREATED).body("{}"); @@ -97,7 +81,7 @@ public class UserController { } @PostMapping(value = "/{user}/platforms/remove", consumes = "application/json", produces = "application/json") - public ResponseEntity removePlatformForUser(@PathVariable("user") int userId, @RequestBody RemovePlatformRequest request) { + public ResponseEntity removePlatformForUser(@PathVariable("user") int userId, @RequestBody RemovePlatform request) { var result = userService.removePlatform(userId, request); if (result == 0) { return ResponseEntity.status(HttpStatus.CREATED).body("{}"); diff --git a/backend/src/main/java/achievements/data/APIResponse.java b/backend/src/main/java/achievements/data/APIResponse.java new file mode 100644 index 0000000..01136e8 --- /dev/null +++ b/backend/src/main/java/achievements/data/APIResponse.java @@ -0,0 +1,126 @@ +package achievements.data; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class APIResponse { + + public static class Game { + + public static class Achievement { + @JsonProperty("name") + private String name; + @JsonProperty("description") + private String description; + @JsonProperty("stages") + private int stages; + @JsonProperty("progress") + private int progress; + @JsonProperty("thumbnail") + private String thumbnail; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public int getStages() { + return stages; + } + + public void setStages(int stages) { + this.stages = stages; + } + + public int getProgress() { + return progress; + } + + public void setProgress(int progress) { + this.progress = progress; + } + + public String getThumbnail() { + return thumbnail; + } + + public void setThumbnail(String thumbnail) { + this.thumbnail = thumbnail; + } + } + + @JsonProperty("platformGameId") + private String platformGameId; + @JsonProperty("name") + private String name; + @JsonProperty("played") + private boolean played; + @JsonProperty("thumbnail") + private String thumbnail; + @JsonProperty("achievements") + private List achievements; + + public String getPlatformGameId() { + return platformGameId; + } + + public void setPlatformGameId(String platformGameId) { + this.platformGameId = platformGameId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isPlayed() { + return played; + } + + public void setPlayed(boolean played) { + this.played = played; + } + + public String getThumbnail() { + return thumbnail; + } + + public void setThumbnail(String thumbnail) { + this.thumbnail = thumbnail; + } + + public List getAchievements() { + return achievements; + } + + public void setAchievements(List achievements) { + this.achievements = achievements; + } + } + + @JsonProperty("games") + private List games; + + public List getGames() { + return games; + } + + public void setGames(List games) { + this.games = games; + } +} diff --git a/backend/src/main/java/achievements/data/Achievement.java b/backend/src/main/java/achievements/data/Achievement.java index c4f8127..ae3188f 100644 --- a/backend/src/main/java/achievements/data/Achievement.java +++ b/backend/src/main/java/achievements/data/Achievement.java @@ -1,122 +1,38 @@ package achievements.data; -import achievements.data.query.NumericFilter; -import achievements.data.query.StringFilter; import com.fasterxml.jackson.annotation.JsonProperty; public class Achievement { - public static class Query { - - @JsonProperty("sessionKey") - private String sessionKey; - @JsonProperty("name") - private StringFilter name; - @JsonProperty("stages") - private NumericFilter stages; - @JsonProperty("completion") - private NumericFilter completion; - @JsonProperty("difficulty") - private NumericFilter difficulty; - @JsonProperty("quality") - private NumericFilter quality; - - public Query(String sessionKey, StringFilter name, NumericFilter stages, NumericFilter completion, NumericFilter difficulty, NumericFilter quality) { - this.sessionKey = sessionKey; - this.name = name; - this.stages = stages; - this.completion = completion; - this.difficulty = difficulty; - this.quality = quality; - } - - public String getSessionKey() { - return sessionKey; - } - - public void setSessionKey(String sessionKey) { - this.sessionKey = sessionKey; - } - - public StringFilter getName() { - return name; - } - - public void setName(StringFilter name) { - this.name = name; - } - - public NumericFilter getStages() { - return stages; - } - - public void setStages(NumericFilter stages) { - this.stages = stages; - } - - public NumericFilter getCompletion() { - return completion; - } - - public void setCompletion(NumericFilter completion) { - this.completion = completion; - } - - public NumericFilter getDifficulty() { - return difficulty; - } - - public void setDifficulty(NumericFilter difficulty) { - this.difficulty = difficulty; - } - - public NumericFilter getQuality() { - return quality; - } - - public void setQuality(NumericFilter quality) { - this.quality = quality; - } - } - @JsonProperty("ID") - private int id; + private int ID; @JsonProperty("game") - private int gameId; + private String game; @JsonProperty("name") private String name; @JsonProperty("description") private String description; - @JsonProperty("stages") - private int stages; @JsonProperty("completion") - private float completion; + private Integer completion; @JsonProperty("difficulty") - private float difficulty; + private Float difficulty; @JsonProperty("quality") - private float quality; + private Float quality; - public Achievement(int id, int gameId, String name, String description, int stages, float completion, float difficulty, float quality) { - this.id = id; - this.gameId = gameId; - this.name = name; - this.description = description; - this.stages = stages; - this.completion = completion; - this.difficulty = difficulty; - this.quality = quality; + public int getID() { + return ID; } - public int getId() { return id; } - - public void setId(int id) { this.id = id; } - - public int getGameId() { - return gameId; + public void setID(int ID) { + this.ID = ID; } - public void setGameId(int gameId) { - this.gameId = gameId; + public String getGame() { + return game; + } + + public void setGame(String game) { + this.game = game; } public String getName() { return name; } @@ -127,31 +43,27 @@ public class Achievement { public void setDescription(String description) { this.description = description; } - public int getStages() { return stages; } - - public void setStages(int stages) { this.stages = stages; } - - public float getCompletion() { + public Integer getCompletion() { return completion; } - public void setCompletion(float completion) { + public void setCompletion(Integer completion) { this.completion = completion; } - public float getDifficulty() { + public Float getDifficulty() { return difficulty; } - public void setDifficulty(float difficulty) { + public void setDifficulty(Float difficulty) { this.difficulty = difficulty; } - public float getQuality() { + public Float getQuality() { return quality; } - public void setQuality(float quality) { + public void setQuality(Float quality) { this.quality = quality; } } diff --git a/backend/src/main/java/achievements/data/Game.java b/backend/src/main/java/achievements/data/Game.java index 1299b0e..bcd4f6d 100644 --- a/backend/src/main/java/achievements/data/Game.java +++ b/backend/src/main/java/achievements/data/Game.java @@ -1,20 +1,11 @@ package achievements.data; -import achievements.data.query.StringFilter; import com.fasterxml.jackson.annotation.JsonProperty; -import java.util.ArrayList; import java.util.List; public class Game { - public static class Query { - @JsonProperty("name") - private StringFilter name; - @JsonProperty("platforms") - private StringFilter platforms; - } - @JsonProperty("ID") private int id; @JsonProperty("name") @@ -24,13 +15,6 @@ public class Game { @JsonProperty("achievementCount") private int achievementCount; - public Game(int id, String name, String platform) { - this.id = id; - this.name = name; - this.platforms = new ArrayList<>(); - this.platforms.add(platform); - } - public int getId() { return id; } public void setId(int id) { this.id = id; } diff --git a/backend/src/main/java/achievements/data/Profile.java b/backend/src/main/java/achievements/data/Profile.java index 80c01cc..f5ab91d 100644 --- a/backend/src/main/java/achievements/data/Profile.java +++ b/backend/src/main/java/achievements/data/Profile.java @@ -1,6 +1,5 @@ package achievements.data; -import achievements.data.query.StringFilter; import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; diff --git a/backend/src/main/java/achievements/data/Session.java b/backend/src/main/java/achievements/data/Session.java index 179b21b..e88da56 100644 --- a/backend/src/main/java/achievements/data/Session.java +++ b/backend/src/main/java/achievements/data/Session.java @@ -11,14 +11,17 @@ public class Session { private int id; @JsonProperty("hue") private int hue; + @JsonProperty("admin") + private boolean admin; @JsonIgnore private boolean used; - public Session(String key, int id, int hue) { - this.key = key; - this.id = id; - this.hue = hue; - this.used = false; + public Session(String key, int id, int hue, boolean admin) { + this.key = key; + this.id = id; + this.hue = hue; + this.admin = admin; + this.used = false; } public String getKey() { @@ -45,7 +48,15 @@ public class Session { this.hue = hue; } - public boolean getUsed() { + public boolean isAdmin() { + return admin; + } + + public void setAdmin(boolean admin) { + this.admin = admin; + } + + public boolean isUsed() { return used; } diff --git a/backend/src/main/java/achievements/data/query/AddPlatformRequest.java b/backend/src/main/java/achievements/data/query/AddPlatform.java similarity index 95% rename from backend/src/main/java/achievements/data/query/AddPlatformRequest.java rename to backend/src/main/java/achievements/data/query/AddPlatform.java index fcc6098..cebfee2 100644 --- a/backend/src/main/java/achievements/data/query/AddPlatformRequest.java +++ b/backend/src/main/java/achievements/data/query/AddPlatform.java @@ -2,7 +2,7 @@ package achievements.data.query; import com.fasterxml.jackson.annotation.JsonProperty; -public class AddPlatformRequest { +public class AddPlatform { @JsonProperty("sessionKey") private String sessionKey; @JsonProperty("platformId") diff --git a/backend/src/main/java/achievements/data/query/NumericFilter.java b/backend/src/main/java/achievements/data/query/NumericFilter.java deleted file mode 100644 index a9fddac..0000000 --- a/backend/src/main/java/achievements/data/query/NumericFilter.java +++ /dev/null @@ -1,32 +0,0 @@ -package achievements.data.query; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public class NumericFilter { - - @JsonProperty("min") - private Float min; - @JsonProperty("max") - private Float max; - - public NumericFilter(Float min, Float max) { - this.min = min; - this.max = max; - } - - public Float getMin() { - return min; - } - - public void setMin(Float min) { - this.min = min; - } - - public Float getMax() { - return max; - } - - public void setMax(Float max) { - this.max = max; - } -} diff --git a/backend/src/main/java/achievements/data/query/RemovePlatformRequest.java b/backend/src/main/java/achievements/data/query/RemovePlatform.java similarity index 92% rename from backend/src/main/java/achievements/data/query/RemovePlatformRequest.java rename to backend/src/main/java/achievements/data/query/RemovePlatform.java index 4f88d6b..76bf798 100644 --- a/backend/src/main/java/achievements/data/query/RemovePlatformRequest.java +++ b/backend/src/main/java/achievements/data/query/RemovePlatform.java @@ -2,7 +2,7 @@ package achievements.data.query; import com.fasterxml.jackson.annotation.JsonProperty; -public class RemovePlatformRequest { +public class RemovePlatform { @JsonProperty("sessionKey") private String sessionKey; @JsonProperty("platformId") diff --git a/backend/src/main/java/achievements/data/query/SearchAchievements.java b/backend/src/main/java/achievements/data/query/SearchAchievements.java new file mode 100644 index 0000000..e46d2ee --- /dev/null +++ b/backend/src/main/java/achievements/data/query/SearchAchievements.java @@ -0,0 +1,97 @@ +package achievements.data.query; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class SearchAchievements { + + @JsonProperty("searchTerm") + private String searchTerm; + @JsonProperty("userId") + private Integer userId; + @JsonProperty("completed") + private boolean completed; + @JsonProperty("minCompletion") + private Float minCompletion; + @JsonProperty("maxCompletion") + private Float maxCompletion; + @JsonProperty("minDifficulty") + private Float minDifficulty; + @JsonProperty("maxDifficulty") + private Float maxDifficulty; + @JsonProperty("minQuality") + private Float minQuality; + @JsonProperty("maxQuality") + private Float maxQuality; + + public String getSearchTerm() { + return searchTerm; + } + + public void setSearchTerm(String searchTerm) { + this.searchTerm = searchTerm; + } + + public Integer getUserId() { + return userId; + } + + public void setUserId(Integer userId) { + this.userId = userId; + } + + public boolean isCompleted() { + return completed; + } + + public void setCompleted(boolean completed) { + this.completed = completed; + } + + public Float getMinCompletion() { + return minCompletion; + } + + public void setMinCompletion(Float minCompletion) { + this.minCompletion = minCompletion; + } + + public Float getMaxCompletion() { + return maxCompletion; + } + + public void setMaxCompletion(Float maxCompletion) { + this.maxCompletion = maxCompletion; + } + + public Float getMinDifficulty() { + return minDifficulty; + } + + public void setMinDifficulty(Float minDifficulty) { + this.minDifficulty = minDifficulty; + } + + public Float getMaxDifficulty() { + return maxDifficulty; + } + + public void setMaxDifficulty(Float maxDifficulty) { + this.maxDifficulty = maxDifficulty; + } + + public Float getMinQuality() { + return minQuality; + } + + public void setMinQuality(Float minQuality) { + this.minQuality = minQuality; + } + + public Float getMaxQuality() { + return maxQuality; + } + + public void setMaxQuality(Float maxQuality) { + this.maxQuality = maxQuality; + } +} diff --git a/backend/src/main/java/achievements/data/query/StringFilter.java b/backend/src/main/java/achievements/data/query/StringFilter.java deleted file mode 100644 index a6f93d5..0000000 --- a/backend/src/main/java/achievements/data/query/StringFilter.java +++ /dev/null @@ -1,21 +0,0 @@ -package achievements.data.query; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public class StringFilter { - - @JsonProperty("query") - private String query; - - public StringFilter(String query) { - this.query = query; - } - - public String getQuery() { - return query; - } - - public void setQuery(String query) { - this.query = query; - } -} diff --git a/backend/src/main/java/achievements/misc/APIList.java b/backend/src/main/java/achievements/misc/APIList.java new file mode 100644 index 0000000..99ea3d1 --- /dev/null +++ b/backend/src/main/java/achievements/misc/APIList.java @@ -0,0 +1,41 @@ +package achievements.misc; + +import achievements.apis.PlatformAPI; +import achievements.apis.SteamAPI; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import javax.annotation.PostConstruct; +import java.util.HashMap; +import java.util.Map; + +@Component +public class APIList { + + @Autowired + private RestTemplate rest; + + public final Map apis = new HashMap<>(); + + @PostConstruct + private void init() { + /*db = dbs.getConnection(); + try { + + var stmt = db.prepareCall("{call GetPlatforms()}"); + var results = stmt.executeQuery(); + + while (results.next()) { + var id = results.getInt("ID"); + + // Wanted to pull some skekery with dynamic class loading and external api jars, but...time is of the essence and I need to cut scope as much as possible + apis.put(id, new ????(id, rest)); + } + } catch (Exception e) { + e.printStackTrace(); + }*/ + + apis.put(0, new SteamAPI(0, rest)); + } +} diff --git a/backend/src/main/java/achievements/misc/SessionManager.java b/backend/src/main/java/achievements/misc/SessionManager.java index 61a66f8..5237967 100644 --- a/backend/src/main/java/achievements/misc/SessionManager.java +++ b/backend/src/main/java/achievements/misc/SessionManager.java @@ -13,12 +13,12 @@ public class SessionManager { private HashMap sessions; public SessionManager() { - sessions = new HashMap(); + sessions = new HashMap<>(); } - public Session generate(int user, int hue) { + public Session generate(int user, int hue, boolean admin) { var key = HashManager.encode(HashManager.generateBytes(16)); - var session = new Session(key, user, hue); + var session = new Session(key, user, hue, admin); sessions.put(key, session); return session; } @@ -32,8 +32,13 @@ public class SessionManager { } public boolean validate(int user, String key) { - var foreign = sessions.get(key); - return foreign != null && user == foreign.getId(); + var session = sessions.get(key); + return session != null && user == session.getId(); + } + + public boolean validateAdmin(int user, String key) { + var session = sessions.get(key); + return session != null && user == session.getId() && session.isAdmin(); } public boolean refresh(String key) { @@ -51,7 +56,7 @@ public class SessionManager { public void clean() { var remove = new ArrayList(); sessions.forEach((key, session) -> { - if (!session.getUsed()) { + if (!session.isUsed()) { remove.add(session.getKey()); } else { session.setUsed(false); diff --git a/backend/src/main/java/achievements/services/APIService.java b/backend/src/main/java/achievements/services/APIService.java new file mode 100644 index 0000000..8eabb9f --- /dev/null +++ b/backend/src/main/java/achievements/services/APIService.java @@ -0,0 +1,111 @@ +package achievements.services; + +import achievements.misc.APIList; +import achievements.misc.DbConnection; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import javax.annotation.PostConstruct; +import java.io.File; +import java.io.FileOutputStream; +import java.sql.Connection; +import java.sql.Types; + +@Service +public class APIService { + + @Autowired + private RestTemplate rest; + @Autowired + private APIList apis; + @Autowired + private DbConnection dbs; + private Connection db; + + @PostConstruct + private void init() { + db = dbs.getConnection(); + } + + private String getFileType(String imagePath) { + var path = imagePath.split("\\."); + return path[path.length - 1]; + } + + public int importUserPlatform(int userId, int platformId, String platformUserId) { + try { + var response = apis.apis.get(platformId).get(platformUserId); + + var addIfNotGame = db.prepareCall("{call AddIfNotGame(?, ?, ?)}"); + var addGameToPlatform = db.prepareCall("{call AddGameToPlatform(?, ?, ?)}"); + var addGameToUser = db.prepareCall("{call AddGameToPlatform(?, ?, ?)}"); + var addIfNotAchievement = db.prepareCall("{call AddIfNotAchievement(?, ?, ?, ?, ?, ?)}"); + var setAchievementProgressForUser = db.prepareCall("{call SetAchievementProgressForUser(?, ?, ?, ?)}"); + + addIfNotGame.registerOutParameter(3, Types.INTEGER); + addIfNotAchievement.registerOutParameter(6, Types.INTEGER); + + for (var game : response.getGames()) { + addIfNotGame.setString(1, game.getName()); + addIfNotGame.setString(2, getFileType(game.getThumbnail())); + addIfNotGame.execute(); + var gameId = addIfNotGame.getInt(3); + + addGameToPlatform.setInt(1, gameId); + addGameToPlatform.setInt(2, platformId); + addGameToPlatform.setString(3, platformUserId); + addGameToPlatform.execute(); + + var gameThumbnail = new File("storage/images/game/" + gameId + "." + getFileType(game.getThumbnail())); + if (!gameThumbnail.exists()) { + var bytes = rest.getForObject(game.getThumbnail(), byte[].class); + var stream = new FileOutputStream(gameThumbnail); + stream.write(bytes); + stream.close(); + } + + addGameToUser.setInt(1, gameId); + addGameToUser.setInt(2, userId); + addGameToUser.setInt(3, platformId); + addGameToUser.execute(); + + for (var achievement : game.getAchievements()) { + addIfNotAchievement.setInt(1, gameId); + addIfNotAchievement.setString(2, achievement.getName()); + addIfNotAchievement.setString(3, achievement.getDescription()); + addIfNotAchievement.setInt(4, achievement.getStages()); + addIfNotAchievement.setString(5, getFileType(achievement.getThumbnail())); + addIfNotAchievement.execute(); + var achievementId = addIfNotAchievement.getInt(6); + + var achievementIcon = new File("storage/images/achievement/" + achievementId + "." + getFileType(achievement.getThumbnail())); + if (!achievementIcon.exists()) { + var bytes = rest.getForObject(achievement.getThumbnail(), byte[].class); + var stream = new FileOutputStream(achievementIcon); + stream.write(bytes); + stream.close(); + } + + if (game.isPlayed()) { + setAchievementProgressForUser.setInt(1, userId); + setAchievementProgressForUser.setInt(2, platformId); + setAchievementProgressForUser.setInt(3, achievementId); + setAchievementProgressForUser.setInt(4, achievement.getProgress()); + setAchievementProgressForUser.execute(); + } + } + } + + addIfNotGame.close(); + addGameToPlatform.close(); + addIfNotAchievement.close(); + setAchievementProgressForUser.close(); + + return 0; + } catch (Exception e) { + e.printStackTrace(); + } + return -1; + } +} diff --git a/backend/src/main/java/achievements/services/AchievementService.java b/backend/src/main/java/achievements/services/AchievementService.java new file mode 100644 index 0000000..20f6d79 --- /dev/null +++ b/backend/src/main/java/achievements/services/AchievementService.java @@ -0,0 +1,34 @@ +package achievements.services; + +import achievements.misc.DbConnection; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.sql.Connection; + +@Service +public class AchievementService { + + @Autowired + private DbConnection dbs; + private Connection db; + + @Autowired + private ImageService imageService; + + @PostConstruct + private void init() { + db = dbs.getConnection(); + } + + public String[] getIcon(int achievementId) { + try { + var stmt = db.prepareCall("{call GetAchievementIcon(?)}"); + return imageService.getImageType(stmt, achievementId); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/backend/src/main/java/achievements/services/AuthenticationService.java b/backend/src/main/java/achievements/services/AuthenticationService.java index d42efe9..4bfeeaa 100644 --- a/backend/src/main/java/achievements/services/AuthenticationService.java +++ b/backend/src/main/java/achievements/services/AuthenticationService.java @@ -68,7 +68,8 @@ public class AuthenticationService { statement.getInt(1), session.generate( statement.getInt(6), - statement.getInt(7) + statement.getInt(7), + false ) ); statement.close(); @@ -95,7 +96,8 @@ public class AuthenticationService { 0, session.generate( result.getInt("ID"), - result.getInt("Hue") + result.getInt("Hue"), + result.getBoolean("Admin") ) ); } else { diff --git a/backend/src/main/java/achievements/services/GameService.java b/backend/src/main/java/achievements/services/GameService.java new file mode 100644 index 0000000..128b21e --- /dev/null +++ b/backend/src/main/java/achievements/services/GameService.java @@ -0,0 +1,34 @@ +package achievements.services; + +import achievements.misc.DbConnection; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.sql.Connection; + +@Service +public class GameService { + + @Autowired + private DbConnection dbs; + private Connection db; + + @Autowired + private ImageService imageService; + + @PostConstruct + private void init() { + db = dbs.getConnection(); + } + + public String[] getIcon(int gameId) { + try { + var stmt = db.prepareCall("{call GetAchievementIcon(?)}"); + return imageService.getImageType(stmt, gameId); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/backend/src/main/java/achievements/services/ImageService.java b/backend/src/main/java/achievements/services/ImageService.java new file mode 100644 index 0000000..2acf4a8 --- /dev/null +++ b/backend/src/main/java/achievements/services/ImageService.java @@ -0,0 +1,80 @@ +package achievements.services; + +import org.apache.tomcat.util.http.fileupload.IOUtils; +import org.springframework.stereotype.Service; + +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.sql.CallableStatement; +import java.sql.SQLException; +import java.util.HashMap; + +@Service +public class ImageService { + public static final HashMap MIME_TO_EXT = new HashMap<>(); + public static final HashMap EXT_TO_MIME = new HashMap<>(); + static { + MIME_TO_EXT.put("apng", "apng"); + MIME_TO_EXT.put("avif", "avif"); + MIME_TO_EXT.put("gif", "gif" ); + MIME_TO_EXT.put("jpeg", "jpg" ); + MIME_TO_EXT.put("png", "png" ); + MIME_TO_EXT.put("svg+xml", "svg" ); + MIME_TO_EXT.put("webp", "webp"); + + EXT_TO_MIME.put("apng", "apng" ); + EXT_TO_MIME.put("avif", "avif" ); + EXT_TO_MIME.put("gif", "gif" ); + EXT_TO_MIME.put("jpg", "jpeg" ); + EXT_TO_MIME.put("png", "png" ); + EXT_TO_MIME.put("svg", "svg+xml"); + EXT_TO_MIME.put("webp", "webp" ); + } + + public String[] getImageType(CallableStatement stmt, int id) { + try { + stmt.setInt(1, id); + + var result = stmt.executeQuery(); + if (result.next()) { + var type = result.getString(1); + if (type != null) { + return new String[] { id + "." + type, EXT_TO_MIME.get(type) }; + } + } + } catch (SQLException e) { + e.printStackTrace(); + } catch (NumberFormatException e) { + e.printStackTrace(); + } + return null; + } + + public void send(String[] image, String type, HttpServletResponse response) { + var file = (File) null; + var mimeType = (String) null; + if (image == null) { + file = new File("storage/images/default/" + type + ".png"); + mimeType = "png"; + } else { + file = new File("storage/images/" + type + "/" + image[0]); + mimeType = image[1]; + } + try { + var stream = new FileInputStream(file); + IOUtils.copy(stream, response.getOutputStream()); + + response.setStatus(200); + response.setContentType("image/" + mimeType); + response.flushBuffer(); + stream.close(); + + return; + } catch (IOException e) { + e.printStackTrace(); + } + response.setStatus(500); + } +} diff --git a/backend/src/main/java/achievements/services/PlatformService.java b/backend/src/main/java/achievements/services/PlatformService.java index ab1d7f1..ad1ad2e 100644 --- a/backend/src/main/java/achievements/services/PlatformService.java +++ b/backend/src/main/java/achievements/services/PlatformService.java @@ -14,8 +14,21 @@ public class PlatformService { private DbConnection dbs; private Connection db; + @Autowired + private ImageService imageService; + @PostConstruct private void init() { db = dbs.getConnection(); } + + public String[] getIcon(int platformId) { + try { + var stmt = db.prepareCall("{call GetPlatformIcon(?)}"); + return imageService.getImageType(stmt, platformId); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } } diff --git a/backend/src/main/java/achievements/services/SearchService.java b/backend/src/main/java/achievements/services/SearchService.java new file mode 100644 index 0000000..734e6f0 --- /dev/null +++ b/backend/src/main/java/achievements/services/SearchService.java @@ -0,0 +1,59 @@ +package achievements.services; + +import achievements.data.Achievement; +import achievements.data.query.SearchAchievements; +import achievements.misc.DbConnection; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +@Service +public class SearchService { + + @Autowired + private DbConnection dbs; + private Connection db; + + @PostConstruct + private void init() { + db = dbs.getConnection(); + } + + public List searchAchievements(SearchAchievements query) { + try { + var stmt = db.prepareCall("{call SearchAchievements(?, ?, ?, ?, ?, ?, ?, ?, ?)}"); + stmt.setString(1, query.getSearchTerm()); + stmt.setBoolean(3, query.isCompleted()); + if (query.getUserId() != null) { stmt.setInt( 2, query.getUserId() ); } else { stmt.setString(2, null); } + if (query.getMinCompletion() != null) { stmt.setFloat(4, query.getMinCompletion()); } else { stmt.setString(4, null); } + if (query.getMaxCompletion() != null) { stmt.setFloat(5, query.getMaxCompletion()); } else { stmt.setString(5, null); } + if (query.getMinDifficulty() != null) { stmt.setFloat(6, query.getMinDifficulty()); } else { stmt.setString(6, null); } + if (query.getMaxDifficulty() != null) { stmt.setFloat(7, query.getMaxDifficulty()); } else { stmt.setString(7, null); } + if (query.getMinQuality() != null) { stmt.setFloat(8, query.getMinQuality() ); } else { stmt.setString(8, null); } + if (query.getMaxQuality() != null) { stmt.setFloat(9, query.getMaxQuality() ); } else { stmt.setString(9, null); } + var results = stmt.executeQuery(); + + var achievements = new ArrayList(); + while (results.next()) { + var achievement = new Achievement(); + achievement.setID (results.getInt ("ID" )); + achievement.setGame (results.getString("Game" )); + achievement.setName (results.getString("Name" )); + achievement.setCompletion(results.getInt ("Completion")); if (results.wasNull()) { achievement.setCompletion(null); } + achievement.setDifficulty(results.getFloat ("Difficulty")); if (results.wasNull()) { achievement.setDifficulty(null); } + achievement.setQuality (results.getFloat ("Quality" )); if (results.wasNull()) { achievement.setQuality (null); } + achievements.add(achievement); + } + + return achievements; + } catch (SQLException e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/backend/src/main/java/achievements/services/UserService.java b/backend/src/main/java/achievements/services/UserService.java index 7f677d9..3e1386c 100644 --- a/backend/src/main/java/achievements/services/UserService.java +++ b/backend/src/main/java/achievements/services/UserService.java @@ -1,19 +1,24 @@ package achievements.services; import achievements.data.Profile; -import achievements.data.query.AddPlatformRequest; -import achievements.data.query.RemovePlatformRequest; +import achievements.data.query.AddPlatform; +import achievements.data.query.RemovePlatform; import achievements.data.query.SetUsername; import achievements.misc.DbConnection; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import org.springframework.util.FileCopyUtils; +import org.springframework.web.multipart.MultipartFile; import javax.annotation.PostConstruct; +import java.io.File; +import java.io.FileOutputStream; import java.sql.Connection; import java.sql.SQLException; import java.sql.Types; import java.util.ArrayList; -import java.util.HashMap; + +import static achievements.services.ImageService.MIME_TO_EXT; @Service public class UserService { @@ -25,6 +30,12 @@ public class UserService { @Autowired private AuthenticationService auth; + @Autowired + private APIService apiService; + + @Autowired + private ImageService imageService; + @PostConstruct private void init() { db = dbs.getConnection(); @@ -97,55 +108,51 @@ public class UserService { return -1; } - private static final HashMap VALID_IMAGE_TYPES = new HashMap<>(); - static { - VALID_IMAGE_TYPES.put("apng", "apng"); - VALID_IMAGE_TYPES.put("avif", "avif"); - VALID_IMAGE_TYPES.put("gif", "gif" ); - VALID_IMAGE_TYPES.put("jpeg", "jpg" ); - VALID_IMAGE_TYPES.put("png", "png" ); - VALID_IMAGE_TYPES.put("svg+xml", "svg" ); - VALID_IMAGE_TYPES.put("webp", "webp"); - } - public String[] getProfileImageType(int userId) { + public String[] getProfileImage(int userId) { try { var stmt = db.prepareCall("{call GetUserImage(?)}"); - stmt.setInt(1, userId); - - var result = stmt.executeQuery(); - if (result.next()) { - var type = result.getString("PFP"); - if (type == null) { - return new String[] { "default", "png", "png" }; - } else { - return new String[] { Integer.toString(userId), VALID_IMAGE_TYPES.get(type), type }; - } - } - } catch (SQLException e) { - e.printStackTrace(); - } catch (NumberFormatException e) { + return imageService.getImageType(stmt, userId); + } catch (Exception e) { e.printStackTrace(); } return null; } - public String setProfileImageType(int userId, String sessionKey, String type) { + public String setProfileImage(int userId, String sessionKey, MultipartFile file) { try { + var type = file.getContentType(); if (type.matches("image/.*")) { type = type.substring(6); - var extension = VALID_IMAGE_TYPES.get(type); + type = MIME_TO_EXT.get(type); if (!auth.session().validate(userId, sessionKey)) { return "forbidden"; - } else if (extension == null) { + } else if (type == null) { return "unsupported_type"; } else { - var stmt = db.prepareCall("{call SetUserImage(?, ?)}"); + var stmt = db.prepareCall("{call SetUserImage(?, ?, ?)}"); stmt.setInt(1, userId); stmt.setString(2, type); + stmt.registerOutParameter(3, Types.VARCHAR); stmt.execute(); + var oldType = stmt.getString(3); - return extension; + // Delete old file + if (oldType != null && type != oldType) { + var oldFile = new File("storage/images/user/" + userId + "." + oldType); + if (oldFile.exists()) { + oldFile.delete(); + } + } + + // Save new file (will overwrite old if file type didn't change) + { + var image = new FileOutputStream("storage/images/user/" + userId + "." + type); + FileCopyUtils.copy(file.getInputStream(), image); + image.close(); + } + + return "success"; } } else { return "not_an_image"; @@ -156,28 +163,41 @@ public class UserService { return "unknown"; } - public int addPlatform(int userId, AddPlatformRequest request) { - try { - if (auth.session().validate(userId, request.getSessionKey())) { - var stmt = db.prepareCall("{call AddPlatform(?, ?, ?)}"); - stmt.setInt(1, userId); - stmt.setInt(2, request.getPlatformId()); - stmt.setString(3, request.getPlatformUserId()); + public int addPlatform(int userId, AddPlatform request) { + if (auth.session().validate(userId, request.getSessionKey())) { + try { + db.setAutoCommit(false); + try { + var stmt = db.prepareCall("{call AddUserToPlatform(?, ?, ?)}"); + stmt.setInt(1, userId); + stmt.setInt(2, request.getPlatformId()); + stmt.setString(3, request.getPlatformUserId()); - stmt.execute(); + stmt.execute(); - return 0; + int successful = apiService.importUserPlatform(userId, request.getPlatformId(), request.getPlatformUserId()); + + if (successful == 0) { + db.commit(); + db.setAutoCommit(true); + return 0; + } + } catch (Exception e) { + e.printStackTrace(); + } + db.rollback(); + db.setAutoCommit(true); + } catch(SQLException e){ + e.printStackTrace(); } - } catch (Exception e) { - e.printStackTrace(); } return -1; } - public int removePlatform(int userId, RemovePlatformRequest request) { + public int removePlatform(int userId, RemovePlatform request) { try { if (auth.session().validate(userId, request.getSessionKey())) { - var stmt = db.prepareCall("{call RemovePlatform(?, ?)}"); + var stmt = db.prepareCall("{call RemoveUserFromPlatform(?, ?)}"); stmt.setInt(1, userId); stmt.setInt(2, request.getPlatformId()); diff --git a/frontend/server.js b/frontend/server.js index c92aa21..c1ac62f 100644 --- a/frontend/server.js +++ b/frontend/server.js @@ -30,10 +30,16 @@ app.get("/login", (req, res) => { res.sendFile(path.join(__dirname + "/webpage/login.html")); }); app.get("/", (req, res) => { - res.sendFile(path.join(__dirname + "/webpage/index.html")); + res.sendFile(path.join(__dirname + "/webpage/search_achievements.html")); }); -app.get("/about", (req, res) => { - res.sendFile(path.join(__dirname + "/webpage/about.html")); +app.get("/achievements", (req, res) => { + res.sendFile(path.join(__dirname + "/webpage/search_achievements.html")); +}); +app.get("/users", (req, res) => { + res.sendFile(path.join(__dirname + "/webpage/search_users.html")); +}); +app.get("/games", (req, res) => { + res.sendFile(path.join(__dirname + "/webpage/search_games.html")); }); app.get("/profile/:id", (req, res) => { res.sendFile(path.join(__dirname + "/webpage/profile.html")); diff --git a/frontend/webpage/about.html b/frontend/webpage/about.html deleted file mode 100644 index eb54e54..0000000 --- a/frontend/webpage/about.html +++ /dev/null @@ -1,42 +0,0 @@ - - - - - Achievements Project - - - - - - - -
-
-
- -
-
-
-

Collate achievement data from multiple platforms into a single location. Explore achievement data of yourself and others.

-
-
-
-
- - - - - \ No newline at end of file diff --git a/frontend/webpage/profile.html b/frontend/webpage/profile.html index 8c67db2..8a56589 100644 --- a/frontend/webpage/profile.html +++ b/frontend/webpage/profile.html @@ -28,6 +28,10 @@
+
+

Contemplating...

+ Loading Symbol +
-
+ - + + \ No newline at end of file diff --git a/frontend/webpage/search_users.html b/frontend/webpage/search_users.html new file mode 100644 index 0000000..a4d63c4 --- /dev/null +++ b/frontend/webpage/search_users.html @@ -0,0 +1,133 @@ + + + + + Achievements Project + + + + + + + + +
+ +
+ + + + + + \ No newline at end of file diff --git a/frontend/webpage/static/res/loading.svg b/frontend/webpage/static/res/loading.svg new file mode 100644 index 0000000..8debb61 --- /dev/null +++ b/frontend/webpage/static/res/loading.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/webpage/static/scripts/common.js b/frontend/webpage/static/scripts/common.js index 6bec535..218d6cb 100644 --- a/frontend/webpage/static/scripts/common.js +++ b/frontend/webpage/static/scripts/common.js @@ -25,7 +25,6 @@ const loadSession = async () => { await fetch(`/api/auth/refresh`, { method: 'POST', - mode: 'cors', headers: { 'Content-Type': 'application/json' }, @@ -52,8 +51,10 @@ const commonTemplates = async () => { { section: "right" } ]); template.apply("navbar-section-left").values([ - { item: "project", title: "Project" }, - { item: "about", title: "About" } + { item: "achievements", title: "Achievements" }, + { item: "users", title: "Users" }, + { item: "games", title: "Games" }, + { item: "import", title: "Import" } ]); if (session) { template.apply("navbar-section-right").values([ @@ -62,34 +63,40 @@ const commonTemplates = async () => { ]); } else { template.apply("navbar-section-right").values([ - { item: "login", title: "Login" } + { item: "login", title: "Login" } ]); } }; +const loadLazyImages = () => { + const imgs = document.querySelectorAll(".lazy-img"); + for (const img of imgs) { + img.src = img.dataset.src; + } +} + const connectNavbar = () => { const navItems = document.querySelectorAll(".navbar-item"); + if (!session || !session.admin) { + document.querySelector("#navbar-item-import").remove(); + } + for (const item of navItems) { if (item.dataset.pageName === "logout") { item.addEventListener("click", (clickEvent) => { fetch(`/api/auth/logout`, { method: 'POST', - mode: 'cors', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ key: session.key }) - }) - .then(response => { - session = undefined; - window.location.href = "/login"; }); + session = undefined; + window.location.href = "/login"; }); } else if (item.dataset.pageName === "profile") { item.addEventListener("click", (clickEvent) => window.location.href = `/profile/${session.id}`); - } else if (item.dataset.pageName === "project") { - item.addEventListener("click", (clickEvent) => window.location.href = `/`); } else { item.addEventListener("click", (clickEvent) => window.location.href = `/${item.dataset.pageName}`); } diff --git a/frontend/webpage/static/scripts/index.js b/frontend/webpage/static/scripts/index.js deleted file mode 100644 index 3c33957..0000000 --- a/frontend/webpage/static/scripts/index.js +++ /dev/null @@ -1,24 +0,0 @@ -const expandTemplates = async () => { - await commonTemplates(); -} - -const loadFilters = () => { - const filtersButton = document.querySelector("#filter-dropdown-stack"); - const filters = document.querySelector("#list-page-filters-flex"); - - filtersButton.addEventListener("click", (clickEvent) => { - filtersButton.classList.toggle("active"); - filters.classList.toggle("active"); - }); -} - -window.addEventListener("load", async (loadEvent) => { - loadRoot(); - loadSession(); - - await expandTemplates(); - await template.expand(); - - connectNavbar(); - loadFilters(); -}); \ No newline at end of file diff --git a/frontend/webpage/static/scripts/login.js b/frontend/webpage/static/scripts/login.js index 8f7f8fc..13a3f5a 100644 --- a/frontend/webpage/static/scripts/login.js +++ b/frontend/webpage/static/scripts/login.js @@ -11,6 +11,7 @@ window.addEventListener("load", async (loadEvent) => { password: document.querySelector("#password"), confirm: document.querySelector("#confirm" ) }; + fields.email.focus(); const createUser = document.querySelector("#create-user-button"); const login = document.querySelector("#login-button"); @@ -80,7 +81,6 @@ window.addEventListener("load", async (loadEvent) => { freeze(); fetch(`/api/auth/create_user`, { method: 'POST', - mode: 'cors', headers: { 'Content-Type': 'application/json' }, @@ -141,7 +141,6 @@ window.addEventListener("load", async (loadEvent) => { freeze(); fetch(`/api/auth/login`, { method: 'POST', - mode: 'cors', headers: { 'Content-Type': 'application/json' }, diff --git a/frontend/webpage/static/scripts/profile.js b/frontend/webpage/static/scripts/profile.js index a78b783..cc3c5db 100644 --- a/frontend/webpage/static/scripts/profile.js +++ b/frontend/webpage/static/scripts/profile.js @@ -2,14 +2,25 @@ let profileId = window.location.pathname.split('/').pop(); let isReturn = false; let profileData = null; const loadProfile = () => { - { - const lists = document.querySelectorAll(".profile-list"); + const lists = document.querySelectorAll(".profile-list"); + const checkLists = () => { for (const list of lists) { - if (list.querySelectorAll(".profile-entry").length === 0) { - list.parentElement.removeChild(list); + let found = false; + const entries = list.querySelectorAll(".profile-entry"); + for (const entry of entries) { + if (window.getComputedStyle(entry).getPropertyValue('display') !== 'none') { + found = true; + break; + } + } + if (!found) { + list.style.display = 'none'; + } else { + list.style.display = 'block'; } } } + checkLists(); { const validImageFile = (type) => { @@ -32,7 +43,6 @@ const loadProfile = () => { if (usernameField.value !== '') { fetch(`/api/user/${profileId}/username`, { method: 'POST', - mode: 'cors', headers: { 'Content-Type': 'application/json' }, @@ -89,7 +99,6 @@ const loadProfile = () => { fetch(`/api/user/${profileId}/image`, { method: 'POST', - mode: 'cors', body: data }).then(response => { if (upload.classList.contains("active")) { @@ -141,6 +150,7 @@ const loadProfile = () => { for (const platform of platforms) { platform.classList.toggle("editing"); } + checkLists(); }; editPlatformsButton.addEventListener("click", togglePlatformEdit); savePlatformsButton.addEventListener("click", togglePlatformEdit); @@ -156,7 +166,6 @@ const loadProfile = () => { steamButtons[1].addEventListener("click", (clickEvent) => { fetch(`/api/user/${profileId}/platforms/remove`, { method: 'POST', - mode: 'cors', headers: { 'Content-Type': 'application/json' }, @@ -213,11 +222,11 @@ const expandTemplates = async () => { template.apply("profile-platforms-list").promise(profileData.then(data => data.platforms.map(platform => ({ platform_id: platform.id, - img: `Steam Logo`, + img: `Steam Logo`, name: platform.name, connected: platform.connected ? "connected" : "", add: - (platform.id === 0 ? `Add` : + (platform.id === 0 ? `Add` : (platform.id === 1 ? `

Coming soon...

` : (platform.id === 2 ? `

Coming soon...

` : ""))) @@ -228,6 +237,7 @@ const expandTemplates = async () => { window.addEventListener("load", async (loadEvent) => { await loadCommon(); + var importing = document.querySelector("#importing"); if (!/\d+/.test(profileId)) { isReturn = true; const platform = profileId; @@ -238,6 +248,9 @@ window.addEventListener("load", async (loadEvent) => { delete session.lastProfile; } + const importingText = importing.querySelector("#importing-text"); + importingText.textContent = `Importing from ${platform}...`; + importing.style.display = `flex`; if (platform === 'steam') { const query = new URLSearchParams(window.location.search); @@ -246,9 +259,8 @@ window.addEventListener("load", async (loadEvent) => { } else { // Regex courtesy of https://github.com/liamcurry/passport-steam/blob/master/lib/passport-steam/strategy.js var steamId = /^https?:\/\/steamcommunity\.com\/openid\/id\/(\d+)$/.exec(query.get('openid.claimed_id'))[1]; - await fetch("/api/user/platforms/add", { + await fetch(`/api/user/${profileId}/platforms/add`, { method: 'POST', - mode: 'cors', headers: { 'Content-Type': 'application/json' }, @@ -266,13 +278,15 @@ window.addEventListener("load", async (loadEvent) => { } else { // Handle error } + importing.remove(); - profileData = fetch(`/api/user/${profileId}`, { method: 'GET', mode: 'cors' }) + profileData = fetch(`/api/user/${profileId}`, { method: 'GET' }) .then(response => response.json()); await expandTemplates(); await template.expand(); + loadLazyImages(); connectNavbar(); loadProfile(); }); \ No newline at end of file diff --git a/frontend/webpage/static/scripts/search.js b/frontend/webpage/static/scripts/search.js new file mode 100644 index 0000000..c2cac2a --- /dev/null +++ b/frontend/webpage/static/scripts/search.js @@ -0,0 +1,26 @@ +const expandTemplates = async () => { + await commonTemplates(); +} + +const loadFilters = () => { + const filtersButton = document.querySelector("#filter-dropdown-stack"); + const filtersSection = document.querySelector("#list-page-filters-flex"); + + filtersButton.addEventListener("click", (clickEvent) => { + filtersButton.classList.toggle("active"); + filtersSection.classList.toggle("active"); + }); + + const filterCheckboxes = document.querySelectorAll(".list-page-filter-checkbox"); + for (const checkbox of filterCheckboxes) { + checkbox.parentElement.addEventListener("click", (clickEvent) => { + checkbox.parentElement.classList.toggle("selected"); + }) + } +} + +const loadCommonSearch = async () => { + await loadCommon(); + + await expandTemplates(); +}; \ No newline at end of file diff --git a/frontend/webpage/static/scripts/search_achievements.js b/frontend/webpage/static/scripts/search_achievements.js new file mode 100644 index 0000000..5a17ce9 --- /dev/null +++ b/frontend/webpage/static/scripts/search_achievements.js @@ -0,0 +1,108 @@ +let templateList = null; +let templateText = null; +const saveTemplate = () => { + const templateElement = document.querySelector("#achievement-list-template"); + templateList = templateElement.parentElement; + templateText = templateElement.outerHTML; + templateElement.remove(); +}; + +const loadAchievementSearch = () => { + const loading = document.querySelector("#loading-results"); + + + const searchButton = document.querySelector("#achievement-search-button"); + const searchField = document.querySelector("#achievement-search-field" ); + + const completed = document.querySelector("#completed-filter"); + const minCompletion = document.querySelector("#min-completion-filter"); + const maxCompletion = document.querySelector("#max-completion-filter"); + const minDifficulty = document.querySelector("#min-difficulty-filter"); + const maxDifficulty = document.querySelector("#max-difficulty-filter"); + const minQuality = document.querySelector("#min-quality-filter" ); + const maxQuality = document.querySelector("#max-quality-filter" ); + + let canSearch = true; + const loadList = async () => { + if (canSearch) { + canSearch = false; + + const body = { + searchTerm: searchField.value, + userId: completed.classList.contains('active') ? session.id : null, + completed: completed.classList.contains('active'), + minCompletion: minCompletion.value === '' ? null : Number(minCompletion.value), + maxCompletion: maxCompletion.value === '' ? null : Number(maxCompletion.value), + minDifficulty: minDifficulty.value === '' ? null : Number(minDifficulty.value), + maxDifficulty: maxDifficulty.value === '' ? null : Number(maxDifficulty.value), + minQuality: minQuality.value === '' ? null : Number(minQuality.value ), + maxQuality: maxQuality.value === '' ? null : Number(maxQuality.value ), + }; + console.log(body); + let successful = true; + if (Number.isNaN(body.minCompletion)) { successful = false; minCompletion.style.backgroundColor = 'var(--error)'; } else { minCompletion.style.backgroundColor = 'var(--foreground)'; } + if (Number.isNaN(body.maxCompletion)) { successful = false; maxCompletion.style.backgroundColor = 'var(--error)'; } else { maxCompletion.style.backgroundColor = 'var(--foreground)'; } + if (Number.isNaN(body.minDifficulty)) { successful = false; minDifficulty.style.backgroundColor = 'var(--error)'; } else { minDifficulty.style.backgroundColor = 'var(--foreground)'; } + if (Number.isNaN(body.maxDifficulty)) { successful = false; maxDifficulty.style.backgroundColor = 'var(--error)'; } else { maxDifficulty.style.backgroundColor = 'var(--foreground)'; } + if (Number.isNaN(body.minQuality )) { successful = false; minQuality.style.backgroundColor = 'var(--error)'; } else { minQuality.style.backgroundColor = 'var(--foreground)'; } + if (Number.isNaN(body.maxQuality )) { successful = false; maxQuality.style.backgroundColor = 'var(--error)'; } else { maxQuality.style.backgroundColor = 'var(--foreground)'; } + + if (!successful) { + canSearch = true; + return; + } + + for (const entry of templateList.querySelectorAll(".list-page-entry")) { + entry.remove(); + } + templateList.innerHTML += templateText; + loading.style.display = 'block'; + + const data = fetch("/api/achievements", { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(body) + }) + .then(response => response.json()) + + template.clear(); + template.apply('achievements-page-list').promise(data.then(data => data.map(item => ({ + achievement_id: item.ID, + achievement_name: item.name, + game_name: item.game, + completion: item.completion == null ? 'N/A' : item.completion + '%', + difficulty: item.difficulty == null ? 'N/A' : item.difficulty + ' / 10', + quality: item.quality == null ? 'N/A' : item.quality + ' / 10' + })))); + await template.expand(); + data.then(data => { + loading.style.display = 'none'; + canSearch = true; + loadLazyImages(); + }); + } + }; + + searchButton.addEventListener("click", loadList); + searchField.addEventListener("keydown", (keyEvent) => { + if (keyEvent.key === 'Enter') { + loadList(); + } + }); + + loadList(); +}; + +window.addEventListener("load", async (loadEvent) => { + await loadCommonSearch(); + + saveTemplate(); + await template.expand(); + + connectNavbar(); + loadFilters(); + + await loadAchievementSearch(); +}); \ No newline at end of file diff --git a/frontend/webpage/static/scripts/template.js b/frontend/webpage/static/scripts/template.js index 661f3e1..cb3541f 100644 --- a/frontend/webpage/static/scripts/template.js +++ b/frontend/webpage/static/scripts/template.js @@ -136,6 +136,10 @@ var template = template || {}; } }; + template.clear = () => { + templateEntryMap.clear(); + } + const parseType = (type) => { let result = type.match(/^\s*(\w+)\s*(?:<(.*)>)?\s*$/); let id = result[1]; diff --git a/frontend/webpage/static/styles/common.css b/frontend/webpage/static/styles/common.css index ad9f5e0..a45006d 100644 --- a/frontend/webpage/static/styles/common.css +++ b/frontend/webpage/static/styles/common.css @@ -70,6 +70,15 @@ html, body { background-color: var(--accent-value3); } +@keyframes load { + from { transform: rotateZ(0deg ); } + to { transform: rotateZ(360deg); } +} + +.ap-loading { + animation: 1.5s cubic-bezier(0.4, 0.15, 0.6, 0.85) 0s infinite running load; +} + .ap-button { color: var(--foreground); background-color: var(--accent-value2); @@ -226,201 +235,3 @@ html, body { background-color: var(--accent-value3); } - -.list-page-search { - box-sizing: border-box; - - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; -} - -.list-page-search > label, -.list-page-search > input { - box-sizing: border-box; - padding: 12px 20px; - - color: var(--foreground); - - font-size: 24px; -} - -.list-page-search > label { - background-color: var(--accent-value2); -} - -.list-page-search > label:hover { - background-color: var(--accent-value3); -} - -.list-page-search > label:active { - background-color: var(--accent-value1); - - transition-property: background-color; - transition-duration: 0.15s; -} - -.list-page-search > input { - background-color: var(--distinction); - - border: 0; - - flex-grow: 1; - - outline: none; - - transition-property: background-color, color; - transition-duration: 0.075s; -} - -.list-page-search > input:focus { - background-color: var(--foreground); - - color: var(--background); -} - -.list-page-partitions { - box-sizing: border-box; - - width: 100%; - height: max-content; - - display: flex; - flex-direction: row; - justify-content: center; - align-items: flex-start; -} - -.list-page-filter-partition { - width: 20%; - max-width: 640px; -} - -.list-page-filter-chunk { - background-color: var(--distinction); - - width: 100%; - height: 100%; -} - -.list-page-filter { - box-sizing: border-box; - - width: 100%; - height: max-content; - - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; -} - -.list-page-filter-checkbox { - width: 28px; - height: 28px; - - background-color: var(--foreground); - - border: 3px solid var(--foreground); - border-radius: 8px; - - transition-property: background-color, border-color; - transition-duration: 0.15s; -} - -.list-page-filter:hover > .list-page-filter-checkbox { - background-color: var(--foreground); - border-color: var(--selected-accent1); -} - -.list-page-filter.selected > .list-page-filter-checkbox { - background-color: var(--selected-accent1); - border-color: var(--selected-accent1); -} - -.list-page-filter.selected:hover > .list-page-filter-checkbox { - background-color: var(--selected-accent0); - border-color: var(--selected-accent1); -} - -.list-page-filter-name { - margin: 0; - padding: 16px; - - color: var(--foreground); - - font-size: 24px; - - user-select: none; -} - -.list-page-list-partition { - box-sizing: border-box; - - flex-grow: 1; -} - -.list-page-list { - border-radius: 8px; - - overflow: hidden; -} - -.list-page-header { - width: 100%; - height: 64px; - - background-color: var(--accent-value2); - - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - - color: var(--foreground); - font-size: 24px; - text-overflow: ellipsis; - white-space: nowrap; -} - -.list-page-entry { - width: 100%; - height: 64px; - - background-color: var(--distinction); - - display: flex; - flex-direction: row; - justify-content: center; - align-items: center; - - color: var(--foreground); - font-size: 24px; -} - -.list-page-entry-icon { - width: 64px; - height: 64px; - - flex-grow: 0; -} - -.list-page-entry-text { - box-sizing: border-box; - - margin: 0; - padding: 0 12px; - height: 64px; - line-height: 64px; - - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - border-top: 1px solid var(--background); -} - -.list-page-header > .list-page-entry-text { - border: 0; -} diff --git a/frontend/webpage/static/styles/index.css b/frontend/webpage/static/styles/index.css deleted file mode 100644 index 92be846..0000000 --- a/frontend/webpage/static/styles/index.css +++ /dev/null @@ -1,111 +0,0 @@ -#index-page { - max-width: 1600px; -} - -#list-page-search-filters { - width: 100%; - height: max-content; -} - -#list-page-search-dropdown { - display: flex; - flex-direction: row; - align-items: center; -} - -#search-wrapper { - width: 100%; -} - -#list-page-search-pair { - flex-grow: 1; -} - -#filter-dropdown-wrapper { - box-sizing: border-box; - height: 84px; - width: 84px; -} - -#filter-dropdown-stack { - width: 100%; - height: 100%; - - position: relative; -} - -#filter-dropdown-stack.active { - transform: rotateZ(-90deg); -} - -#filter-dropdown-button { - position: absolute; - left: 0; - top: 0; - - height: 100%; - - display: block; -} - -#filter-dropdown-stack:hover > #filter-dropdown-button { - display: none; -} - -#filter-dropdown-button-hover { - position: absolute; - left: 0; - top: 0; - - height: 100%; - - display: none; -} - -#filter-dropdown-stack:hover > #filter-dropdown-button-hover { - display: block; -} - -#list-page-filters-flex { - display: none; - - width: 100%; - height: max-content; - - flex-direction: row; -} - -#list-page-filters-flex.active { - display: flex; -} - -.list-page-filter-section { - box-sizing: border-box; - - flex-basis: 0; - flex-grow: 1; - - height: 100%; - - display: flex; - flex-direction: column; -} - -#list-page-filters-background { - background-color: var(--distinction); -} - -.list-page-entry-text.achievement-name { - flex-grow: 3; - flex-basis: 0; -} - -.list-page-entry-text.achievement-description { - flex-grow: 6; - flex-basis: 0; -} - -.list-page-entry-text.achievement-stages { - flex-grow: 1; - flex-basis: 0; -} diff --git a/frontend/webpage/static/styles/login.css b/frontend/webpage/static/styles/login.css index abdc471..84c99d5 100644 --- a/frontend/webpage/static/styles/login.css +++ b/frontend/webpage/static/styles/login.css @@ -2,8 +2,6 @@ --form-spacing: 48px; --element-spacing: 12px; - - --error: #F95959; } #login-page { diff --git a/frontend/webpage/static/styles/profile.css b/frontend/webpage/static/styles/profile.css index 1192c49..f6f3c12 100644 --- a/frontend/webpage/static/styles/profile.css +++ b/frontend/webpage/static/styles/profile.css @@ -2,6 +2,26 @@ max-width: 1600px; } +#importing { + flex-direction: column; + align-items: center; + + display: none; +} + +#importing-text { + margin: 0; + height: 96px; + font-size: 64px; + line-height: 96px; + color: var(--foreground); +} + +#importing-loading { + height: 64px; + width: 64px; +} + .profile-list { width: 100%; height: max-content; @@ -150,7 +170,7 @@ border-radius: 8px; object-fit: contain; - background-color: var(--background); + background-color: var(--background-dark); position: absolute; } @@ -178,7 +198,7 @@ border-radius: 8px; - background-color: var(--background); + background-color: var(--background-dark); opacity: 0.8; display: block; diff --git a/frontend/webpage/static/styles/search.css b/frontend/webpage/static/styles/search.css new file mode 100644 index 0000000..2f4cd90 --- /dev/null +++ b/frontend/webpage/static/styles/search.css @@ -0,0 +1,330 @@ +#list-page-search-filters { + width: 100%; + height: max-content; +} + +#list-page-search-dropdown { + display: flex; + flex-direction: row; + align-items: center; +} + +#search-wrapper { + width: 100%; +} + +#list-page-search-pair { + flex-grow: 1; +} + +#filter-dropdown-wrapper { + box-sizing: border-box; + height: 84px; + width: 84px; +} + +#filter-dropdown-stack { + width: 100%; + height: 100%; + + position: relative; +} + +#filter-dropdown-stack.active { + transform: rotateZ(-90deg); +} + +#filter-dropdown-button { + position: absolute; + left: 0; + top: 0; + + height: 100%; + + display: block; +} + +#filter-dropdown-stack:hover > #filter-dropdown-button { + display: none; +} + +#filter-dropdown-button-hover { + position: absolute; + left: 0; + top: 0; + + height: 100%; + + display: none; +} + +#filter-dropdown-stack:hover > #filter-dropdown-button-hover { + display: block; +} + +#list-page-filters-flex { + display: none; + + width: 100%; + height: max-content; + + flex-direction: row; +} + +#list-page-filters-flex.active { + display: flex; +} + +.list-page-filter-section { + box-sizing: border-box; + + flex-basis: max-content; + flex-grow: 1; + + height: 100%; + + display: flex; + flex-direction: column; +} + +.list-page-filter-partition { + width: 20%; + max-width: 640px; +} + +.list-page-filter-chunk { + background-color: var(--distinction); + + width: 100%; + height: 100%; +} + +.list-page-filter { + box-sizing: border-box; + + width: 100%; + height: max-content; + + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; +} + +.list-page-filter-checkbox { + width: 28px; + height: 28px; + + background-color: var(--foreground); + + border: 3px solid var(--foreground); + border-radius: 8px; + + transition-property: background-color, border-color; + transition-duration: 0.15s; +} + +.list-page-filter:hover > .list-page-filter-checkbox { + background-color: var(--foreground); + border-color: var(--selected-accent1); +} + +.list-page-filter.selected > .list-page-filter-checkbox { + background-color: var(--selected-accent1); + border-color: var(--selected-accent1); +} + +.list-page-filter.selected:hover > .list-page-filter-checkbox { + background-color: var(--selected-accent0); + border-color: var(--selected-accent1); +} + +.list-page-filter-name, +.list-page-filter-label { + margin: 0; + padding: 16px; + + color: var(--foreground); + + font-size: 24px; + + user-select: none; +} + +.list-page-filter-label { + width: 40%; +} + +.list-page-filter-param { + padding: 4px; + width: 25%; + + font-size: 24px; + color: var(--background); + background-color: var(--foreground); + + border-radius: 8px; + + border: 0; + outline: none; +} + +#list-page-filters-background { + background-color: var(--distinction); +} + +.list-page-entry-text { + flex-basis: 0; +} + +.page.search { + max-width: 1720px; +} + +.list-page-search { + box-sizing: border-box; + + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; +} + +.list-page-search > label, +.list-page-search > input { + box-sizing: border-box; + padding: 12px 20px; + + color: var(--foreground); + + font-size: 24px; +} + +.list-page-search > label { + background-color: var(--accent-value2); +} + +.list-page-search > label:hover { + background-color: var(--accent-value3); +} + +.list-page-search > label:active { + background-color: var(--accent-value1); + + transition-property: background-color; + transition-duration: 0.15s; +} + +.list-page-search > input { + background-color: var(--distinction); + + border: 0; + + flex-grow: 1; + + outline: none; + + transition-property: background-color, color; + transition-duration: 0.075s; +} + +.list-page-search > input:focus { + background-color: var(--foreground); + + color: var(--background); +} + +.list-page-partitions { + box-sizing: border-box; + + width: 100%; + height: max-content; + + display: flex; + flex-direction: row; + justify-content: center; + align-items: flex-start; +} + + +.list-page-list-partition { + box-sizing: border-box; + + flex-grow: 1; +} + +.list-page-list { + border-radius: 8px; + + overflow: hidden; +} + +.list-page-header { + width: 100%; + height: 64px; + + background-color: var(--accent-value2); + + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + + color: var(--foreground); + font-size: 24px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.list-page-entry { + width: 100%; + height: 64px; + + background-color: var(--distinction); + + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + + color: var(--foreground); + font-size: 24px; +} + +.list-page-entry-icon { + width: 64px; + height: 64px; + + flex-grow: 0; +} + +.list-page-entry-text { + box-sizing: border-box; + + margin: 0; + padding: 0 12px; + height: 64px; + line-height: 64px; + + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.list-page-entry > .list-page-entry-text { + border-top: 1px solid var(--background); + border-left: 1px solid var(--background); +} + +.list-page-header > .list-page-entry-text { + border-left: 1px solid var(--accent-value0); +} + +#loading-results { + margin: 16px 0; + width: 100%; + height: 64px; + object-fit: contain; + + display: none; +} \ No newline at end of file diff --git a/frontend/webpage/static/styles/search_achievements.css b/frontend/webpage/static/styles/search_achievements.css new file mode 100644 index 0000000..7643733 --- /dev/null +++ b/frontend/webpage/static/styles/search_achievements.css @@ -0,0 +1,5 @@ +.list-page-entry-text.achievement-game-name { flex-grow: 1.75; } +.list-page-entry-text.achievement-name { flex-grow: 2; } +.list-page-entry-text.achievement-completion { flex-grow: 1; } +.list-page-entry-text.achievement-quality { flex-grow: 1; } +.list-page-entry-text.achievement-difficulty { flex-grow: 1; } \ No newline at end of file diff --git a/frontend/webpage/static/styles/theme.css b/frontend/webpage/static/styles/theme.css index c42e13e..24ecaf9 100644 --- a/frontend/webpage/static/styles/theme.css +++ b/frontend/webpage/static/styles/theme.css @@ -18,4 +18,6 @@ --selected-accent0: #2266CC; --selected-accent1: #3388FF; + + --error: #F95959; } diff --git a/sql/AuthProcs.sql b/sql/AuthProcs.sql index e760caa..c8717ac 100644 Binary files a/sql/AuthProcs.sql and b/sql/AuthProcs.sql differ diff --git a/sql/DataProcs.sql b/sql/DataProcs.sql new file mode 100644 index 0000000..f778d8b --- /dev/null +++ b/sql/DataProcs.sql @@ -0,0 +1,639 @@ +--------------------------------------- +-- GET USER NAME AND STATS PROCEDURE -- +--------------------------------------- + +CREATE PROCEDURE GetUserNameAndStats( + @userId INT, + @username VARCHAR(32) OUTPUT, + @completed INT OUTPUT, + @average INT OUTPUT, + @perfect INT OUTPUT +) +AS + +SELECT @username = Username +FROM [User] +WHERE ID = @userId + +IF @username IS NULL +BEGIN + PRINT 'No user with the specified ID was found' + RETURN 1 +END + +SELECT @completed = SUM(Completed) +FROM GameCompletionByUser +WHERE UserID = @userId + +SELECT @average = AVG((Completed * 100) / Total) +FROM GameCompletionByUser +WHERE UserID = @userId + +SELECT @perfect = COUNT(GameID) +FROM GameCompletionByUser +WHERE UserID = @userId AND Completed = Total + +RETURN 0 +GO + +SELECT * FROM [User] + +---------------------------------- +-- GET USER PLATFORMS PROCEDURE -- +---------------------------------- + +CREATE PROCEDURE GetUserPlatforms( + @userId INT +) +AS +IF NOT @userId IN (SELECT ID FROM [User]) +BEGIN + PRINT 'No user with the specified ID was found' + RETURN 1 +END +SELECT [Platform].ID, [PlatformName], (CASE WHEN UserID IS NOT NULL THEN 1 ELSE 0 END) AS Connected +FROM [Platform] +LEFT JOIN IsOn ON IsOn.PlatformID = [Platform].ID AND UserID = @userId +ORDER BY [Platform].ID +RETURN 0 +GO + +-------------------------------- +-- GET USER RATINGS PROCEDURE -- +-------------------------------- + +CREATE PROCEDURE GetUserRatings( + @userId INT +) +AS +IF NOT @userId IN (SELECT ID FROM [User]) +BEGIN + PRINT 'No user with the specified ID was found' + RETURN 1 +END +SELECT Game.Name AS GameName, Achievement.Name AS AchievementName, Quality, Difficulty, Rating.[Description] +FROM Rating +JOIN Achievement ON Achievement.ID = Rating.AchievementID +JOIN Game ON Game.ID = Achievement.GameID +WHERE UserID = @userId +RETURN 0 +GO + +------------------------------ +-- GET USER IMAGE PROCEDURE -- +------------------------------ + +CREATE PROCEDURE GetUserImage( + @userId INT +) +AS +IF NOT @userId IN (SELECT ID FROM [User]) +BEGIN + PRINT 'No user with the specified ID was found' + RETURN 1 +END +SELECT ProfileImage FROM [User] WHERE ID = @userId +RETURN 0 +GO + +------------------ +-- SET USERNAME -- +------------------ + +CREATE PROCEDURE SetUsername( + @userId INT, + @username VARCHAR(32) +) +AS +IF NOT @userId IN (SELECT ID FROM [User]) +BEGIN + PRINT 'No user with the specified ID was found' + RETURN 1 +END +UPDATE [User] SET Username = @username WHERE ID = @userId +RETURN 0 +GO + +------------------------------ +-- SET USER IMAGE PROCEDURE -- +------------------------------ + +CREATE PROCEDURE SetUserImage( + @userId INT, + @type ImageType, + @oldType ImageType OUTPUT +) +AS +IF NOT @userId IN (SELECT ID FROM [User]) +BEGIN + PRINT 'No user with the specified ID was found' + RETURN 1 +END +SELECT @oldType = ProfileImage FROM [User] WHERE ID = @userId +UPDATE [User] SET ProfileImage = @type WHERE ID = @userId +RETURN 0 +GO + +-------------------------- +-- ADD USER TO PLATFORM -- +-------------------------- + +CREATE PROCEDURE AddUserToPlatform( + @userId INT, + @platformId INT, + @platformUserID VARCHAR(32) +) +AS +IF NOT @userId IN (SELECT ID FROM [User]) +BEGIN + PRINT 'No user with the specified ID was found' + RETURN 1 +END +IF NOT @platformId IN (SELECT ID FROM [Platform]) +BEGIN + PRINT 'No platform with the specified ID was found' + RETURN 2 +END +IF EXISTS (SELECT * FROM IsOn WHERE UserID = @userId AND PlatformID = @platformId) +BEGIN + PRINT 'User already exists on specified platform' + RETURN 3 +END +INSERT INTO IsOn VALUES (@userId, @platformId, @platformUserId) +RETURN 0 +GO + +------------------------------- +-- REMOVE USER FROM PLATFORM -- +------------------------------- + +CREATE PROCEDURE RemoveUserFromPlatform( + @userId INT, + @platformId INT +) +AS +IF NOT @userId IN (SELECT ID FROM [User]) +BEGIN + PRINT 'No user with the specified ID was found' + RETURN 1 +END +IF NOT @platformId IN (SELECT ID FROM [Platform]) +BEGIN + PRINT 'No platform with the specified ID was found' + RETURN 2 +END +IF NOT EXISTS (SELECT UserID FROM IsOn WHERE UserID = @userId AND PlatformID = @platformId) +BEGIN + PRINT 'User does not exist on specified platform' + RETURN 3 +END +DELETE FROM IsOn WHERE UserID = @userId AND PlatformID = @platformId +DELETE FROM Progress WHERE UserID = @userId AND PlatformID = @platformId +DELETE FROM Owns WHERE UserID = @userId AND PlatformID = @platformId +RETURN 0 +GO + +------------------ +-- ADD PLATFORM -- +------------------ + +CREATE PROCEDURE AddPlatform( + @name VARCHAR(32), + @platformId INT OUTPUT +) +AS +IF @name IS NULL +BEGIN + PRINT 'Platform name cannot be null' + RETURN 1 +END +INSERT INTO [Platform] VALUES (@name) +SET @platformId = @@IDENTITY +RETURN 0 +GO + +--------------------- +-- REMOVE PLATFORM -- +--------------------- + +CREATE PROCEDURE RemovePlatform( + @platformId INT +) +AS +IF NOT @platformId IN (SELECT ID FROM [Platform]) +BEGIN + PRINT 'No platform with the specified ID was found' + RETURN 1 +END +IF @platformId IN (SELECT PlatformID FROM ExistsOn) +BEGIN + PRINT 'All games must be removed from the specified platform before it can be removed' + RETURN 2 +END +DELETE FROM [Platform] WHERE ID = @platformId +RETURN 0 +GO + +------------------- +-- GET PLATFORMS -- +------------------- + +CREATE PROCEDURE GetPlatforms +AS +SELECT ID, PlatformName FROM [Platform] +RETURN 0 +GO + +----------------------- +-- GET PLATFORM NAME -- +----------------------- + +CREATE PROCEDURE GetPlatformName( + @platformId INT, + @name VARCHAR(32) OUTPUT +) +AS +SELECT @name = PlatformName FROM [Platform] WHERE ID = @platformId +IF @name IS NULL +BEGIN + PRINT 'No platform with the specified ID was found' + RETURN 1 +END +RETURN 0 +GO + +----------------------- +-- GET PLATFORM ICON -- +----------------------- + +CREATE PROCEDURE GetPlatformIcon( + @platformId INT +) +AS +IF NOT @platformId IN (SELECT ID FROM [Platform]) +BEGIN + PRINT 'No platform with the specified ID was found' + RETURN 1 +END +SELECT Icon FROM [Platform] WHERE ID = @platformId +RETURN 0 +GO + +-------------- +-- ADD GAME -- +-------------- + +CREATE PROCEDURE AddGame( + @name VARCHAR(32), + @image ImageType, + @gameId INT OUTPUT +) +AS +IF @name IS NULL +BEGIN + PRINT 'Game name cannot be null' + RETURN 1 +END +IF @name IN (SELECT [Name] FROM Game) +BEGIN + PRINT 'Game with specified name already exists' + RETURN 2 +END +INSERT INTO Game VALUES (@name, @image) +SET @gameId = @@IDENTITY +RETURN 0 +GO + +--------------------- +-- ADD IF NOT GAME -- +--------------------- + +CREATE PROCEDURE AddIfNotGame( + @name VARCHAR(32), + @image VARCHAR(11), + @gameId INT OUTPUT +) +AS +IF @name IS NULL +BEGIN + PRINT 'Game name cannot be null' + RETURN 1 +END +-- Ideally game name wouldn't have to be unique, but I don't know of another way to sync games across platforms when they share no IDing system +IF NOT @name IN (SELECT [Name] FROM Game) +BEGIN + INSERT INTO Game VALUES (@name, @image) +END +SELECT @gameId = ID FROM Game WHERE [Name] = @name +RETURN 0 +GO + +----------------- +-- REMOVE GAME -- +----------------- + +CREATE PROCEDURE RemoveGame( + @gameId INT +) +AS +IF NOT @gameId IN (SELECT ID FROM Game) +BEGIN + PRINT 'No game with the specified ID was found' + RETURN 1 +END +DELETE FROM Game WHERE ID = @gameId +RETURN 0 +GO + +------------------- +-- GET GAME ICON -- +------------------- + +CREATE PROCEDURE GetGameIcon( + @gameId INT +) +AS +IF NOT @gameId IN (SELECT ID FROM [Game]) +BEGIN + PRINT 'No game with the specified ID was found' + RETURN 1 +END +SELECT Icon FROM [Game] WHERE ID = @gameId +RETURN 0 +GO + +---------------------- +-- ADD GAME TO USER -- +---------------------- + +CREATE PROCEDURE AddGameToUser( + @gameId INT, + @userId INT, + @platformId INT +) +AS +IF NOT @gameId IN (SELECT ID FROM Game) +BEGIN + PRINT 'No game with the specified ID was found' + RETURN 1 +END +IF NOT @userId IN (SELECT ID FROM [User]) +BEGIN + PRINT 'No user with the specified ID was found' + RETURN 2 +END +IF NOT @platformId IN (SELECT ID FROM [Platform]) +BEGIN + PRINT 'No platform with the specified ID was found' + RETURN 3 +END +IF NOT EXISTS (SELECT * FROM IsOn WHERE UserID = @userId AND PlatformID = @platformId) +BEGIN + PRINT 'User is not on specified platform' + RETURN 4 +END +IF EXISTS (SELECT * FROM Owns WHERE GameID = @gameId AND UserID = @userId AND PlatformID = @platformId) +BEGIN + PRINT 'Game is already owned by specified user on specified platform' + RETURN 5 +END +INSERT INTO Owns VALUES (@userId, @gameId, @platformId) +RETURN 0 +GO + +--------------------------- +-- REMOVE GAME FROM USER -- +--------------------------- + +CREATE PROCEDURE RemoveGameFromUser( + @gameId INT, + @userId INT, + @platformId INT +) +AS +IF NOT @gameId IN (SELECT ID FROM Game) +BEGIN + PRINT 'No game with the specified ID was found' + RETURN 1 +END +IF NOT @userId IN (SELECT ID FROM [User]) +BEGIN + PRINT 'No user with the specified ID was found' + RETURN 2 +END +IF NOT @platformId IN (SELECT ID FROM [Platform]) +BEGIN + PRINT 'No platform with the specified ID was found' + RETURN 3 +END +IF NOT EXISTS (SELECT * FROM Owns WHERE GameID = @gameId AND UserID = @userId AND PlatformID = @platformId) +BEGIN + PRINT 'Game is not owned by specified user on specified platform' + RETURN 4 +END +DELETE FROM Owns WHERE UserID = @userId AND GameID = @gameId AND PlatformID = @platformId +RETURN 0 +GO + +-------------------------- +-- ADD GAME TO PLATFORM -- +-------------------------- + +CREATE PROCEDURE AddGameToPlatform( + @gameId INT, + @platformId INT, + @platformGameId VARCHAR(32) +) +AS +IF NOT @gameId IN (SELECT ID FROM Game) +BEGIN + PRINT 'No game with the specified ID was found' + RETURN 1 +END +IF NOT @platformId IN (SELECT ID FROM [Platform]) +BEGIN + PRINT 'No platform with the specified ID was found' + RETURN 2 +END +IF EXISTS (SELECT * FROM ExistsOn WHERE GameID = @gameId AND PlatformID = @platformId) +BEGIN + PRINT 'Game already exists on specified platform' + RETURN 3 +END +INSERT INTO ExistsOn VALUES (@gameId, @platformId, @platformGameId) +RETURN 0 +GO + +------------------------------- +-- REMOVE GAME FROM PLATFORM -- +------------------------------- + +CREATE PROCEDURE RemoveGameFromPlatform( + @gameId INT, + @platformId INT +) +AS +IF NOT @gameId IN (SELECT ID FROM Game) +BEGIN + PRINT 'No game with the specified ID was found' + RETURN 1 +END +IF NOT @platformId IN (SELECT ID FROM [Platform]) +BEGIN + PRINT 'No platform with the specified ID was found' + RETURN 2 +END +IF NOT EXISTS (SELECT * FROM ExistsOn WHERE GameID = @gameId AND PlatformID = @platformId) +BEGIN + PRINT 'Game does not exist on specified platform' + RETURN 3 +END +DELETE FROM ExistsOn WHERE GameID = @gameId AND PlatformID = @platformId +RETURN 0 +GO + +--------------------- +-- ADD ACHIEVEMENT -- +--------------------- + +CREATE PROCEDURE AddAchievement( + @gameId INT, + @name VARCHAR(128), + @description VARCHAR(512), + @stages INT, + @image ImageType, + @achievementId INT OUTPUT +) +AS +IF NOT @gameId IN (SELECT ID FROM Game) +BEGIN + PRINT 'No game with the specified ID was found' + RETURN 1 +END +IF @name IS NULL +BEGIN + PRINT 'Achievement name cannot be null' + RETURN 2 +END +IF @stages IS NULL +BEGIN + PRINT 'Achievement stages cannot be null' + RETURN 3 +END +IF @name IN (SELECT [Name] FROM Achievement WHERE GameID = @gameId) +BEGIN + PRINT 'Achievement with specified name already exists for specified game' + RETURN 4 +END +INSERT INTO Achievement VALUES (@gameId, @name, @description, @stages, @image) +SET @achievementId = @@IDENTITY +RETURN 0 +GO + +---------------------------- +-- ADD IF NOT ACHIEVEMENT -- +---------------------------- + +CREATE PROCEDURE AddIfNotAchievement( + @gameId INT, + @name VARCHAR(128), + @description VARCHAR(512), + @stages INT, + @image VARCHAR(11), + @achievementId INT OUTPUT +) +AS +IF NOT @gameId IN (SELECT ID FROM Game) +BEGIN + PRINT 'No game with the specified ID was found' + RETURN 1 +END +IF @name IS NULL +BEGIN + PRINT 'Achievement name cannot be null' + RETURN 2 +END +IF @stages IS NULL +BEGIN + PRINT 'Achievement stages cannot be null' + RETURN 3 +END +IF NOT @name IN (SELECT [Name] FROM Achievement WHERE GameID = @gameId) +BEGIN + INSERT INTO Achievement VALUES (@gameId, @name, @description, @stages, @image) +END +SELECT @achievementId = ID FROM Achievement WHERE [Name] = @name AND GameID = @gameId +RETURN 0 +GO + +------------------------ +-- REMOVE ACHIEVEMENT -- +------------------------ + +CREATE PROCEDURE RemoveAchievement( + @achievementId INT +) +AS +IF NOT @achievementId IN (SELECT ID FROM Achievement) +BEGIN + PRINT 'No achievement with the specified ID was found' + RETURN 1 +END +DELETE FROM Achievement WHERE ID = @achievementId +RETURN 0 +GO + +-------------------------- +-- GET ACHIEVEMENT ICON -- +-------------------------- + +CREATE PROCEDURE GetAchievementIcon( + @achievementId INT +) +AS +IF NOT @achievementId IN (SELECT ID FROM Achievement) +BEGIN + PRINT 'No achievement with the specified ID was found' + RETURN 1 +END +SELECT Icon FROM Achievement WHERE ID = @achievementId +RETURN 0 +GO + +--------------------------------------- +-- SET ACHIEVEMENT PROGRESS FOR USER -- +--------------------------------------- + +CREATE PROCEDURE SetAchievementProgressForUser( + @userId INT, + @platformId INT, + @achievementId INT, + @progress INT +) +AS +IF NOT @userId IN (SELECT ID FROM [User]) +BEGIN + PRINT 'No user with the specified ID was found' + RETURN 1 +END +IF NOT @platformId IN (SELECT ID FROM [Platform]) +BEGIN + PRINT 'No platform with the specified ID was found' + RETURN 2 +END +IF NOT @achievementId IN (SELECT ID FROM Achievement) +BEGIN + PRINT 'No achievement with the specified ID was found' + RETURN 3 +END +IF EXISTS (SELECT * FROM Progress WHERE AchievementID = @achievementId AND UserID = @userId AND PlatformID = @platformId) +BEGIN + UPDATE Progress SET Progress = @progress WHERE AchievementID = @achievementId AND UserID = @userId AND PlatformID = @platformId +END +ELSE +BEGIN + INSERT INTO Progress VALUES (@userId, @platformId, @achievementId, @progress) +END +RETURN 0 +GO + diff --git a/sql/SearchProcs.sql b/sql/SearchProcs.sql new file mode 100644 index 0000000..867015d --- /dev/null +++ b/sql/SearchProcs.sql @@ -0,0 +1,60 @@ +----------------------- +-- SEARCH ACHIEVEMENTS -- +----------------------- + +CREATE PROCEDURE SearchAchievements( + @searchTerm VARCHAR(32), + @userId INT, + @completed BIT, + @minCompletion FLOAT, + @maxCompletion FLOAT, + @minDifficulty FLOAT, + @maxDifficulty FLOAT, + @minQuality FLOAT, + @maxQuality FLOAT +) +AS +IF @userId IS NULL AND @completed = 1 +BEGIN + PRINT 'Cannot search for completed achievements with no user specified' + RETURN 1 +END + +IF @searchTerm IS NULL OR @searchTerm = '' + SET @searchTerm = '%' +ELSE + SET @searchTerm = '%' + @searchTerm + '%' +PRINT @searchTerm + +IF NOT @userId IS NULL + SELECT TOP 100 Game.[Name] AS Game, Achievement.[Name], Completion, Difficulty, Quality + FROM Achievement + JOIN MaxProgress ON AchievementID = Achievement.ID AND UserID = @userId + JOIN Game ON Game.ID = GameID + JOIN AchievementCompletion AC ON AC.ID = Achievement.ID + JOIN AchievementRatings AR ON AR.ID = Achievement.ID + WHERE (Game.[Name] LIKE @searchTerm OR Achievement.[Name] LIKE @searchTerm) + AND (@completed <> 1 OR Progress = Stages ) + AND (@minCompletion IS NULL OR @minCompletion <= Completion) + AND (@maxCompletion IS NULL OR @maxCompletion >= Completion) + AND (@minDifficulty IS NULL OR @minDifficulty <= Difficulty) + AND (@maxDifficulty IS NULL OR @maxDifficulty >= Difficulty) + AND (@minQuality IS NULL OR @minQuality <= Quality ) + AND (@maxQuality IS NULL OR @maxQuality >= Quality ) +ELSE + SELECT TOP 100 Achievement.ID, Game.[Name] AS Game, Achievement.[Name], Completion, Quality, Difficulty + FROM Achievement + JOIN Game ON Game.ID = GameID + JOIN AchievementCompletion AC ON AC.ID = Achievement.ID + JOIN AchievementRatings AR ON AR.ID = Achievement.ID + WHERE (Game.[Name] LIKE @searchTerm OR Achievement.[Name] LIKE @searchTerm) + AND (@minCompletion IS NULL OR @minCompletion <= Completion) + AND (@maxCompletion IS NULL OR @maxCompletion >= Completion) + AND (@minDifficulty IS NULL OR @minDifficulty <= Difficulty) + AND (@maxDifficulty IS NULL OR @maxDifficulty >= Difficulty) + AND (@minQuality IS NULL OR @minQuality <= Quality ) + AND (@maxQuality IS NULL OR @maxQuality >= Quality ) +RETURN 0 +GO + +EXEC SearchAchievements '', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL \ No newline at end of file diff --git a/sql/CreateTables.sql b/sql/Tables.sql similarity index 91% rename from sql/CreateTables.sql rename to sql/Tables.sql index 10f86aa..87b6fa8 100644 --- a/sql/CreateTables.sql +++ b/sql/Tables.sql @@ -27,6 +27,9 @@ ----------------------------- +--CREATE TYPE ImageType FROM VARCHAR(4) NULL +--GO + CREATE TABLE [User] ( ID INT IDENTITY(0, 1) NOT NULL, Email VARCHAR(254) NOT NULL, @@ -36,7 +39,9 @@ CREATE TABLE [User] ( Hue INT NOT NULL CONSTRAINT HueDefault DEFAULT 0 CONSTRAINT HueConstraint CHECK (0 <= Hue AND Hue <= 360), - PFP VARCHAR(11) NULL, + ProfileImage ImageType, + [Admin] BIT NOT NULL + CONSTRAINT AdmivDefault DEFAULT 0, Verified BIT NOT NULL CONSTRAINT VerifiedDefault DEFAULT 0 PRIMARY KEY(ID) @@ -44,14 +49,15 @@ CREATE TABLE [User] ( CREATE TABLE [Platform] ( ID INT IDENTITY(0, 1) NOT NULL, - PlatformName VARCHAR(32) NOT NULL + PlatformName VARCHAR(32) NOT NULL, + Icon ImageType PRIMARY KEY(ID) ) CREATE TABLE [Game] ( ID INT IDENTITY(0, 1) NOT NULL, Name VARCHAR(32) NOT NULL, - Thumbnail VARCHAR(256) NULL + Icon ImageType PRIMARY KEY(ID) ) @@ -61,7 +67,7 @@ CREATE TABLE [Achievement] ( Name VARCHAR(128) NOT NULL, Description VARCHAR(512) NULL, Stages INT NOT NULL, - Thumbnail VARCHAR(256) NULL + Icon ImageType PRIMARY KEY(ID) FOREIGN KEY(GameID) REFERENCES [Game](ID) ON UPDATE CASCADE @@ -89,7 +95,7 @@ CREATE TABLE [Progress] ( PlatformID INT NOT NULL, AchievementID INT NOT NULL, Progress INT NOT NULL - PRIMARY KEY(UserID, AchievementID) + PRIMARY KEY(UserID, PlatformID, AchievementID) FOREIGN KEY(UserID) REFERENCES [User](ID) ON UPDATE CASCADE ON DELETE CASCADE, @@ -117,7 +123,7 @@ CREATE TABLE [IsOn] ( CREATE TABLE [ExistsOn] ( GameID INT NOT NULL, PlatformID INT NOT NULL, - PlatformGameID INT NOT NULL + PlatformGameID VARCHAR(32) NOT NULL PRIMARY KEY(GameID, PlatformID) FOREIGN KEY(GameID) REFERENCES [Game](ID) ON UPDATE CASCADE diff --git a/sql/UserData.sql b/sql/UserData.sql deleted file mode 100644 index 630b2af..0000000 --- a/sql/UserData.sql +++ /dev/null @@ -1,186 +0,0 @@ ---------------------------------------- --- GET USER NAME AND STATS PROCEDURE -- ---------------------------------------- - -CREATE PROCEDURE GetUserNameAndStats( - @userId INT, - @username VARCHAR(32) OUTPUT, - @completed INT OUTPUT, - @average INT OUTPUT, - @perfect INT OUTPUT -) -AS -BEGIN TRANSACTION - -SELECT @username = Username -FROM [User] -WHERE ID = @userId - -IF @username IS NULL -BEGIN - PRINT 'No user found with specified id' - ROLLBACK TRANSACTION - RETURN 1 -END - -DECLARE @progress TABLE (GameID INT, Completed INT, Total INT) -INSERT INTO @progress - SELECT GameID, SUM(CASE WHEN Progress.Progress = Achievement.Stages THEN 1 ELSE 0 END) AS Completed, COUNT(AchievementID) AS Total - FROM Achievement - JOIN Progress ON - Progress.UserID = @userId - AND Progress.AchievementID = Achievement.ID - GROUP BY GameID -COMMIT TRANSACTION - -SELECT @completed = SUM(Completed) -FROM @progress - -SELECT @average = AVG((Completed * 100) / Total) -FROM @progress - -SELECT @perfect = COUNT(GameID) -FROM @progress -WHERE Completed = Total - -RETURN 0 -GO - ----------------------------------- --- GET USER PLATFORMS PROCEDURE -- ----------------------------------- - -CREATE PROCEDURE GetUserPlatforms( - @userId INT -) -AS -SELECT [Platform].ID, [PlatformName], (CASE WHEN UserID IS NOT NULL THEN 1 ELSE 0 END) AS Connected -FROM [Platform] -LEFT JOIN IsOn ON IsOn.PlatformID = [Platform].ID -ORDER BY [Platform].ID -GO - --------------------------------- --- GET USER RATINGS PROCEDURE -- --------------------------------- - -CREATE PROCEDURE GetUserRatings( - @userId INT -) -AS -SELECT Game.Name AS GameName, Achievement.Name AS AchievementName, Quality, Difficulty, Rating.[Description] -FROM Rating -JOIN Achievement ON Achievement.ID = Rating.AchievementID -JOIN Game ON Game.ID = Achievement.GameID -WHERE UserID = @userId -GO - ------------------------------- --- GET USER IMAGE PROCEDURE -- ------------------------------- - -CREATE PROCEDURE GetUserImage( - @userId INT -) -AS -IF NOT EXISTS (SELECT * FROM [User] WHERE ID = @userId) -BEGIN - PRINT 'No user with specified ID found' - RETURN 1 -END -SELECT PFP FROM [User] WHERE ID = @userId -RETURN 0 -GO - ------------------- --- SET USERNAME -- ------------------- - -CREATE PROCEDURE SetUsername( - @userId INT, - @username VARCHAR(32) -) -AS -IF NOT EXISTS (SELECT * FROM [User] WHERE ID = @userId) -BEGIN - PRINT 'No user with specified ID found' - RETURN 1 -END -UPDATE [User] SET Username = @username WHERE ID = @userId -RETURN 0 -GO - ------------------------------- --- SET USER IMAGE PROCEDURE -- ------------------------------- - -CREATE PROCEDURE SetUserImage( - @userId INT, - @type VARCHAR(11) -) -AS -IF NOT EXISTS (SELECT * FROM [User] WHERE ID = @userId) -BEGIN - PRINT 'No user with specified ID found' - RETURN 1 -END -UPDATE [User] SET PFP = @type WHERE ID = @userId -RETURN 0 -GO - ---------------------------- --- ADD USER TO PROCEDURE -- ---------------------------- - -CREATE PROCEDURE AddPlatform( - @userId INT, - @platformId INT, - @platformUserID VARCHAR(32) -) -AS -IF NOT EXISTS (SELECT * FROM [User] WHERE ID = @userId) -BEGIN - PRINT 'No user with specified ID found' - RETURN 1 -END -IF NOT EXISTS (SELECT * FROM [Platform] WHERE ID = @platformId) -BEGIN - PRINT 'No platform with specified ID found' - RETURN 2 -END -IF EXISTS (SELECT * FROM IsOn WHERE UserID = @userId AND PlatformID = @platformId) -BEGIN - PRINT 'User already exists on platform' - RETURN 3 -END -INSERT INTO IsOn VALUES (@userId, @platformId, @platformUserId) -RETURN 0 -GO - --------------------------------- --- REMOVE USER FROM PROCEDURE -- --------------------------------- - -CREATE PROCEDURE RemovePlatform( - @userId INT, - @platformId INT -) -AS -IF NOT EXISTS (SELECT * FROM [User] WHERE ID = @userId) -BEGIN - PRINT 'No user with specified ID found' - RETURN 1 -END -IF NOT EXISTS (SELECT * FROM [Platform] WHERE ID = @platformId) -BEGIN - PRINT 'No platform with specified ID found' - RETURN 2 -END -IF NOT EXISTS (SELECT * FROM IsOn WHERE UserID = @userId AND PlatformID = @platformId) -BEGIN - PRINT 'User does not exist on platform' - RETURN 3 -END -DELETE FROM IsOn WHERE UserID = @userId AND PlatformID = @platformId -RETURN 0 -GO diff --git a/sql/Views.sql b/sql/Views.sql new file mode 100644 index 0000000..7cbc3a7 --- /dev/null +++ b/sql/Views.sql @@ -0,0 +1,35 @@ +-- The maximum progress a user has on an achievement across all platforms +CREATE VIEW MaxProgress +AS + SELECT UserID, AchievementID, MAX(Progress) AS Progress + FROM Progress + GROUP BY UserID, AchievementID +GO + +-- List of games and users with the number of completed achievements out of the total achievements the user has completed +CREATE VIEW GameCompletionByUser +AS + SELECT UserID, GameID, SUM(CASE WHEN Progress = Stages THEN 1 ELSE 0 END) AS Completed, COUNT(AchievementID) AS Total + FROM Achievement + JOIN MaxProgress ON AchievementID = Achievement.ID + GROUP BY UserID, GameID +GO + +-- List of achievements and the percentage of people who have completed it +CREATE VIEW AchievementCompletion +AS + SELECT Achievement.ID, (CASE WHEN COUNT(UserID) = 0 THEN NULL ELSE (SUM(CASE WHEN Progress = Stages THEN 1 ELSE 0 END) * 100 / COUNT(UserID)) END) AS Completion + FROM Achievement + LEFT JOIN MaxProgress ON AchievementID = Achievement.ID + GROUP BY Achievement.ID +GO + +-- List of achievements and their average quality and difficulty ratings filling with null as necessary +CREATE VIEW AchievementRatings +AS + SELECT Achievement.ID, AVG(Quality) AS Quality, AVG(Difficulty) AS Difficulty + FROM Achievement + LEFT JOIN Rating ON AchievementID = Achievement.ID + GROUP BY Achievement.ID +GO +