소스 검색

Register and Login

Marcin Jaborski 3 년 전
부모
커밋
15c46c97b9
80개의 변경된 파일2139개의 추가작업 그리고 451개의 파일을 삭제
  1. 38 2
      project-back/pom.xml
  2. 86 0
      project-back/src/main/java/com/example/projectback/controllers/AuthController.java
  3. 26 0
      project-back/src/main/java/com/example/projectback/controllers/ExampleSecurityRESTController.java
  4. 29 0
      project-back/src/main/java/com/example/projectback/controllers/StudentController.java
  5. 31 0
      project-back/src/main/java/com/example/projectback/message/request/LoginForm.java
  6. 41 0
      project-back/src/main/java/com/example/projectback/message/request/SignUpForm.java
  7. 46 0
      project-back/src/main/java/com/example/projectback/message/response/JwtResponse.java
  8. 17 0
      project-back/src/main/java/com/example/projectback/message/response/ResponseMessage.java
  9. 40 0
      project-back/src/main/java/com/example/projectback/model/Role.java
  10. 6 0
      project-back/src/main/java/com/example/projectback/model/RoleName.java
  11. 56 0
      project-back/src/main/java/com/example/projectback/model/Student.java
  12. 66 0
      project-back/src/main/java/com/example/projectback/model/User.java
  13. 11 0
      project-back/src/main/java/com/example/projectback/repository/RoleRepository.java
  14. 8 0
      project-back/src/main/java/com/example/projectback/repository/StudentRepository.java
  15. 11 0
      project-back/src/main/java/com/example/projectback/repository/UserRepository.java
  16. 75 0
      project-back/src/main/java/com/example/projectback/security/WebSecurityConfig.java
  17. 19 0
      project-back/src/main/java/com/example/projectback/security/jwt/JwtAuthEntryPoint.java
  18. 57 0
      project-back/src/main/java/com/example/projectback/security/jwt/JwtAuthTokenFilter.java
  19. 57 0
      project-back/src/main/java/com/example/projectback/security/jwt/JwtProvider.java
  20. 27 0
      project-back/src/main/java/com/example/projectback/security/services/UserDetailsServiceImpl.java
  21. 92 0
      project-back/src/main/java/com/example/projectback/security/services/UserPrinciple.java
  22. 13 0
      project-back/src/main/resources/application.properties
  23. 2 0
      project-back/src/main/resources/data.sql
  24. 57 0
      project-back/src/test/java/com/example/projectback/controllers/StudentControllerTest.java
  25. 2 0
      project-front/angular.json
  26. 66 0
      project-front/package-lock.json
  27. 3 1
      project-front/package.json
  28. 0 0
      project-front/src/app/admin/admin.component.css
  29. 3 0
      project-front/src/app/admin/admin.component.html
  30. 25 0
      project-front/src/app/admin/admin.component.spec.ts
  31. 26 0
      project-front/src/app/admin/admin.component.ts
  32. 14 0
      project-front/src/app/app.component.css
  33. 12 438
      project-front/src/app/app.component.html
  34. 3 3
      project-front/src/app/app.component.spec.ts
  35. 27 1
      project-front/src/app/app.component.ts
  36. 40 5
      project-front/src/app/app.module.ts
  37. 7 0
      project-front/src/app/auth/auth-interceptor.spec.ts
  38. 24 0
      project-front/src/app/auth/auth-interceptor.ts
  39. 12 0
      project-front/src/app/auth/auth.service.spec.ts
  40. 29 0
      project-front/src/app/auth/auth.service.ts
  41. 7 0
      project-front/src/app/auth/jwt-response.spec.ts
  42. 6 0
      project-front/src/app/auth/jwt-response.ts
  43. 7 0
      project-front/src/app/auth/login-info.spec.ts
  44. 10 0
      project-front/src/app/auth/login-info.ts
  45. 7 0
      project-front/src/app/auth/signup-info.spec.ts
  46. 13 0
      project-front/src/app/auth/signup-info.ts
  47. 12 0
      project-front/src/app/auth/token-storage.service.spec.ts
  48. 52 0
      project-front/src/app/auth/token-storage.service.ts
  49. 16 0
      project-front/src/app/guards/role.guard.spec.ts
  50. 31 0
      project-front/src/app/guards/role.guard.ts
  51. 0 0
      project-front/src/app/home/home.component.css
  52. 15 0
      project-front/src/app/home/home.component.html
  53. 25 0
      project-front/src/app/home/home.component.spec.ts
  54. 28 0
      project-front/src/app/home/home.component.ts
  55. 23 0
      project-front/src/app/login/login.component.css
  56. 25 0
      project-front/src/app/login/login.component.html
  57. 25 0
      project-front/src/app/login/login.component.spec.ts
  58. 74 0
      project-front/src/app/login/login.component.ts
  59. 23 0
      project-front/src/app/material/material.module.ts
  60. 23 0
      project-front/src/app/register/register.component.css
  61. 32 0
      project-front/src/app/register/register.component.html
  62. 25 0
      project-front/src/app/register/register.component.spec.ts
  63. 64 0
      project-front/src/app/register/register.component.ts
  64. 12 0
      project-front/src/app/services/user.service.spec.ts
  65. 22 0
      project-front/src/app/services/user.service.ts
  66. 7 0
      project-front/src/app/students/student.model.spec.ts
  67. 15 0
      project-front/src/app/students/student.model.ts
  68. 16 0
      project-front/src/app/students/student.service.spec.ts
  69. 116 0
      project-front/src/app/students/student.service.ts
  70. 0 0
      project-front/src/app/students/students.component.css
  71. 34 0
      project-front/src/app/students/students.component.html
  72. 25 0
      project-front/src/app/students/students.component.spec.ts
  73. 58 0
      project-front/src/app/students/students.component.ts
  74. 0 0
      project-front/src/app/user/user.component.css
  75. 3 0
      project-front/src/app/user/user.component.html
  76. 25 0
      project-front/src/app/user/user.component.spec.ts
  77. 25 0
      project-front/src/app/user/user.component.ts
  78. 18 0
      project-front/src/app/validation.ts
  79. 4 1
      project-front/src/index.html
  80. 12 0
      project-front/src/styles.css

+ 38 - 2
project-back/pom.xml

@@ -14,14 +14,50 @@
 	<name>project-back</name>
 	<description>project-back</description>
 	<properties>
-		<java.version>11</java.version>
+		<java.version>17</java.version>
 	</properties>
 	<dependencies>
 		<dependency>
 			<groupId>org.springframework.boot</groupId>
 			<artifactId>spring-boot-starter-web</artifactId>
 		</dependency>
-
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-jpa</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.data</groupId>
+            <artifactId>spring-data-jpa</artifactId>
+            <version>2.5.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.postgresql</groupId>
+            <artifactId>postgresql</artifactId>
+            <scope>runtime</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.dataformat</groupId>
+            <artifactId>jackson-dataformat-xml</artifactId>
+            <version>2.13.1</version>
+        </dependency>
+		<dependency>
+			<groupId>javax.servlet</groupId>
+			<artifactId>jstl</artifactId>
+			<version>1.2</version>
+		</dependency>
+		<dependency>
+			<groupId>javax.validation</groupId>
+			<artifactId>validation-api</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-security</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>com.10duke.client.jwt</groupId>
+			<artifactId>jjwt</artifactId>
+			<version>1.1.0</version>
+		</dependency>
 		<dependency>
 			<groupId>org.springframework.boot</groupId>
 			<artifactId>spring-boot-starter-test</artifactId>

+ 86 - 0
project-back/src/main/java/com/example/projectback/controllers/AuthController.java

@@ -0,0 +1,86 @@
+package com.example.projectback.controllers;
+
+import com.example.projectback.message.request.LoginForm;
+import com.example.projectback.message.request.SignUpForm;
+import com.example.projectback.message.response.JwtResponse;
+import com.example.projectback.message.response.ResponseMessage;
+import com.example.projectback.model.Role;
+import com.example.projectback.model.RoleName;
+import com.example.projectback.model.User;
+import com.example.projectback.repository.RoleRepository;
+import com.example.projectback.repository.UserRepository;
+import com.example.projectback.security.jwt.JwtProvider;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+import java.util.HashSet;
+import java.util.Set;
+
+@RestController
+@CrossOrigin(origins = "http://localhost:4200")
+@RequestMapping("/auth")
+public class AuthController {
+
+    AuthenticationManager authenticationManager;
+    UserRepository userRepository;
+    RoleRepository roleRepository;
+    PasswordEncoder passwordEncoder;
+    JwtProvider jwtProvider;
+
+    public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, RoleRepository roleRepository, PasswordEncoder passwordEncoder, JwtProvider jwtProvider) {
+        this.authenticationManager = authenticationManager;
+        this.userRepository = userRepository;
+        this.roleRepository = roleRepository;
+        this.passwordEncoder = passwordEncoder;
+        this.jwtProvider = jwtProvider;
+    }
+
+    @PostMapping("/login")
+    public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginForm loginRequest) {
+        Authentication authentication = authenticationManager.authenticate(
+                new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword()));
+        SecurityContextHolder.getContext().setAuthentication(authentication);
+
+        String jwt = jwtProvider.generateJwtToken(authentication);
+        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
+
+        return ResponseEntity.ok(new JwtResponse(jwt, userDetails.getUsername(), userDetails.getAuthorities()));
+    }
+
+    @PostMapping("/signup")
+    public ResponseEntity<?> registerUser(@Valid @RequestBody SignUpForm signUpRequest) {
+        if (userRepository.existsByEmail(signUpRequest.getEmail())) {
+            return new ResponseEntity<>(new ResponseMessage("Fail -> Email is already taken"), HttpStatus.BAD_REQUEST);
+        }
+
+        User user = new User(signUpRequest.getEmail(), passwordEncoder.encode(signUpRequest.getPassword()));
+
+        Set<String> strRoles = signUpRequest.getRole();
+        Set<Role> roles = new HashSet<>();
+
+        strRoles.forEach(role -> {
+            if ("admin".equals(role)) {
+                Role adminRole = roleRepository.findByName(RoleName.ROLE_ADMIN)
+                        .orElseThrow(() -> new RuntimeException("Fail -> Cause: Admin Role not found."));
+                roles.add(adminRole);
+            }
+            if ("user".equals(role)) {
+                Role userRole = roleRepository.findByName(RoleName.ROLE_USER)
+                        .orElseThrow(() -> new RuntimeException("Fail -> Cause: User Role not found"));
+                roles.add(userRole);
+            }
+        });
+
+        user.setRoles(roles);
+        userRepository.save(user);
+        return new ResponseEntity<>(new ResponseMessage("User registered successfully"), HttpStatus.OK);
+    }
+}

+ 26 - 0
project-back/src/main/java/com/example/projectback/controllers/ExampleSecurityRESTController.java

@@ -0,0 +1,26 @@
+package com.example.projectback.controllers;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@CrossOrigin(origins = "http://localhost:4200", maxAge = 3600)
+@RequestMapping("/exampleSecurity")
+public class ExampleSecurityRESTController {
+
+    @GetMapping("/user")
+    @PreAuthorize("hasRole('USER') or hasRole('ADMIN')")
+    public String userAccess() {
+        return ">>> User Contents!";
+    }
+
+    @GetMapping("/admin")
+    @PreAuthorize("hasRole('ADMIN')")
+    public String adminAccess() {
+        return ">>> Admin Contents";
+    }
+
+}

+ 29 - 0
project-back/src/main/java/com/example/projectback/controllers/StudentController.java

@@ -0,0 +1,29 @@
+package com.example.projectback.controllers;
+
+import com.example.projectback.model.Student;
+import com.example.projectback.repository.StudentRepository;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@CrossOrigin(origins = "http://localhost:4200")
+@RequestMapping("/students")
+public class StudentController {
+    private final StudentRepository studentRepository;
+
+    @Autowired
+    public StudentController(StudentRepository studentRepository) {this.studentRepository = studentRepository;}
+
+    @GetMapping
+    public List<Student> getAllStudents() {return studentRepository.findAll();}
+
+    @PostMapping
+    public ResponseEntity<Student> addStudent(@RequestBody Student student) {
+        studentRepository.save(student);
+        return new ResponseEntity<>(student, HttpStatus.CREATED);
+    }
+}

+ 31 - 0
project-back/src/main/java/com/example/projectback/message/request/LoginForm.java

@@ -0,0 +1,31 @@
+package com.example.projectback.message.request;
+
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
+
+public class LoginForm {
+    @NotBlank
+    @Size(min=3, max = 60)
+    private String email;
+
+    @NotBlank
+    @Size(min=8, max = 60)
+    private String password;
+
+    public String getEmail() {
+        return email;
+    }
+
+    public void setEmail(String email) {
+        this.email = email;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+}

+ 41 - 0
project-back/src/main/java/com/example/projectback/message/request/SignUpForm.java

@@ -0,0 +1,41 @@
+package com.example.projectback.message.request;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
+import java.util.Set;
+
+public class SignUpForm {
+    @NotBlank
+    @Size(min = 3, max = 60)
+    private String email;
+
+    @NotBlank
+    @Size(min = 8, max = 60)
+    private String password;
+
+    private Set<String> role;
+
+    public String getEmail() {
+        return email;
+    }
+
+    public void setEmail(String email) {
+        this.email = email;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    public Set<String> getRole() {
+        return role;
+    }
+
+    public void setRole(Set<String> role) {
+        this.role = role;
+    }
+}

+ 46 - 0
project-back/src/main/java/com/example/projectback/message/response/JwtResponse.java

@@ -0,0 +1,46 @@
+package com.example.projectback.message.response;
+
+import org.springframework.security.core.GrantedAuthority;
+
+import java.util.Collection;
+
+public class JwtResponse {
+    private String token;
+    private String type = "Bearer";
+    private String username;
+    private final Collection<? extends GrantedAuthority> authorities;
+
+    public JwtResponse(String token, String username, Collection<? extends GrantedAuthority> authorities) {
+        this.token = token;
+        this.username = username;
+        this.authorities = authorities;
+    }
+
+    public String getToken() {
+        return token;
+    }
+
+    public void setToken(String token) {
+        this.token = token;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public void setType(String type) {
+        this.type = type;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public void setUsername(String username) {
+        this.username = username;
+    }
+
+    public Collection<? extends GrantedAuthority> getAuthorities() {
+        return authorities;
+    }
+}

+ 17 - 0
project-back/src/main/java/com/example/projectback/message/response/ResponseMessage.java

@@ -0,0 +1,17 @@
+package com.example.projectback.message.response;
+
+public class ResponseMessage {
+    private String message;
+
+    public ResponseMessage(String message) {
+        this.message = message;
+    }
+
+    public String getMessage() {
+        return message;
+    }
+
+    public void setMessage(String message) {
+        this.message = message;
+    }
+}

+ 40 - 0
project-back/src/main/java/com/example/projectback/model/Role.java

@@ -0,0 +1,40 @@
+package com.example.projectback.model;
+
+import org.hibernate.annotations.NaturalId;
+
+import javax.persistence.*;
+
+@Entity
+public class Role {
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    @Enumerated(EnumType.STRING)
+    @NaturalId
+    private RoleName name;
+
+    public Role() {
+    }
+
+    public Role(RoleName name) {
+        this.name = name;
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public RoleName getName() {
+        return name;
+    }
+
+    public void setName(RoleName name) {
+        this.name = name;
+    }
+}
+

+ 6 - 0
project-back/src/main/java/com/example/projectback/model/RoleName.java

@@ -0,0 +1,6 @@
+package com.example.projectback.model;
+
+public enum RoleName {
+    ROLE_USER,
+    ROLE_ADMIN
+}

+ 56 - 0
project-back/src/main/java/com/example/projectback/model/Student.java

@@ -0,0 +1,56 @@
+package com.example.projectback.model;
+
+import javax.persistence.Entity;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+
+@Entity
+public class Student {
+    @Id
+    @GeneratedValue
+    private long id;
+    private String firstname;
+    private String lastname;
+    private String email;
+    private String telephone;
+
+    public String getFirstname() {
+        return firstname;
+    }
+
+    public void setFirstname(String firstname) {
+        this.firstname = firstname;
+    }
+
+    public String getLastname() {
+        return lastname;
+    }
+
+    public void setLastname(String lastname) {
+        this.lastname = lastname;
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    public void setEmail(String email) {
+        this.email = email;
+    }
+
+    public String getTelephone() {
+        return telephone;
+    }
+
+    public void setTelephone(String telephone) {
+        this.telephone = telephone;
+    }
+
+    public long getId() {
+        return id;
+    }
+
+    public void setId(long id) {
+        this.id = id;
+    }
+}

+ 66 - 0
project-back/src/main/java/com/example/projectback/model/User.java

@@ -0,0 +1,66 @@
+package com.example.projectback.model;
+
+import javax.persistence.*;
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.Size;
+import java.util.HashSet;
+import java.util.Set;
+
+@Entity
+@Table(name = "users")
+public class User {
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+
+    @NotBlank
+    @Size(min = 3, max = 60)
+    private String email;
+
+    @NotBlank
+    @Size(min=8, max = 60)
+    private String password;
+
+    @ManyToMany(fetch = FetchType.EAGER)
+    private Set<Role> roles = new HashSet<>();
+
+    public User() {
+    }
+
+    public User(@NotBlank @Size(min = 3, max = 60) String email, @NotBlank @Size(min = 8, max = 60) String password) {
+        this.email = email;
+        this.password = password;
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    public void setEmail(String username) {
+        this.email = username;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    public Set<Role> getRoles() {
+        return roles;
+    }
+
+    public void setRoles(Set<Role> roles) {
+        this.roles = roles;
+    }
+}

+ 11 - 0
project-back/src/main/java/com/example/projectback/repository/RoleRepository.java

@@ -0,0 +1,11 @@
+package com.example.projectback.repository;
+
+import com.example.projectback.model.Role;
+import com.example.projectback.model.RoleName;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+public interface RoleRepository extends JpaRepository<Role, Long> {
+    Optional<Role> findByName(RoleName roleName);
+}

+ 8 - 0
project-back/src/main/java/com/example/projectback/repository/StudentRepository.java

@@ -0,0 +1,8 @@
+package com.example.projectback.repository;
+
+import com.example.projectback.model.Student;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface StudentRepository extends JpaRepository<Student, Long> {
+    Student findById(long id);
+}

+ 11 - 0
project-back/src/main/java/com/example/projectback/repository/UserRepository.java

@@ -0,0 +1,11 @@
+package com.example.projectback.repository;
+
+import com.example.projectback.model.User;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+public interface UserRepository extends JpaRepository<User, Long> {
+    Optional<User> findByEmail(String email);
+    Boolean existsByEmail(String email);
+}

+ 75 - 0
project-back/src/main/java/com/example/projectback/security/WebSecurityConfig.java

@@ -0,0 +1,75 @@
+package com.example.projectback.security;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
+import com.example.projectback.security.jwt.JwtAuthEntryPoint;
+import com.example.projectback.security.jwt.JwtAuthTokenFilter;
+import com.example.projectback.security.services.UserDetailsServiceImpl;
+
+@Configuration
+@EnableWebSecurity
+@EnableGlobalMethodSecurity(prePostEnabled = true)
+public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
+
+    UserDetailsServiceImpl userDetailsService;
+
+    private final JwtAuthEntryPoint unauthorizedHandler;
+
+    public WebSecurityConfig(UserDetailsServiceImpl userDetailsService, JwtAuthEntryPoint unauthorizedHandler) {
+        this.userDetailsService = userDetailsService;
+        this.unauthorizedHandler = unauthorizedHandler;
+    }
+
+    @Bean
+    public JwtAuthTokenFilter authenticationJwtTokenFilter() {
+        return new JwtAuthTokenFilter();
+    }
+
+    @Override
+    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
+        authenticationManagerBuilder.userDetailsService(userDetailsService)
+                .passwordEncoder(passwordEncoder());
+    }
+
+    @Bean
+    @Override
+    public AuthenticationManager authenticationManagerBean() throws Exception {
+        return super.authenticationManagerBean();
+    }
+
+    @Bean
+    public PasswordEncoder passwordEncoder() {
+        return new BCryptPasswordEncoder();
+    }
+
+    @Override
+    protected void configure(HttpSecurity http) throws Exception {
+        http.cors().and().csrf().disable().
+                authorizeRequests()
+                .antMatchers("/auth/**").permitAll()
+                // next line for secured app
+                //.antMatchers("/restApi/students/**").hasAnyRole("ADMIN","USER") //hasRole("ADMIN")
+                // next app for not secured version (lecture with REST)
+                .antMatchers("/students/**").permitAll()
+                .antMatchers("/exampleSecurity/user").hasRole("USER")
+                .antMatchers("/exampleSecurity/admin").hasRole("ADMIN")
+                .anyRequest().authenticated()
+                .and()
+                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
+                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
+
+        http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
+
+    }
+}

+ 19 - 0
project-back/src/main/java/com/example/projectback/security/jwt/JwtAuthEntryPoint.java

@@ -0,0 +1,19 @@
+package com.example.projectback.security.jwt;
+
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+@Component
+public class JwtAuthEntryPoint implements AuthenticationEntryPoint {
+
+    @Override
+    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
+        httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error -> Unauthorized");
+    }
+}

+ 57 - 0
project-back/src/main/java/com/example/projectback/security/jwt/JwtAuthTokenFilter.java

@@ -0,0 +1,57 @@
+package com.example.projectback.security.jwt;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
+import org.springframework.web.filter.OncePerRequestFilter;
+import com.example.projectback.security.services.UserDetailsServiceImpl;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+public class JwtAuthTokenFilter extends OncePerRequestFilter {
+
+    @Autowired
+    private JwtProvider tokenProvider;
+
+    @Autowired
+    private UserDetailsServiceImpl userDetailsService;
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
+        try {
+
+            String jwt = getJwt(httpServletRequest);
+            if (jwt != null && tokenProvider.validateJwtToken(jwt)) {
+                String username = tokenProvider.getUserNameFromJwtToken(jwt);
+
+                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
+                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
+                        userDetails, null, userDetails.getAuthorities());
+                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(httpServletRequest));
+
+                SecurityContextHolder.getContext().setAuthentication(authentication);
+            }
+        } catch (Exception e) {
+            logger.error("Can NOT set user authentication -> Message: {}", e);
+        }
+
+        filterChain.doFilter(httpServletRequest, httpServletResponse);
+    }
+
+    private String getJwt(HttpServletRequest request) {
+        String authHeader = request.getHeader("Authorization");
+
+        if (authHeader != null && authHeader.startsWith("Bearer ")) {
+            return authHeader.replace("Bearer ", "");
+        }
+
+        return null;
+    }
+
+}

+ 57 - 0
project-back/src/main/java/com/example/projectback/security/jwt/JwtProvider.java

@@ -0,0 +1,57 @@
+package com.example.projectback.security.jwt;
+
+import io.jsonwebtoken.*;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Component;
+import com.example.projectback.security.services.UserPrinciple;
+
+import java.util.Date;
+
+@Component
+public class JwtProvider {
+
+    @Value("${com.example.project-back.jwtSecret}")
+    private String jwtSecret;
+
+    @Value("${com.example.project-back.jwtExpiration}")
+    private int jwtExpiration;
+
+    public String generateJwtToken(Authentication authentication) {
+        UserPrinciple userPrinciple = (UserPrinciple) authentication.getPrincipal();
+
+        return Jwts.builder()
+                .setSubject(userPrinciple.getUsername())
+                .setIssuedAt(new Date())
+                .setExpiration(new Date((new Date()).getTime() + jwtExpiration*1000))
+                .signWith(SignatureAlgorithm.HS512, jwtSecret)
+                .compact();
+    }
+
+    public boolean validateJwtToken(String authToken) {
+        try {
+            Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
+            return true;
+        } catch (SignatureException e) {
+            System.out.println("Invalid JWT signature -> Message: {} " + e);
+        } catch (MalformedJwtException e) {
+            System.out.println("Invalid JWT token -> Message: {}" + e);
+        } catch (ExpiredJwtException e) {
+            System.out.println("Expired JWT token -> Message: {}" + e);
+        } catch (UnsupportedJwtException e) {
+            System.out.println("Unsupported JWT token -> Message: {}" + e);
+        } catch (IllegalArgumentException e) {
+            System.out.println("JWT claims string is empty -> Message: {}" + e);
+        }
+
+        return false;
+    }
+
+    public String getUserNameFromJwtToken(String token) {
+        return Jwts.parser()
+                .setSigningKey(jwtSecret)
+                .parseClaimsJws(token)
+                .getBody().getSubject();
+    }
+
+}

+ 27 - 0
project-back/src/main/java/com/example/projectback/security/services/UserDetailsServiceImpl.java

@@ -0,0 +1,27 @@
+package com.example.projectback.security.services;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import com.example.projectback.model.User;
+import com.example.projectback.repository.UserRepository;
+
+@Service
+public class UserDetailsServiceImpl  implements UserDetailsService {
+
+    @Autowired
+    UserRepository userRepository;
+
+    @Override
+    @Transactional
+    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+
+        User user = userRepository.findByEmail(username).orElseThrow(
+                () -> new UsernameNotFoundException("User Not Found with -> username: " + username));
+        return UserPrinciple.build(user);
+    }
+
+}

+ 92 - 0
project-back/src/main/java/com/example/projectback/security/services/UserPrinciple.java

@@ -0,0 +1,92 @@
+package com.example.projectback.security.services;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.security.core.userdetails.UserDetails;
+import com.example.projectback.model.User;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class UserPrinciple implements UserDetails {
+
+    private Long id;
+
+    private String username;
+
+    @JsonIgnore
+    private String password;
+
+    private Collection<? extends GrantedAuthority> authorities;
+
+    public UserPrinciple(Long id, String username, String password, Collection<? extends GrantedAuthority> authorities) {
+        this.id = id;
+        this.username = username;
+        this.password = password;
+        this.authorities = authorities;
+    }
+
+    public static UserPrinciple build(User user) {
+        List<GrantedAuthority> authorities = user.getRoles().stream().map(role ->
+                new SimpleGrantedAuthority(role.getName().name())
+        ).collect(Collectors.toList());
+
+        return new UserPrinciple(
+                user.getId(),
+                user.getEmail(),
+                user.getPassword(),
+                authorities
+        );
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    @Override
+    public String getUsername() {
+        return username;
+    }
+
+    @Override
+    public String getPassword() {
+        return password;
+    }
+
+    @Override
+    public Collection<? extends GrantedAuthority> getAuthorities() {
+        return authorities;
+    }
+
+    @Override
+    public boolean isAccountNonExpired() {
+        return true;
+    }
+
+    @Override
+    public boolean isAccountNonLocked() {
+        return true;
+    }
+
+    @Override
+    public boolean isCredentialsNonExpired() {
+        return true;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return true;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+        if (obj == null || getClass() != obj.getClass()) return false;
+
+        UserPrinciple user = (UserPrinciple) obj;
+        return Objects.equals(id, user.id);
+    }
+}

+ 13 - 0
project-back/src/main/resources/application.properties

@@ -1 +1,14 @@
+# suppress inspection "UnusedProperty" for whole file
+spring.datasource.url=jdbc:postgresql://localhost:5432/iwa_project
+spring.datasource.username=postgres
+spring.datasource.password=admin
+spring.datasource.tomcat.test-while-idle=true
+spring.datasource.tomcat.validation-query=SELECT 1
+spring.sql.init.mode=always
 
+spring.jpa.show-sql=true
+spring.jpa.hibernate.ddl-auto=update
+spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
+
+com.example.project-back.jwtSecret=7fe711b2Hd97aQ4b7dC85daH498db577b72b7fe711b2Hd97aQ4b7dC85daH498db577b72b7fe711b2Hd97aQ4b7dC85daH498db577b72b
+com.example.project-back.jwtExpiration=3600

+ 2 - 0
project-back/src/main/resources/data.sql

@@ -0,0 +1,2 @@
+INSERT INTO role (name) SELECT 'ROLE_ADMIN' WHERE NOT EXISTS(SELECT * FROM role WHERE role.name='ROLE_ADMIN');
+INSERT INTO role (name) SELECT 'ROLE_USER' WHERE NOT EXISTS(SELECT * FROM role WHERE role.name='ROLE_USER');

+ 57 - 0
project-back/src/test/java/com/example/projectback/controllers/StudentControllerTest.java

@@ -0,0 +1,57 @@
+package com.example.projectback.controllers;
+
+import com.example.projectback.model.Student;
+import com.example.projectback.repository.StudentRepository;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.MvcResult;
+import org.springframework.test.web.servlet.RequestBuilder;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
+import org.springframework.web.context.WebApplicationContext;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(SpringExtension.class)
+@WebMvcTest(StudentController.class)
+class StudentControllerTest {
+
+    @MockBean
+    private StudentRepository studentRepository;
+
+    @Autowired
+    private WebApplicationContext webApplicationContext;
+
+    @Autowired
+    private MockMvc mvc;
+
+    @Test
+    void addStudent() throws Exception {
+        mvc.perform(MockMvcRequestBuilders.post("/students").contentType(MediaType.APPLICATION_JSON)
+                .content("{\"firstname\": \"John\", \"lastname\": \"Smith\", \"email\": \"john.smith@example.com\", \"telephone\": \"600345624\"}")).andExpect(MockMvcResultMatchers.status().isCreated());
+    }
+
+    @Test
+    void getAllStudents() throws Exception {
+        Student testStudent = new Student();
+        testStudent.setFirstname("John");
+        testStudent.setLastname("Smith");
+        testStudent.setEmail("john.smith@example.com");
+        testStudent.setTelephone("600345624");
+
+        when(studentRepository.findAll()).thenReturn(List.of(testStudent));
+
+        RequestBuilder request = MockMvcRequestBuilders.get("/students");
+        mvc.perform(request).andExpect(MockMvcResultMatchers.status().isOk()).andExpect(MockMvcResultMatchers.jsonPath("$[0].firstname").value("John"));
+    }
+
+}

+ 2 - 0
project-front/angular.json

@@ -27,6 +27,7 @@
               "src/assets"
             ],
             "styles": [
+              "./node_modules/@angular/material/prebuilt-themes/pink-bluegrey.css",
               "src/styles.css"
             ],
             "scripts": []
@@ -94,6 +95,7 @@
               "src/assets"
             ],
             "styles": [
+              "./node_modules/@angular/material/prebuilt-themes/pink-bluegrey.css",
               "src/styles.css"
             ],
             "scripts": []

+ 66 - 0
project-front/package-lock.json

@@ -9,10 +9,12 @@
       "version": "0.0.0",
       "dependencies": {
         "@angular/animations": "~13.3.0",
+        "@angular/cdk": "^13.3.7",
         "@angular/common": "~13.3.0",
         "@angular/compiler": "~13.3.0",
         "@angular/core": "~13.3.0",
         "@angular/forms": "~13.3.0",
+        "@angular/material": "^13.3.7",
         "@angular/platform-browser": "~13.3.0",
         "@angular/platform-browser-dynamic": "~13.3.0",
         "@angular/router": "~13.3.0",
@@ -347,6 +349,28 @@
         "@angular/core": "13.3.8"
       }
     },
+    "node_modules/@angular/cdk": {
+      "version": "13.3.7",
+      "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-13.3.7.tgz",
+      "integrity": "sha512-HtGqlrt4+ikbpzooF0LT/uMW6fgRJxLRUoOwkTY1oHhfNXhQaE2p8XEUH2qshl28aCIF8r8zrb6jpd4VqC+tyg==",
+      "dependencies": {
+        "tslib": "^2.3.0"
+      },
+      "optionalDependencies": {
+        "parse5": "^5.0.0"
+      },
+      "peerDependencies": {
+        "@angular/common": "^13.0.0 || ^14.0.0-0",
+        "@angular/core": "^13.0.0 || ^14.0.0-0",
+        "rxjs": "^6.5.3 || ^7.4.0"
+      }
+    },
+    "node_modules/@angular/cdk/node_modules/parse5": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
+      "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==",
+      "optional": true
+    },
     "node_modules/@angular/cli": {
       "version": "13.3.5",
       "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-13.3.5.tgz",
@@ -536,6 +560,23 @@
         "rxjs": "^6.5.3 || ^7.4.0"
       }
     },
+    "node_modules/@angular/material": {
+      "version": "13.3.7",
+      "resolved": "https://registry.npmjs.org/@angular/material/-/material-13.3.7.tgz",
+      "integrity": "sha512-CXdLvohaxl3Nii6I70pEJX2FZRRkBPNiocUNP39hFHXf2PW/eNYCN8TS5DG2uOAaR2wyPwrXrU95mDjgvlqXqQ==",
+      "dependencies": {
+        "tslib": "^2.3.0"
+      },
+      "peerDependencies": {
+        "@angular/animations": "^13.0.0 || ^14.0.0-0",
+        "@angular/cdk": "13.3.7",
+        "@angular/common": "^13.0.0 || ^14.0.0-0",
+        "@angular/core": "^13.0.0 || ^14.0.0-0",
+        "@angular/forms": "^13.0.0 || ^14.0.0-0",
+        "@angular/platform-browser": "^13.0.0 || ^14.0.0-0",
+        "rxjs": "^6.5.3 || ^7.4.0"
+      }
+    },
     "node_modules/@angular/platform-browser": {
       "version": "13.3.8",
       "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-13.3.8.tgz",
@@ -11890,6 +11931,23 @@
         "tslib": "^2.3.0"
       }
     },
+    "@angular/cdk": {
+      "version": "13.3.7",
+      "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-13.3.7.tgz",
+      "integrity": "sha512-HtGqlrt4+ikbpzooF0LT/uMW6fgRJxLRUoOwkTY1oHhfNXhQaE2p8XEUH2qshl28aCIF8r8zrb6jpd4VqC+tyg==",
+      "requires": {
+        "parse5": "^5.0.0",
+        "tslib": "^2.3.0"
+      },
+      "dependencies": {
+        "parse5": {
+          "version": "5.1.1",
+          "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
+          "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==",
+          "optional": true
+        }
+      }
+    },
     "@angular/cli": {
       "version": "13.3.5",
       "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-13.3.5.tgz",
@@ -12020,6 +12078,14 @@
         "tslib": "^2.3.0"
       }
     },
+    "@angular/material": {
+      "version": "13.3.7",
+      "resolved": "https://registry.npmjs.org/@angular/material/-/material-13.3.7.tgz",
+      "integrity": "sha512-CXdLvohaxl3Nii6I70pEJX2FZRRkBPNiocUNP39hFHXf2PW/eNYCN8TS5DG2uOAaR2wyPwrXrU95mDjgvlqXqQ==",
+      "requires": {
+        "tslib": "^2.3.0"
+      }
+    },
     "@angular/platform-browser": {
       "version": "13.3.8",
       "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-13.3.8.tgz",

+ 3 - 1
project-front/package.json

@@ -11,10 +11,12 @@
   "private": true,
   "dependencies": {
     "@angular/animations": "~13.3.0",
+    "@angular/cdk": "^13.3.7",
     "@angular/common": "~13.3.0",
     "@angular/compiler": "~13.3.0",
     "@angular/core": "~13.3.0",
     "@angular/forms": "~13.3.0",
+    "@angular/material": "^13.3.7",
     "@angular/platform-browser": "~13.3.0",
     "@angular/platform-browser-dynamic": "~13.3.0",
     "@angular/router": "~13.3.0",
@@ -36,4 +38,4 @@
     "karma-jasmine-html-reporter": "~1.7.0",
     "typescript": "~4.6.2"
   }
-}
+}

+ 0 - 0
project-front/src/app/admin/admin.component.css


+ 3 - 0
project-front/src/app/admin/admin.component.html

@@ -0,0 +1,3 @@
+<h4>Content from Server</h4>
+{{board}}
+{{errorMessage}}

+ 25 - 0
project-front/src/app/admin/admin.component.spec.ts

@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { AdminComponent } from './admin.component';
+
+describe('AdminComponent', () => {
+  let component: AdminComponent;
+  let fixture: ComponentFixture<AdminComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ AdminComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(AdminComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 26 - 0
project-front/src/app/admin/admin.component.ts

@@ -0,0 +1,26 @@
+import { Component, OnInit } from '@angular/core';
+import {UserService} from '../services/user.service';
+
+@Component({
+  selector: 'app-admin',
+  templateUrl: './admin.component.html',
+  styleUrls: ['./admin.component.css']
+})
+export class AdminComponent implements OnInit {
+  board?: string;
+  errorMessage?: string;
+
+  constructor(private userService: UserService) { }
+
+  ngOnInit() {
+    this.userService.getAdminPage().subscribe(
+      data => {
+        this.board = data;
+      },
+      error => {
+        this.errorMessage = `${error.status}: ${JSON.parse(error.error).message}`;
+      }
+    );
+  }
+
+}

+ 14 - 0
project-front/src/app/app.component.css

@@ -0,0 +1,14 @@
+mat-toolbar {
+    display: flex;
+    justify-content: space-between;
+}
+
+mat-toolbar .right {
+    display: flex;
+    gap: 20px;
+}
+
+mat-toolbar .right a {
+    color: white;
+    text-decoration: none;
+}

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 12 - 438
project-front/src/app/app.component.html


+ 3 - 3
project-front/src/app/app.component.spec.ts

@@ -16,16 +16,16 @@ describe('AppComponent', () => {
     expect(app).toBeTruthy();
   });
 
-  it(`should have as title 'project-front'`, () => {
+  it(`should have as title 'angular13-iwa2022-http-students'`, () => {
     const fixture = TestBed.createComponent(AppComponent);
     const app = fixture.componentInstance;
-    expect(app.title).toEqual('project-front');
+    expect(app.title).toEqual('angular13-iwa2022-http-students');
   });
 
   it('should render title', () => {
     const fixture = TestBed.createComponent(AppComponent);
     fixture.detectChanges();
     const compiled = fixture.nativeElement as HTMLElement;
-    expect(compiled.querySelector('.content span')?.textContent).toContain('project-front app is running!');
+    expect(compiled.querySelector('.content span')?.textContent).toContain('angular13-iwa2022-http-students app is running!');
   });
 });

+ 27 - 1
project-front/src/app/app.component.ts

@@ -1,4 +1,5 @@
 import { Component } from '@angular/core';
+import { TokenStorageService } from "./auth/token-storage.service";
 
 @Component({
   selector: 'app-root',
@@ -6,5 +7,30 @@ import { Component } from '@angular/core';
   styleUrls: ['./app.component.css']
 })
 export class AppComponent {
-  title = 'project-front';
+  title = 'angular13-iwa2022-http-students';
+  private roles?: string[];
+  authority?: string;
+
+  constructor(private tokenStorage: TokenStorageService, private token: TokenStorageService) {
+  }
+
+  ngOnInit() {
+    if (this.tokenStorage.getToken()) {
+      this.roles = this.tokenStorage.getAuthorities();
+      this.roles.every(role => {
+        if (role === 'ROLE_ADMIN') {
+          this.authority = 'admin';
+          return false;
+        }
+        this.authority = 'user';
+        return true;
+      });
+    }
+  }
+
+  logout() {
+    this.token.signOut();
+    window.location.reload();
+  }
+
 }

+ 40 - 5
project-front/src/app/app.module.ts

@@ -1,16 +1,51 @@
 import { NgModule } from '@angular/core';
 import { BrowserModule } from '@angular/platform-browser';
-
 import { AppComponent } from './app.component';
+import { HomeComponent } from './home/home.component';
+import { StudentsComponent } from './students/students.component';
+import { RouterModule, Routes } from "@angular/router";
+import { HttpClientModule } from "@angular/common/http";
+import { FormsModule, ReactiveFormsModule } from "@angular/forms";
+import { UserComponent } from "./user/user.component";
+import { AdminComponent } from "./admin/admin.component";
+import { LoginComponent } from "./login/login.component";
+import { RegisterComponent } from "./register/register.component";
+import { httpInterceptorProviders } from './auth/auth-interceptor';
+import { RoleGuard } from "./guards/role.guard";
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { MaterialModule } from "./material/material.module";
+
+const routes: Routes = [
+  { path: 'home', component: HomeComponent },
+  { path: 'students', component: StudentsComponent },
+  { path: 'user', component: UserComponent, canActivate: [RoleGuard], data: { roles: ['ROLE_USER', 'ROLE_ADMIN'] }, },
+  { path: 'admin', component: AdminComponent, canActivate: [RoleGuard], data: { roles: ['ROLE_ADMIN'] }, },
+  { path: 'auth/login', component: LoginComponent },
+  { path: 'auth/signup', component: RegisterComponent },
+  { path: '', redirectTo: 'home', pathMatch: 'full' }
+];
 
 @NgModule({
   declarations: [
-    AppComponent
+    AppComponent,
+    HomeComponent,
+    StudentsComponent,
+    LoginComponent,
+    RegisterComponent,
+    UserComponent,
+    AdminComponent
   ],
   imports: [
-    BrowserModule
+    BrowserModule,
+    HttpClientModule,
+    FormsModule,
+    RouterModule.forRoot(routes),
+    BrowserAnimationsModule,
+    MaterialModule,
+    ReactiveFormsModule
   ],
-  providers: [],
+  providers: [httpInterceptorProviders],
   bootstrap: [AppComponent]
 })
-export class AppModule { }
+export class AppModule {
+}

+ 7 - 0
project-front/src/app/auth/auth-interceptor.spec.ts

@@ -0,0 +1,7 @@
+import { AuthInterceptor } from './auth-interceptor';
+
+describe('AuthInterceptor', () => {
+  it('should create an instance', () => {
+    //expect(new AuthInterceptor()).toBeTruthy();
+  });
+});

+ 24 - 0
project-front/src/app/auth/auth-interceptor.ts

@@ -0,0 +1,24 @@
+import {Injectable} from '@angular/core';
+import {HTTP_INTERCEPTORS, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
+import {TokenStorageService} from './token-storage.service';
+
+const TOKEN_HEADER_KEY = 'Authorization';
+
+@Injectable()
+export class AuthInterceptor implements HttpInterceptor {
+
+  constructor(private token: TokenStorageService) { }
+
+  intercept(req: HttpRequest<any>, next: HttpHandler) {
+    let authReq = req;
+    const token = this.token.getToken();
+    if (token != null) {
+      authReq = req.clone({ headers: req.headers.set(TOKEN_HEADER_KEY, 'Bearer ' + token) });
+    }
+    return next.handle(authReq);
+  }
+}
+
+export const httpInterceptorProviders = [
+  { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
+];

+ 12 - 0
project-front/src/app/auth/auth.service.spec.ts

@@ -0,0 +1,12 @@
+import { TestBed } from '@angular/core/testing';
+
+import { AuthService } from './auth.service';
+
+describe('AuthService', () => {
+  beforeEach(() => TestBed.configureTestingModule({}));
+
+  it('should be created', () => {
+    const service: AuthService = TestBed.get(AuthService);
+    expect(service).toBeTruthy();
+  });
+});

+ 29 - 0
project-front/src/app/auth/auth.service.ts

@@ -0,0 +1,29 @@
+import { Injectable } from '@angular/core';
+import {HttpClient, HttpHeaders} from '@angular/common/http';
+import {LoginInfo} from './login-info';
+import {Observable} from 'rxjs';
+import {JwtResponse} from './jwt-response';
+import {SignupInfo} from './signup-info';
+
+const httpOptions = {
+  headers: new HttpHeaders({'Content-Type': 'application/json'})
+};
+
+@Injectable({
+  providedIn: 'root'
+})
+export class AuthService {
+
+  private loginUrl = 'http://localhost:8080/auth/login';
+  private signupUrl = 'http://localhost:8080/auth/signup';
+
+  constructor(private http: HttpClient) { }
+
+  attemptAuth(credentials: LoginInfo): Observable<JwtResponse> {
+    return this.http.post<JwtResponse>(this.loginUrl, credentials, httpOptions);
+  }
+
+  signUp(info: SignupInfo): Observable<string> {
+    return this.http.post<string>(this.signupUrl, info, httpOptions);
+  }
+}

+ 7 - 0
project-front/src/app/auth/jwt-response.spec.ts

@@ -0,0 +1,7 @@
+import { JwtResponse } from './jwt-response';
+
+describe('JwtResponse', () => {
+  it('should create an instance', () => {
+    expect(new JwtResponse()).toBeTruthy();
+  });
+});

+ 6 - 0
project-front/src/app/auth/jwt-response.ts

@@ -0,0 +1,6 @@
+export class JwtResponse {
+  token?: string;
+  type?: string;
+  username?: string;
+  authorities?: string[];
+}

+ 7 - 0
project-front/src/app/auth/login-info.spec.ts

@@ -0,0 +1,7 @@
+import { LoginInfo } from './login-info';
+
+describe('LoginInfo', () => {
+  it('should create an instance', () => {
+    // expect(new LoginInfo()).toBeTruthy();
+  });
+});

+ 10 - 0
project-front/src/app/auth/login-info.ts

@@ -0,0 +1,10 @@
+export class LoginInfo {
+
+  email: string;
+  password: string;
+
+  constructor(email: string, password: string) {
+    this.email = email;
+    this.password = password;
+  }
+}

+ 7 - 0
project-front/src/app/auth/signup-info.spec.ts

@@ -0,0 +1,7 @@
+import { SignupInfo } from './signup-info';
+
+describe('SignupInfo', () => {
+  it('should create an instance', () => {
+    expect(new SignupInfo()).toBeTruthy();
+  });
+});

+ 13 - 0
project-front/src/app/auth/signup-info.ts

@@ -0,0 +1,13 @@
+export class SignupInfo {
+
+  email: string;
+  role: string[];
+  password: string;
+
+
+  constructor(email: string, password: string) {
+    this.email = email;
+    this.role = ['user'];
+    this.password = password;
+  }
+}

+ 12 - 0
project-front/src/app/auth/token-storage.service.spec.ts

@@ -0,0 +1,12 @@
+import { TestBed } from '@angular/core/testing';
+
+import { TokenStorageService } from './token-storage.service';
+
+describe('TokenStorageService', () => {
+  beforeEach(() => TestBed.configureTestingModule({}));
+
+  it('should be created', () => {
+    const service: TokenStorageService = TestBed.get(TokenStorageService);
+    expect(service).toBeTruthy();
+  });
+});

+ 52 - 0
project-front/src/app/auth/token-storage.service.ts

@@ -0,0 +1,52 @@
+import { Injectable } from '@angular/core';
+
+const TOKEN_KEY = 'AuthToken';
+const USERNAME_KEY = 'AuthUsername';
+const AUTHORITIES_KEY = 'AuthAuthorities';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class TokenStorageService {
+  private roles: Array<string> = [];
+  constructor() { }
+
+  signOut() {
+    window.sessionStorage.clear();
+  }
+
+  public saveToken(token: string) {
+    window.sessionStorage.removeItem(TOKEN_KEY);
+    window.sessionStorage.setItem(TOKEN_KEY, token);
+  }
+
+  public getToken(): string {
+    return sessionStorage.getItem(TOKEN_KEY) || '{}';
+  }
+
+  public saveUsername(username: string) {
+    window.sessionStorage.removeItem(USERNAME_KEY);
+    window.sessionStorage.setItem(USERNAME_KEY, username);
+  }
+
+  public getUsername(): string {
+    return sessionStorage.getItem(USERNAME_KEY) || '{}';
+  }
+
+  public saveAuthorities(authorities: string[]) {
+    window.sessionStorage.removeItem(AUTHORITIES_KEY);
+    window.sessionStorage.setItem(AUTHORITIES_KEY, JSON.stringify(authorities));
+  }
+
+  public getAuthorities(): string[] {
+    this.roles = [];
+
+    if (sessionStorage.getItem(TOKEN_KEY)) {
+      JSON.parse(sessionStorage.getItem(AUTHORITIES_KEY) || '{}').forEach((authority: { authority: string; }) => {
+        this.roles.push(authority.authority);
+      });
+    }
+
+    return this.roles;
+  }
+}

+ 16 - 0
project-front/src/app/guards/role.guard.spec.ts

@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { RoleGuard } from './role.guard';
+
+describe('RoleGuard', () => {
+  let guard: RoleGuard;
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({});
+    guard = TestBed.inject(RoleGuard);
+  });
+
+  it('should be created', () => {
+    expect(guard).toBeTruthy();
+  });
+});

+ 31 - 0
project-front/src/app/guards/role.guard.ts

@@ -0,0 +1,31 @@
+import { Injectable } from '@angular/core';
+import {ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree} from '@angular/router';
+import { Observable } from 'rxjs';
+import {TokenStorageService} from "../auth/token-storage.service";
+
+@Injectable({
+  providedIn: 'root'
+})
+export class RoleGuard implements CanActivate {
+
+  constructor(private tokenStorageService: TokenStorageService, private router: Router) {
+  }
+
+  canActivate(
+    route: ActivatedRouteSnapshot,
+    state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
+
+    // check if any role from authorities list is in the routing list defined
+    for (let i = 0; i < route.data['roles'].length; i++) {
+      for (let j = 0; j < this.tokenStorageService.getAuthorities().length; j++) {
+        if (route.data['roles'][i] === this.tokenStorageService.getAuthorities()[j]) {
+          return true;
+        }
+      }
+    }
+
+    // otherwise redirect to specified url
+      this.router.navigateByUrl('').then();
+      return false;
+  }
+}

+ 0 - 0
project-front/src/app/home/home.component.css


+ 15 - 0
project-front/src/app/home/home.component.html

@@ -0,0 +1,15 @@
+<p>home component works!</p>
+<div *ngIf="info.token != '{}'; else loggedOut">
+  <h5 class="text-primary">Your Information</h5>
+  <p>
+    <strong>Username:</strong> {{info.username}}<br/>
+    <strong>Roles:</strong> {{info.authorities}}<br />
+    <strong>Token:</strong> {{info.token}}.
+  </p>
+  <button class="btn btn-secondary" (click)="logout()">Logout</button>
+</div>
+
+<ng-template #loggedOut>
+  Please login.
+</ng-template>
+

+ 25 - 0
project-front/src/app/home/home.component.spec.ts

@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { HomeComponent } from './home.component';
+
+describe('HomeComponent', () => {
+  let component: HomeComponent;
+  let fixture: ComponentFixture<HomeComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [ HomeComponent ]
+    })
+    .compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(HomeComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 28 - 0
project-front/src/app/home/home.component.ts

@@ -0,0 +1,28 @@
+import { Component, OnInit } from '@angular/core';
+import {TokenStorageService} from "../auth/token-storage.service";
+
+@Component({
+  selector: 'app-home',
+  templateUrl: './home.component.html',
+  styleUrls: ['./home.component.css']
+})
+export class HomeComponent implements OnInit {
+
+  info: any;
+
+  constructor(private token: TokenStorageService) { }
+
+  ngOnInit() {
+    this.info = {
+      token: this.token.getToken(),
+      username: this.token.getUsername(),
+      authorities: this.token.getAuthorities()
+    };
+  }
+
+  logout() {
+    this.token.signOut();
+    window.location.reload();
+  }
+
+}

+ 23 - 0
project-front/src/app/login/login.component.css

@@ -0,0 +1,23 @@
+.wrapper {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: calc(100vh - 64px);
+}
+
+.container {
+    width: 400px;
+    height: 75vh;
+    background-color: #3d3d3d;
+    border-radius: 20px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+form {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 10px;
+}

+ 25 - 0
project-front/src/app/login/login.component.html

@@ -0,0 +1,25 @@
+<div class="wrapper">
+  <div class="container">
+    <form [formGroup]="form" (ngSubmit)="onSubmit()">
+      <h1>My account</h1>
+      <mat-form-field appearance="fill">
+        <mat-label>Email</mat-label>
+        <input matInput formControlName="email">
+        <mat-error *ngIf="f['email'].errors?.['required']">Email is required</mat-error>
+        <mat-error *ngIf="f['email'].errors?.['email']">Email is invalid</mat-error>
+      </mat-form-field>
+
+      <mat-form-field appearance="fill">
+        <mat-label>Password</mat-label>
+        <input type="password" matInput formControlName="password">
+        <mat-error *ngIf="f['password'].errors?.['required']">Password is required</mat-error>
+        <mat-error *ngIf="f['password'].errors?.['minlength']">Password must have at least 8 characters
+        </mat-error>
+      </mat-form-field>
+
+      <button mat-raised-button color="primary">Login</button>
+      <span>Don't have an account?</span>
+      <a mat-button [routerLink]="['/auth/signup']">Sign up</a>
+    </form>
+  </div>
+</div>

+ 25 - 0
project-front/src/app/login/login.component.spec.ts

@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { LoginComponent } from './login.component';
+
+describe('LoginComponent', () => {
+  let component: LoginComponent;
+  let fixture: ComponentFixture<LoginComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ LoginComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(LoginComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 74 - 0
project-front/src/app/login/login.component.ts

@@ -0,0 +1,74 @@
+import { Component, OnInit } from '@angular/core';
+import { LoginInfo } from '../auth/login-info';
+import { AuthService } from '../auth/auth.service';
+import { TokenStorageService } from '../auth/token-storage.service';
+import { MatSnackBar } from "@angular/material/snack-bar";
+import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from "@angular/forms";
+import { Router } from "@angular/router";
+
+@Component({
+  selector: 'app-login',
+  templateUrl: './login.component.html',
+  styleUrls: ['./login.component.css']
+})
+export class LoginComponent implements OnInit {
+  form: FormGroup = new FormGroup({
+    email: new FormControl(''),
+    password: new FormControl(''),
+  });
+  token?: string;
+  isLoggedIn = false;
+  isLoginFailed = false;
+  roles: string[] = [];
+  private loginInfo?: LoginInfo;
+
+  constructor(private authService: AuthService,
+              private tokenStorage: TokenStorageService,
+              private _snackBar: MatSnackBar,
+              private formBuilder: FormBuilder,
+              private router: Router) {
+  }
+
+  ngOnInit() {
+    if (this.tokenStorage.getToken() != null && this.tokenStorage.getToken() != '{}') {
+      this.isLoggedIn = true;
+      this.roles = this.tokenStorage.getAuthorities();
+    }
+    this.form = this.formBuilder.group({
+      email: ['', [Validators.required, Validators.email]],
+      password: ['', [Validators.required, Validators.minLength(8)]],
+    })
+  }
+
+  get f(): { [key: string]: AbstractControl } {
+    return this.form.controls;
+  }
+
+  onSubmit() {
+    if (this.form.invalid) {
+      return;
+    }
+
+    this.loginInfo = new LoginInfo(this.form.value.email, this.form.value.password);
+
+    this.authService.attemptAuth(this.loginInfo).subscribe(
+      data => {
+        this.tokenStorage.saveToken(data.token || '{}');
+        this.tokenStorage.saveUsername(data.username || '{}');
+        this.tokenStorage.saveAuthorities(data.authorities || []);
+
+        this.isLoginFailed = false;
+        this.isLoggedIn = true;
+        this.token = this.tokenStorage.getToken();
+        this.roles = this.tokenStorage.getAuthorities();
+        this.router.navigate(['/home']).then(() => {
+        });
+      },
+      error => {
+        this._snackBar.open(error.error?.message || 'Oops! Error occurred', 'Dismiss');
+        this.isLoginFailed = true;
+      }
+    );
+  }
+
+}

+ 23 - 0
project-front/src/app/material/material.module.ts

@@ -0,0 +1,23 @@
+import { NgModule } from '@angular/core';
+import { MatButtonModule } from "@angular/material/button";
+import { MatToolbarModule } from "@angular/material/toolbar";
+import { MatFormFieldModule } from "@angular/material/form-field";
+import { MatInputModule } from "@angular/material/input";
+import { MatSnackBarModule } from "@angular/material/snack-bar";
+import { MatStepperModule } from "@angular/material/stepper";
+
+const MaterialComponents = [
+  MatButtonModule,
+  MatToolbarModule,
+  MatFormFieldModule,
+  MatInputModule,
+  MatSnackBarModule,
+  MatStepperModule,
+];
+
+@NgModule({
+  imports: [MaterialComponents],
+  exports: [MaterialComponents]
+})
+export class MaterialModule {
+}

+ 23 - 0
project-front/src/app/register/register.component.css

@@ -0,0 +1,23 @@
+.wrapper {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: calc(100vh - 64px);
+}
+
+.container {
+    width: 400px;
+    height: 75vh;
+    background-color: #3d3d3d;
+    border-radius: 20px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+}
+
+form {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    gap: 10px;
+}

+ 32 - 0
project-front/src/app/register/register.component.html

@@ -0,0 +1,32 @@
+<div class="wrapper">
+  <div class="container">
+    <form [formGroup]="form" (ngSubmit)="onSubmit()">
+      <h1>Create new account</h1>
+      <mat-form-field appearance="fill">
+        <mat-label>Email</mat-label>
+        <input matInput formControlName="email">
+        <mat-error *ngIf="f['email'].errors?.['required']">Email is required</mat-error>
+        <mat-error *ngIf="f['email'].errors?.['email']">Email is invalid</mat-error>
+      </mat-form-field>
+
+      <mat-form-field appearance="fill">
+        <mat-label>Password</mat-label>
+        <input type="password" matInput formControlName="password">
+        <mat-error *ngIf="f['password'].errors?.['required']">Password is required</mat-error>
+        <mat-error *ngIf="f['password'].errors?.['minlength']">Password must have at least 8 characters
+        </mat-error>
+      </mat-form-field>
+
+      <mat-form-field appearance="fill">
+        <mat-label>Confirm Password</mat-label>
+        <input type="password" matInput formControlName="confirmPassword">
+        <mat-error *ngIf="f['confirmPassword'].errors?.['required']">Re-type the password</mat-error>
+        <mat-error *ngIf="f['confirmPassword'].errors?.['matching']">Passwords aren't matching</mat-error>
+      </mat-form-field>
+
+      <button mat-raised-button color="primary">Sign up</button>
+      <span>Already have an account?</span>
+      <a mat-button [routerLink]="['/auth/login']">Log in</a>
+    </form>
+  </div>
+</div>

+ 25 - 0
project-front/src/app/register/register.component.spec.ts

@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { RegisterComponent } from './register.component';
+
+describe('RegisterComponent', () => {
+  let component: RegisterComponent;
+  let fixture: ComponentFixture<RegisterComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ RegisterComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(RegisterComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 64 - 0
project-front/src/app/register/register.component.ts

@@ -0,0 +1,64 @@
+import { Component, OnInit } from '@angular/core';
+import { AuthService } from '../auth/auth.service';
+import { SignupInfo } from '../auth/signup-info';
+import { AbstractControl, FormBuilder, FormControl, FormGroup, Validators } from "@angular/forms";
+import Validation from "../validation";
+import { MatSnackBar } from "@angular/material/snack-bar";
+import { Router } from "@angular/router";
+
+@Component({
+  selector: 'app-register',
+  templateUrl: './register.component.html',
+  styleUrls: ['./register.component.css']
+})
+
+export class RegisterComponent implements OnInit {
+  form: FormGroup = new FormGroup({
+    email: new FormControl(''),
+    password: new FormControl(''),
+    confirmPassword: new FormControl(''),
+  });
+  signupInfo?: SignupInfo;
+  isSignedUp = false;
+  isSignUpFailed = false;
+
+  constructor(private authService: AuthService,
+              private formBuilder: FormBuilder,
+              private _snackBar: MatSnackBar,
+              private router: Router) {
+  }
+
+  ngOnInit(): void {
+    this.form = this.formBuilder.group({
+      email: ['', [Validators.required, Validators.email]],
+      password: ['', [Validators.required, Validators.minLength(8), Validators.maxLength(40)]],
+      confirmPassword: ['', Validators.required]
+    }, {
+      validators: [Validation.match('password', 'confirmPassword')]
+    })
+  }
+
+  get f(): { [key: string]: AbstractControl } {
+    return this.form.controls;
+  }
+
+  onSubmit() {
+    if (this.form.invalid) {
+      return;
+    }
+
+    this.signupInfo = new SignupInfo(this.form.value.email, this.form.value.password);
+
+    this.authService.signUp(this.signupInfo).subscribe(
+      data => {
+        this.isSignedUp = true;
+        this.isSignUpFailed = false;
+        this.router.navigate(['/auth/login']);
+      },
+      error => {
+        this._snackBar.open(error.error?.message || 'Oops! Error occurred', 'Dismiss');
+        this.isSignUpFailed = true;
+      }
+    );
+  }
+}

+ 12 - 0
project-front/src/app/services/user.service.spec.ts

@@ -0,0 +1,12 @@
+import { TestBed } from '@angular/core/testing';
+
+import { UserService } from './user.service';
+
+describe('UserService', () => {
+  beforeEach(() => TestBed.configureTestingModule({}));
+
+  it('should be created', () => {
+    const service: UserService = TestBed.get(UserService);
+    expect(service).toBeTruthy();
+  });
+});

+ 22 - 0
project-front/src/app/services/user.service.ts

@@ -0,0 +1,22 @@
+import { Injectable } from '@angular/core';
+import {Observable} from 'rxjs';
+import {HttpClient} from '@angular/common/http';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class UserService {
+
+  private userUrl = 'http://localhost:8080/exampleSecurity/user';
+  private adminUrl = 'http://localhost:8080/exampleSecurity/admin';
+
+  constructor(private http: HttpClient) { }
+
+  getUserPage(): Observable<string> {
+    return this.http.get(this.userUrl, { responseType: 'text' });
+  }
+
+  getAdminPage(): Observable<string> {
+    return this.http.get(this.adminUrl, { responseType: 'text' });
+  }
+}

+ 7 - 0
project-front/src/app/students/student.model.spec.ts

@@ -0,0 +1,7 @@
+import { Student } from './student.model';
+
+describe('Student', () => {
+  it('should create an instance', () => {
+    expect(new Student()).toBeTruthy();
+  });
+});

+ 15 - 0
project-front/src/app/students/student.model.ts

@@ -0,0 +1,15 @@
+export class Student {
+  id?: number;
+  firstname: string;
+  lastname: string;
+  email: string;
+  telephone: string;
+
+  constructor(firstname: string, lastname: string, email: string, telephone: string) {
+    this.firstname = firstname;
+    this.lastname = lastname;
+    this.email = email;
+    this.telephone = telephone;
+  }
+
+}

+ 16 - 0
project-front/src/app/students/student.service.spec.ts

@@ -0,0 +1,16 @@
+import { TestBed } from '@angular/core/testing';
+
+import { StudentService } from './student.service';
+
+describe('StudentService', () => {
+  let service: StudentService;
+
+  beforeEach(() => {
+    TestBed.configureTestingModule({});
+    service = TestBed.inject(StudentService);
+  });
+
+  it('should be created', () => {
+    expect(service).toBeTruthy();
+  });
+});

+ 116 - 0
project-front/src/app/students/student.service.ts

@@ -0,0 +1,116 @@
+import { Injectable } from '@angular/core';
+import {HttpClient, HttpHeaders} from "@angular/common/http";
+import {BehaviorSubject, catchError, Observable, of, tap} from "rxjs";
+import {Student} from "./student.model";
+
+const httpOptions = {
+  headers: new HttpHeaders({ 'Content-Type': 'application/json' })
+};
+
+@Injectable({
+  providedIn: 'root'
+})
+export class StudentService {
+
+  private studentsUrl = 'http://localhost:8080/students';
+
+  constructor(private http: HttpClient) { }
+
+  // for automatic update of number of students in parent component
+  public totalItems: BehaviorSubject<number> = new BehaviorSubject<number>(0);
+  getCartItems() {
+    return this.totalItems.asObservable();
+  }
+
+  /** GET students from the server */
+  getStudents(): Observable<Student[]> {
+    return this.http.get<Student[]>(this.studentsUrl);
+  }
+
+  /** POST: add a new student to the server */
+  addStudent(student: Student): Observable<Student> {
+    return this.http.post<Student>(this.studentsUrl, student, httpOptions).pipe(
+      tap((studentAdded: Student) => this.log(`added student id=${studentAdded.id}`)),
+      catchError(this.handleError<Student>('addStudent'))
+    );
+  }
+
+  /** GET student by id. Will 404 if id not found */
+  getStudent(id: number): Observable<Student> {
+    const url = `${this.studentsUrl}/${id}`;
+    return this.http.get<Student>(url).pipe(
+      tap(_ => this.log(`fetched student id=${id}`)),
+      catchError(this.handleError<Student>(`getStudent id=${id}`))
+    );
+  }
+
+  /** DELETE: delete the student from the server */
+  deleteStudent(student: Student | number): Observable<Student> {
+    const id = typeof student === 'number' ? student : student.id;
+    const url = `${this.studentsUrl}/${id}`;
+    return this.http.delete<Student>(url, httpOptions).pipe(
+      tap(_ => this.log(`deleted student id=${id}`)),
+      catchError(this.handleError<Student>('deleteStudent'))
+    );
+  }
+
+  /** DELETE: delete the student from the server */
+  deleteStudents(): Observable<Student> {
+    return this.http.delete<Student>(this.studentsUrl, httpOptions).pipe(
+      tap(_ => this.log(`deleted students`)),
+      catchError(this.handleError<Student>('deleteStudents'))
+    );
+  }
+
+  /** PUT: update the student on the server */
+  updateStudent(student: Student): Observable<any> {
+    return this.http.put(this.studentsUrl, student, httpOptions).pipe(
+      tap(_ => this.log(`updated student id=${student.id}`)),
+      catchError(this.handleError<any>('updateStudent'))
+    );
+  }
+
+  /**
+   * Handle Http operation that failed.
+   * Let the app continue.
+   * @param operation - name of the operation that failed
+   * @param result - optional value to return as the observable result
+   */
+  private handleError<T>(operation = 'operation', result?: T) {
+    return (error: any): Observable<T> => {
+
+      // TODO: send the error to remote logging infrastructure
+      console.error(error); // log to console instead
+
+      // TODO: better job of transforming error for user consumption
+      this.log(`${operation} failed: ${error.message}`);
+
+      // Let the app keep running by returning an empty result.
+      return of(result as T);
+    };
+  }
+
+  /** Log a StudentService message with the MessageService */
+  private log(message: string) {
+    console.log('StudentService: ' + message);
+  }
+
+  /** GET number of students from the server */
+  getStudentsCounter(): Observable<number> {
+    const url = `${this.studentsUrl}/counter`;
+    return this.http.get<number>(url);
+  }
+
+  //updatedCounter(dataAsParams) {
+  //  this.totalItems.next(dataAsParams);
+  //}
+
+
+  /** PUT: update the student on the server */
+  updateStudents(students: Student[]): Observable<Student[]> {
+    return this.http.put<Student[]>(this.studentsUrl, students, httpOptions).pipe(
+      tap(_ => this.log(`updated student id=${students}`)),
+      catchError(this.handleError<any>('updateStudent'))
+    );
+  }
+}

+ 0 - 0
project-front/src/app/students/students.component.css


+ 34 - 0
project-front/src/app/students/students.component.html

@@ -0,0 +1,34 @@
+<h2>Students</h2>
+
+<div>
+  <label>Student firstname:
+    <input #studentFirstName />
+  </label>
+  <br>
+  <label>Student lastname:
+    <input #studentLastName />
+  </label>
+  <br>
+  <label>Student email:
+    <input #studentEmail />
+  </label>
+  <br>
+  <label>Student telephone:
+    <input #studentTelephone />
+  </label>
+  <br>
+  <!-- (click) passes input value to add() and then clears the input -->
+  <button (click)="add(studentFirstName.value, studentLastName.value, studentEmail.value, studentTelephone.value);
+                  studentFirstName.value=''; studentLastName.value=''; studentEmail.value=''; studentTelephone.value=''">
+    add
+  </button>
+</div>
+
+<ul>
+  <li *ngFor="let student of studentList">
+    <span class="badge">{{student.id}}</span> {{ student.firstname }} {{ student.lastname }} {{ student.email }} {{ student.telephone }}
+    <button title="delete" (click)="delete(student)">delete</button>
+  </li>
+</ul>
+<button title="delete all" (click)="deleteAll()">delete all</button>
+

+ 25 - 0
project-front/src/app/students/students.component.spec.ts

@@ -0,0 +1,25 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { StudentsComponent } from './students.component';
+
+describe('StudentsComponent', () => {
+  let component: StudentsComponent;
+  let fixture: ComponentFixture<StudentsComponent>;
+
+  beforeEach(async () => {
+    await TestBed.configureTestingModule({
+      declarations: [ StudentsComponent ]
+    })
+    .compileComponents();
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(StudentsComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 58 - 0
project-front/src/app/students/students.component.ts

@@ -0,0 +1,58 @@
+import { Component, OnInit } from '@angular/core';
+import {Student} from "./student.model";
+import {StudentService} from "./student.service";
+
+@Component({
+  selector: 'app-students',
+  templateUrl: './students.component.html',
+  styleUrls: ['./students.component.css']
+})
+export class StudentsComponent implements OnInit {
+  studentList?: Student[];
+  student?: Student;
+
+  constructor(private studentService: StudentService) { }
+
+  ngOnInit() { this.getStudents();   }
+
+  getStudents(): void {
+    this.studentService.getStudents()
+      .subscribe(studentList => this.studentList = studentList);
+  }
+
+  add(firstname: string, lastname: string, email: string, telephone: string): void {
+    firstname = firstname.trim();
+    lastname = lastname.trim();
+    email = email.trim();
+    telephone = telephone.trim();
+    this.studentService.addStudent({ firstname, lastname, email, telephone } as Student)
+      .subscribe({
+        next: (student: Student) => { this.studentList?.push(student); },
+        error: () => {},
+        complete: () => {
+          if (this.studentList != undefined) {
+            this.studentService.totalItems.next(this.studentList.length);
+          }
+        }
+  });
+  }
+
+  delete(student: Student): void {
+    this.studentList = this.studentList?.filter(c => c !== student);
+    this.studentService.deleteStudent(student).subscribe(() => {
+        // for automatic update of number of students in parent component
+      if(this.studentList != undefined) {
+        this.studentService.totalItems.next(this.studentList.length);
+      }
+      }
+    );
+  }
+
+  deleteAll(): void {
+    this.studentService.deleteStudents().subscribe(() => {
+      }
+    );
+  }
+
+}
+

+ 0 - 0
project-front/src/app/user/user.component.css


+ 3 - 0
project-front/src/app/user/user.component.html

@@ -0,0 +1,3 @@
+<h4>Content from Server</h4>
+{{board}}
+{{errorMessage}}

+ 25 - 0
project-front/src/app/user/user.component.spec.ts

@@ -0,0 +1,25 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { UserComponent } from './user.component';
+
+describe('UserComponent', () => {
+  let component: UserComponent;
+  let fixture: ComponentFixture<UserComponent>;
+
+  beforeEach(async(() => {
+    TestBed.configureTestingModule({
+      declarations: [ UserComponent ]
+    })
+    .compileComponents();
+  }));
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(UserComponent);
+    component = fixture.componentInstance;
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+});

+ 25 - 0
project-front/src/app/user/user.component.ts

@@ -0,0 +1,25 @@
+import { Component, OnInit } from '@angular/core';
+import {UserService} from '../services/user.service';
+
+@Component({
+  selector: 'app-user',
+  templateUrl: './user.component.html',
+  styleUrls: ['./user.component.css']
+})
+export class UserComponent implements OnInit {
+  board?: string;
+  errorMessage?: string;
+
+  constructor(private userService: UserService) { }
+
+  ngOnInit() {
+    this.userService.getUserPage().subscribe(
+      data => {
+        this.board = data;
+      },
+      error => {
+        this.errorMessage = `${error.status}: ${JSON.parse(error.error).message}`;
+      }
+    );
+  }
+}

+ 18 - 0
project-front/src/app/validation.ts

@@ -0,0 +1,18 @@
+import { AbstractControl, ValidatorFn } from "@angular/forms";
+
+export default class Validation {
+  static match(controlName: string, checkControlName: string): ValidatorFn {
+    return (controls: AbstractControl) => {
+      const control = controls.get(controlName);
+      const checkControl = controls.get(checkControlName);
+      if (checkControl?.errors && !checkControl.errors['matching']) {
+        return null;
+      }
+      if (control?.value !== checkControl?.value) {
+        checkControl?.setErrors({ matching: true });
+        return { matching: true };
+      }
+      return null;
+    }
+  }
+}

+ 4 - 1
project-front/src/index.html

@@ -6,8 +6,11 @@
   <base href="/">
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <link rel="icon" type="image/x-icon" href="favicon.ico">
+  <link rel="preconnect" href="https://fonts.gstatic.com">
+  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
+  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
 </head>
-<body>
+<body class="mat-typography">
   <app-root></app-root>
 </body>
 </html>

+ 12 - 0
project-front/src/styles.css

@@ -1 +1,13 @@
 /* You can add global styles to this file, and also import other style files */
+@import "../node_modules/@angular/material/prebuilt-themes/pink-bluegrey.css";
+
+html, body {
+    height: 100%;
+}
+
+body {
+    margin: 0;
+    font-family: Roboto, "Helvetica Neue", sans-serif;
+    background-color: #313131;
+    color: white;
+}

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.