From 4f8b45f0747b3be7c266a1815590dc1eb946f74a Mon Sep 17 00:00:00 2001 From: jeffcheasey88 Date: Fri, 8 Sep 2023 02:18:19 +0200 Subject: [PATCH] Merge from my branch september --- .gitignore | 1 + PeerAtCodeFramework.jar | Bin 484562 -> 486684 bytes database-schem.sql | 126 ++++++++++++++ src/dev/peerat/backend/Configuration.java | 31 ++-- src/dev/peerat/backend/Main.java | 60 ++++--- src/dev/peerat/backend/model/Chapter.java | 9 + src/dev/peerat/backend/model/Player.java | 10 +- .../backend/repository/DatabaseQuery.java | 3 + .../repository/DatabaseRepository.java | 109 +++++------- .../peerat/backend/routes/ChapterElement.java | 30 ++-- .../peerat/backend/routes/ChapterList.java | 5 +- .../backend/routes/DynamicLeaderboard.java | 4 +- .../backend/routes/MailConfirmation.java | 96 ----------- .../backend/routes/admins/DynamicLogs.java | 56 +++++++ .../backend/routes/users/ChangePassword.java | 36 ++++ .../backend/routes/users/ForgotPassword.java | 25 +++ .../backend/routes/{ => users}/Login.java | 34 ++-- .../routes/users/MailConfirmation.java | 156 ++++++++++++++++++ .../backend/routes/users/ProfileSettings.java | 58 +++++++ .../peerat/backend/routes/users/Register.java | 74 +++++++++ .../peerat/backend/utils/FormResponse.java | 49 ++++++ src/dev/peerat/backend/utils/Mail.java | 4 +- .../backend/TestDatabaseRepository.java | 64 +++++++ .../backend/{webclient => }/WebClient.java | 2 +- .../backend/routes/PlayerDetailsTests.java | 2 +- .../dev/peerat/backend/routes/ScoreTests.java | 2 +- .../peerat/backend/routes/TmpRoutesTests.java | 2 +- .../peerat/backend/routes/TriggerTests.java | 2 +- .../userstories/BaseUserStoriesTest.java | 30 ++++ .../backend/userstories/LoginTests.java | 79 +++++++++ .../backend/userstories/RegisterTests.java | 90 ++++++++++ 31 files changed, 1002 insertions(+), 247 deletions(-) create mode 100644 database-schem.sql delete mode 100644 src/dev/peerat/backend/routes/MailConfirmation.java create mode 100644 src/dev/peerat/backend/routes/admins/DynamicLogs.java create mode 100644 src/dev/peerat/backend/routes/users/ChangePassword.java create mode 100644 src/dev/peerat/backend/routes/users/ForgotPassword.java rename src/dev/peerat/backend/routes/{ => users}/Login.java (64%) create mode 100644 src/dev/peerat/backend/routes/users/MailConfirmation.java create mode 100644 src/dev/peerat/backend/routes/users/ProfileSettings.java create mode 100644 src/dev/peerat/backend/routes/users/Register.java create mode 100644 src/dev/peerat/backend/utils/FormResponse.java create mode 100644 test/dev/peerat/backend/TestDatabaseRepository.java rename test/dev/peerat/backend/{webclient => }/WebClient.java (99%) create mode 100644 test/dev/peerat/backend/userstories/BaseUserStoriesTest.java create mode 100644 test/dev/peerat/backend/userstories/LoginTests.java create mode 100644 test/dev/peerat/backend/userstories/RegisterTests.java diff --git a/.gitignore b/.gitignore index 38fb1c2..e567647 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ bin/ .project config.txt +config-test.txt dist/ testApi/ .apt_generated/* diff --git a/PeerAtCodeFramework.jar b/PeerAtCodeFramework.jar index 781d0521c74c2b77c17fadb292e3f60f3d005322..ef73b462747045c300978accaef3264d76d14b78 100644 GIT binary patch delta 27902 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 O$=#^a?QX+qtNR}@z!F{n delta 26263 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 diff --git a/database-schem.sql b/database-schem.sql new file mode 100644 index 0000000..504140e --- /dev/null +++ b/database-schem.sql @@ -0,0 +1,126 @@ +DROP TABLE IF EXISTS `containsTags`; +DROP TABLE IF EXISTS `tags`; +DROP TABLE IF EXISTS `containsBadges`; +DROP TABLE IF EXISTS `badges`; +DROP TABLE IF EXISTS `containsGroups`; +DROP TABLE IF EXISTS `nextPart`; +DROP TABLE IF EXISTS `groups`; +DROP TABLE IF EXISTS `completions`; +DROP TABLE IF EXISTS `players`; +DROP TABLE IF EXISTS `puzzles`; +DROP TABLE IF EXISTS `chapters`; + +CREATE TABLE `players` ( + `id_player` int(11) NOT NULL AUTO_INCREMENT, + `pseudo` varchar(100) NOT NULL, + `email` varchar(100) NOT NULL, + `passwd` varchar(150) NOT NULL, + `firstname` varchar(100) NOT NULL, + `lastname` varchar(100) NOT NULL, + `description` varchar(200) DEFAULT NULL, + `avatar` blob DEFAULT NULL, + PRIMARY KEY (`id_player`) +) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +CREATE TABLE badges ( + id_badge int(11) NOT NULL AUTO_INCREMENT, + name varchar(50) NOT NULL, + logo mediumblob DEFAULT NULL, + level int(11) DEFAULT 1, + PRIMARY KEY (id_badge) +); + +CREATE TABLE `chapters` ( + `id_chapter` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(150) NOT NULL, + `start_date` datetime DEFAULT NULL, + `end_date` datetime DEFAULT NULL, + PRIMARY KEY (`id_chapter`) +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +CREATE TABLE `puzzles` ( + `id_puzzle` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(150) NOT NULL, + `content` text NOT NULL, + `soluce` blob NOT NULL, + `verify` text DEFAULT NULL, + `score_max` int(11) NOT NULL, + `fk_chapter` int(11) NOT NULL, + PRIMARY KEY (`id_puzzle`), + KEY `fk_chapter` (`fk_chapter`), + CONSTRAINT `puzzles_ibfk_1` FOREIGN KEY (`fk_chapter`) REFERENCES `chapters` (`id_chapter`) +) ENGINE=InnoDB AUTO_INCREMENT=49 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + + +CREATE TABLE `groups` ( + `id_group` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(150) DEFAULT NULL, + `fk_chapter` int(11) DEFAULT NULL, + `fk_puzzle` int(11) DEFAULT NULL, + PRIMARY KEY (`id_group`), + KEY `fk_chapter` (`fk_chapter`), + KEY `fk_puzzle` (`fk_puzzle`), + CONSTRAINT `groups_ibfk_1` FOREIGN KEY (`fk_chapter`) REFERENCES `chapters` (`id_chapter`), + CONSTRAINT `groups_ibfk_2` FOREIGN KEY (`fk_puzzle`) REFERENCES `puzzles` (`id_puzzle`) +) ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + + +CREATE TABLE `nextPart` ( + `origin` int(11) NOT NULL, + `next` int(11) NOT NULL, + PRIMARY KEY (`origin`,`next`), + KEY `next` (`next`), + CONSTRAINT `nextPart_ibfk_1` FOREIGN KEY (`origin`) REFERENCES `puzzles` (`id_puzzle`), + CONSTRAINT `nextPart_ibfk_2` FOREIGN KEY (`next`) REFERENCES `puzzles` (`id_puzzle`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + + +CREATE TABLE `completions` ( + `id_completion` int(11) NOT NULL AUTO_INCREMENT, + `fk_puzzle` int(11) NOT NULL, + `fk_player` int(11) NOT NULL, + `tries` int(11) DEFAULT 0, + `code` blob DEFAULT NULL, + `score` int(11) DEFAULT 0, + `fileName` varchar(100) DEFAULT NULL, + PRIMARY KEY (`id_completion`), + KEY `fk_puzzle` (`fk_puzzle`), + KEY `fk_player` (`fk_player`), + CONSTRAINT `completions_ibfk_1` FOREIGN KEY (`fk_puzzle`) REFERENCES `puzzles` (`id_puzzle`), + CONSTRAINT `completions_ibfk_2` FOREIGN KEY (`fk_player`) REFERENCES `players` (`id_player`) +) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +CREATE TABLE `tags` ( + `id_tag` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(50) NOT NULL, + PRIMARY KEY (`id_tag`) +) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +CREATE TABLE `containsBadges` ( + `fk_player` int(11) NOT NULL, + `fk_badge` int(11) NOT NULL, + PRIMARY KEY (`fk_player`,`fk_badge`), + KEY `fk_badge` (`fk_badge`), + CONSTRAINT `containsBadges_ibfk_1` FOREIGN KEY (`fk_player`) REFERENCES `players` (`id_player`), + CONSTRAINT `containsBadges_ibfk_2` FOREIGN KEY (`fk_badge`) REFERENCES `badges` (`id_badge`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +CREATE TABLE `containsGroups` ( + `fk_player` int(11) NOT NULL, + `fk_group` int(11) NOT NULL, + PRIMARY KEY (`fk_player`,`fk_group`), + KEY `fk_group` (`fk_group`), + CONSTRAINT `containsGroups_ibfk_1` FOREIGN KEY (`fk_player`) REFERENCES `players` (`id_player`), + CONSTRAINT `containsGroups_ibfk_2` FOREIGN KEY (`fk_group`) REFERENCES `groups` (`id_group`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +CREATE TABLE `containsTags` ( + `fk_tag` int(11) NOT NULL, + `fk_puzzle` int(11) NOT NULL, + PRIMARY KEY (`fk_tag`,`fk_puzzle`), + KEY `fk_puzzle` (`fk_puzzle`), + CONSTRAINT `containsTags_ibfk_1` FOREIGN KEY (`fk_tag`) REFERENCES `tags` (`id_tag`), + CONSTRAINT `containsTags_ibfk_2` FOREIGN KEY (`fk_puzzle`) REFERENCES `puzzles` (`id_puzzle`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + + diff --git a/src/dev/peerat/backend/Configuration.java b/src/dev/peerat/backend/Configuration.java index 8e05047..5eea2f7 100644 --- a/src/dev/peerat/backend/Configuration.java +++ b/src/dev/peerat/backend/Configuration.java @@ -35,20 +35,22 @@ public class Configuration { private String mailUsername; private String mailPassword; private String mailSmtpHost; - private int mailSmptPort; + private int mailSmtpPort; private String mailFromAddress; + + private String git_token; - private File file; + private File _file; public Configuration(String path) { - this.file = new File(path); - System.out.println("Config on " + file.getAbsolutePath()); + this._file = new File(path); + System.out.println("Config on " + _file.getAbsolutePath()); } public void load() throws Exception { - if (!this.file.exists()) + if (!this._file.exists()) return; - BufferedReader reader = new BufferedReader(new FileReader(this.file)); + BufferedReader reader = new BufferedReader(new FileReader(this._file)); String line; while ((line = reader.readLine()) != null) { String[] split = line.split("="); @@ -101,14 +103,13 @@ public class Configuration { } public void save() throws Exception { - if (!file.exists()) { - File parent = file.getParentFile(); - if (!parent.exists()) - parent.mkdirs(); - file.createNewFile(); + if (!_file.exists()) { + File parent = _file.getParentFile(); + if(!parent.exists()) parent.mkdirs(); + _file.createNewFile(); } Field[] fields = getClass().getDeclaredFields(); - BufferedWriter writer = new BufferedWriter(new FileWriter(file)); + BufferedWriter writer = new BufferedWriter(new FileWriter(_file)); for (Field field : fields) { field.setAccessible(true); if (field.getName().startsWith("_")) @@ -187,7 +188,11 @@ public class Configuration { this.mailUsername, this.mailPassword, this.mailSmtpHost, - this.mailSmptPort, + this.mailSmtpPort, this.mailFromAddress); } + + public String getGitToken(){ + return this.git_token; + } } \ No newline at end of file diff --git a/src/dev/peerat/backend/Main.java b/src/dev/peerat/backend/Main.java index 41d87bd..f19b519 100644 --- a/src/dev/peerat/backend/Main.java +++ b/src/dev/peerat/backend/Main.java @@ -16,18 +16,21 @@ import dev.peerat.backend.routes.ChapterElement; import dev.peerat.backend.routes.ChapterList; import dev.peerat.backend.routes.DynamicLeaderboard; import dev.peerat.backend.routes.Leaderboard; -import dev.peerat.backend.routes.Login; -import dev.peerat.backend.routes.MailConfirmation; import dev.peerat.backend.routes.PlayerDetails; import dev.peerat.backend.routes.PuzzleElement; import dev.peerat.backend.routes.PuzzleResponse; -import dev.peerat.backend.routes.Register; import dev.peerat.backend.routes.Result; +import dev.peerat.backend.routes.admins.DynamicLogs; import dev.peerat.backend.routes.groups.GroupCreate; import dev.peerat.backend.routes.groups.GroupJoin; import dev.peerat.backend.routes.groups.GroupList; import dev.peerat.backend.routes.groups.GroupQuit; -import dev.peerat.backend.utils.Mail; +import dev.peerat.backend.routes.users.ChangePassword; +import dev.peerat.backend.routes.users.ForgotPassword; +import dev.peerat.backend.routes.users.Login; +import dev.peerat.backend.routes.users.MailConfirmation; +import dev.peerat.backend.routes.users.ProfileSettings; +import dev.peerat.backend.routes.users.Register; import dev.peerat.framework.Context; import dev.peerat.framework.HttpReader; import dev.peerat.framework.HttpWriter; @@ -47,15 +50,19 @@ public class Main{ DatabaseRepository repo = new DatabaseRepository(config); Router router = new Router() .configureJwt( - (builder) -> builder.setExpectedIssuer(config.getTokenIssuer()), - (claims) -> { - claims.setIssuer(config.getTokenIssuer()); // who creates the token and signs it - claims.setExpirationTimeMinutesInTheFuture(config.getTokenExpiration()); - }, - (claims) -> new PeerAtUser(claims)); - - router.addDefaultHeaders(RequestType.GET, "Access-Control-Allow-Origin: *"); - router.addDefaultHeaders(RequestType.POST, "Access-Control-Allow-Origin: *"); + (builder) -> builder.setExpectedIssuer(config.getTokenIssuer()), + (claims) -> { + claims.setIssuer(config.getTokenIssuer()); // who creates the token and signs it + claims.setExpirationTimeMinutesInTheFuture(config.getTokenExpiration()); + }, + (claims) -> new PeerAtUser(claims)) + .activeReOrdering(). + addDefaultHeaders(RequestType.GET, "Access-Control-Allow-Origin: *"). + addDefaultHeaders(RequestType.POST, "Access-Control-Allow-Origin: *"). + addDefaultHeaders(RequestType.OPTIONS, + "Access-Control-Allow-Origin: *", + "Access-Control-Allow-Methods: *", + "Access-Control-Allow-Headers: *"); router.setDefault((matcher, context, reader, writer) -> { context.response(404); @@ -65,10 +72,7 @@ public class Main{ router.register(new Response(){ @Route(path = "^(.*)$", type = OPTIONS) public void exec(Matcher matcher, Context context, HttpReader reader, HttpWriter writer) throws Exception { - context.response(200, - "Access-Control-Allow-Origin: *", - "Access-Control-Allow-Methods: *", - "Access-Control-Allow-Headers: *"); + context.response(200); } }); @@ -84,26 +88,30 @@ public class Main{ private static void initRoutes(Router router, DatabaseRepository repo, Configuration config){ Map playersWaiting = new HashMap<>(); + router.register(new Register(repo, playersWaiting)); + router.register(new MailConfirmation(repo, router, config.getUsersFiles(), config.getGitToken(), playersWaiting)); + router.register(new Login(repo, router)); + router.register(new ProfileSettings(repo)); + router.register(new ChangePassword(repo)); + router.register(new ForgotPassword()); + + router.register(new DynamicLogs(repo, router.getLogger())); + router.register(new ChapterElement(repo)); router.register(new ChapterList(repo)); router.register(new PuzzleElement(repo)); - router.register(new Register(repo, router, config.getUsersFiles(), playersWaiting)); - router.register(new MailConfirmation(repo, router, config.getUsersFiles(), playersWaiting)); - router.register(new Login(repo, router)); router.register(new Result(repo)); router.register(new Leaderboard(repo)); router.register(new PlayerDetails(repo)); router.register(new BadgeDetails(repo)); Locker groupLock = new Locker<>(); - router.register(new GroupCreate(repo, groupLock, config.getGroupJoinMinutes())); - - DynamicLeaderboard dlb = new DynamicLeaderboard(repo); - router.register(dlb); - - Locker leaderboard = dlb.getLocker(); + Locker leaderboard = new Locker<>(); + router.register(new DynamicLeaderboard(repo, leaderboard)); router.register(new PuzzleResponse(repo, config.getUsersFiles(), leaderboard)); + + router.register(new GroupCreate(repo, groupLock, config.getGroupJoinMinutes())); router.register(new GroupList(repo)); router.register(new GroupJoin(repo, config.getGroupJoinMinutes(), config.getGroupQuitMinutes(), leaderboard)); router.register(new GroupQuit(repo, config.getGroupJoinMinutes(), leaderboard)); diff --git a/src/dev/peerat/backend/model/Chapter.java b/src/dev/peerat/backend/model/Chapter.java index 0167932..dc35c49 100644 --- a/src/dev/peerat/backend/model/Chapter.java +++ b/src/dev/peerat/backend/model/Chapter.java @@ -1,6 +1,7 @@ package dev.peerat.backend.model; import java.sql.Timestamp; +import java.time.LocalDateTime; import java.util.List; public class Chapter { @@ -33,6 +34,14 @@ public class Chapter { public void setPuzzles(List puzzles) { this.puzzles = puzzles; } + + public boolean isInCurrentTime(){ + LocalDateTime now = LocalDateTime.now(); + boolean show = true; + if(startDate != null) show &= now.isAfter(startDate.toLocalDateTime()); + if(endDate != null) show &= now.isBefore(endDate.toLocalDateTime()); + return show; + } public Timestamp getStartDate() { return startDate; diff --git a/src/dev/peerat/backend/model/Player.java b/src/dev/peerat/backend/model/Player.java index 53d5575..a532052 100644 --- a/src/dev/peerat/backend/model/Player.java +++ b/src/dev/peerat/backend/model/Player.java @@ -41,6 +41,10 @@ public class Player implements Comparable { // For player find in Map during register process this.email = email; } + + public void setPseudo(String pseudo){ + this.pseudo = pseudo; + } public String getPseudo() { return this.pseudo; @@ -185,8 +189,8 @@ public class Player implements Comparable { } @Override - public int hashCode() { - return Objects.hash(email, pseudo); + public int hashCode(){ + return Objects.hash(email); } @Override @@ -198,6 +202,6 @@ public class Player implements Comparable { if (getClass() != obj.getClass()) return false; Player other = (Player) obj; - return Objects.equals(email, other.email) && Objects.equals(pseudo, other.pseudo); + return Objects.equals(email, other.email); } } diff --git a/src/dev/peerat/backend/repository/DatabaseQuery.java b/src/dev/peerat/backend/repository/DatabaseQuery.java index 8d4ce39..b0ba59f 100644 --- a/src/dev/peerat/backend/repository/DatabaseQuery.java +++ b/src/dev/peerat/backend/repository/DatabaseQuery.java @@ -69,6 +69,7 @@ public enum DatabaseQuery { // PLAYERS GET_PLAYER_SIMPLE("SELECT pseudo, email, firstname, lastname, description FROM players WHERE id_player = ?"), + GET_PLAYER_PSEUDO("SELECT * FROM players WHERE pseudo = ?"), GET_PLAYER_DETAILS("SELECT p.*, g.*\r\n" + "FROM players p\r\n" + "LEFT OUTER JOIN containsGroups cg ON p.id_player = cg.fk_player\r\n" @@ -79,6 +80,8 @@ public enum DatabaseQuery { GET_PLAYER_DETAILS_BY_PSEUDO(GET_PLAYER_DETAILS, "p.pseudo = ? GROUP BY g.name ORDER BY g.fk_chapter, g.fk_puzzle;"), GET_PLAYER_COMPLETIONS("select c.*, p.name from completions c left join puzzles p on c.fk_puzzle = p.id_puzzle where fk_player = ?;"), GET_PLAYER_RANK("SELECT * FROM (SELECT fk_player, RANK() OVER(ORDER BY SUM(score) DESC) rank FROM completions c LEFT JOIN players p ON p.id_player = c.fk_player GROUP BY fk_player ORDER BY rank) AS ranks WHERE ranks.fk_player = ?;"), + UPDATE_PLAYER_INFO("UPDATE players SET pseudo = ?, email = ?, first_name = ?, last_name = ? WHERE id_player = ?"), + UPDATE_PLAYER_PASSWORD("UPDATE players SET passwd = ? WHERE id_player = ?"), // BADGES GET_BADGE("SELECT * FROM badges WHERE id_badge = ?"), GET_BADGES_OF_PLAYER( diff --git a/src/dev/peerat/backend/repository/DatabaseRepository.java b/src/dev/peerat/backend/repository/DatabaseRepository.java index 259fa88..87bbe74 100644 --- a/src/dev/peerat/backend/repository/DatabaseRepository.java +++ b/src/dev/peerat/backend/repository/DatabaseRepository.java @@ -32,68 +32,6 @@ public class DatabaseRepository { public DatabaseRepository(Configuration config) { this.config = config; } -// testTrigger(); -// } -// -// private void testTrigger(){ -// try { -// ensureConnection(); -// }catch(Exception e){ -// e.printStackTrace(); -// } -// System.out.println("connection ensured"); -// -// try { -// PreparedStatement log = this.con.prepareStatement("DROP TABLE mycustomlog;"); -// log.execute(); -// }catch(Exception e){ -// e.printStackTrace(); -// } -// System.out.println("log dropped"); -// -// try { -// PreparedStatement log = this.con.prepareStatement("CREATE TABLE mycustomlog(\r\n" -// + " message VARCHAR(255),\r\n" -// + " primary key (message)\r\n" -// + ");"); -// log.execute(); -// }catch(Exception e){ -// e.printStackTrace(); -// } -// System.out.println("log created"); -// -// try { -// System.out.println(DatabaseQuery.FIRST_TRY.toString()); -// DatabaseQuery.FIRST_TRY.prepare(this.con).execute(); -// }catch(Exception e){ -// e.printStackTrace(); -// } -// -// System.out.println("trigger inserted"); -// -// try { -// insertCompletion(new Completion(1, 1, 1, null, 1)); -// } catch (SQLException e1) { -// e1.printStackTrace(); -// } -// -// try { -// showLog(); -// } catch (Exception e) { -// e.printStackTrace(); -// } -// System.out.println("------------------------------"); -// } -// -// private void showLog() throws Exception{ -// ensureConnection(); -// -// PreparedStatement stmt = this.con.prepareStatement("SELECT * FROM mycustomlog"); -// ResultSet result = stmt.executeQuery(); -// while(result.next()){ -// System.out.println("[LOG] "+result.getString("message")); -// } -// } private void ensureConnection() throws SQLException { if (con == null || (!con.isValid(5))) { @@ -277,6 +215,50 @@ public class DatabaseRepository { } return null; } + + public boolean updatePseudo(int id, Player player, String pseudo){ + try{ + PreparedStatement statment = DatabaseQuery.GET_PLAYER_PSEUDO.prepare(this.con); + statment.setString(1, pseudo); + ResultSet result = statment.executeQuery(); + if(result.next()) return false; + statment = DatabaseQuery.UPDATE_PLAYER_INFO.prepare(this.con); + statment.setString(1, player.getPseudo()); + statment.setString(2, player.getEmail()); + statment.setString(3, player.getFirstname()); + statment.setString(4, player.getLastname()); + statment.setInt(5, id); + return statment.executeUpdate() > 0; + }catch(Exception e){ + e.printStackTrace(); + } + return false; + } + + public void updateProfile(int id, Player player, String lastname, String firstname){ + try{ + PreparedStatement statment = DatabaseQuery.UPDATE_PLAYER_INFO.prepare(this.con); + statment.setString(1, player.getPseudo()); + statment.setString(2, player.getEmail()); + statment.setString(3, firstname); + statment.setString(4, lastname); + statment.setInt(5, id); + statment.executeUpdate(); + }catch(Exception e){ + e.printStackTrace(); + } + } + + public void updatePassword(int id, String password){ + try{ + PreparedStatement statment = DatabaseQuery.UPDATE_PLAYER_PASSWORD.prepare(this.con); + statment.setString(1, Password.hash(password).withArgon2().getResult()); + statment.setInt(2, id); + statment.executeUpdate(); + }catch(Exception e){ + e.printStackTrace(); + } + } public Player getPlayerDetails(int idPlayer) { return getPlayerDetails(idPlayer, null); @@ -520,7 +502,6 @@ public class DatabaseRepository { */ public int register(String pseudo, String email, String password, String firstname, String lastname, String description, String sgroup, String avatar) { - Hash hash = Password.hash(password).withArgon2(); try { ensureConnection(); con.setAutoCommit(false); @@ -528,7 +509,7 @@ public class DatabaseRepository { Statement.RETURN_GENERATED_KEYS)) { playerStatement.setString(1, pseudo); playerStatement.setString(2, email); - playerStatement.setString(3, hash.getResult()); + playerStatement.setString(3, Password.hash(password).withArgon2().getResult()); playerStatement.setString(4, firstname); playerStatement.setString(5, lastname); playerStatement.setString(6, description); diff --git a/src/dev/peerat/backend/routes/ChapterElement.java b/src/dev/peerat/backend/routes/ChapterElement.java index a54ee61..74e75b0 100644 --- a/src/dev/peerat/backend/routes/ChapterElement.java +++ b/src/dev/peerat/backend/routes/ChapterElement.java @@ -34,23 +34,23 @@ public class ChapterElement implements Response { JSONObject chapterJSON = new JSONObject(); chapterJSON.put("id", chapter.getId()); chapterJSON.put("name", chapter.getName()); - if (chapter.getStartDate() != null) - chapterJSON.put("startDate", chapter.getStartDate().toString()); - if (chapter.getEndDate() != null) - chapterJSON.put("endDate", chapter.getEndDate().toString()); - JSONArray puzzles = new JSONArray(); + boolean show = chapter.isInCurrentTime(); + chapterJSON.put("show", show); PeerAtUser user = context.getUser(); - for (Puzzle puzzle : chapter.getPuzzles()){ - JSONObject puzzleJSON = new JSONObject(); - puzzleJSON.put("id", puzzle.getId()); - puzzleJSON.put("name", puzzle.getName()); - puzzleJSON.put("scoreMax", puzzle.getScoreMax()); - if (puzzle.getTags() != null) puzzleJSON.put("tags", puzzle.getJsonTags()); - int score = this.databaseRepo.getScore(user.getId(), puzzle.getId()); - if(score >= 0) puzzleJSON.put("score", score); - puzzles.add(puzzleJSON); + if(show){ + JSONArray puzzles = new JSONArray(); + for (Puzzle puzzle : chapter.getPuzzles()){ + JSONObject puzzleJSON = new JSONObject(); + puzzleJSON.put("id", puzzle.getId()); + puzzleJSON.put("name", puzzle.getName()); + puzzleJSON.put("scoreMax", puzzle.getScoreMax()); + if (puzzle.getTags() != null) puzzleJSON.put("tags", puzzle.getJsonTags()); + int score = this.databaseRepo.getScore(user.getId(), puzzle.getId()); + if(score >= 0) puzzleJSON.put("score", score); + puzzles.add(puzzleJSON); + } + chapterJSON.put("puzzles", puzzles); } - chapterJSON.put("puzzles", puzzles); context.response(200); writer.write(chapterJSON.toJSONString()); } else { diff --git a/src/dev/peerat/backend/routes/ChapterList.java b/src/dev/peerat/backend/routes/ChapterList.java index c5ccedc..19b6a16 100644 --- a/src/dev/peerat/backend/routes/ChapterList.java +++ b/src/dev/peerat/backend/routes/ChapterList.java @@ -35,10 +35,7 @@ public class ChapterList implements Response { JSONObject chapterJSON = new JSONObject(); chapterJSON.put("id", chapter.getId()); chapterJSON.put("name", chapter.getName()); - if (chapter.getStartDate() != null) - chapterJSON.put("startDate", chapter.getStartDate().toString()); - if (chapter.getEndDate() != null) - chapterJSON.put("endDate", chapter.getEndDate().toString()); + chapterJSON.put("show", chapter.isInCurrentTime()); chaptersJSON.add(chapterJSON); } context.response(200); diff --git a/src/dev/peerat/backend/routes/DynamicLeaderboard.java b/src/dev/peerat/backend/routes/DynamicLeaderboard.java index 08ffea5..b2df758 100644 --- a/src/dev/peerat/backend/routes/DynamicLeaderboard.java +++ b/src/dev/peerat/backend/routes/DynamicLeaderboard.java @@ -16,9 +16,9 @@ public class DynamicLeaderboard extends Leaderboard{ private Locker locker; - public DynamicLeaderboard(DatabaseRepository databaseRepo){ + public DynamicLeaderboard(DatabaseRepository databaseRepo, Locker locker){ super(databaseRepo); - this.locker = new Locker<>(); + this.locker = locker; } public Locker getLocker(){ diff --git a/src/dev/peerat/backend/routes/MailConfirmation.java b/src/dev/peerat/backend/routes/MailConfirmation.java deleted file mode 100644 index 62ca4a3..0000000 --- a/src/dev/peerat/backend/routes/MailConfirmation.java +++ /dev/null @@ -1,96 +0,0 @@ -package dev.peerat.backend.routes; - -import static dev.peerat.framework.RequestType.POST; - -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Map; -import java.util.regex.Matcher; - -import org.json.simple.JSONObject; - -import dev.peerat.backend.bonus.extract.RouteDoc; -import dev.peerat.backend.model.PeerAtUser; -import dev.peerat.backend.model.Player; -import dev.peerat.backend.repository.DatabaseRepository; -import dev.peerat.framework.Context; -import dev.peerat.framework.HttpReader; -import dev.peerat.framework.HttpWriter; -import dev.peerat.framework.Response; -import dev.peerat.framework.Route; -import dev.peerat.framework.Router; - -public class MailConfirmation implements Response { - - private DatabaseRepository databaseRepo; - private Router router; - private String usersFilesPath; - private Map playersWaiting; - - public MailConfirmation(DatabaseRepository databaseRepo, Router router, String initUsersFilesPath, - Map playersWaiting) { - this.databaseRepo = databaseRepo; - this.router = router; - usersFilesPath = initUsersFilesPath; - } - - @RouteDoc(path = "/confirmation", responseCode = 200, responseDescription = "L'utilisateur est inscrit") - @RouteDoc(responseCode = 403, responseDescription = "L'utilisateur est connecté") - @RouteDoc(responseCode = 400, responseDescription = "Aucune données fournie / données invalide") - - @Route(path = "^\\/confirmation$", type = POST) - public void exec(Matcher matcher, Context context, HttpReader reader, HttpWriter writer) throws Exception { - if (context.getUser() != null) { - context.response(403); - return; - } - JSONObject informations = reader.readJson(); - if (informations != null) { - boolean allNecessaryFieldsFilled = informations.containsKey("email") && informations.containsKey("code") - && informations.containsKey("passwd"); - if (!allNecessaryFieldsFilled) { - context.response(400); - return; - } - String email = (String) informations.get("email"); - String password = (String) informations.get("passwd"); - int code = (int) informations.get("code"); - - Player newPlayer = getPlayerFromEmail(email); - if (newPlayer != null && code == playersWaiting.get(newPlayer)) { - String pseudo = newPlayer.getPseudo(); - int id; - if ((id = databaseRepo.register(pseudo, email, password, newPlayer.getFirstname(), newPlayer.getLastname(), "", "", "")) >= 0) { - context.response(200, "Access-Control-Expose-Headers: Authorization", - "Authorization: Bearer " + this.router.createAuthUser(new PeerAtUser(id))); - createFolderToSaveSourceCode(pseudo); - return; - } else { - context.response(400); - JSONObject error = new JSONObject(); - error.put("username_valid", pseudo); - error.put("email_valid", email); - writer.write(error.toJSONString()); - return; - } - } - } - context.response(400); - } - - private void createFolderToSaveSourceCode(String pseudo) throws IOException { - - Files.createDirectories(Paths.get(String.format("%s/%s", usersFilesPath, pseudo))); - } - - private Player getPlayerFromEmail(String email) { - Player toMatch = new Player(email); - for (Player p: playersWaiting.keySet()) { - if (p.equals(toMatch)) { - return p; - } - } - return null; - } -} diff --git a/src/dev/peerat/backend/routes/admins/DynamicLogs.java b/src/dev/peerat/backend/routes/admins/DynamicLogs.java new file mode 100644 index 0000000..ac74691 --- /dev/null +++ b/src/dev/peerat/backend/routes/admins/DynamicLogs.java @@ -0,0 +1,56 @@ +package dev.peerat.backend.routes.admins; + +import java.util.regex.Matcher; + +import org.json.simple.JSONObject; + +import dev.peerat.backend.model.PeerAtUser; +import dev.peerat.backend.repository.DatabaseRepository; +import dev.peerat.framework.Context; +import dev.peerat.framework.HttpReader; +import dev.peerat.framework.HttpWriter; +import dev.peerat.framework.Locker; +import dev.peerat.framework.Locker.Key; +import dev.peerat.framework.Response; +import dev.peerat.framework.Route; + +public class DynamicLogs implements Response{ + + private Locker locker; //Context + private DatabaseRepository repo; + + public DynamicLogs(DatabaseRepository repo, Locker locker){ + this.repo = repo; + this.locker = locker; + } + + @Route(path = "^/admin/logs$", needLogin = true, websocket = true) + public void exec(Matcher matcher, Context context, HttpReader reader, HttpWriter writer) throws Exception { + //check if admin + + Key key = new Key(); + + locker.init(key); + try { + while(!reader.isClosed()){ + Context instance = locker.getValue(key); + JSONObject json = new JSONObject(); + json.put("logged", instance.isLogged()); + if(instance.isLogged()) json.put("pseudo", repo.getPlayer(instance.getUser().getId()).getPseudo()); + json.put("path", instance.getPath()); + json.put("type", instance.getType()); + json.put("code", instance.getResponseCode()); + + writer.write(json.toJSONString()); + writer.flush(); + locker.lock(key); + } + }catch(Exception e){ + e.printStackTrace(); + } + locker.remove(key); + } + + + +} diff --git a/src/dev/peerat/backend/routes/users/ChangePassword.java b/src/dev/peerat/backend/routes/users/ChangePassword.java new file mode 100644 index 0000000..ab4bff6 --- /dev/null +++ b/src/dev/peerat/backend/routes/users/ChangePassword.java @@ -0,0 +1,36 @@ +package dev.peerat.backend.routes.users; + +import java.util.regex.Matcher; + +import org.jose4j.json.internal.json_simple.JSONObject; + +import dev.peerat.backend.bonus.extract.RouteDoc; +import dev.peerat.backend.model.PeerAtUser; +import dev.peerat.backend.repository.DatabaseRepository; +import dev.peerat.framework.Context; +import dev.peerat.framework.HttpReader; +import dev.peerat.framework.HttpWriter; +import dev.peerat.framework.RequestType; +import dev.peerat.framework.Response; +import dev.peerat.framework.Route; + +public class ChangePassword implements Response{ + + private DatabaseRepository repo; + + public ChangePassword(DatabaseRepository repo){ + this.repo = repo; + } + + @RouteDoc(path = "/user/cpw", responseCode = 200, responseDescription = "L'utilisateur a mis à jours sont mots de passe") + @RouteDoc(responseCode = 400, responseDescription = "L'utilisateur a envoyer un mots de passe invalide") + + @Route(path = "^/user/cpw$", type = RequestType.POST, needLogin = true) + public void exec(Matcher matcher, Context context, HttpReader reader, HttpWriter writer) throws Exception { + String password = (String) reader.readJson().get("password"); + + repo.updatePassword(context.getUser().getId(), password); + context.response(200); + } + +} diff --git a/src/dev/peerat/backend/routes/users/ForgotPassword.java b/src/dev/peerat/backend/routes/users/ForgotPassword.java new file mode 100644 index 0000000..fd25fcc --- /dev/null +++ b/src/dev/peerat/backend/routes/users/ForgotPassword.java @@ -0,0 +1,25 @@ +package dev.peerat.backend.routes.users; + +import java.util.regex.Matcher; + +import dev.peerat.framework.Context; +import dev.peerat.framework.HttpReader; +import dev.peerat.framework.HttpWriter; +import dev.peerat.framework.Response; +import dev.peerat.framework.Route; + +public class ForgotPassword implements Response{ + + @Route(path = "^/user/fpw$") + public void exec(Matcher matcher, Context context, HttpReader reader, HttpWriter writer) throws Exception { + if(context.isLogged()){ + context.response(403); + return; + } + + + } + + + +} diff --git a/src/dev/peerat/backend/routes/Login.java b/src/dev/peerat/backend/routes/users/Login.java similarity index 64% rename from src/dev/peerat/backend/routes/Login.java rename to src/dev/peerat/backend/routes/users/Login.java index 95dccfb..9cb11b8 100644 --- a/src/dev/peerat/backend/routes/Login.java +++ b/src/dev/peerat/backend/routes/users/Login.java @@ -1,4 +1,4 @@ -package dev.peerat.backend.routes; +package dev.peerat.backend.routes.users; import static dev.peerat.framework.RequestType.POST; @@ -9,19 +9,19 @@ import org.json.simple.JSONObject; import dev.peerat.backend.bonus.extract.RouteDoc; import dev.peerat.backend.model.PeerAtUser; import dev.peerat.backend.repository.DatabaseRepository; +import dev.peerat.backend.utils.FormResponse; import dev.peerat.framework.Context; import dev.peerat.framework.HttpReader; import dev.peerat.framework.HttpWriter; -import dev.peerat.framework.Response; import dev.peerat.framework.Route; import dev.peerat.framework.Router; -public class Login implements Response { +public class Login extends FormResponse{ private DatabaseRepository databaseRepo; private Router router; - public Login(DatabaseRepository databaseRepo, Router router) { + public Login(DatabaseRepository databaseRepo, Router router){ this.databaseRepo = databaseRepo; this.router = router; } @@ -32,23 +32,23 @@ public class Login implements Response { @Route(path = "^\\/login$", type = POST) public void exec(Matcher matcher, Context context, HttpReader reader, HttpWriter writer) throws Exception { - if (context.getUser() != null) { + if(context.isLogged()){ context.response(403); return; } - JSONObject informations = reader.readJson(); - if (informations != null) { - String pseudo = (String) informations.get("pseudo"); - String password = (String) informations.get("passwd"); - int id; - if ((id = databaseRepo.login(pseudo, password)) >= 0) { - context.response(200, - "Access-Control-Expose-Headers: Authorization", - "Authorization: Bearer " + this.router.createAuthUser(new PeerAtUser(id))); - return; - } + JSONObject json = json(reader); + if(!areValids("pseudo", "passwd")){ + context.response(400); + return; + } + int id; + if((id = databaseRepo.login((String)json.get("pseudo"), (String)json.get("passwd"))) >= 0){ + context.response(200, + "Access-Control-Expose-Headers: Authorization", + "Authorization: Bearer " + this.router.createAuthUser(new PeerAtUser(id))); + }else{ + context.response(400); } - context.response(400); } } diff --git a/src/dev/peerat/backend/routes/users/MailConfirmation.java b/src/dev/peerat/backend/routes/users/MailConfirmation.java new file mode 100644 index 0000000..d650495 --- /dev/null +++ b/src/dev/peerat/backend/routes/users/MailConfirmation.java @@ -0,0 +1,156 @@ +package dev.peerat.backend.routes.users; + +import static dev.peerat.framework.RequestType.POST; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.Map; +import java.util.Base64.Encoder; +import java.util.regex.Matcher; + +import javax.net.ssl.HttpsURLConnection; + +import org.json.simple.JSONAware; +import org.json.simple.JSONObject; + +import dev.peerat.backend.bonus.extract.RouteDoc; +import dev.peerat.backend.model.PeerAtUser; +import dev.peerat.backend.model.Player; +import dev.peerat.backend.repository.DatabaseRepository; +import dev.peerat.backend.utils.FormResponse; +import dev.peerat.framework.Context; +import dev.peerat.framework.HttpReader; +import dev.peerat.framework.HttpWriter; +import dev.peerat.framework.Route; +import dev.peerat.framework.Router; + +public class MailConfirmation extends FormResponse { + + private DatabaseRepository databaseRepo; + private Router router; + private String usersFilesPath; + private KeyPairGenerator generator; + private Encoder encoder; + private String gitToken; + private Map playersWaiting; + + public MailConfirmation( + DatabaseRepository databaseRepo, + Router router, + String initUsersFilesPath, + String gitToken, + Map playersWaiting){ + this.databaseRepo = databaseRepo; + this.router = router; + usersFilesPath = initUsersFilesPath; + this.gitToken = gitToken; + try { + generator = KeyPairGenerator.getInstance("RSA"); + generator.initialize(2048); //a changer ? + } catch (NoSuchAlgorithmException e){ + e.printStackTrace(); + } + encoder = Base64.getEncoder(); + } + + @RouteDoc(path = "/confirmation", responseCode = 200, responseDescription = "L'utilisateur est inscrit") + @RouteDoc(responseCode = 403, responseDescription = "L'utilisateur est connecté") + @RouteDoc(responseCode = 400, responseDescription = "Aucune données fournie / données invalide") + + @Route(path = "^\\/confirmation$", type = POST) + public void exec(Matcher matcher, Context context, HttpReader reader, HttpWriter writer) throws Exception { + if (context.isLogged()){ + context.response(403); + return; + } + JSONObject json = json(reader); + if(!areValids("email","code","passwd")){ + context.response(400); + return; + } + String email = (String) json.get("email"); + String password = (String) json.get("passwd"); + int code = (int) json.get("code"); + + Player newPlayer = getPlayerFromEmail(email); + if (newPlayer != null && code == playersWaiting.get(newPlayer)){ + playersWaiting.remove(newPlayer); + String pseudo = newPlayer.getPseudo(); + int id; + if ((id = databaseRepo.register(pseudo, email, password, newPlayer.getFirstname(), newPlayer.getLastname(), "", "", "")) >= 0) { + context.response(200, + "Access-Control-Expose-Headers: Authorization", + "Authorization: Bearer " + this.router.createAuthUser(new PeerAtUser(id))); + createFolderToSaveSourceCode(pseudo); + generateGitKey(email, pseudo, password); + return; + } else { + context.response(400); + JSONObject error = new JSONObject(); + error.put("username_valid", pseudo); + error.put("email_valid", email); + writer.write(error.toJSONString()); + return; + } + } + context.response(400); + } + + private void createFolderToSaveSourceCode(String pseudo) throws IOException { + + Files.createDirectories(Paths.get(String.format("%s/%s", usersFilesPath, pseudo))); + } + + private Player getPlayerFromEmail(String email) { + Player toMatch = new Player(email); + for (Player p: playersWaiting.keySet()) { + if (p.equals(toMatch)) { + return p; + } + } + return null; + } + + private String generateGitKey(String email, String pseudo, String password) throws Exception{ + KeyPair pair = generator.generateKeyPair(); //doit être unique ??? + + JSONObject createUser = new JSONObject(); + createUser.put("email", email); + createUser.put("username", pseudo); + createUser.put("password", password); + post("https://git-users.peerat.dev/api/v1/admin/users/", createUser); + + JSONObject sendKey = new JSONObject(); + sendKey.put("key", new String(encoder.encode(pair.getPrivate().getEncoded()))); //add ssh-rsa au début ? + sendKey.put("read_only", false); + sendKey.put("title", "peer_at_code_auto_push_key_"+pseudo); + post("https://git-users.peerat.dev/api/v1/admin/users/"+pseudo+"/keys", sendKey); + + return new String(encoder.encode(pair.getPrivate().getEncoded())); + } + + private void post(String url, JSONAware json) throws Exception{ + HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection(); + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type","application/json"); + connection.setRequestProperty("Authorization","Bearer "+this.gitToken); + connection.setDoInput(true); + connection.setDoOutput(true); + + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(connection.getOutputStream())); + writer.write(json.toJSONString()); + writer.flush(); + writer.close(); + + int response = connection.getResponseCode(); + if(response != 201) throw new Exception("Call to "+url+" failed with response code "+response); + } +} diff --git a/src/dev/peerat/backend/routes/users/ProfileSettings.java b/src/dev/peerat/backend/routes/users/ProfileSettings.java new file mode 100644 index 0000000..8293824 --- /dev/null +++ b/src/dev/peerat/backend/routes/users/ProfileSettings.java @@ -0,0 +1,58 @@ +package dev.peerat.backend.routes.users; + +import java.util.regex.Matcher; + +import org.json.simple.JSONObject; + +import dev.peerat.backend.bonus.extract.RouteDoc; +import dev.peerat.backend.model.PeerAtUser; +import dev.peerat.backend.model.Player; +import dev.peerat.backend.repository.DatabaseRepository; +import dev.peerat.framework.Context; +import dev.peerat.framework.HttpReader; +import dev.peerat.framework.HttpWriter; +import dev.peerat.framework.RequestType; +import dev.peerat.framework.Response; +import dev.peerat.framework.Route; + +public class ProfileSettings implements Response{ + + private DatabaseRepository repo; + + public ProfileSettings(DatabaseRepository repo){ + this.repo = repo; + } + + @RouteDoc(path = "/user/settings", responseCode = 200, responseDescription = "L'utilisateur a mis à jours sont profile") + @RouteDoc(responseCode = 400, responseDescription = "L'utilisateur a envoyer une donnée unique, déjà utilisée") + + @Route(path = "^/user/settings$", type = RequestType.POST, needLogin = true) + public void exec(Matcher matcher, Context context, HttpReader reader, HttpWriter writer) throws Exception { + JSONObject json = reader.readJson(); + + String pseudo = (String) json.get("pseudo"); + String email = (String) json.get("email"); + String firstname = (String) json.get("firstname"); + String lastname = (String) json.get("lastname"); + Player player = repo.getPlayer(context.getUser().getId()); + + if(!player.getPseudo().equals(pseudo)){ + if(!repo.updatePseudo(context.getUser().getId(), player, pseudo)){ + context.response(400); + return; + } + player.setPseudo(pseudo); + } + + if(!player.getEmail().equals(email)){ + + } + + if((!player.getFirstname().equals(firstname)) || (!player.getLastname().equals(lastname))){ + repo.updateProfile(context.getUser().getId(), player, lastname, firstname); + } + + context.response(200); + } + +} diff --git a/src/dev/peerat/backend/routes/users/Register.java b/src/dev/peerat/backend/routes/users/Register.java new file mode 100644 index 0000000..9aeef35 --- /dev/null +++ b/src/dev/peerat/backend/routes/users/Register.java @@ -0,0 +1,74 @@ +package dev.peerat.backend.routes.users; + +import static dev.peerat.framework.RequestType.POST; + +import java.util.Map; +import java.util.Random; +import java.util.regex.Matcher; + +import org.json.simple.JSONObject; + +import dev.peerat.backend.bonus.extract.RouteDoc; +import dev.peerat.backend.model.PeerAtUser; +import dev.peerat.backend.model.Player; +import dev.peerat.backend.repository.DatabaseRepository; +import dev.peerat.backend.utils.FormResponse; +import dev.peerat.framework.Context; +import dev.peerat.framework.HttpReader; +import dev.peerat.framework.HttpWriter; +import dev.peerat.framework.Route; + +public class Register extends FormResponse { + + private DatabaseRepository databaseRepo; + private Map playersWaiting; + + public Register(DatabaseRepository databaseRepo, Map playersWaiting){ + this.databaseRepo = databaseRepo; + this.playersWaiting = playersWaiting; + } + + @RouteDoc(path = "/register", responseCode = 200, responseDescription = "L'utilisateur est inscrit") + @RouteDoc(responseCode = 403, responseDescription = "L'utilisateur est connecté") + @RouteDoc(responseCode = 400, responseDescription = "Aucune données fournie / données invalide") + + @Route(path = "^\\/register$", type = POST) + public void exec(Matcher matcher, Context context, HttpReader reader, HttpWriter writer) throws Exception{ + if (context.isLogged()){ + context.response(403); + return; + } + JSONObject json = json(reader); + if(!areValids("pseudo","email","firstname","lastname")){ + context.response(400); + return; + } + + String pseudo = (String) json.get("pseudo"); + String email = (String) json.get("email"); + String firstname = (String) json.get("firstname"); + String lastname = (String) json.get("lastname"); + + boolean pseudoAvailable = databaseRepo.checkPseudoAvailability(pseudo); + boolean emailAvailable = databaseRepo.checkEmailAvailability(email); + if(pseudoAvailable && emailAvailable){ + Player player = new Player(pseudo, email, firstname, lastname); + playersWaiting.put(player, codeGenerator()); + context.response(200); + }else{ + context.response(400); + JSONObject error = new JSONObject(); + error.put("username_valid", pseudoAvailable); + error.put("email_valid", emailAvailable); + writer.write(error.toJSONString()); + } + } + + private int codeGenerator() { + int min = 1000; + int max = 9999; + return new Random().nextInt((max-min)) + min; + + } + +} diff --git a/src/dev/peerat/backend/utils/FormResponse.java b/src/dev/peerat/backend/utils/FormResponse.java new file mode 100644 index 0000000..e91c9ea --- /dev/null +++ b/src/dev/peerat/backend/utils/FormResponse.java @@ -0,0 +1,49 @@ +package dev.peerat.backend.utils; + +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Pattern; + +import org.json.simple.JSONAware; + +import dev.peerat.framework.HttpReader; +import dev.peerat.framework.Response; + +public abstract class FormResponse implements Response{ + + private JSONAware json; + private Map checker; + + public FormResponse(){ + this.checker = new HashMap<>(); + } + + public void validator(String key, Pattern regex){ + this.checker.put(key, regex); + } + + public T json(HttpReader reader) throws Exception{ + return (T) (this.json = reader.readJson()); + } + + public boolean hasFields(String... fields){ + Map map = (Map)json; + for(String field : fields){ + if(!map.containsKey(field)) return false; + } + return true; + } + + public boolean areValids(String... fields){ + Map map = (Map)json; + for(String field : fields){ + String value = (String) map.get(field); + if(value == null) return false; + if(value.isEmpty()) return false; + Pattern pattern = checker.get(field); + if(pattern == null) continue; + if(!pattern.matcher(value).matches()) return false; + } + return true; + } +} diff --git a/src/dev/peerat/backend/utils/Mail.java b/src/dev/peerat/backend/utils/Mail.java index b6e1137..b82f03a 100644 --- a/src/dev/peerat/backend/utils/Mail.java +++ b/src/dev/peerat/backend/utils/Mail.java @@ -42,8 +42,8 @@ public class Mail { msg.addHeader("format", "flowed"); msg.addHeader("Content-Transfer-Encoding", "8bit"); - msg.setFrom(new InternetAddress("ping@peerat.dev", "NoReply-JD")); - msg.setReplyTo(InternetAddress.parse("ping@peerat.dev", false)); + msg.setFrom(new InternetAddress(fromAddress, "NoReply-JD")); + msg.setReplyTo(InternetAddress.parse(fromAddress, false)); msg.setSubject(subject, "UTF-8"); msg.setText(text, "UTF-8"); msg.setSentDate(new Date()); diff --git a/test/dev/peerat/backend/TestDatabaseRepository.java b/test/dev/peerat/backend/TestDatabaseRepository.java new file mode 100644 index 0000000..4e00be3 --- /dev/null +++ b/test/dev/peerat/backend/TestDatabaseRepository.java @@ -0,0 +1,64 @@ +package dev.peerat.backend; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.sql.Connection; +import java.sql.SQLException; + +import dev.peerat.backend.repository.DatabaseRepository; + +public class TestDatabaseRepository extends DatabaseRepository{ + + private Connection con; + private String schem; + + public TestDatabaseRepository(Configuration config, File databaseSchem){ + super(config); + + try{ + schem = ""; + BufferedReader reader = new BufferedReader(new FileReader(databaseSchem)); + String line; + while((line = reader.readLine()) != null) schem+=line; + reader.close(); + }catch(Exception e){ + e.printStackTrace(); + } + } + + public void init(){ + try { + Method method = DatabaseRepository.class.getDeclaredMethod("ensureConnection"); + method.setAccessible(true); + method.invoke(this); + + Field field = DatabaseRepository.class.getDeclaredField("con"); + field.setAccessible(true); + this.con = (Connection) field.get(this); + }catch(Exception e){ + e.getCause().printStackTrace(); + } + } + + public void close(){ + try { + this.con.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + + public void reset(){ + try{ + String[] split = schem.split(";"); + for(String statment : split){ + this.con.prepareStatement(statment).execute(); + } + }catch(Exception e){ + e.printStackTrace(); + } + } +} diff --git a/test/dev/peerat/backend/webclient/WebClient.java b/test/dev/peerat/backend/WebClient.java similarity index 99% rename from test/dev/peerat/backend/webclient/WebClient.java rename to test/dev/peerat/backend/WebClient.java index 59f0872..392cfaa 100644 --- a/test/dev/peerat/backend/webclient/WebClient.java +++ b/test/dev/peerat/backend/WebClient.java @@ -1,4 +1,4 @@ -package dev.peerat.backend.webclient; +package dev.peerat.backend; import static org.junit.Assert.fail; diff --git a/test/dev/peerat/backend/routes/PlayerDetailsTests.java b/test/dev/peerat/backend/routes/PlayerDetailsTests.java index d8c8260..3dcc745 100644 --- a/test/dev/peerat/backend/routes/PlayerDetailsTests.java +++ b/test/dev/peerat/backend/routes/PlayerDetailsTests.java @@ -9,7 +9,7 @@ import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; import dev.peerat.backend.Main; -import dev.peerat.backend.webclient.WebClient; +import dev.peerat.backend.WebClient; @TestInstance(Lifecycle.PER_CLASS) class PlayerDetailsTests { diff --git a/test/dev/peerat/backend/routes/ScoreTests.java b/test/dev/peerat/backend/routes/ScoreTests.java index c81ddfc..cbffc0a 100644 --- a/test/dev/peerat/backend/routes/ScoreTests.java +++ b/test/dev/peerat/backend/routes/ScoreTests.java @@ -10,7 +10,7 @@ import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; import dev.peerat.backend.Main; -import dev.peerat.backend.webclient.WebClient; +import dev.peerat.backend.WebClient; @TestInstance(Lifecycle.PER_CLASS) public class ScoreTests { diff --git a/test/dev/peerat/backend/routes/TmpRoutesTests.java b/test/dev/peerat/backend/routes/TmpRoutesTests.java index b6079e3..b5617a0 100644 --- a/test/dev/peerat/backend/routes/TmpRoutesTests.java +++ b/test/dev/peerat/backend/routes/TmpRoutesTests.java @@ -10,7 +10,7 @@ import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; import dev.peerat.backend.Main; -import dev.peerat.backend.webclient.WebClient; +import dev.peerat.backend.WebClient; @TestInstance(Lifecycle.PER_CLASS) public class TmpRoutesTests { diff --git a/test/dev/peerat/backend/routes/TriggerTests.java b/test/dev/peerat/backend/routes/TriggerTests.java index a150c8c..8609c8d 100644 --- a/test/dev/peerat/backend/routes/TriggerTests.java +++ b/test/dev/peerat/backend/routes/TriggerTests.java @@ -9,7 +9,7 @@ import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; import dev.peerat.backend.Main; -import dev.peerat.backend.webclient.WebClient; +import dev.peerat.backend.WebClient; @TestInstance(Lifecycle.PER_CLASS) public class TriggerTests { diff --git a/test/dev/peerat/backend/userstories/BaseUserStoriesTest.java b/test/dev/peerat/backend/userstories/BaseUserStoriesTest.java new file mode 100644 index 0000000..10ced61 --- /dev/null +++ b/test/dev/peerat/backend/userstories/BaseUserStoriesTest.java @@ -0,0 +1,30 @@ +package dev.peerat.backend.userstories; + +import java.io.File; + +import dev.peerat.backend.Configuration; +import dev.peerat.backend.TestDatabaseRepository; + +public class BaseUserStoriesTest { + + private Configuration config; + private TestDatabaseRepository repo; + + public BaseUserStoriesTest(){} + + public void init() throws Exception{ + this.config = new Configuration("config-test.txt"); + this.repo = new TestDatabaseRepository(config, new File("database-schem.sql")); + + this.config.load(); + } + + public Configuration getConfig(){ + return this.config; + } + + public TestDatabaseRepository getRepository(){ + return this.repo; + } + +} diff --git a/test/dev/peerat/backend/userstories/LoginTests.java b/test/dev/peerat/backend/userstories/LoginTests.java new file mode 100644 index 0000000..105e636 --- /dev/null +++ b/test/dev/peerat/backend/userstories/LoginTests.java @@ -0,0 +1,79 @@ +package dev.peerat.backend.userstories; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; + +import dev.peerat.backend.Main; +import dev.peerat.backend.WebClient; + +@TestInstance(Lifecycle.PER_METHOD) +public class LoginTests extends BaseUserStoriesTest{ + + private Thread server; + private WebClient client; + + @BeforeEach + public void init() throws Exception{ + Class.forName("com.mysql.cj.jdbc.Driver"); + + super.init(); + getRepository().init(); + getRepository().reset(); + + server = new Thread(new Runnable(){ + @Override + public void run(){ + try { + Main.main(null); + } catch (Exception e){ + e.printStackTrace(); + }; + } + }); + server.start(); + client = new WebClient("localhost", 80); + + try { + client.register("user", "password", "mail@peerat.dev", "firstname", "lastname", "description"); + client.assertResponseCode(200); + client.disconnect(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @AfterEach + public void stop(){ + server.interrupt(); + } + + @Test + public void normalLogin() throws Exception{ + client.auth("user", "password"); + client.assertResponseCode(200); + + } + + @Test + public void wrongPassword() throws Exception{ + client.auth("user", "password1"); + client.assertResponseCode(400); + } + + @Test + public void wrongUsername() throws Exception{ + client.auth("user1", "password"); + client.assertResponseCode(400); + } + + @Test + public void alreadyLoggedin() throws Exception{ + client.auth("user", "password"); + client.assertResponseCode(200); + client.auth("user", "password"); + client.assertResponseCode(403); + } +} diff --git a/test/dev/peerat/backend/userstories/RegisterTests.java b/test/dev/peerat/backend/userstories/RegisterTests.java new file mode 100644 index 0000000..2e69e92 --- /dev/null +++ b/test/dev/peerat/backend/userstories/RegisterTests.java @@ -0,0 +1,90 @@ +package dev.peerat.backend.userstories; + +import org.json.simple.JSONObject; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; + +import dev.peerat.backend.Main; +import dev.peerat.backend.WebClient; + +@TestInstance(Lifecycle.PER_METHOD) +public class RegisterTests extends BaseUserStoriesTest{ + + private Thread server; + private WebClient client; + + @BeforeEach + public void init() throws Exception{ + Class.forName("com.mysql.cj.jdbc.Driver"); + + super.init(); + getRepository().init(); + getRepository().reset(); + + server = new Thread(new Runnable(){ + @Override + public void run(){ + try { + Main.main(null); + } catch (Exception e){ + e.printStackTrace(); + }; + } + }); + server.start(); + client = new WebClient("localhost", 80); + } + + @AfterEach + public void stop(){ + server.interrupt(); + getRepository().close(); + } + + @Test + public void normalRegister() throws Exception{ + client.register("test", "test", "test@peerat.dev", "te", "st", "my desc"); + client.assertResponseCode(200); + } + + @Test + public void pseudoAlreadyUse() throws Exception{ + client.register("test", "test", "test@peerat.dev", "te", "st", "my desc"); + client.assertResponseCode(200); + client.disconnect(); + client.register("test", "test", "test1@peerat.dev", "te", "st", "my desc"); + client.assertResponseCode(400); + } + + @Test + public void emailAlreadyUse() throws Exception{ + client.register("test", "test", "test@peerat.dev", "te", "st", "my desc"); + client.assertResponseCode(200); + client.disconnect(); + client.register("test1", "test", "test@peerat.dev", "te", "st", "my desc"); + client.assertResponseCode(400); + } + + @Test + public void emptyField() throws Exception{ + client.register("","","",",","",""); + client.assertResponseCode(400); + } + + @Test + public void lostField() throws Exception{ + client.route("/register", "POST", new JSONObject().toJSONString()); + client.assertResponseCode(400); + } + + @Test + public void alreadyLoggedin() throws Exception{ + client.register("test", "test", "test@peerat.dev", "te", "st", "my desc"); + client.assertResponseCode(200); + client.register("test1", "test", "test@peerat.dev", "te", "st", "my desc"); + client.assertResponseCode(403); + } +}