diff --git a/backend/.gitignore b/backend/.gitignore index ef5b413..16c407e 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -22,3 +22,9 @@ 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 diff --git a/backend/src/main/java/achievements/Application.java b/backend/src/main/java/achievements/Application.java index 4eac2aa..add0fab 100644 --- a/backend/src/main/java/achievements/Application.java +++ b/backend/src/main/java/achievements/Application.java @@ -1,20 +1,23 @@ package achievements; -import achievements.misc.DbConnectionService; +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.context.annotation.Bean; +import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -@SpringBootApplication +@SpringBootApplication(exclude = SecurityAutoConfiguration.class) +@EnableScheduling public class Application { public static void main(String[] args) { var context = SpringApplication.run(Application.class, args); // Verify the database connection succeeded - var db = context.getBean(DbConnectionService.class); + var db = context.getBean(DbConnection.class); if (db.getConnection() == null) { SpringApplication.exit(context, () -> 0); } @@ -25,9 +28,9 @@ public class Application { return new WebMvcConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { - registry - .addMapping("/*") - .allowedOrigins("*"); + registry + .addMapping("/**") + .allowedOrigins("*"); } }; } diff --git a/backend/src/main/java/achievements/controllers/DataController.java b/backend/src/main/java/achievements/controllers/DataController.java deleted file mode 100644 index e518812..0000000 --- a/backend/src/main/java/achievements/controllers/DataController.java +++ /dev/null @@ -1,11 +0,0 @@ -package achievements.controllers; - -import achievements.data.Profile; -import org.springframework.web.bind.annotation.RequestBody; - -public class DataController { - - public void getProfile(@RequestBody Profile.Query query) { - - } -} diff --git a/backend/src/main/java/achievements/controllers/LoginController.java b/backend/src/main/java/achievements/controllers/LoginController.java index 01eceb5..42bfa51 100644 --- a/backend/src/main/java/achievements/controllers/LoginController.java +++ b/backend/src/main/java/achievements/controllers/LoginController.java @@ -7,13 +7,13 @@ import achievements.services.AuthenticationService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; 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.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import static org.springframework.web.bind.annotation.RequestMethod.POST; - @RestController +@RequestMapping("/auth") public class LoginController { @Autowired @@ -26,17 +26,11 @@ public class LoginController { * * -1 => Unknown error */ - @RequestMapping(value = "/create_user", method = POST, consumes = "application/json", produces = "application/json") + @PostMapping(value = "/create_user", consumes = "application/json", produces = "application/json") public ResponseEntity createUser(@RequestBody User user) { var response = authService.createUser(user); if (response.status == 0) { - return ResponseEntity.ok( - new Session( - authService.session().generate(response.id), - response.id, - response.hue - ) - ); + return ResponseEntity.status(HttpStatus.CREATED).body(response.session); } else if (response.status > 0) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new APError(response.status)); } else { @@ -56,17 +50,11 @@ public class LoginController { * * -1 => Unknown error */ - @RequestMapping(value = "/login", method = POST, consumes = "application/json", produces = "application/json") + @PostMapping(value = "/login", consumes = "application/json", produces = "application/json") public ResponseEntity login(@RequestBody User user) { var response = authService.login(user); if (response.status == 0) { - return ResponseEntity.ok( - new Session( - authService.session().generate(response.id), - response.id, - response.hue - ) - ); + return ResponseEntity.ok(response.session); } else if (response.status > 0) { // Hardcoded 1 response code return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(new APError(1)); @@ -75,7 +63,16 @@ public class LoginController { } } - @RequestMapping(value = "/logout", method = POST, consumes = "application/json", produces = "application/json") + @PostMapping(value = "/refresh", consumes = "application/json", produces = "application/json") + public ResponseEntity refresh(@RequestBody Session key) { + if (authService.refresh(key)) { + return ResponseEntity.ok("{}"); + } else { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("{}"); + } + } + + @PostMapping(value = "/logout", consumes = "application/json") public ResponseEntity logout(@RequestBody Session session) { authService.logout(session); return ResponseEntity.ok("{}"); diff --git a/backend/src/main/java/achievements/controllers/PlatformController.java b/backend/src/main/java/achievements/controllers/PlatformController.java new file mode 100644 index 0000000..4da5ecf --- /dev/null +++ b/backend/src/main/java/achievements/controllers/PlatformController.java @@ -0,0 +1,42 @@ +package achievements.controllers; + +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.RestController; + +import javax.servlet.http.HttpServletResponse; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +@RestController +public class PlatformController { + + @Autowired + private PlatformService platforms; + + @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(); + + } + } +} diff --git a/backend/src/main/java/achievements/controllers/UserController.java b/backend/src/main/java/achievements/controllers/UserController.java new file mode 100644 index 0000000..cca7fd6 --- /dev/null +++ b/backend/src/main/java/achievements/controllers/UserController.java @@ -0,0 +1,108 @@ +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.SetUsername; +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") +public class UserController { + + @Autowired + private UserService userService; + + @GetMapping(value = "/{user}", produces = "application/json") + public ResponseEntity getProfile(@PathVariable("user") int user) { + var profile = userService.getProfile(user); + if (profile == null) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new APError(1, "Failed to get user profile")); + } else { + return ResponseEntity.ok(profile); + } + } + + @PostMapping(value = "/{user}/username", consumes = "application/json", produces = "application/json") + public ResponseEntity setUsername(@PathVariable("user") int userId, @RequestBody SetUsername username) { + var name = userService.setUsername(userId, username); + if (name == 0) { + return ResponseEntity.status(HttpStatus.CREATED).body("{}"); + } + return ResponseEntity.badRequest().body("{}"); + } + + @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(); + } + } + } + + @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()); + 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(); + return ResponseEntity.status(HttpStatus.CREATED).body("{ \"code\": 0, \"message\": \"Success\" }"); + } + + } catch (Exception e) { + e.printStackTrace(); + } + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("{ \"code\": -1, \"message\": \"Unknown error\" }"); + } + + @PostMapping(value = "/{user}/platforms/add", consumes = "application/json", produces = "application/json") + public ResponseEntity addPlatformForUser(@PathVariable("user") int userId, @RequestBody AddPlatformRequest request) { + var result = userService.addPlatform(userId, request); + if (result == 0) { + return ResponseEntity.status(HttpStatus.CREATED).body("{}"); + } else { + return ResponseEntity.badRequest().body("{}"); + } + } + + @PostMapping(value = "/{user}/platforms/remove", consumes = "application/json", produces = "application/json") + public ResponseEntity removePlatformForUser(@PathVariable("user") int userId, @RequestBody RemovePlatformRequest request) { + var result = userService.removePlatform(userId, request); + if (result == 0) { + return ResponseEntity.status(HttpStatus.CREATED).body("{}"); + } else { + return ResponseEntity.badRequest().body("{}"); + } + } +} diff --git a/backend/src/main/java/achievements/data/APPostRequest.java b/backend/src/main/java/achievements/data/APPostRequest.java new file mode 100644 index 0000000..fcf1b25 --- /dev/null +++ b/backend/src/main/java/achievements/data/APPostRequest.java @@ -0,0 +1,17 @@ +package achievements.data; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class APPostRequest { + + @JsonProperty("key") + private String key; + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } +} diff --git a/backend/src/main/java/achievements/data/Profile.java b/backend/src/main/java/achievements/data/Profile.java index c505eda..80c01cc 100644 --- a/backend/src/main/java/achievements/data/Profile.java +++ b/backend/src/main/java/achievements/data/Profile.java @@ -7,37 +7,53 @@ import java.util.List; public class Profile { - public static class Query { - @JsonProperty("username") - private StringFilter string; + public static class Platform { + @JsonProperty + private int id; + @JsonProperty("name") + private String name; + @JsonProperty("connected") + private boolean connected; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean getConnected() { + return connected; + } + + public void setConnected(boolean connected) { + this.connected = connected; + } } - @JsonProperty("id") - private int id; @JsonProperty("username") private String username; - @JsonProperty("plaforms") - private List platforms; - @JsonProperty("games") - private List games; - @JsonProperty("achievements") - private List achievements; - - public Profile(int id, String username, List platforms, List games, List achievements) { - this.id = id; - this.username = username; - this.platforms = platforms; - this.games = games; - this.achievements = achievements; - } - - public int getId() { - return id; - } - - public void setId(int id) { - this.id = id; - } + @JsonProperty("completed") + private int completed; + @JsonProperty("average") + private Integer average; + @JsonProperty("perfect") + private int perfect; + @JsonProperty("noteworthy") + private List noteworthy; + @JsonProperty("platforms") + private List platforms; + /*@JsonProperty("ratings") + private List ratings;*/ public String getUsername() { return username; @@ -47,27 +63,43 @@ public class Profile { this.username = username; } - public List getPlatforms() { + public int getCompleted() { + return completed; + } + + public void setCompleted(int completed) { + this.completed = completed; + } + + public Integer getAverage() { + return average; + } + + public void setAverage(Integer average) { + this.average = average; + } + + public int getPerfect() { + return perfect; + } + + public void setPerfect(int perfect) { + this.perfect = perfect; + } + + public List getNoteworthy() { + return noteworthy; + } + + public void setNoteworthy(List noteworthy) { + this.noteworthy = noteworthy; + } + + public List getPlatforms() { return platforms; } - public void setPlatforms(List platforms) { + public void setPlatforms(List platforms) { this.platforms = platforms; } - - public List getGames() { - return games; - } - - public void setGames(List games) { - this.games = games; - } - - public List getAchievements() { - return achievements; - } - - public void setAchievements(List achievements) { - this.achievements = achievements; - } } diff --git a/backend/src/main/java/achievements/data/Session.java b/backend/src/main/java/achievements/data/Session.java index 5aaabe1..179b21b 100644 --- a/backend/src/main/java/achievements/data/Session.java +++ b/backend/src/main/java/achievements/data/Session.java @@ -1,5 +1,6 @@ package achievements.data; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; public class Session { @@ -10,11 +11,14 @@ public class Session { private int id; @JsonProperty("hue") private int hue; + @JsonIgnore + private boolean used; public Session(String key, int id, int hue) { - this.key = key; - this.id = id; - this.hue = hue; + this.key = key; + this.id = id; + this.hue = hue; + this.used = false; } public String getKey() { @@ -40,4 +44,12 @@ public class Session { public void setHue(int hue) { this.hue = hue; } + + public boolean getUsed() { + return used; + } + + public void setUsed(boolean used) { + this.used = used; + } } diff --git a/backend/src/main/java/achievements/data/query/AddPlatformRequest.java b/backend/src/main/java/achievements/data/query/AddPlatformRequest.java new file mode 100644 index 0000000..fcc6098 --- /dev/null +++ b/backend/src/main/java/achievements/data/query/AddPlatformRequest.java @@ -0,0 +1,36 @@ +package achievements.data.query; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class AddPlatformRequest { + @JsonProperty("sessionKey") + private String sessionKey; + @JsonProperty("platformId") + private int platformId; + @JsonProperty("platformUserId") + private String platformUserId; + + public String getSessionKey() { + return sessionKey; + } + + public void setSessionKey(String sessionKey) { + this.sessionKey = sessionKey; + } + + public int getPlatformId() { + return platformId; + } + + public void setPlatformId(int platformId) { + this.platformId = platformId; + } + + public String getPlatformUserId() { + return platformUserId; + } + + public void setPlatformUserId(String platformUserId) { + this.platformUserId = platformUserId; + } +} diff --git a/backend/src/main/java/achievements/data/query/RemovePlatformRequest.java b/backend/src/main/java/achievements/data/query/RemovePlatformRequest.java new file mode 100644 index 0000000..4f88d6b --- /dev/null +++ b/backend/src/main/java/achievements/data/query/RemovePlatformRequest.java @@ -0,0 +1,26 @@ +package achievements.data.query; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class RemovePlatformRequest { + @JsonProperty("sessionKey") + private String sessionKey; + @JsonProperty("platformId") + private int platformId; + + public String getSessionKey() { + return sessionKey; + } + + public void setSessionKey(String sessionKey) { + this.sessionKey = sessionKey; + } + + public int getPlatformId() { + return platformId; + } + + public void setPlatformId(int platformId) { + this.platformId = platformId; + } +} diff --git a/backend/src/main/java/achievements/data/query/SetUsername.java b/backend/src/main/java/achievements/data/query/SetUsername.java new file mode 100644 index 0000000..50dca0e --- /dev/null +++ b/backend/src/main/java/achievements/data/query/SetUsername.java @@ -0,0 +1,36 @@ +package achievements.data.query; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class SetUsername { + @JsonProperty("sessionKey") + private String sessionKey; + @JsonProperty("userId") + private int userId; + @JsonProperty("username") + private String username; + + public String getSessionKey() { + return sessionKey; + } + + public void setSessionKey(String sessionKey) { + this.sessionKey = sessionKey; + } + + public int getUserId() { + return userId; + } + + public void setUserId(int userId) { + this.userId = userId; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } +} diff --git a/backend/src/main/java/achievements/misc/DbConnectionService.java b/backend/src/main/java/achievements/misc/DbConnection.java similarity index 94% rename from backend/src/main/java/achievements/misc/DbConnectionService.java rename to backend/src/main/java/achievements/misc/DbConnection.java index d10120a..1d7ea59 100644 --- a/backend/src/main/java/achievements/misc/DbConnectionService.java +++ b/backend/src/main/java/achievements/misc/DbConnection.java @@ -10,7 +10,7 @@ import java.sql.SQLException; import com.microsoft.sqlserver.jdbc.SQLServerDataSource; @Component -public class DbConnectionService { +public class DbConnection { private Connection connection; @@ -23,7 +23,7 @@ public class DbConnectionService { @Value("${database.user.password}") private String password; - public DbConnectionService() {} + public DbConnection() {} @PostConstruct public void connect() { diff --git a/backend/src/main/java/achievements/misc/SessionManager.java b/backend/src/main/java/achievements/misc/SessionManager.java index 942482e..61a66f8 100644 --- a/backend/src/main/java/achievements/misc/SessionManager.java +++ b/backend/src/main/java/achievements/misc/SessionManager.java @@ -1,26 +1,64 @@ package achievements.misc; +import achievements.data.Session; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.ArrayList; import java.util.HashMap; +@Component public class SessionManager { - private HashMap session; + private HashMap sessions; public SessionManager() { - session = new HashMap(); + sessions = new HashMap(); } - public String generate(Integer user) { + public Session generate(int user, int hue) { var key = HashManager.encode(HashManager.generateBytes(16)); - session.put(key, user); - return key; + var session = new Session(key, user, hue); + sessions.put(key, session); + return session; } public int getUser(String key) { - return session.get(key); + return sessions.get(key).getId(); } public void remove(String key) { - session.remove(key); + sessions.remove(key); + } + + public boolean validate(int user, String key) { + var foreign = sessions.get(key); + return foreign != null && user == foreign.getId(); + } + + public boolean refresh(String key) { + var foreign = sessions.get(key); + if (foreign != null) { + foreign.setUsed(true); + return true; + } else { + return false; + } + } + + // Clean up inactive sessions + @Scheduled(cron = "0 */30 * * * *") + public void clean() { + var remove = new ArrayList(); + sessions.forEach((key, session) -> { + if (!session.getUsed()) { + remove.add(session.getKey()); + } else { + session.setUsed(false); + } + }); + for (var session : remove) { + sessions.remove(session); + } } } diff --git a/backend/src/main/java/achievements/services/AuthenticationService.java b/backend/src/main/java/achievements/services/AuthenticationService.java index 5ff9f78..d42efe9 100644 --- a/backend/src/main/java/achievements/services/AuthenticationService.java +++ b/backend/src/main/java/achievements/services/AuthenticationService.java @@ -2,7 +2,7 @@ package achievements.services; import achievements.data.Session; import achievements.data.User; -import achievements.misc.DbConnectionService; +import achievements.misc.DbConnection; import achievements.misc.Password; import achievements.misc.SessionManager; import org.springframework.beans.factory.annotation.Autowired; @@ -16,37 +16,33 @@ public class AuthenticationService { public static class LoginResponse { public int status; - public Integer id; - public int hue; + public Session session; public LoginResponse() { this.status = 0; - this.id = null; - this.hue = 0; } public LoginResponse(int status) { - this.status = status; - this.id = null; + this.status = status; + this.session = null; } - public LoginResponse(int status, int id, int hue) { - this.status = status; - this.id = id; - this.hue = hue; + public LoginResponse(int status, Session session) { + this.status = status; + this.session = session; } } @Autowired - private DbConnectionService dbs; + private DbConnection dbs; private Connection db; + @Autowired private SessionManager session; @PostConstruct private void init() { db = dbs.getConnection(); - session = new SessionManager(); } public LoginResponse createUser(User user) { @@ -70,8 +66,10 @@ public class AuthenticationService { statement.execute(); var response = new LoginResponse( statement.getInt(1), - statement.getInt(6), - statement.getInt(7) + session.generate( + statement.getInt(6), + statement.getInt(7) + ) ); statement.close(); @@ -93,7 +91,13 @@ public class AuthenticationService { var salt = result.getString("Salt"); var hash = result.getString("Password"); if (Password.validate(salt, user.getPassword(), hash)) { - response = new LoginResponse(0, result.getInt("ID"), result.getInt("Hue")); + response = new LoginResponse( + 0, + session.generate( + result.getInt("ID"), + result.getInt("Hue") + ) + ); } else { response = new LoginResponse(2); } @@ -107,6 +111,8 @@ public class AuthenticationService { return response; } + public boolean refresh(Session key) { return session.refresh(key.getKey()); } + public void logout(Session key) { session.remove(key.getKey()); } diff --git a/backend/src/main/java/achievements/services/DataService.java b/backend/src/main/java/achievements/services/DataService.java deleted file mode 100644 index af76afd..0000000 --- a/backend/src/main/java/achievements/services/DataService.java +++ /dev/null @@ -1,40 +0,0 @@ -package achievements.services; - -import achievements.data.Achievement; -import achievements.data.Game; -import achievements.data.Profile; -import achievements.misc.DbConnectionService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import javax.annotation.PostConstruct; -import java.sql.Connection; -import java.util.List; - -@Service -public class DataService { - - @Autowired - private DbConnectionService dbs; - private Connection db; - - @Autowired - private AuthenticationService auth; - - @PostConstruct - private void init() { - db = dbs.getConnection(); - } - - /*public List getUsers() { - - } - - public List getGames() { - - } - - public List getProfiles() { - - }*/ -} diff --git a/backend/src/main/java/achievements/services/PlatformService.java b/backend/src/main/java/achievements/services/PlatformService.java new file mode 100644 index 0000000..ab1d7f1 --- /dev/null +++ b/backend/src/main/java/achievements/services/PlatformService.java @@ -0,0 +1,21 @@ +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 PlatformService { + + @Autowired + private DbConnection dbs; + private Connection db; + + @PostConstruct + private void init() { + db = dbs.getConnection(); + } +} diff --git a/backend/src/main/java/achievements/services/UserService.java b/backend/src/main/java/achievements/services/UserService.java new file mode 100644 index 0000000..7f677d9 --- /dev/null +++ b/backend/src/main/java/achievements/services/UserService.java @@ -0,0 +1,193 @@ +package achievements.services; + +import achievements.data.Profile; +import achievements.data.query.AddPlatformRequest; +import achievements.data.query.RemovePlatformRequest; +import achievements.data.query.SetUsername; +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.sql.Types; +import java.util.ArrayList; +import java.util.HashMap; + +@Service +public class UserService { + + @Autowired + private DbConnection dbs; + private Connection db; + + @Autowired + private AuthenticationService auth; + + @PostConstruct + private void init() { + db = dbs.getConnection(); + } + + public Profile getProfile(int userId) { + try { + var profile = (Profile) null; + { + var stmt = db.prepareCall("{? = call GetUserNameAndStats(?, ?, ?, ?, ?)}"); + stmt.registerOutParameter(1, Types.INTEGER); + stmt.setInt(2, userId); + stmt.registerOutParameter(3, Types.VARCHAR); + stmt.registerOutParameter(4, Types.INTEGER); + stmt.registerOutParameter(5, Types.INTEGER); + stmt.registerOutParameter(6, Types.INTEGER); + + stmt.execute(); + if (stmt.getInt(1) == 0) { + profile = new Profile(); + profile.setUsername(stmt.getString(3)); + profile.setCompleted(stmt.getInt(4)); + var average = stmt.getString(5); + profile.setPerfect(stmt.getInt(6)); + + if (average != null) { + profile.setAverage(Integer.parseInt(average)); + } + } + } + + { + var stmt = db.prepareCall("{call GetUserPlatforms(?)}"); + stmt.setInt(1, userId); + + var results = stmt.executeQuery(); + var platforms = new ArrayList(); + while (results.next()) { + var platform = new Profile.Platform(); + platform.setId (results.getInt ("ID" )); + platform.setName (results.getString ("PlatformName")); + platform.setConnected(results.getBoolean("Connected" )); + platforms.add(platform); + } + profile.setPlatforms(platforms); + } + + return profile; + } catch (SQLException e) { + e.printStackTrace(); + } catch (NumberFormatException e) { + e.printStackTrace(); + } + return null; + } + + public int setUsername(int userId, SetUsername username) { + try { + if (auth.session().validate(userId, username.getSessionKey()) && username.getUsername().length() > 0 && username.getUsername().length() <= 32) { + var stmt = db.prepareCall("{call SetUsername(?, ?)}"); + stmt.setInt(1, userId); + stmt.setString(2, username.getUsername()); + + stmt.execute(); + return 0; + } + } catch (Exception e) { + e.printStackTrace(); + } + 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) { + 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) { + e.printStackTrace(); + } + return null; + } + + public String setProfileImageType(int userId, String sessionKey, String type) { + try { + if (type.matches("image/.*")) { + type = type.substring(6); + var extension = VALID_IMAGE_TYPES.get(type); + if (!auth.session().validate(userId, sessionKey)) { + return "forbidden"; + } else if (extension == null) { + return "unsupported_type"; + } else { + var stmt = db.prepareCall("{call SetUserImage(?, ?)}"); + stmt.setInt(1, userId); + stmt.setString(2, type); + + stmt.execute(); + + return extension; + } + } else { + return "not_an_image"; + } + } catch (Exception e) { + e.printStackTrace(); + } + 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()); + + stmt.execute(); + + return 0; + } + } catch (Exception e) { + e.printStackTrace(); + } + return -1; + } + + public int removePlatform(int userId, RemovePlatformRequest request) { + try { + if (auth.session().validate(userId, request.getSessionKey())) { + var stmt = db.prepareCall("{call RemovePlatform(?, ?)}"); + stmt.setInt(1, userId); + stmt.setInt(2, request.getPlatformId()); + + stmt.execute(); + + return 0; + } + } catch (Exception e) { + e.printStackTrace(); + } + return -1; + } +} diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index 589c0f1..7b8b2c0 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -1,2 +1,6 @@ server.port = 4730 spring.application.name = Achievements Project +spring.jackson.default-property-inclusion=always + +server.session.cookie.secure = false + diff --git a/frontend/config/base.json b/frontend/config/base.json index 0967ef4..1260d47 100644 --- a/frontend/config/base.json +++ b/frontend/config/base.json @@ -1 +1,5 @@ -{} +{ + "hosts": { + "backend": "https://localhost:4730" + } +} diff --git a/frontend/config/debug.json b/frontend/config/debug.json index 912143b..b5c0161 100644 --- a/frontend/config/debug.json +++ b/frontend/config/debug.json @@ -2,6 +2,9 @@ "extends": [ "config/base.json" ], + "hosts": { + "frontend": "http://localhost:8080" + }, "build": "debug", "port": 8080 } diff --git a/frontend/config/release.json b/frontend/config/release.json index d8c144f..1f6800c 100644 --- a/frontend/config/release.json +++ b/frontend/config/release.json @@ -2,6 +2,9 @@ "extends": [ "config/base.json" ], + "hosts": { + "frontend": "http://localhost" + }, "build": "release", "port": 80 } diff --git a/frontend/package.json b/frontend/package.json index 38b83cc..a026d25 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -3,17 +3,19 @@ "version": "1.0.0", "description": "Cross platform achievement tracker", "repository": "github:Gnarwhal/AchievementProject", - "main": "static_server.js", + "main": "server.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "debug": "node static_server.js config/debug.json", - "release": "node static_server.js config/release.json" + "debug": "node server.js config/debug.json", + "release": "node server.js config/release.json" }, "author": "", "license": "ISC", "dependencies": { "express": "^4.17.1", "morgan": "^1.10.0", + "passport": "^0.4.1", + "passport-steam": "^1.0.15", "promptly": "^3.2.0" } } diff --git a/frontend/server.js b/frontend/server.js new file mode 100644 index 0000000..c92aa21 --- /dev/null +++ b/frontend/server.js @@ -0,0 +1,62 @@ +const fs = require('fs' ); +const path = require('path' ); + +const https = require('https' ); +const express = require('express' ); +const morgan = require('morgan' ); +const passport = require('passport'); +const SteamStrategy = require('passport-steam').Strategy; + +const promptly = require('promptly'); + +const config = require('./config.js').load(process.argv[2]); + +if (config.build === 'debug') { + process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0; +} + +console.log(`Running server at '${config.hosts.frontend}'`); + +passport.use(new SteamStrategy({ + returnURL: `${config.hosts.frontend}/profile/steam`, + realm: `${config.hosts.frontend}`, + profile: false, +})); + +const app = express(); +app.use("/", morgan("dev")); +app.use("/static", express.static("webpage/static")); +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")); +}); +app.get("/about", (req, res) => { + res.sendFile(path.join(__dirname + "/webpage/about.html")); +}); +app.get("/profile/:id", (req, res) => { + res.sendFile(path.join(__dirname + "/webpage/profile.html")); +}); +app.get("/auth/steam", passport.authenticate('steam'), (req, res) => {}); + +// --- API Forward --- // + +app.use("/api/*", (req, res) => { + res.redirect(307, `${config.hosts.backend}/${req.params[0]}`) +}); + +// ------------------- // + +const server = app.listen(config.port); + +const prompt = input => { + if (/q(?:uit)?|exit/i.test(input)) { + server.close(); + } else { + promptly.prompt('') + .then(prompt); + } +}; + +prompt(); \ No newline at end of file diff --git a/frontend/static_server.js b/frontend/static_server.js deleted file mode 100644 index 1ac3862..0000000 --- a/frontend/static_server.js +++ /dev/null @@ -1,26 +0,0 @@ -const express = require('express' ); -const morgan = require('morgan' ); -const promptly = require('promptly'); - -const config = require('./config.js').load(process.argv[2]); - -if (config.build === 'debug') { - process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0; -} - -const app = express(); -app.use("/", morgan("dev")); -app.use("/", express.static("webpage")); - -const server = app.listen(config.port); - -const prompt = input => { - if (/q(?:uit)?|exit/i.test(input)) { - server.close(); - } else { - promptly.prompt('') - .then(prompt); - } -}; - -prompt(); \ No newline at end of file diff --git a/frontend/webpage/about.html b/frontend/webpage/about.html new file mode 100644 index 0000000..eb54e54 --- /dev/null +++ b/frontend/webpage/about.html @@ -0,0 +1,42 @@ + + + + + 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/index.html b/frontend/webpage/index.html index 0c7ee88..dc587ce 100644 --- a/frontend/webpage/index.html +++ b/frontend/webpage/index.html @@ -4,9 +4,9 @@ Achievements Project - - - + + +
- +
- - + + + \ No newline at end of file diff --git a/frontend/webpage/login.html b/frontend/webpage/login.html index c6418f3..1614fa9 100644 --- a/frontend/webpage/login.html +++ b/frontend/webpage/login.html @@ -3,9 +3,9 @@ Achievements Project | Login - - - + + + @@ -44,7 +44,8 @@ - - + + + \ No newline at end of file diff --git a/frontend/webpage/profile.html b/frontend/webpage/profile.html new file mode 100644 index 0000000..8c67db2 --- /dev/null +++ b/frontend/webpage/profile.html @@ -0,0 +1,138 @@ + + + + + Achievements Project + + + + + + + +
+
+
+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/frontend/webpage/res/dummy_achievement.png b/frontend/webpage/res/dummy_achievement.png deleted file mode 100644 index 3441a23..0000000 Binary files a/frontend/webpage/res/dummy_achievement.png and /dev/null differ diff --git a/frontend/webpage/res/dummy_game.png b/frontend/webpage/res/dummy_game.png deleted file mode 100644 index e98587b..0000000 Binary files a/frontend/webpage/res/dummy_game.png and /dev/null differ diff --git a/frontend/webpage/res/guest_pfp.png b/frontend/webpage/res/guest_pfp.png deleted file mode 100644 index 83e1bfc..0000000 Binary files a/frontend/webpage/res/guest_pfp.png and /dev/null differ diff --git a/frontend/webpage/res/old_temp_pfp.png b/frontend/webpage/res/old_temp_pfp.png deleted file mode 100644 index a33bede..0000000 Binary files a/frontend/webpage/res/old_temp_pfp.png and /dev/null differ diff --git a/frontend/webpage/res/psn.png b/frontend/webpage/res/psn.png deleted file mode 100644 index cc2a8b2..0000000 Binary files a/frontend/webpage/res/psn.png and /dev/null differ diff --git a/frontend/webpage/res/steam.png b/frontend/webpage/res/steam.png deleted file mode 100644 index 3098061..0000000 Binary files a/frontend/webpage/res/steam.png and /dev/null differ diff --git a/frontend/webpage/res/xbox.png b/frontend/webpage/res/xbox.png deleted file mode 100644 index 6f2730c..0000000 Binary files a/frontend/webpage/res/xbox.png and /dev/null differ diff --git a/frontend/webpage/scripts/index.js b/frontend/webpage/scripts/index.js deleted file mode 100644 index 1aeb9c7..0000000 --- a/frontend/webpage/scripts/index.js +++ /dev/null @@ -1,130 +0,0 @@ -let session = null; -const loadSession = () => { - session = JSON.parse(window.sessionStorage.getItem('session')); - if (session) { - document.querySelector(":root").style.setProperty('--accent-hue', session.hue); - } -}; - -const expandTemplates = async () => { - template.apply("navbar").values([ - { section: "left" }, - { section: "right" } - ]); - template.apply("navbar-section-left").values([ - { item: "games", title: "Games" }, - { item: "achievements", title: "Achievements" } - ]); - if (session) { - template.apply("navbar-section-right").values([ - { item: "profile", title: "Profile" }, - { item: "logout", title: "Logout" } - ]); - } else { - template.apply("navbar-section-right").values([ - { item: "login", title: "Login" } - ]); - } - template.apply("content-body").values([ - { page: "games", title: "Games" }, - { page: "achievements", title: "Achievements" }, - { page: "profile", title: "Profile" } - ]); - template.apply("extern-games-page" ).values("games_page" ); - template.apply("extern-achievements-page").values("achievements_page"); - template.apply("extern-profile-page" ).values("profile_page" ); - - await template.expand(); -}; - -let pages = null; -const loadPages = () => { - pages = document.querySelectorAll(".page"); -} - -const connectNavbar = () => { - const navItems = document.querySelectorAll(".navbar-item"); - - for (const item of navItems) { - if (item.dataset.pageName === "logout") { - item.addEventListener("click", (clickEvent) => { - fetch('https://localhost:4730/logout', { - method: 'POST', - mode: 'cors', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ key: session.key }) - }) - .then(response => { - window.sessionStorage.removeItem('session'); - window.location.href = "/login.html"; - }); - }); - } else if (item.dataset.pageName === "login") { - item.addEventListener("click", (clickEvent) => window.location.href = "/login.html"); - } else { - item.addEventListener("click", (clickEvent) => { - const navItemPageId = item.dataset.pageName + "-page" - for (const page of pages) { - if (page.id === navItemPageId) { - page.style.display = "block"; - } else { - page.style.display = "none"; - } - } - }); - } - } -}; - -const connectProfile = () => { - const games = document.querySelector("#profile-games-header"); - const achievements = document.querySelector("#profile-achievements-header"); - - games.children[0].addEventListener("click", (clickEvent) => { - for (const page of pages) { - if (page.id === "games-page") { - page.style.display = "block"; - } else { - page.style.display = "none"; - } - } - }); - - achievements.children[0].addEventListener("click", (clickEvent) => { - for (page of pages) { - if (page.id === "achievements-page") { - page.style.display = "block"; - } else { - page.style.display = "none"; - } - } - }); -} - -const loadFilters = () => { - const filters = document.querySelectorAll(".list-page-filter"); - for (let filter of filters) { - filter.addEventListener("click", (clickEvent) => { - if (filter.classList.contains("selected")) { - filter.classList.remove("selected"); - } else { - filter.classList.add("selected"); - } - }); - } -} - -window.addEventListener("load", async (loadEvent) => { - loadSession(); - - await expandTemplates(); - - loadPages(); - - connectNavbar(); - connectProfile(); - - loadFilters(); -}); diff --git a/frontend/webpage/static/res/cancel-hover.svg b/frontend/webpage/static/res/cancel-hover.svg new file mode 100644 index 0000000..bae7ec4 --- /dev/null +++ b/frontend/webpage/static/res/cancel-hover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/webpage/static/res/cancel.svg b/frontend/webpage/static/res/cancel.svg new file mode 100644 index 0000000..f344111 --- /dev/null +++ b/frontend/webpage/static/res/cancel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/webpage/static/res/completion.svg b/frontend/webpage/static/res/completion.svg new file mode 100644 index 0000000..218b4ba --- /dev/null +++ b/frontend/webpage/static/res/completion.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/webpage/static/res/dropdown-hover.svg b/frontend/webpage/static/res/dropdown-hover.svg new file mode 100644 index 0000000..2382e2b --- /dev/null +++ b/frontend/webpage/static/res/dropdown-hover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/webpage/static/res/dropdown.svg b/frontend/webpage/static/res/dropdown.svg new file mode 100644 index 0000000..fed35a7 --- /dev/null +++ b/frontend/webpage/static/res/dropdown.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/webpage/static/res/edit-hover.svg b/frontend/webpage/static/res/edit-hover.svg new file mode 100644 index 0000000..63ab387 --- /dev/null +++ b/frontend/webpage/static/res/edit-hover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/webpage/static/res/edit.svg b/frontend/webpage/static/res/edit.svg new file mode 100644 index 0000000..8c1299c --- /dev/null +++ b/frontend/webpage/static/res/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/webpage/static/res/save-hover.svg b/frontend/webpage/static/res/save-hover.svg new file mode 100644 index 0000000..0f23a7c --- /dev/null +++ b/frontend/webpage/static/res/save-hover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/webpage/static/res/save.svg b/frontend/webpage/static/res/save.svg new file mode 100644 index 0000000..be0a911 --- /dev/null +++ b/frontend/webpage/static/res/save.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/webpage/static/res/upload-hover.svg b/frontend/webpage/static/res/upload-hover.svg new file mode 100644 index 0000000..6a91f83 --- /dev/null +++ b/frontend/webpage/static/res/upload-hover.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/webpage/static/res/upload-invalid.svg b/frontend/webpage/static/res/upload-invalid.svg new file mode 100644 index 0000000..2fe8002 --- /dev/null +++ b/frontend/webpage/static/res/upload-invalid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/webpage/static/res/upload.svg b/frontend/webpage/static/res/upload.svg new file mode 100644 index 0000000..cf41f16 --- /dev/null +++ b/frontend/webpage/static/res/upload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/webpage/static/scripts/about.js b/frontend/webpage/static/scripts/about.js new file mode 100644 index 0000000..b64ba7a --- /dev/null +++ b/frontend/webpage/static/scripts/about.js @@ -0,0 +1,8 @@ +window.addEventListener("load", async (loadEvent) => { + await loadCommon(); + + await commonTemplates(); + await template.expand(); + + connectNavbar(); +}); \ No newline at end of file diff --git a/frontend/webpage/static/scripts/common.js b/frontend/webpage/static/scripts/common.js new file mode 100644 index 0000000..6bec535 --- /dev/null +++ b/frontend/webpage/static/scripts/common.js @@ -0,0 +1,97 @@ +let root = null; +const loadRoot = () => { + const rootElement = document.documentElement; + + root = {}; + root.getProperty = (name) => window.getComputedStyle(document.documentElement).getPropertyValue(name); + root.setProperty = (name, value) => { + rootElement.style.setProperty(name, value); + } +}; + +let session = null; +const loadSession = async () => { + window.addEventListener('beforeunload', (beforeUnloadEvent) => { + if (session) { + window.sessionStorage.setItem('session', JSON.stringify(session)); + } else { + window.sessionStorage.removeItem('session'); + } + }); + + session = JSON.parse(window.sessionStorage.getItem('session')); + if (session) { + root.setProperty('--accent-hue', session.hue); + + await fetch(`/api/auth/refresh`, { + method: 'POST', + mode: 'cors', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ key: session.key }) + }) + .then(async response => ({ status: response.status, data: await response.json() })) + .then(response => { + if (response.status !== 200 && window.location.pathname != "/login") { + delete session.key; + window.location.href = "/login"; + } + }); + } +}; + +const loadCommon = async () => { + loadRoot(); + await loadSession(); +} + +const commonTemplates = async () => { + template.apply("navbar").values([ + { section: "left" }, + { section: "right" } + ]); + template.apply("navbar-section-left").values([ + { item: "project", title: "Project" }, + { item: "about", title: "About" } + ]); + if (session) { + template.apply("navbar-section-right").values([ + { item: "profile", title: "Profile" }, + { item: "logout", title: "Logout" } + ]); + } else { + template.apply("navbar-section-right").values([ + { item: "login", title: "Login" } + ]); + } +}; + +const connectNavbar = () => { + const navItems = document.querySelectorAll(".navbar-item"); + + 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"; + }); + }); + } 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}`); + } + } +}; \ No newline at end of file diff --git a/frontend/webpage/static/scripts/index.js b/frontend/webpage/static/scripts/index.js new file mode 100644 index 0000000..3c33957 --- /dev/null +++ b/frontend/webpage/static/scripts/index.js @@ -0,0 +1,24 @@ +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/scripts/login.js b/frontend/webpage/static/scripts/login.js similarity index 91% rename from frontend/webpage/scripts/login.js rename to frontend/webpage/static/scripts/login.js index 5bd4295..8f7f8fc 100644 --- a/frontend/webpage/scripts/login.js +++ b/frontend/webpage/static/scripts/login.js @@ -1,6 +1,7 @@ -window.addEventListener("load", (loadEvent) => { - let session = window.sessionStorage.getItem('session'); - if (session) { +window.addEventListener("load", async (loadEvent) => { + await loadCommon(); + + if (session && session.key) { window.location.href = '/'; } @@ -18,6 +19,11 @@ window.addEventListener("load", (loadEvent) => { const header = document.querySelector("#login-header-text"); const error = document.querySelector("#error-message"); + if (session) { + error.style.display = "block"; + error.textContent = "You have been signed out due to inactivity"; + } + const raiseError = (errorFields, message) => { for (const key in fields) { if (errorFields.includes(key)) { @@ -72,7 +78,7 @@ window.addEventListener("load", (loadEvent) => { raiseError([ "password", "confirm" ], "Password cannot be empty"); } else { freeze(); - fetch('https://localhost:4730/create_user', { + fetch(`/api/auth/create_user`, { method: 'POST', mode: 'cors', headers: { @@ -81,10 +87,10 @@ window.addEventListener("load", (loadEvent) => { body: JSON.stringify({ email: fields.email.value, username: fields.username.value, password: fields.password.value }) }) .then(async response => ({ status: response.status, data: await response.json() })) - .then(response =>{ + .then(response => { const data = response.data; - if (response.status === 200) { - window.sessionStorage.setItem('session', JSON.stringify(data)); + if (response.status === 201) { + session = data; window.location.href = "/"; } else if (response.status === 500) { raiseError([], "Internal server error :("); @@ -95,6 +101,9 @@ window.addEventListener("load", (loadEvent) => { } else if (data.code === 2) { raiseError([ "email" ], "Invalid email address"); fields.email.value = ''; + } else { + raiseError([ "email" ], "Server is bad :p"); + fields.email.value = ''; } } }) @@ -130,7 +139,7 @@ window.addEventListener("load", (loadEvent) => { raiseError([ "password" ], "Password cannot be empty"); } else { freeze(); - fetch('https://localhost:4730/login', { + fetch(`/api/auth/login`, { method: 'POST', mode: 'cors', headers: { @@ -142,8 +151,7 @@ window.addEventListener("load", (loadEvent) => { .then(response => { const data = response.data; if (response.status === 200) { - console.log(data); - window.sessionStorage.setItem('session', JSON.stringify(data)); + session = data; window.location.href = "/"; } else if (response.status === 500) { raiseError([], "Internal server error :("); diff --git a/frontend/webpage/static/scripts/profile.js b/frontend/webpage/static/scripts/profile.js new file mode 100644 index 0000000..a78b783 --- /dev/null +++ b/frontend/webpage/static/scripts/profile.js @@ -0,0 +1,278 @@ +let profileId = window.location.pathname.split('/').pop(); +let isReturn = false; +let profileData = null; +const loadProfile = () => { + { + const lists = document.querySelectorAll(".profile-list"); + for (const list of lists) { + if (list.querySelectorAll(".profile-entry").length === 0) { + list.parentElement.removeChild(list); + } + } + } + + { + const validImageFile = (type) => { + return type === "image/apng" + || type === "image/avif" + || type === "image/gif" + || type === "image/jpeg" + || type === "image/png" + || type === "image/svg+xml" + || type === "image/webp" + } + + const editProfileButton = document.querySelector("#info-edit-stack"); + const saveProfileButton = document.querySelector("#info-save-stack"); + + const usernameText = document.querySelector("#profile-info-username-text"); + const usernameField = document.querySelector("#profile-info-username-field"); + + const finalizeName = () => { + if (usernameField.value !== '') { + fetch(`/api/user/${profileId}/username`, { + method: 'POST', + mode: 'cors', + headers: { + 'Content-Type': 'application/json' + }, + body: `{ "sessionKey": "${session.key}", "username": "${usernameField.value}" }` + }).then(response => { + if (response.status === 201) { + usernameText.textContent = usernameField.value.substring(0, 32); + } + }); + } + }; + + usernameField.addEventListener("input", (inputEvent) => { + if (usernameField.value.length > 32) { + usernameField.value = usernameField.value.substring(0, 32); + } + }); + + const pfp = document.querySelector("#profile-info-pfp-img"); + const pfpStack = document.querySelector("#profile-info-pfp"); + const upload = document.querySelector("#profile-info-pfp-upload"); + const uploadHover = document.querySelector("#profile-info-pfp-upload-hover"); + const uploadInvalid = document.querySelector("#profile-info-pfp-upload-invalid"); + + const togglePlatformEdit = (clickEvent) => { + editProfileButton.classList.toggle("active"); + saveProfileButton.classList.toggle("active"); + usernameText.classList.toggle("active"); + usernameField.classList.toggle("active"); + upload.classList.toggle("active"); + uploadHover.classList.toggle("active"); + uploadInvalid.classList.toggle("active"); + pfpStack.classList.remove("hover"); + pfpStack.classList.remove("invalid"); + }; + editProfileButton.addEventListener("click", togglePlatformEdit); + editProfileButton.addEventListener("click", () => { + usernameField.value = usernameText.textContent; + }); + saveProfileButton.addEventListener("click", togglePlatformEdit); + saveProfileButton.addEventListener("click", finalizeName); + + pfpStack.addEventListener("drop", (dropEvent) => { + if (upload.classList.contains("active")) { + dropEvent.preventDefault(); + pfpStack.classList.remove("hover"); + pfpStack.classList.remove("invalid"); + if (dropEvent.dataTransfer.files) { + const file = dropEvent.dataTransfer.files[0]; + if (validImageFile(file.type)) { + const data = new FormData(); + data.append('session', new Blob([`{ "key": "${session.key}" }`], { type: `application/json` })); + data.append('file', file); + + fetch(`/api/user/${profileId}/image`, { + method: 'POST', + mode: 'cors', + body: data + }).then(response => { + if (upload.classList.contains("active")) { + if (response.status === 201) { + pfp.src = `/api/user/${profileId}/image?time=${Date.now()}`; + } else { + pfpStack.classList.add("invalid"); + } + } + }); + return; + } + } + pfpStack.classList.add("invalid"); + } + }); + pfpStack.addEventListener("dragover", (dragEvent) => { + if (upload.classList.contains("active")) { + dragEvent.preventDefault(); + } + }); + pfpStack.addEventListener("dragenter", (dragEvent) => { + if (upload.classList.contains("active")) { + pfpStack.classList.remove("hover"); + pfpStack.classList.remove("invalid"); + if (dragEvent.dataTransfer.types.includes("application/x-moz-file")) { + pfpStack.classList.add("hover"); + } else { + pfpStack.classList.add("invalid"); + } + } + }); + pfpStack.addEventListener("dragleave", (dragEvent) => { + if (upload.classList.contains("active")) { + pfpStack.classList.remove("hover"); + pfpStack.classList.remove("invalid"); + } + }); + } + + { + const editPlatformsButton = document.querySelector("#platform-edit-stack"); + const savePlatformsButton = document.querySelector("#platform-save-stack"); + const platforms = document.querySelectorAll("#profile-platforms .profile-entry"); + + const togglePlatformEdit = (clickEvent) => { + editPlatformsButton.classList.toggle("active"); + savePlatformsButton.classList.toggle("active"); + for (const platform of platforms) { + platform.classList.toggle("editing"); + } + }; + editPlatformsButton.addEventListener("click", togglePlatformEdit); + savePlatformsButton.addEventListener("click", togglePlatformEdit); + + const steamButtons = [ + document.querySelector("#add-steam"), + document.querySelector("#platform-0"), + ]; + + steamButtons[0].addEventListener("click", (clickEvent) => { + window.location.href = "/auth/steam"; + }); + steamButtons[1].addEventListener("click", (clickEvent) => { + fetch(`/api/user/${profileId}/platforms/remove`, { + method: 'POST', + mode: 'cors', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ sessionKey: session.key, platformId: 0 }) + }); + steamButtons[1].parentElement.classList.remove("connected"); + }); + + if (isReturn) { + editPlatformsButton.click(); + } + } + + // Canvasing + + const completionCanvas = document.querySelector("#profile-completion-canvas"); + const completionText = document.querySelector("#profile-completion-text"); + + const STROKE_WIDTH = 0.18; + const style = window.getComputedStyle(completionCanvas); + const context = completionCanvas.getContext('2d'); + + const drawCanvas = () => profileData.then(data => { + const width = Number(style.getPropertyValue('width').slice(0, -2)); + const height = width; + + context.canvas.width = width; + context.canvas.height = height; + context.clearRect(0, 0, width, height); + context.strokeStyle = root.getProperty('--accent-value3'); + context.lineWidth = (width / 2) * STROKE_WIDTH; + context.beginPath(); + context.arc(width / 2, height / 2, (width / 2) * (1 - STROKE_WIDTH / 2), -0.5 * Math.PI, (-0.5 + (data.average === null ? 0 : (data.average / 100) * 2)) * Math.PI); + context.stroke(); + }); + + window.addEventListener('resize', drawCanvas); + drawCanvas(); + + if (profileId === session.id) { + document.querySelector("#profile-page").classList.add("self"); + } +} + +const expandTemplates = async () => { + await commonTemplates(); + template.apply("profile-page").promise(profileData.then(data => ({ + id: profileId, + username: data.username, + completed: data.completed, + average: data.average === null ? "N/A" : data.average + "%", + perfect: data.perfect, + }))); + template.apply("profile-platforms-list").promise(profileData.then(data => + data.platforms.map(platform => ({ + platform_id: platform.id, + img: `Steam Logo`, + name: platform.name, + connected: platform.connected ? "connected" : "", + add: + (platform.id === 0 ? `Add` : + (platform.id === 1 ? `

Coming soon...

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

Coming soon...

` : + ""))) + })) + )); +} + +window.addEventListener("load", async (loadEvent) => { + await loadCommon(); + + if (!/\d+/.test(profileId)) { + isReturn = true; + const platform = profileId; + if (!session) { + window.location.href = "/404"; + } else { + profileId = session.lastProfile; + delete session.lastProfile; + } + + if (platform === 'steam') { + const query = new URLSearchParams(window.location.search); + + if (query.get('openid.mode') === 'cancel') { + + } 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", { + method: 'POST', + mode: 'cors', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ sessionKey: session.key, userId: profileId, platformId: 0, platformUserId: `${steamId}` }) + }); + } + } + + window.history.replaceState({}, '', `/profile/${profileId}`); + } else if (/\d+/.test(profileId)) { + profileId = Number(profileId); + if (session) { + session.lastProfile = profileId; + } + } else { + // Handle error + } + + profileData = fetch(`/api/user/${profileId}`, { method: 'GET', mode: 'cors' }) + .then(response => response.json()); + + await expandTemplates(); + await template.expand(); + + connectNavbar(); + loadProfile(); +}); \ No newline at end of file diff --git a/frontend/webpage/scripts/template.js b/frontend/webpage/static/scripts/template.js similarity index 99% rename from frontend/webpage/scripts/template.js rename to frontend/webpage/static/scripts/template.js index ecc1a88..661f3e1 100644 --- a/frontend/webpage/scripts/template.js +++ b/frontend/webpage/static/scripts/template.js @@ -151,7 +151,7 @@ var template = template || {}; const data = child.dataset.template.split(/\s*:\s*/); return { id: data[0], - typeCapture: parseType(data[1] || 'Begin'), + typeCapture: parseType(data[1] || 'Basic'), element: child }; }); diff --git a/frontend/webpage/static/styles/about.css b/frontend/webpage/static/styles/about.css new file mode 100644 index 0000000..bca40de --- /dev/null +++ b/frontend/webpage/static/styles/about.css @@ -0,0 +1,13 @@ +#about-page { + max-width: 1600px; +} + +#about-text { + margin: 0; + padding: 16px; + + font-size: 18px; + color: var(--foreground); + + background-color: var(--distinction); +} \ No newline at end of file diff --git a/frontend/webpage/styles/common.css b/frontend/webpage/static/styles/common.css similarity index 94% rename from frontend/webpage/styles/common.css rename to frontend/webpage/static/styles/common.css index 9598841..ad9f5e0 100644 --- a/frontend/webpage/styles/common.css +++ b/frontend/webpage/static/styles/common.css @@ -134,8 +134,6 @@ html, body { background-color: var(--background); box-shadow: 0px 0px 5px 10px var(--shadow-color); - - display: none; } .page-subsection { @@ -171,17 +169,28 @@ html, body { .page-subheader { margin-bottom: 16px; + + width: 100%; +} + +.page-subheader-flex { + width: 100%; + display: flex; + justify-content: space-between; + align-items: center; } .page-header-text, .page-subheader-text { - width: max-content; - margin: 0 0 0.25em; color: var(--foreground); cursor: default; + + overflow: hidden; + text-overflow: ellipsis; + flex-grow: 1; } .page-header-text.link, @@ -199,10 +208,17 @@ html, body { font-size: 48px; } -.page-subheader-text { +.page-subheader-text, +.page-subheader-icon { font-size: 32px; } +.page-subheader-icon { + margin: 0 0 0.25em; + height: 32px; + padding: 0; +} + .page-header-separator, .page-subheader-separator { width: 100%; @@ -214,8 +230,6 @@ html, body { .list-page-search { box-sizing: border-box; - margin: 16px; - display: flex; flex-direction: row; justify-content: center; @@ -285,6 +299,9 @@ html, body { .list-page-filter-chunk { background-color: var(--distinction); + + width: 100%; + height: 100%; } .list-page-filter { @@ -394,7 +411,6 @@ html, body { margin: 0; padding: 0 12px; - width: 0; height: 64px; line-height: 64px; diff --git a/frontend/webpage/static/styles/index.css b/frontend/webpage/static/styles/index.css new file mode 100644 index 0000000..92be846 --- /dev/null +++ b/frontend/webpage/static/styles/index.css @@ -0,0 +1,111 @@ +#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/styles/login.css b/frontend/webpage/static/styles/login.css similarity index 100% rename from frontend/webpage/styles/login.css rename to frontend/webpage/static/styles/login.css diff --git a/frontend/webpage/static/styles/profile.css b/frontend/webpage/static/styles/profile.css new file mode 100644 index 0000000..1192c49 --- /dev/null +++ b/frontend/webpage/static/styles/profile.css @@ -0,0 +1,401 @@ +#profile-page { + max-width: 1600px; +} + +.profile-list { + width: 100%; + height: max-content; + + border-radius: 8px; + + overflow: hidden; + + box-shadow: 0px 0px 8px 8px var(--shadow-color); +} + +.profile-entry { + overflow: hidden; + + height: 64px; + + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + + background-color: var(--distinction); +} + +.profile-entry-left { + height: 100%; + + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; +} + +.profile-entry-right { + height: 100%; + + display: flex; + flex-direction: row; + justify-content: flex-end; + align-items: center; +} + +.profile-entry-icon { + width: 64px; + + flex-grow: 0; +} + +.profile-entry-text { + box-sizing: border-box; + + margin: 0; + padding: 0px 16px; + + height: 100%; + line-height: 64px; + + color: var(--foreground); + + font-size: 24px; + + border-top: 1px solid var(--background); + + flex-basis: max-content; + flex-grow: 0; +} + +.top > .profile-entry-text { + border: 0; +} + +.profile-entry-text.platform-name { + flex-grow: 1; +} + +#profile-section-1 { + box-sizing: border-box; + + width: 100%; + height: max-content; + + display: flex; + flex-direction: row; + justify-content: center; + align-items: flex-start; +} + +#profile-info { + width: 50%; + height: max-content; + + max-width: 480px; +} + +#profile-info-username-text.active, +#profile-info-username-field { + display: none; +} + +#profile-info-username-field.active { + display: block; +} + +#profile-info-username-field { + margin-right: 8px; + padding: 4px; + font-size: 24px; + color: var(--background); + background-color: var(--foreground); + + border-radius: 8px; + + border: 0; + outline: none; +} + +#profile-info-pfp-border { + box-sizing: border-box; + + padding: 24px; + background-color: var(--distinction); + + width: 100%; + height: max-content; + + box-shadow: 0px 0px 8px 8px var(--shadow-color); + + border-radius: 8px; +} + +#profile-info-pfp { + top: 0; + left: 0; + width: 100%; + height: 100%; + + border-radius: 8px; + + position: relative; +} + +#profile-info-pfp-img { + display: block; + width: 100%; + height: 100%; + border-radius: 8px; + object-fit: contain; + + background-color: var(--background); + + position: absolute; +} + +#profile-info-pfp-vignette { + top: 0; + left: 0; + width: 100%; + height: 100%; + + box-shadow: inset 0px 0px 30px 10px var(--shadow-color); + + border-radius: 8px; + + position: absolute; + z-index: 1; +} + +#profile-info-pfp-upload, +#profile-info-pfp-upload-hover, +#profile-info-pfp-upload-invalid { + top: 0; + left: 0; + width: 100%; + + border-radius: 8px; + + background-color: var(--background); + opacity: 0.8; + + display: block; + + visibility: hidden; +} + +#profile-info-pfp-upload { + position: relative; +} + +#profile-info-pfp-upload-hover, +#profile-info-pfp-upload-invalid { + position: absolute; +} + +#profile-info-pfp #profile-info-pfp-upload.active, +#profile-info-pfp.hover #profile-info-pfp-upload-hover.active, +#profile-info-pfp.invalid #profile-info-pfp-upload-invalid.active { + visibility: visible; +} + +#profile-info-pfp.hover #profile-info-pfp-upload.active, +#profile-info-pfp.invalid #profile-info-pfp-upload.active { + visibility: hidden; +} + +#profile-stats { + flex-grow: 1; + + display: flex; + flex-direction: row; +} + +#profile-stats-numeric { + flex-grow: 1; + max-width: 300px; + + display: flex; + flex-direction: column; + justify-content: space-between; +} + +#profile-completion-stack { + width: 100%; + height: max-content; + position: relative; +} + +#profile-completion-background { + width: 100%; + display: block; +} + +#profile-completion-canvas { + width: 100%; + height: 100%; + + position: absolute; + left: 0; + top: 0; +} + +#profile-completion-text { + margin: 0; + + width: 100%; + height: 100%; + + color: var(--foreground); + font-size: 64px; + + position: absolute; + left: 0; + top: 0; + + display: flex; + justify-content: center; + align-items: center; +} + +#profile-perfect-text { + margin: 0; + + height: 48px; + + color: var(--foreground); + + font-size: 48px; + line-height: 48px; + text-align: center; +} + +#profile-section-2 { + box-sizing: border-box; + + width: 100%; + height: max-content; + + display: flex; + flex-direction: row; + justify-content: center; + align-items: flex-start; +} + +#profile-hardest { + flex-grow: 1; + height: 100%; +} + +#profile-platforms { + flex-grow: 1; + max-width: 480px; +} + +#profile-platforms .profile-entry { + display: none; +} + +#profile-platforms .profile-entry-text { + color: var(--foreground); +} + +#profile-platforms .profile-entry.connected, +#profile-platforms .profile-entry.editing { + display: flex; +} + +#profile-page .profile-edit-stack, +#profile-page .profile-save-stack { + display: none; +} + +#profile-page.self .profile-edit-stack, +#profile-page.self .profile-save-stack.active { + display: block; + width: max-content; + height: max-content; +} + +#profile-page.self .profile-edit-stack.active, +#profile-page.self .profile-save-stack { + display: none; +} + +.profile-edit-stack:hover > .profile-edit-hover, +.profile-edit { + display: block; +} + +.profile-edit-stack:hover > .profile-edit, +.profile-edit-hover { + display: none; +} + +.profile-save-stack:hover > .profile-save-hover, +.profile-save { + display: block; +} + +.profile-save-stack:hover > .profile-save, +.profile-save-hover { + display: none; +} + +.profile-entry .platform-remove-stack, +.profile-entry .platform-add, +.profile-entry .platform-unsupported, +.profile-entry.connected.editing .platform-add { + border-top: 1px solid var(--background); + display: none; +} + +.profile-entry.connected.editing .platform-remove-stack { + box-sizing: border-box; + display: block; + height: 100%; + width: max-content; + flex-grow: 0; +} + +.platform-remove, .platform-remove-hover { + box-sizing: border-box; + padding: 12px; + height: 100%; +} + +.profile-entry.connected.editing .platform-remove-stack .platform-remove, +.profile-entry.connected.editing .platform-remove-stack:hover .platform-remove-hover { + display: block; +} + +.profile-entry.connected.editing .platform-remove-stack:hover .platform-remove, +.profile-entry.connected.editing .platform-remove-stack .platform-remove-hover { + display: none; +} + +.profile-entry .platform-add { + box-sizing: border-box; + height: 100%; + padding: 16px 8px; +} + +.profile-entry.editing .platform-add { + display: block; +} + +.profile-entry.editing .platform-unsupported { + box-sizing: border-box; + display: block; + margin: 0; + padding: 0% 2%; + line-height: 63px; + font-size: 24px; + color: var(--foreground-disabled); +} + +#profile-ratings { + flex-grow: 1; +} diff --git a/frontend/webpage/styles/theme.css b/frontend/webpage/static/styles/theme.css similarity index 70% rename from frontend/webpage/styles/theme.css rename to frontend/webpage/static/styles/theme.css index 64eea3e..c42e13e 100644 --- a/frontend/webpage/styles/theme.css +++ b/frontend/webpage/static/styles/theme.css @@ -1,9 +1,10 @@ :root { - --background-dark: #111117; - --background: #22222A; - --foreground-dark: #AAAAAA; - --foreground: #EEEEEE; - --distinction: #44444C; + --background-dark: #111117; + --background: #22222A; + --foreground-disabled: #77777D; + --foreground-dark: #AAAAAA; + --foreground: #EEEEEE; + --distinction: #44444C; --accent-hue: 0; diff --git a/frontend/webpage/styles/index.css b/frontend/webpage/styles/index.css deleted file mode 100644 index 2933370..0000000 --- a/frontend/webpage/styles/index.css +++ /dev/null @@ -1,232 +0,0 @@ -html, body { - background-color: var(--background-dark); - - margin: 0; - border: 0; - padding: 0; - width: 100%; - height: 100%; - - font-family: sans-serif; -} - -#games-page { - max-width: 1920px; -} - -.list-page-entry-text.game-name { - flex-grow: 1; -} - -.list-page-entry-text.game-description { - flex-grow: 2; -} - -#achievements-page { - max-width: 1920px; -} - -.list-page-entry-text.achievement-name { - flex-grow: 4; -} - -.list-page-entry-text.achievement-description { - box-sizing: border-box; - - margin: 0; - padding: 0 12px; - width: 0; - - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - - flex-grow: 8; -} - -.list-page-entry-text.achievement-stages { - flex-grow: 1; -} - -#profile-page { - max-width: 1600px; - - display: block; -} - -#profile-section-1 { - box-sizing: border-box; - - width: 100%; - height: max-content; - - display: flex; - flex-direction: row; - justify-content: center; - align-items: flex-start; -} - -#profile-info { - width: 50%; - height: max-content; - - max-width: 480px; -} - -#profile-info-pfp-border { - box-sizing: border-box; - - padding: 24px; - background-color: var(--distinction); - - width: 100%; - max-width: 640px; - height: max-content; - - box-shadow: 0px 0px 8px 8px var(--shadow-color); - - border-radius: 8px; -} - -#profile-info-pfp { - width: 100%; - height: 100%; - - position: relative; -} - -#profile-info-pfp-vignette { - top: 0%; - left: 0%; - width: 100%; - height: 100%; - - box-shadow: inset 0px 0px 50px 30px var(--shadow-color); - - border-radius: 8px; - - position: absolute; - z-index: 1; -} - -#profile-info-pfp-img { - display: block; - width: 100%; - border-radius: 8px; - - position: relative; -} - -.profile-list { - width: 100%; - height: max-content; - - border-radius: 8px; - - overflow: hidden; - - box-shadow: 0px 0px 8px 8px var(--shadow-color); -} - -.profile-entry { - overflow: hidden; - - height: 64px; - - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - - background-color: var(--distinction); -} - -.profile-entry.accented { - background-color: var(--accent-value1); -} - -.profile-entry-left { - height: 100%; - - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: center; -} - -.profile-entry-right { - height: 100%; - - display: flex; - flex-direction: row; - justify-content: flex-end; - align-items: center; -} - -.profile-entry-icon { - width: 64px; - - flex-grow: 0; -} - -.profile-entry-text { - box-sizing: border-box; - - margin: 0; - padding: 0px 16px; - - height: 100%; - line-height: 64px; - - color: var(--foreground); - - font-size: 24px; - - border-top: 1px solid var(--background); - - flex-grow: 0; -} - -.top > .profile-entry-text { - border: 0; -} - -.accented > .profile-entry-text { - border-color: var(--accent-value0); -} - -.profile-entry-text.platform-name, -.profile-entry-text.game-name, -.profile-entry-text.achievement-name { - flex-grow: 1; -} - -.profile-entry-text.accented { - display: none; -} - -.profile-entry.accented .profile-entry-text.accented { - display: block; -} - -#profile-platforms { - flex-grow: 1; -} - -#profile-section-2 { - box-sizing: border-box; - - width: 100%; - height: max-content; - - display: flex; - flex-direction: row; - justify-content: center; - align-items: flex-start; -} - -#profile-games, -#profile-achievements { - width: 50%; - height: max-content; -} diff --git a/frontend/webpage/templates/achievements_page.html.template b/frontend/webpage/templates/achievements_page.html.template deleted file mode 100644 index 421f452..0000000 --- a/frontend/webpage/templates/achievements_page.html.template +++ /dev/null @@ -1,54 +0,0 @@ -
- -
-
-
-
-

Filters

-
-
-
-
-
-
-

My Games

-
-
-
-

In Progress

-
-
-
-

Completed

-
-
-
-
-
-
-
-
-
-

-

Name

-

Description

-

Stages

-
- -
-
-
-
-
diff --git a/frontend/webpage/templates/games_page.html.template b/frontend/webpage/templates/games_page.html.template deleted file mode 100644 index ceceacd..0000000 --- a/frontend/webpage/templates/games_page.html.template +++ /dev/null @@ -1,65 +0,0 @@ -
- -
-
-
-
-

Filters

-
-
-
-
-
-
-

Games Owned

-
-
-
-
-
-
-
-
-
-

-

Name

-

Description

-
-
- Achievement Icon.png -

Latin

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

-
-
- Achievement Icon.png -

Latin

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

-
-
- Achievement Icon.png -

Latin

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

-
-
- Achievement Icon.png -

Latin

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

-
-
- Achievement Icon.png -

Latin

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

-
-
- Achievement Icon.png -

Latin

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.

-
-
-
-
-
-
\ No newline at end of file diff --git a/frontend/webpage/templates/profile_page.html.template b/frontend/webpage/templates/profile_page.html.template deleted file mode 100644 index 80d556d..0000000 --- a/frontend/webpage/templates/profile_page.html.template +++ /dev/null @@ -1,172 +0,0 @@ -
-
-
-
-

Jane Doe

-
-
-
-
- User's Profile Pictuer -
-
-
-
-
-
-
-
-

Platforms

-
-
-
-
- Steam Logo -

Steam

-

Connected

-
-
- Xbox Logo -

Xbox Live

-

Connected

-
-
- PSN Logo -

PSN

-

Connected

-
-
-
-
-
-
-
-
-
- -
-
-
-
- Some Game Icon -

Latin

-
-
- Some Game Icon -

Latin

-
-
- Some Game Icon -

Latin

-
-
- Some Game Icon -

Latin

-
-
- Some Game Icon -

Latin

-
-
- Some Game Icon -

Latin

-
-
- Some Game Icon -

Latin

-
-
- Some Game Icon -

Latin

-
-
- Some Game Icon -

Latin

-
-
- Some Game Icon -

Latin

-
-
- Some Game Icon -

Latin

-
-
-
-
-
-
-
- -
-
-
-
- Some Achievement Icon -

Lorem Ipsum

-

Completed

-
-
- Some Achievement Icon -

Lorem Ipsum

-

Completed

-
-
- Some Achievement Icon -

Lorem Ipsum

-

Completed

-
-
- Some Achievement Icon -

Lorem Ipsum

-

Completed

-
-
- Some Achievement Icon -

Lorem Ipsum

-

Completed

-
-
- Some Achievement Icon -

Lorem Ipsum

-

Completed

-
-
- Some Achievement Icon -

Lorem Ipsum

-

Completed

-
-
- Some Achievement Icon -

Lorem Ipsum

-

Completed

-
-
- Some Achievement Icon -

Lorem Ipsum

-

Completed

-
-
- Some Achievement Icon -

Lorem Ipsum

-

Completed

-
-
- Some Achievement Icon -

Lorem Ipsum

-

Completed

-
-
- Some Achievement Icon -

Lorem Ipsum

-

Completed

-
-
- Some Achievement Icon -

Lorem Ipsum

-

Completed

-
-
-
-
-
\ No newline at end of file diff --git a/sql/CreateUser.sql b/sql/AuthProcs.sql similarity index 53% rename from sql/CreateUser.sql rename to sql/AuthProcs.sql index 6d8d685..e760caa 100644 Binary files a/sql/CreateUser.sql and b/sql/AuthProcs.sql differ diff --git a/sql/CreateTables.sql b/sql/CreateTables.sql index 2d11253..10f86aa 100644 --- a/sql/CreateTables.sql +++ b/sql/CreateTables.sql @@ -36,6 +36,7 @@ CREATE TABLE [User] ( Hue INT NOT NULL CONSTRAINT HueDefault DEFAULT 0 CONSTRAINT HueConstraint CHECK (0 <= Hue AND Hue <= 360), + PFP VARCHAR(11) NULL, Verified BIT NOT NULL CONSTRAINT VerifiedDefault DEFAULT 0 PRIMARY KEY(ID) @@ -102,7 +103,8 @@ CREATE TABLE [Progress] ( CREATE TABLE [IsOn] ( UserID INT NOT NULL, - PlatformID INT NOT NULL + PlatformID INT NOT NULL, + PlatformUserID VARCHAR(32) NOT NULL PRIMARY KEY(UserID, PlatformID) FOREIGN KEY(UserID) REFERENCES [User](ID) ON UPDATE CASCADE diff --git a/sql/GetUserLogin.sql b/sql/GetUserLogin.sql deleted file mode 100644 index f00d750..0000000 --- a/sql/GetUserLogin.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE PROCEDURE GetUserLogin( - @email VARCHAR(254) -) AS -BEGIN TRANSACTION -SELECT Id, Salt, [Password], Hue FROM [User] WHERE Email = @email -COMMIT TRANSACTION \ No newline at end of file diff --git a/sql/UserData.sql b/sql/UserData.sql new file mode 100644 index 0000000..630b2af --- /dev/null +++ b/sql/UserData.sql @@ -0,0 +1,186 @@ +--------------------------------------- +-- 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/tmp/unknown.png b/tmp/unknown.png deleted file mode 100644 index 83e1bfc..0000000 Binary files a/tmp/unknown.png and /dev/null differ