From 739f4c54ddbef6330016c38bff9416dad8303a63 Mon Sep 17 00:00:00 2001
From: Gabriel Copat <copatg@cardiff.ac.uk>
Date: Mon, 11 Dec 2023 12:51:54 +0000
Subject: [PATCH] Added login validation with Spring Security

---
 build.gradle                                  |  4 +-
 .../security/SecurityConfiguration.java       | 49 +++++++++++++++++++
 .../java/Team5/SmartTowns/users/NewUser.java  |  4 ++
 .../java/Team5/SmartTowns/users/User.java     |  3 +-
 .../SmartTowns/users/UserController.java      | 43 +++++++++++-----
 .../SmartTowns/users/UserRepository.java      |  8 +--
 .../SmartTowns/users/UserRepositoryJDBC.java  | 41 ++++++++--------
 src/main/resources/application.properties     |  1 +
 src/main/resources/data.sql                   | 13 -----
 src/main/resources/schema.sql                 | 19 ++++---
 src/main/resources/static/css/login.css       |  7 ++-
 src/main/resources/static/scripts/userPage.js |  6 +--
 src/main/resources/static/sql/user-data.sql   |  7 +++
 .../static/sql/user-progress-data.sql         |  7 +++
 src/main/resources/templates/users/login.html |  8 +--
 .../templates/users/userProfile.html          |  3 +-
 16 files changed, 155 insertions(+), 68 deletions(-)
 create mode 100644 src/main/java/Team5/SmartTowns/security/SecurityConfiguration.java
 create mode 100644 src/main/resources/static/sql/user-data.sql
 create mode 100644 src/main/resources/static/sql/user-progress-data.sql

diff --git a/build.gradle b/build.gradle
index 0025adb3..23bab4e1 100644
--- a/build.gradle
+++ b/build.gradle
@@ -27,7 +27,8 @@ dependencies {
 	implementation 'org.springframework.boot:spring-boot-starter-web'
 	implementation 'org.springframework.boot:spring-boot-starter-jdbc'
 	implementation 'org.mariadb.jdbc:mariadb-java-client:2.1.2'
-    testImplementation 'junit:junit:4.13.1'
+	implementation 'org.springframework.boot:spring-boot-starter-security'
+	testImplementation 'junit:junit:4.13.1'
     compileOnly 'org.projectlombok:lombok'
     testImplementation 'org.projectlombok:lombok:1.18.28'
     compileOnly 'org.projectlombok:lombok'
@@ -35,6 +36,7 @@ dependencies {
 	annotationProcessor 'org.projectlombok:lombok'
 	testImplementation 'org.springframework.boot:spring-boot-starter-test'
 	implementation 'org.springframework.boot:spring-boot-starter-validation'
+	implementation 'org.springframework.boot:spring-boot-starter-security'
 	// https://mvnrepository.com/artifact/org.webjars/openlayers
 	implementation group: 'org.webjars', name: 'openlayers', version: '5.2.0'
 }
diff --git a/src/main/java/Team5/SmartTowns/security/SecurityConfiguration.java b/src/main/java/Team5/SmartTowns/security/SecurityConfiguration.java
new file mode 100644
index 00000000..0956fccf
--- /dev/null
+++ b/src/main/java/Team5/SmartTowns/security/SecurityConfiguration.java
@@ -0,0 +1,49 @@
+package Team5.SmartTowns.security;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.Customizer;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.crypto.password.NoOpPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.provisioning.JdbcUserDetailsManager;
+import org.springframework.security.provisioning.UserDetailsManager;
+import org.springframework.security.web.SecurityFilterChain;
+
+import javax.sql.DataSource;
+
+
+@Configuration
+@EnableWebSecurity
+public class SecurityConfiguration {
+
+
+    @Bean
+    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+        http
+                .authorizeHttpRequests((requests) -> requests
+                        .requestMatchers("/user/**", "/userProfile").authenticated()
+                        .anyRequest().permitAll()
+                )
+                .formLogin((login) -> login
+                        .loginPage("/login").permitAll()
+                        .defaultSuccessUrl("/userProfile")
+                )
+                .logout((logout) -> logout.permitAll());
+
+        return http.build();
+    }
+    @Bean
+    public PasswordEncoder passwordEncoder(){
+        return NoOpPasswordEncoder.getInstance();
+    }
+
+    @Bean
+    public UserDetailsManager userDetailsManager(DataSource dataSource){
+        JdbcUserDetailsManager manager = new JdbcUserDetailsManager(dataSource);
+        return manager;
+    }
+
+
+}
diff --git a/src/main/java/Team5/SmartTowns/users/NewUser.java b/src/main/java/Team5/SmartTowns/users/NewUser.java
index 08799204..aa47adac 100644
--- a/src/main/java/Team5/SmartTowns/users/NewUser.java
+++ b/src/main/java/Team5/SmartTowns/users/NewUser.java
@@ -1,6 +1,7 @@
 package Team5.SmartTowns.users;
 
 import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
 import lombok.Setter;
@@ -12,11 +13,14 @@ public class NewUser {
 
 
     @NotEmpty(message = "You must type in a username.")
+    @NotNull
     String name;
 
     @NotEmpty(message = "You must type in a password.")
+    @NotNull
     String password;
 
     @NotEmpty(message = "You must type in an email.")
+    @NotNull
     String email;
 }
diff --git a/src/main/java/Team5/SmartTowns/users/User.java b/src/main/java/Team5/SmartTowns/users/User.java
index 4ef65c3b..96949fb1 100644
--- a/src/main/java/Team5/SmartTowns/users/User.java
+++ b/src/main/java/Team5/SmartTowns/users/User.java
@@ -27,8 +27,7 @@ public class User {
         this.dragonProgress = dragonProgress;
         imgPath = findImagePath();
     }
-    public User(int id, String email, String name) {
-        this.id = id;
+    public User(String email, String name) {
         this.email = email;
         this.name = name;
         imgPath = findImagePath();
diff --git a/src/main/java/Team5/SmartTowns/users/UserController.java b/src/main/java/Team5/SmartTowns/users/UserController.java
index c937643d..aaa697e5 100644
--- a/src/main/java/Team5/SmartTowns/users/UserController.java
+++ b/src/main/java/Team5/SmartTowns/users/UserController.java
@@ -6,13 +6,14 @@ import Team5.SmartTowns.rewards.RewardsRepository;
 import Team5.SmartTowns.rewards.Sticker;
 import jakarta.validation.Valid;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.User;
 import org.springframework.stereotype.Controller;
 import org.springframework.ui.Model;
 import org.springframework.validation.BindingResult;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.servlet.ModelAndView;
 
-import java.util.Arrays;
 import java.util.List;
 
 @Controller
@@ -29,6 +30,7 @@ public class UserController {
     public ModelAndView getLoginPage() {
         ModelAndView mav = new ModelAndView("users/login");
         mav.addObject("user", new NewUser( "", "", ""));
+        System.out.println(userRepository.findUserByName("Admin").getName());
         return mav;
     }
 
@@ -38,16 +40,17 @@ public class UserController {
         // TODO VALIDATE EMAIL INPUT
 
         if (bindingResult.hasErrors()) {
-            System.out.println("Errors");
+            ModelAndView modelAndView = new ModelAndView("users/login");
+            modelAndView.addObject("errors", bindingResult);
+            return modelAndView;
         }
         System.out.println(user.getName());
         System.out.println(user.getPassword());
 
+
         if ( userRepository.doesUserExist(user.getEmail()) ) {
             //TODO return modelandview for user already exists
             System.out.println(user.getEmail() + " already exists");
-            System.out.print("LOG IN:");
-            System.out.println(userRepository.userLogIn(user.email, user.password));
             return mav;
         } else {
             userRepository.addUser(user.name, user.email, user.password);
@@ -56,28 +59,42 @@ public class UserController {
         }
     }
 
+    @GetMapping("/userProfile")
+    public ModelAndView userProfile(){
+        ModelAndView mav = new ModelAndView("users/userProfile");
+        List<Pack> allPacks = rewardsRepository.getAllPacks();
+        mav.addObject("packs", allPacks);
+
+        User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
+        System.out.println(user.getUsername());
+        mav.addObject("user", userRepository.findUserByName("Admin"));
+        mav.addAllObjects(getPackInfo("Admin", 1).getModelMap());
+
+        return mav;
+    }
+
+
     /* USER MAPPING & FUNCTIONS */
-    @GetMapping("/user/{id}")
-    public ModelAndView getUserPage(@PathVariable int id) {
+    @GetMapping("/user/{username}")
+    public ModelAndView getUserPage(@PathVariable String username) {
         ModelAndView mav = new ModelAndView("users/userProfile");
         List<Pack> allPacks = rewardsRepository.getAllPacks();
-        mav.addObject("user", userRepository.getUserById(id));
+        mav.addObject("user", userRepository.findUserByName("Admin"));
         mav.addObject("packs", allPacks);
-        userRepository.addUser("Maria", "MariaEmail", "MariaPassword");
-        userRepository.doesUserExist("MariaEmail");
-        mav.addAllObjects(getPackInfo(id, 1).getModelMap());
+        mav.addAllObjects(getPackInfo(username, 1).getModelMap());
 
         return mav;
     }
 
-    @GetMapping("/packInfo/{userID}/{packID}")
-    public ModelAndView getPackInfo(@PathVariable int userID, @PathVariable int packID) {
+    @GetMapping("/packInfo/{username}/{packID}")
+    public ModelAndView getPackInfo(@PathVariable String username, @PathVariable int packID) {
         /* Displays on page the stickers present in the pack and colour the ones the
         *  user has acquired */
 
         ModelAndView mav = new ModelAndView("users/userFrags :: stickersBox");
         List<Sticker> allStickers = rewardsRepository.getAllStickersFromPack(packID);
-        List<Long> userStickers = userRepository.getUserStickersFromPack(userID, packID);
+        List<Long> userStickers = userRepository.getUserStickersFromPack(username, packID);
+        System.out.println(userStickers);
 
 
 
diff --git a/src/main/java/Team5/SmartTowns/users/UserRepository.java b/src/main/java/Team5/SmartTowns/users/UserRepository.java
index f47b120a..097598e2 100644
--- a/src/main/java/Team5/SmartTowns/users/UserRepository.java
+++ b/src/main/java/Team5/SmartTowns/users/UserRepository.java
@@ -5,10 +5,10 @@ import java.util.List;
 
 public interface UserRepository {
     List<User> getAllUsers();
-    List<Long> getUserStickersFromPack(int userID, int packID);
-    User getUserById(int userID);
-    boolean unlockSticker(int userID, int packID, int stickerID);
+    List<Long> getUserStickersFromPack(String username, int packID);
+    boolean unlockSticker(String username, int packID, int stickerID);
     boolean addUser(String username, String email, String password);
     boolean doesUserExist(String email);
-    boolean userLogIn(String username, String password);
+    User findUserByEmail(String email);
+    User findUserByName(String name);
 }
diff --git a/src/main/java/Team5/SmartTowns/users/UserRepositoryJDBC.java b/src/main/java/Team5/SmartTowns/users/UserRepositoryJDBC.java
index 492d7b1a..69ef84a1 100644
--- a/src/main/java/Team5/SmartTowns/users/UserRepositoryJDBC.java
+++ b/src/main/java/Team5/SmartTowns/users/UserRepositoryJDBC.java
@@ -26,9 +26,8 @@ public class UserRepositoryJDBC implements UserRepository{
 
     private void setUserMapper(){
         userMapper = (rs, i) -> new User(
-                rs.getInt("id"),
                 rs.getString("email"),
-                rs.getString("name")
+                rs.getString("username")
         );
     }
 
@@ -40,29 +39,24 @@ public class UserRepositoryJDBC implements UserRepository{
 
 
     @Override
-    public User getUserById(int userID){
-        String sql= "SELECT * FROM users WHERE id=?";
-        List<User> result = jdbc.query(sql, userMapper, userID);
-        return result.isEmpty() ? null : result.get(0);
+    public List<Long> getUserStickersFromPack(String username, int packID) {
+        String sql = "SELECT stickerID FROM stickerprogress WHERE (username, packID)= (?,?)";
+        return jdbc.queryForList(sql, Long.class, username, packID);
     }
 
     @Override
-    public List<Long> getUserStickersFromPack(int userID, int packID) {
-        String sql = "SELECT stickerID FROM stickerprogress WHERE (userID, packID)= (?,?)";
-        return jdbc.queryForList(sql, Long.class, userID, packID);
-    }
-
-    @Override
-    public boolean unlockSticker(int userID, int packID, int stickerID){
-        String sql = "INSERT INTO stickerprogress (userID, packID, stickerID) VALUES (?,?,?)";
-        jdbc.update(sql, userID, packID, stickerID);
+    public boolean unlockSticker(String username, int packID, int stickerID){
+        String sql = "INSERT INTO stickerprogress (username, packID, stickerID) VALUES (?,?,?)";
+        jdbc.update(sql, username, packID, stickerID);
         return true;
     }
 
     @Override
     public boolean addUser(String username, String email, String password){
-        String query = "INSERT INTO users (name, email, password) VALUES (?, ?, ?)";
+        String query = "INSERT INTO users (username, email, password) VALUES (?, ?, ?);";
+        String query2= "INSERT INTO authorities (username, authority) VALUES (?,?);";
         jdbc.update(query, username, email, password);
+        jdbc.update(query2, username, "USER");
         return true;
     }
     @Override
@@ -70,11 +64,18 @@ public class UserRepositoryJDBC implements UserRepository{
         String query = "SELECT COUNT(email) FROM users WHERE (email) = (?)";
         return !(jdbc.queryForObject(query, Integer.class, email) == 0);
     }
+
     @Override
-    public boolean userLogIn(String email, String password){
-        String query = "SELECT (password) FROM users WHERE (email) = (?)";
-        String dbpassword = jdbc.queryForObject(query, String.class, email);
-        return Objects.equals(dbpassword, password);
+    public User findUserByEmail(String email) {
+        String query = "SELECT * FROM users WHERE (email) = (?)";
+        List<User> result = jdbc.query(query, userMapper, email);
+        return result.isEmpty() ? null : result.get(0);
+    }
+    @Override
+    public User findUserByName(String name) {
+        String query = "SELECT * FROM users WHERE (username) = (?)";
+        List<User> result = jdbc.query(query, userMapper, name);
+        return result.isEmpty() ? null : result.get(0);
     }
 
 }
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 95f46c69..52e81d21 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -3,3 +3,4 @@ spring.datasource.username=root
 spring.datasource.password=comsc
 
 spring.sql.init.mode=always
+spring.sql.init.data-locations=classpath:data.sql, classpath:/static/sql/user-data.sql, classpath:/static/sql/user-progress-data.sql
diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql
index bc731eab..b91b4f71 100644
--- a/src/main/resources/data.sql
+++ b/src/main/resources/data.sql
@@ -1,9 +1,3 @@
-DELETE FROM users;
-INSERT INTO users (id, email, name, password) VALUE (1, 'admin@gmail.com', 'Admin', 'admin');
-INSERT INTO users (email, name, password) VALUE ('hannah@gmail.com', 'Hannah', 'root');
-INSERT INTO users (email, name, password) VALUE ('nigel@gmail.com', 'Nigel', 'root');
-INSERT INTO users (email, name, password) VALUE ('oscar@gmail.com', 'Oscar', 'root');
-
 delete from trails;
 insert into trails ( Name,tru) value ( 'Caerphilly Coffee Trail',false);
 insert into trails ( Name,tru) value ( 'Penarth Dragon Trail',true);
@@ -56,10 +50,3 @@ INSERT INTO stickers (packID, stickerID, name, description, rarity) VALUE (3, 1,
 INSERT INTO stickers (packID, stickerID, name, description, rarity) VALUE (3, 2, 'Welsh Outline', 'Welsh Heritage', '1');
 INSERT INTO stickers (packID, stickerID, name, description, rarity) VALUE (3, 3, 'Welsh Spoon', 'Welsh Heritage', '1');
 
-DELETE FROM stickerprogress;
-INSERT INTO stickerprogress (userID, packID, stickerID) VALUE (1, 1, 1);
-INSERT INTO stickerprogress (userID, packID, stickerID) VALUE (1, 1, 2);
-INSERT INTO stickerprogress (userID, packID, stickerID) VALUE (1, 1, 3);
-INSERT INTO stickerprogress (userID, packID, stickerID) VALUE (1, 1, 5);
-INSERT INTO stickerprogress (userID, packID, stickerID) VALUE (1, 2, 1);
-INSERT INTO stickerprogress (userID, packID, stickerID) VALUE (1, 2, 3);
\ No newline at end of file
diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql
index c7e9993b..81a0c09f 100644
--- a/src/main/resources/schema.sql
+++ b/src/main/resources/schema.sql
@@ -36,12 +36,17 @@ create table if not exists locations
 
 
 CREATE TABLE IF NOT EXISTS users (
-    id bigint auto_increment primary key,
-    email varchar(128) NOT NULL ,
-    name varchar(30) NOT NULL,
+    username varchar(30) primary key NOT NULL,
+    email varchar(128),
     password varchar(30) NOT NULL,
-    dragonProgress int,
-    dragonsLandmarkIDs longtext
+    enabled boolean default true,
+    roles varchar(128)
+);
+
+CREATE TABLE IF NOT EXISTS authorities (
+    id bigint primary key auto_increment NOT NULL,
+    username varchar(30) NOT NULL ,
+    authority varchar(45) NOT NULL
 );
 
 CREATE TABLE IF NOT EXISTS packs (
@@ -64,8 +69,8 @@ CREATE TABLE IF NOT EXISTS stickers (
 
 CREATE TABLE IF NOT EXISTS stickerProgress (
     id bigint auto_increment primary key,
-    userID bigint NOT NULL,
-    FOREIGN KEY (userID) REFERENCES users(id)
+    username varchar(30) NOT NULL,
+    FOREIGN KEY (username) REFERENCES users(username)
         ON DELETE CASCADE
         ON UPDATE RESTRICT,
     packID bigint NOT NULL,
diff --git a/src/main/resources/static/css/login.css b/src/main/resources/static/css/login.css
index 25d221cc..023566c2 100644
--- a/src/main/resources/static/css/login.css
+++ b/src/main/resources/static/css/login.css
@@ -9,6 +9,8 @@
     --font-size-1: 14px;
     --font-size-2: 200px;
     --font-size-3: 300px;
+
+    --error-colour: red;
 }
 
 *{
@@ -363,5 +365,8 @@ and (max-device-width: 640px) {
         border-color: #fff;
     }
 }
-
+.alert {
+    color: var(--error-colour);
+    text-shadow: var(--error-colour) 0 0 10px;
+}
 
diff --git a/src/main/resources/static/scripts/userPage.js b/src/main/resources/static/scripts/userPage.js
index 2c1d5069..a7b03619 100644
--- a/src/main/resources/static/scripts/userPage.js
+++ b/src/main/resources/static/scripts/userPage.js
@@ -1,6 +1,6 @@
-function updatePack(userid, packid) {
-    /* Updates the trail being shown on screen to the one requested by ID */
-    $.get("/packInfo/" + userid + "/" + packid).done(function (fragment) {
+function updatePack(url) {
+    /* Updates the trail being shown on screen */
+    $.get(url).done(function (fragment) {
         let packRewardsWrapper = $("#packRewardsWrapper");
 
         packRewardsWrapper.fadeTo("slow", 0, function () {
diff --git a/src/main/resources/static/sql/user-data.sql b/src/main/resources/static/sql/user-data.sql
new file mode 100644
index 00000000..333ec409
--- /dev/null
+++ b/src/main/resources/static/sql/user-data.sql
@@ -0,0 +1,7 @@
+INSERT INTO users (username, password) VALUE ('Admin', 'admin');
+INSERT INTO users (username, password) VALUE ('Hannah', 'root');
+INSERT INTO users (username, password) VALUE ('Nigel', 'root');
+INSERT INTO users (username, password) VALUE ('Oscar', 'root');
+
+INSERT INTO authorities (username, authority) VALUE ('Admin', 'ADMIN');
+INSERT INTO authorities (username, authority) VALUE ('Hannah', 'USER');
diff --git a/src/main/resources/static/sql/user-progress-data.sql b/src/main/resources/static/sql/user-progress-data.sql
new file mode 100644
index 00000000..e9223384
--- /dev/null
+++ b/src/main/resources/static/sql/user-progress-data.sql
@@ -0,0 +1,7 @@
+DELETE FROM stickerprogress;
+INSERT INTO stickerprogress (username, packID, stickerID) VALUE ('Admin', 1, 1);
+INSERT INTO stickerprogress (username, packID, stickerID) VALUE ('Admin', 1, 2);
+INSERT INTO stickerprogress (username, packID, stickerID) VALUE ('Admin', 1, 3);
+INSERT INTO stickerprogress (username, packID, stickerID) VALUE ('Admin', 1, 5);
+INSERT INTO stickerprogress (username, packID, stickerID) VALUE ('Admin', 2, 1);
+INSERT INTO stickerprogress (username, packID, stickerID) VALUE ('Admin', 2, 3);
\ No newline at end of file
diff --git a/src/main/resources/templates/users/login.html b/src/main/resources/templates/users/login.html
index de23c96f..51c6222e 100644
--- a/src/main/resources/templates/users/login.html
+++ b/src/main/resources/templates/users/login.html
@@ -33,13 +33,15 @@
                 </form>
             </div>
             <div class="form-container sign-in">
-                <form th:object="${user}" action="#" th:action="@{/login/register}" th:method="POST" onsubmit="return loginFormValidation()">
+                <form  name="f" th:action="@{/login}" th:method="POST">
                     <h1>Sign In</h1>
+                    <div th:if="${param.error}" class="alert alert-error">Invalid Username or Password</div>
+                    <div th:if="${param.logout}" class="alert alert-success">Successfully Logged out</div>
                     <label>
-                        <input class="input" th:field="*{email}" id="login-email" type="email" placeholder="Email">
+                        <input class="input" id="username" type="text" name="username" placeholder="Email">
                     </label>
                     <label>
-                        <input class="input" th:field="*{password}" id="login-password" type="password" placeholder="Password">
+                        <input class="input" id="password" type="password" name="password" placeholder="Password">
                     </label>
                     <a href="#" class="text">Forget Your Password?</a>
                     <button type="submit">Sign In</button>
diff --git a/src/main/resources/templates/users/userProfile.html b/src/main/resources/templates/users/userProfile.html
index 94f46709..edbe1391 100644
--- a/src/main/resources/templates/users/userProfile.html
+++ b/src/main/resources/templates/users/userProfile.html
@@ -39,7 +39,8 @@
                 <div th:each="pack : ${packs}"  class="packContainer">
                     <img class="packImg"   th:src="@{'../' + ${pack.getDisplayImg()}}"
                          th:id="'packImg' + ${pack.getId()}" th:alt="${pack.getName()}"
-                         th:onclick="'updatePack(' + ${user.getId()} +',' + ${pack.getId()} +');'">
+                         th:data-url="@{/packInfo/{username}/{packID}(username=${user.getName()}, packID=${pack.getId()})}"
+                         onclick="updatePack(this.getAttribute('data-url'))">
                     <h4 class="packName" th:text="${pack.getName()}"></h4>
                 </div>
             </div>
-- 
GitLab