diff --git a/backend/src/main/java/achievements/Application.java b/backend/src/main/java/achievements/Application.java index 971ab08..4eac2aa 100644 --- a/backend/src/main/java/achievements/Application.java +++ b/backend/src/main/java/achievements/Application.java @@ -1,7 +1,6 @@ package achievements; -import achievements.misc.Password; -import achievements.services.DbConnectionService; +import achievements.misc.DbConnectionService; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; diff --git a/backend/src/main/java/achievements/controllers/Controller.java b/backend/src/main/java/achievements/controllers/Controller.java index bd1a0d3..7595ba5 100644 --- a/backend/src/main/java/achievements/controllers/Controller.java +++ b/backend/src/main/java/achievements/controllers/Controller.java @@ -1,7 +1,6 @@ package achievements.controllers; import achievements.data.Achievements; -import achievements.data.User; import achievements.data.Games; import achievements.data.InternalError; import achievements.services.DbService; @@ -13,7 +12,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import static org.springframework.web.bind.annotation.RequestMethod.GET; -import static org.springframework.web.bind.annotation.RequestMethod.POST; @RestController public class Controller { @@ -64,47 +62,4 @@ public class Controller { return new ResponseEntity("{}", HttpStatus.INTERNAL_SERVER_ERROR); } } - - /** - * Acceptable codes - * 0 => Success - * 1 => Email already registered - * - * -1 => Unknown error - */ - @RequestMapping(value = "/create_user", method = POST, consumes = "application/json", produces = "application/json") - public ResponseEntity createUser(@RequestBody User user) { - var status = db.createUser(user); - if (status == 0) { - return ResponseEntity.ok("{ \"key\": \"aoeuhtns\" }"); - //var sessionKey = db.generateSessionKey(user); - } else { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("{ \"code\": " + status + " }"); - } - } - - /** - * DO NOT RETURN CODE DIRECTLY! - * - * User should only ever recieve -1, 0, or 1. The specific authentication error should be hidden. - * - * Acceptable codes - * 0 => Success - * 1 => Unregistered email address - * 2 => Incorrect password - * - * -1 => Unknown error - */ - @RequestMapping(value = "/login", method = POST, consumes = "application/json", produces = "application/json") - public ResponseEntity login(@RequestBody User user) { - var status = db.login(user); - if (status == 0) { - return ResponseEntity.ok("{ \"key\": \"aoeuhtns\" }"); - } else if (status > 0) { - // Hardcoded 1 response code - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("{ \"code\": 1 }"); - } else { - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("{ \"code\": " + status + " }"); - } - } } diff --git a/backend/src/main/java/achievements/controllers/LoginController.java b/backend/src/main/java/achievements/controllers/LoginController.java new file mode 100644 index 0000000..ff52ce6 --- /dev/null +++ b/backend/src/main/java/achievements/controllers/LoginController.java @@ -0,0 +1,66 @@ +package achievements.controllers; + +import achievements.data.User; +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.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import static org.springframework.web.bind.annotation.RequestMethod.POST; + +@RestController +public class LoginController { + + @Autowired + private AuthenticationService authService; + + /** + * Acceptable codes + * 0 => Success + * 1 => Email already registered + * + * -1 => Unknown error + */ + @RequestMapping(value = "/create_user", method = POST, consumes = "application/json", produces = "application/json") + public ResponseEntity createUser(@RequestBody User user) { + var response = authService.createUser(user); + if (response.status == 0) { + return ResponseEntity.ok("{ \"key\": \"" + authService.session().generate(response.id) + "\", \"id\": " + response.id + " }"); + } else if (response.status > 0) { + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("{ \"code\": " + response.status + " }"); + } else { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("{ \"code\": " + response.status + " }"); + } + } + + /** + * DO NOT RETURN CODE DIRECTLY! + * + * User should only ever recieve -1, 0, or 1. The specific authentication error should be hidden. + * + * Acceptable codes + * 0 => Success + * 1 => Unregistered email address + * 2 => Incorrect password + * + * -1 => Unknown error + */ + @RequestMapping(value = "/login", method = POST, consumes = "application/json", produces = "application/json") + public ResponseEntity login(@RequestParam(value = "guest", required = false) boolean guest, @RequestBody User user) { + var response = guest ? + authService.GUEST : + authService.login(user); + if (response.status == 0) { + return ResponseEntity.ok("{ \"key\": \"" + authService.session().generate(response.id) + "\", \"id\": " + response.id + " }"); + } else if (response.status > 0) { + // Hardcoded 1 response code + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("{ \"code\": 1 }"); + } else { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("{ \"code\": " + response.status + " }"); + } + } +} diff --git a/backend/src/main/java/achievements/services/DbConnectionService.java b/backend/src/main/java/achievements/misc/DbConnectionService.java similarity index 93% rename from backend/src/main/java/achievements/services/DbConnectionService.java rename to backend/src/main/java/achievements/misc/DbConnectionService.java index 99ef0b1..d10120a 100644 --- a/backend/src/main/java/achievements/services/DbConnectionService.java +++ b/backend/src/main/java/achievements/misc/DbConnectionService.java @@ -1,56 +1,56 @@ -package achievements.services; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; - -import javax.annotation.PostConstruct; -import javax.annotation.PreDestroy; -import java.sql.Connection; -import java.sql.SQLException; -import com.microsoft.sqlserver.jdbc.SQLServerDataSource; - -@Component -public class DbConnectionService { - - private Connection connection; - - @Value("${database.server}") - private String serverName; - @Value("${database.name}") - private String databaseName; - @Value("${database.user.name}") - private String username; - @Value("${database.user.password}") - private String password; - - public DbConnectionService() {} - - @PostConstruct - public void connect() { - try { - var dataSource = new SQLServerDataSource(); - dataSource.setServerName (serverName ); - dataSource.setDatabaseName(databaseName); - dataSource.setUser (username ); - dataSource.setPassword (password ); - connection = dataSource.getConnection(); - } catch (SQLException e) { - e.printStackTrace(); - } - } - - public Connection getConnection() { - return this.connection; - } - - @PreDestroy - public void disconnect() { - try { - if (connection != null) { - connection.close(); - } - } catch (SQLException e) { - e.printStackTrace(); - } - } -} +package achievements.misc; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import java.sql.Connection; +import java.sql.SQLException; +import com.microsoft.sqlserver.jdbc.SQLServerDataSource; + +@Component +public class DbConnectionService { + + private Connection connection; + + @Value("${database.server}") + private String serverName; + @Value("${database.name}") + private String databaseName; + @Value("${database.user.name}") + private String username; + @Value("${database.user.password}") + private String password; + + public DbConnectionService() {} + + @PostConstruct + public void connect() { + try { + var dataSource = new SQLServerDataSource(); + dataSource.setServerName (serverName ); + dataSource.setDatabaseName(databaseName); + dataSource.setUser (username ); + dataSource.setPassword (password ); + connection = dataSource.getConnection(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public Connection getConnection() { + return this.connection; + } + + @PreDestroy + public void disconnect() { + try { + if (connection != null) { + connection.close(); + } + } catch (SQLException e) { + e.printStackTrace(); + } + } +} diff --git a/backend/src/main/java/achievements/misc/HashManager.java b/backend/src/main/java/achievements/misc/HashManager.java new file mode 100644 index 0000000..2e133c4 --- /dev/null +++ b/backend/src/main/java/achievements/misc/HashManager.java @@ -0,0 +1,70 @@ +package achievements.misc; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Random; + +public class HashManager { + + private static final Random RANDOM = new SecureRandom(); + + public static byte[] hash(byte[] salt, byte[] password) { + try { + var concat = new byte[salt.length + password.length]; + int i = 0; + for (; i < salt.length; ++i) { + concat[i] = salt[i]; + } + for (int j = 0; j < password.length; ++j) { + concat[i + j] = password[j]; + } + + var md = MessageDigest.getInstance("SHA-256"); + return md.digest(concat); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); + } + return null; + } + + public static String encode(byte[] bytes) { + var chars = new char[bytes.length << 1]; + for (int i = 0; i < bytes.length; ++i) { + chars[(i << 1) ] = toHex(bytes[i] >> 0); + chars[(i << 1) + 1] = toHex(bytes[i] >> 4); + } + return new String(chars); + } + + public static byte[] decode(String data) { + var decoded = new byte[data.length() >> 1]; + for (int i = 0; i < data.length(); i += 2) { + int currentByte = + (fromHex(data.charAt(i )) ) | + (fromHex(data.charAt(i + 1)) << 4); + decoded[i >> 1] = (byte) (currentByte & 0xFF); + } + return decoded; + } + + public static byte[] generateBytes(int length) { + var bytes = new byte[length]; + RANDOM.nextBytes(bytes); + return bytes; + } + + public static char toHex(int halfByte) { + halfByte = halfByte & 0xF; + if (0 <= halfByte && halfByte <= 9 ) return (char) (halfByte + '0' ); + if (10 <= halfByte && halfByte <= 15) return (char) (halfByte + 'a' - 10); + return '0'; + } + + public static int fromHex(char c) { + if ('0' <= c && c <= '9') return c - '0'; + if ('A' <= c && c <= 'F') return c - 'A' + 10; + if ('a' <= c && c <= 'f') return c - 'a' + 10; + return 0; + } +} diff --git a/backend/src/main/java/achievements/misc/Password.java b/backend/src/main/java/achievements/misc/Password.java index 3707221..f96958d 100644 --- a/backend/src/main/java/achievements/misc/Password.java +++ b/backend/src/main/java/achievements/misc/Password.java @@ -1,14 +1,7 @@ package achievements.misc; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.util.Random; - public class Password { - private static final Random RANDOM = new SecureRandom(); - public final String salt; public final String hash; @@ -19,19 +12,17 @@ public class Password { public static Password generate(String password) { // Generate the salt - var salt = new byte[16]; // 128 bits - RANDOM.nextBytes(salt); + var salt = HashManager.generateBytes(16); // 128 bits return new Password( - encode(salt), - encode(hash(salt, password.getBytes())) + HashManager.encode(salt), + HashManager.encode(HashManager.hash(salt, password.getBytes())) ); } public static boolean validate(String salt, String password, String hash) { - System.out.println(salt + ", " + password); - var srcHash = hash(decode(salt), password.getBytes()); - var targetHash = decode(hash); + var srcHash = HashManager.hash(HashManager.decode(salt), password.getBytes()); + var targetHash = HashManager.decode(hash); for (int i = 0; i < srcHash.length; ++i) { if (srcHash[i] != targetHash[i]) { return false; @@ -39,57 +30,4 @@ public class Password { } return true; } - - private static byte[] hash(byte[] salt, byte[] password) { - try { - var concat = new byte[salt.length + password.length]; - int i = 0; - for (; i < salt.length; ++i) { - concat[i] = salt[i]; - } - for (int j = 0; j < password.length; ++j) { - concat[i + j] = password[j]; - } - - var md = MessageDigest.getInstance("SHA-256"); - return md.digest(concat); - } catch (NoSuchAlgorithmException e) { - e.printStackTrace(); - } - return null; - } - - private static String encode(byte[] bytes) { - var chars = new char[bytes.length << 1]; - for (int i = 0; i < bytes.length; ++i) { - chars[(i << 1) ] = toHex(bytes[i] >> 0); - chars[(i << 1) + 1] = toHex(bytes[i] >> 4); - } - return new String(chars); - } - - private static byte[] decode(String data) { - var decoded = new byte[data.length() >> 1]; - for (int i = 0; i < data.length(); i += 2) { - int currentByte = - (fromHex(data.charAt(i )) ) | - (fromHex(data.charAt(i + 1)) << 4); - decoded[i >> 1] = (byte) (currentByte & 0xFF); - } - return decoded; - } - - private static char toHex(int halfByte) { - halfByte = halfByte & 0xF; - if (0 <= halfByte && halfByte <= 9 ) return (char) (halfByte + '0' ); - if (10 <= halfByte && halfByte <= 15) return (char) (halfByte + 'a' - 10); - return '0'; - } - - private static int fromHex(char c) { - if ('0' <= c && c <= '9') return c - '0'; - if ('A' <= c && c <= 'F') return c - 'A' + 10; - if ('a' <= c && c <= 'f') return c - 'a' + 10; - return 0; - } } diff --git a/backend/src/main/java/achievements/misc/SessionManager.java b/backend/src/main/java/achievements/misc/SessionManager.java new file mode 100644 index 0000000..c150242 --- /dev/null +++ b/backend/src/main/java/achievements/misc/SessionManager.java @@ -0,0 +1,28 @@ +package achievements.misc; + +import java.util.HashMap; + +public class SessionManager { + + private HashMap session; + + public SessionManager() { + session = new HashMap(); + } + + public String generate(Integer user) { + var key = HashManager.encode(HashManager.generateBytes(16)); + session.put(key, user); + return key; + } + + public String guest() { + var key = HashManager.encode(HashManager.generateBytes(16)); + session.put(key, null); + return key; + } + + public Integer getUser(String key) { + return session.get(key); + } +} diff --git a/backend/src/main/java/achievements/services/AuthenticationService.java b/backend/src/main/java/achievements/services/AuthenticationService.java new file mode 100644 index 0000000..1807cf7 --- /dev/null +++ b/backend/src/main/java/achievements/services/AuthenticationService.java @@ -0,0 +1,106 @@ +package achievements.services; + +import achievements.data.User; +import achievements.misc.DbConnectionService; +import achievements.misc.Password; +import achievements.misc.SessionManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.sql.*; + +@Service +public class AuthenticationService { + + public static class LoginResponse { + public int status; + public Integer id; + + public LoginResponse() { + this.status = 0; + this.id = null; + } + + public LoginResponse(int status) { + this.status = status; + this.id = null; + } + + public LoginResponse(int status, int id) { + this.status = status; + this.id = id; + } + } + + public static final LoginResponse GUEST = new LoginResponse(); + + @Autowired + private DbConnectionService dbs; + private Connection db; + + private SessionManager session; + + @PostConstruct + private void init() { + db = dbs.getConnection(); + session = new SessionManager(); + } + + public LoginResponse createUser(User user) { + if (!user.getEmail().matches(".+@\\w+\\.\\w+")) { + return new LoginResponse(2); + } + + try { + var statement = db.prepareCall("{? = call CreateUser(?, ?, ?, ?, ?)}"); + statement.registerOutParameter(1, Types.INTEGER); + statement.setString(2, user.getEmail()); + statement.setString(3, user.getUsername()); + + var password = Password.generate(user.getPassword()); + statement.setString(4, password.salt); + statement.setString(5, password.hash); + + statement.registerOutParameter(6, Types.INTEGER); + + statement.execute(); + var response = new LoginResponse(statement.getInt(1), statement.getInt(6)); + statement.close(); + + return response; + } catch (SQLException e) { + e.printStackTrace(); + } + return new LoginResponse(-1); + } + + public LoginResponse login(User user) { + var response = new LoginResponse(-1); + try { + var statement = db.prepareCall("{call GetUserLogin(?)}"); + statement.setString(1, user.email); + + var result = statement.executeQuery(); + if (result.next()) { + var salt = result.getString("Salt"); + var hash = result.getString("Password"); + if (Password.validate(salt, user.getPassword(), hash)) { + response = new LoginResponse(0, result.getInt("ID")); + } else { + response = new LoginResponse(2); + } + } else { + response = new LoginResponse(1); + } + statement.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + return response; + } + + public SessionManager session() { + return session; + } +} diff --git a/backend/src/main/java/achievements/services/DbService.java b/backend/src/main/java/achievements/services/DbService.java index d00df35..4b9d310 100644 --- a/backend/src/main/java/achievements/services/DbService.java +++ b/backend/src/main/java/achievements/services/DbService.java @@ -1,134 +1,92 @@ -package achievements.services; - -import achievements.data.Achievements; -import achievements.data.Games; -import achievements.data.User; -import achievements.misc.Password; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import javax.annotation.PostConstruct; -import java.sql.*; - -@Service -public class DbService { - - @Autowired - private DbConnectionService dbs; - private Connection db; - - @PostConstruct - private void init() { db = dbs.getConnection(); } - - public Achievements getAchievements(String gameName) { - try { - // Create Query - CallableStatement stmt = db.prepareCall("{? = call GetAchievements(?)}"); - stmt.registerOutParameter(1, Types.INTEGER); - stmt.setString(2, gameName); - - // Read Result(s) - ResultSet results = stmt.executeQuery(); - var achievements = new Achievements(); - while (results.next()) { - // Add Result(s) to data class - int achievementGameID = results.getInt("GameID"); - String achievementGameName = results.getString("GameName"); - String achievementName = results.getString("Name"); - String achievementDescription = results.getString("Description"); - int achievementStages = results.getInt("Stages"); - // Checks if getting from specific game or all achievements - if (!gameName.equals("%")) { - achievements.setGameID(achievementGameID); - achievements.setGameName(achievementGameName); - } - achievements.addAchievement(new Achievements.Achievement(achievementName, achievementDescription, achievementStages)); - } - stmt.close(); - return achievements; - } catch (SQLException e) { - e.printStackTrace(); - return null; - } - } - - public Games getGames(String name) { - try { - // Create Query - CallableStatement stmt = db.prepareCall("{? = call GetGame(?)}"); - stmt.registerOutParameter(1, Types.INTEGER); - stmt.setString(2, name); - - // Read Result(s) - ResultSet results = stmt.executeQuery(); - var games = new Games(); - while (results.next()) { - // Add Result(s) to data class - int gameID = results.getInt("ID"); - String gameName = results.getString("Name"); - String gamePlatform = results.getString("PlatformName"); - if (!games.getGames().isEmpty()) { - var lastGame = games.getGames().get(games.getGames().size()-1); - if (lastGame.getId() == gameID) { - lastGame.addToPlatforms(gamePlatform); - } else { - games.addGame(new Games.Game(gameID,gameName,gamePlatform)); - } - } else { - games.addGame(new Games.Game(gameID,gameName,gamePlatform)); - } - - } - stmt.close(); - return games; - } catch (SQLException e) { - e.printStackTrace(); - return null; - } - } - - public int createUser(User user) { - try { - var statement = db.prepareCall("{? = call CreateUser(?, ?, ?, ?)}"); - statement.registerOutParameter(1, Types.INTEGER); - statement.setString(2, user.getEmail()); - statement.setString(3, user.getUsername()); - - var password = Password.generate(user.getPassword()); - statement.setString(4, password.salt); - statement.setString(5, password.hash); - - statement.execute(); - var code = statement.getInt(1); - statement.close(); - - return code; - } catch (SQLException e) { - e.printStackTrace(); - } - return -1; - } - - public int login(User user) { - try { - var statement = db.prepareStatement("SELECT Salt, Password FROM [dbo].[User] WHERE Email = ?"); - statement.setString(1, user.email); - - var result = statement.executeQuery(); - if (result.next()) { - var salt = result.getString("Salt"); - var hash = result.getString("Password"); - if (Password.validate(salt, user.getPassword(), hash)) { - return 0; - } else { - return 2; - } - } else { - return 1; - } - } catch (SQLException e) { - e.printStackTrace(); - } - return -1; - } -} +package achievements.services; + +import achievements.data.Achievements; +import achievements.data.Games; +import achievements.misc.DbConnectionService; +import achievements.misc.SessionManager; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import javax.annotation.PostConstruct; +import java.sql.*; + +@Service +public class DbService { + + @Autowired + private DbConnectionService dbs; + private Connection db; + + @PostConstruct + private void init() { + db = dbs.getConnection(); + } + + public Achievements getAchievements(String gameName) { + try { + // Create Query + CallableStatement stmt = db.prepareCall("{? = call GetAchievements(?)}"); + stmt.registerOutParameter(1, Types.INTEGER); + stmt.setString(2, gameName); + + // Read Result(s) + ResultSet results = stmt.executeQuery(); + var achievements = new Achievements(); + while (results.next()) { + // Add Result(s) to data class + int achievementGameID = results.getInt("GameID"); + String achievementGameName = results.getString("GameName"); + String achievementName = results.getString("Name"); + String achievementDescription = results.getString("Description"); + int achievementStages = results.getInt("Stages"); + // Checks if getting from specific game or all achievements + if (!gameName.equals("%")) { + achievements.setGameID(achievementGameID); + achievements.setGameName(achievementGameName); + } + achievements.addAchievement(new Achievements.Achievement(achievementName, achievementDescription, achievementStages)); + } + stmt.close(); + return achievements; + } catch (SQLException e) { + e.printStackTrace(); + return null; + } + } + + public Games getGames(String name) { + try { + // Create Query + CallableStatement stmt = db.prepareCall("{? = call GetGame(?)}"); + stmt.registerOutParameter(1, Types.INTEGER); + stmt.setString(2, name); + + // Read Result(s) + ResultSet results = stmt.executeQuery(); + var games = new Games(); + while (results.next()) { + // Add Result(s) to data class + int gameID = results.getInt("ID"); + String gameName = results.getString("Name"); + String gamePlatform = results.getString("PlatformName"); + if (!games.getGames().isEmpty()) { + var lastGame = games.getGames().get(games.getGames().size()-1); + if (lastGame.getId() == gameID) { + lastGame.addToPlatforms(gamePlatform); + } else { + games.addGame(new Games.Game(gameID,gameName,gamePlatform)); + } + } else { + games.addGame(new Games.Game(gameID,gameName,gamePlatform)); + } + + } + stmt.close(); + return games; + } catch (SQLException e) { + e.printStackTrace(); + return null; + } + } + +} diff --git a/frontend/webpage/index.html b/frontend/webpage/index.html index 0ca9edc..0c7ee88 100644 --- a/frontend/webpage/index.html +++ b/frontend/webpage/index.html @@ -23,9 +23,11 @@