Lab 6 MVC WITHOUT SPA

This commit is contained in:
Данила Мочалов 2023-05-02 00:06:37 +04:00
parent 36f22805bb
commit 52b7f3d761
19 changed files with 383 additions and 23 deletions

View File

@ -31,6 +31,9 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
} }
tasks.named('test') { tasks.named('test') {

Binary file not shown.

View File

@ -0,0 +1,14 @@
package com.webproglabs.lab1;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class PasswordEncoderConfiguration {
@Bean
public PasswordEncoder createPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}

View File

@ -0,0 +1,71 @@
package com.webproglabs.lab1;
import com.webproglabs.lab1.lab34.controller.mvc_controllers.UserSignupMvcController;
import com.webproglabs.lab1.lab34.model.UserRole;
import com.webproglabs.lab1.lab34.services.ProfileService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final Logger log = LoggerFactory.getLogger(SecurityConfiguration.class);
private static final String LOGIN_URL = "/login";
private final ProfileService userService;
public SecurityConfiguration(ProfileService userService) {
this.userService = userService;
createAdminOnStartup();
}
private void createAdminOnStartup() {
final String admin = "admin";
if (userService.findByLogin(admin) == null) {
log.info("Admin user successfully created");
try {
userService.createUser(admin, admin, admin, UserRole.ADMIN);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().frameOptions().sameOrigin().and()
.cors().and()
.csrf().disable()
.authorizeRequests()
.antMatchers(UserSignupMvcController.SIGNUP_URL).permitAll()
.antMatchers(HttpMethod.GET, LOGIN_URL).permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage(LOGIN_URL).permitAll()
.and()
.logout().permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
@Override
public void configure(WebSecurity web) {
web.ignoring()
.antMatchers("/css/**")
.antMatchers("/js/**")
.antMatchers("/templates/**")
.antMatchers("/webjars/**");
}
}

View File

@ -11,7 +11,7 @@ public class WebConfiguration implements WebMvcConfigurer {
@Override @Override
public void addViewControllers(ViewControllerRegistry registry) { public void addViewControllers(ViewControllerRegistry registry) {
WebMvcConfigurer.super.addViewControllers(registry); WebMvcConfigurer.super.addViewControllers(registry);
registry.addViewController("rest-test"); registry.addViewController("login");
} }
@Override @Override
public void addCorsMappings(CorsRegistry registry) { public void addCorsMappings(CorsRegistry registry) {

View File

@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.webproglabs.lab1.lab34.model.Comment; import com.webproglabs.lab1.lab34.model.Comment;
import com.webproglabs.lab1.lab34.model.Post; import com.webproglabs.lab1.lab34.model.Post;
import com.webproglabs.lab1.lab34.model.Profile; import com.webproglabs.lab1.lab34.model.Profile;
import com.webproglabs.lab1.lab34.model.UserRole;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -16,12 +17,15 @@ public class ProfileDto {
private List<PostDto> posts = new ArrayList<>(); private List<PostDto> posts = new ArrayList<>();
private UserRole role;
public ProfileDto(){} public ProfileDto(){}
public ProfileDto(Profile profile){ public ProfileDto(Profile profile){
this.id = profile.getId(); this.id = profile.getId();
this.login = profile.getLogin(); this.login = profile.getLogin();
this.password = profile.getPassword(); this.password = profile.getPassword();
this.role = profile.getRole();
for(Comment comment: profile.getComments()){ for(Comment comment: profile.getComments()){
comments.add(new CommentDto(comment)); comments.add(new CommentDto(comment));
} }
@ -37,6 +41,9 @@ public class ProfileDto {
public String getLogin() {return login;} public String getLogin() {return login;}
public void setLogin(String login) {this.login = login;} public void setLogin(String login) {this.login = login;}
public String getPassword() {return password;} public String getPassword() {return password;}
public UserRole getRole() {return role;}
public void setPassword(String password) {this.password = password;} public void setPassword(String password) {this.password = password;}
public List<CommentDto> getComments() {return comments;} public List<CommentDto> getComments() {return comments;}
public List<PostDto> getPosts() {return posts;} public List<PostDto> getPosts() {return posts;}

View File

@ -3,9 +3,12 @@ package com.webproglabs.lab1.lab34.controller.mvc_controllers;
import com.webproglabs.lab1.lab34.controller.CommentDto; import com.webproglabs.lab1.lab34.controller.CommentDto;
import com.webproglabs.lab1.lab34.controller.PostDto; import com.webproglabs.lab1.lab34.controller.PostDto;
import com.webproglabs.lab1.lab34.controller.ProfileDto; import com.webproglabs.lab1.lab34.controller.ProfileDto;
import com.webproglabs.lab1.lab34.model.Profile;
import com.webproglabs.lab1.lab34.services.CommentService; import com.webproglabs.lab1.lab34.services.CommentService;
import com.webproglabs.lab1.lab34.services.PostService; import com.webproglabs.lab1.lab34.services.PostService;
import com.webproglabs.lab1.lab34.services.ProfileService; import com.webproglabs.lab1.lab34.services.ProfileService;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -26,6 +29,12 @@ public class FeedMvcController {
@GetMapping @GetMapping
public String getFeedPage(Model model) { public String getFeedPage(Model model) {
UserDetails principal = (UserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
var user = profileService.findByLogin(principal.getUsername());
if (user != null) {
return "redirect:/feed/" + user.getId().toString();
}
model.addAttribute("profiles", profileService.findAllUsers().stream().map(ProfileDto::new).toList()); model.addAttribute("profiles", profileService.findAllUsers().stream().map(ProfileDto::new).toList());
return "feed"; return "feed";
} }

View File

@ -1,7 +1,9 @@
package com.webproglabs.lab1.lab34.controller.mvc_controllers; package com.webproglabs.lab1.lab34.controller.mvc_controllers;
import com.webproglabs.lab1.lab34.controller.ProfileDto; import com.webproglabs.lab1.lab34.controller.ProfileDto;
import com.webproglabs.lab1.lab34.model.UserRole;
import com.webproglabs.lab1.lab34.services.ProfileService; import com.webproglabs.lab1.lab34.services.ProfileService;
import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@ -23,6 +25,7 @@ public class ProfileMvcController {
} }
@GetMapping(value={"profiles"}) @GetMapping(value={"profiles"})
@Secured({UserRole.AsString.ADMIN})
public String getProfiles(Model model) { public String getProfiles(Model model) {
model.addAttribute("profiles", profileService.findAllUsers().stream().map(ProfileDto::new).toList()); model.addAttribute("profiles", profileService.findAllUsers().stream().map(ProfileDto::new).toList());
model.addAttribute("profileDto", new ProfileDto()); model.addAttribute("profileDto", new ProfileDto());

View File

@ -0,0 +1,40 @@
package com.webproglabs.lab1.lab34.controller.mvc_controllers;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
public class UserSignupDto {
@NotBlank
@Size(min = 3, max = 64)
private String login;
@NotBlank
@Size(min = 6, max = 64)
private String password;
@NotBlank
@Size(min = 6, max = 64)
private String passwordConfirm;
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getPasswordConfirm() {
return passwordConfirm;
}
public void setPasswordConfirm(String passwordConfirm) {
this.passwordConfirm = passwordConfirm;
}
}

View File

@ -0,0 +1,49 @@
package com.webproglabs.lab1.lab34.controller.mvc_controllers;
import com.webproglabs.lab1.lab34.model.Profile;
import com.webproglabs.lab1.lab34.services.ProfileService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.validation.Valid;
@Controller
@RequestMapping(UserSignupMvcController.SIGNUP_URL)
public class UserSignupMvcController {
public static final String SIGNUP_URL = "/signup";
private final ProfileService userService;
public UserSignupMvcController(ProfileService userService) {
this.userService = userService;
}
@GetMapping
public String showSignupForm(Model model) {
model.addAttribute("userDto", new UserSignupDto());
return "signup";
}
@PostMapping
public String signup(@ModelAttribute("userDto") @Valid UserSignupDto userSignupDto,
BindingResult bindingResult,
Model model) {
if (bindingResult.hasErrors()) {
model.addAttribute("errors", bindingResult.getAllErrors());
return "signup";
}
try {
final Profile user = userService.createUser(
userSignupDto.getLogin(), userSignupDto.getPassword(), userSignupDto.getPasswordConfirm());
return "redirect:/login?created=" + user.getLogin();
} catch (Exception e) {
model.addAttribute("errors", e.getMessage());
return "signup";
}
}
}

View File

@ -1,6 +1,8 @@
package com.webproglabs.lab1.lab34.model; package com.webproglabs.lab1.lab34.model;
import javax.persistence.*; import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -12,9 +14,22 @@ public class Profile {
@GeneratedValue(strategy = GenerationType.AUTO) @GeneratedValue(strategy = GenerationType.AUTO)
private Long id; private Long id;
@Column(nullable = false, unique = true, length = 64)
@NotBlank
@Size(min = 3, max = 64)
private String login; private String login;
@Column(nullable = false, length = 64)
@NotBlank
@Size(min = 6, max = 64)
private String password; private String password;
private UserRole role;
public UserRole getRole() {
return role;
}
@OneToMany(mappedBy = "owner", orphanRemoval = true, fetch = FetchType.EAGER) @OneToMany(mappedBy = "owner", orphanRemoval = true, fetch = FetchType.EAGER)
private List<Comment> comments = new ArrayList<Comment>(); private List<Comment> comments = new ArrayList<Comment>();
@ -25,6 +40,18 @@ public class Profile {
public Profile(String login, String password, List<Comment> comments, List<Post> posts) { public Profile(String login, String password, List<Comment> comments, List<Post> posts) {
this.login = login; this.login = login;
this.password=password; this.password=password;
this.role = UserRole.USER;
for (int i = 0; i < comments.size(); i++) {
addComment(comments.get(i));
}
for (int i = 0; i < posts.size(); i++) {
addPost(posts.get(i));
}
};
public Profile(String login, String password, List<Comment> comments, List<Post> posts, UserRole role) {
this.login = login;
this.password=password;
this.role = role;
for (int i = 0; i < comments.size(); i++) { for (int i = 0; i < comments.size(); i++) {
addComment(comments.get(i)); addComment(comments.get(i));
} }

View File

@ -0,0 +1,20 @@
package com.webproglabs.lab1.lab34.model;
import org.springframework.security.core.GrantedAuthority;
public enum UserRole implements GrantedAuthority {
ADMIN,
USER;
private static final String PREFIX = "ROLE_";
@Override
public String getAuthority() {
return PREFIX + this.name();
}
public static final class AsString {
public static final String ADMIN = PREFIX + "ADMIN";
public static final String USER = PREFIX + "USER";
}
}

View File

@ -8,4 +8,5 @@ import java.util.List;
public interface ProfileRepository extends JpaRepository<Profile, Long> { public interface ProfileRepository extends JpaRepository<Profile, Long> {
List<Profile> findByLoginLike(String login); List<Profile> findByLoginLike(String login);
Profile findOneByLoginIgnoreCase(String login);
} }

View File

@ -3,23 +3,28 @@ package com.webproglabs.lab1.lab34.services;
import com.webproglabs.lab1.lab34.model.Comment; import com.webproglabs.lab1.lab34.model.Comment;
import com.webproglabs.lab1.lab34.model.Post; import com.webproglabs.lab1.lab34.model.Post;
import com.webproglabs.lab1.lab34.model.Profile; import com.webproglabs.lab1.lab34.model.Profile;
import com.webproglabs.lab1.lab34.repository.CommentRepository; import com.webproglabs.lab1.lab34.model.UserRole;
import com.webproglabs.lab1.lab34.repository.ProfileRepository; import com.webproglabs.lab1.lab34.repository.ProfileRepository;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import javax.persistence.EntityNotFoundException; import javax.persistence.EntityNotFoundException;
import javax.transaction.Transactional; import javax.transaction.Transactional;
import java.util.List; import java.util.*;
import java.util.Optional;
@Service @Service
public class ProfileService { public class ProfileService implements UserDetailsService {
private final ProfileRepository profileRepository; private final ProfileRepository profileRepository;
private final PasswordEncoder passwordEncoder;
public ProfileService(ProfileRepository profileRepository) { public ProfileService(ProfileRepository profileRepository, PasswordEncoder passwordEncoder) {
this.profileRepository = profileRepository; this.profileRepository = profileRepository;
this.passwordEncoder = passwordEncoder;
} }
@Transactional @Transactional
@ -44,7 +49,40 @@ public class ProfileService {
if (!StringUtils.hasText(login) || !StringUtils.hasText(password)) { if (!StringUtils.hasText(login) || !StringUtils.hasText(password)) {
throw new IllegalArgumentException("User data is null or empty"); throw new IllegalArgumentException("User data is null or empty");
} }
final Profile user = new Profile(login, password, comments, posts); final Profile user = new Profile(login, password, comments, posts, UserRole.USER);
return profileRepository.save(user);
}
@Transactional
public Profile addUser(String login, String password, List<Comment> comments, List<Post> posts, UserRole role) {
if (!StringUtils.hasText(login) || !StringUtils.hasText(password)) {
throw new IllegalArgumentException("User data is null or empty");
}
final Profile user = new Profile(login, password, comments, posts, role);
return profileRepository.save(user);
}
@Transactional
public Profile createUser(String login, String password, String passwordConfirm) throws Exception {
if (findByLogin(login) != null) {
throw new Exception("User " + login + " already exists");
}
final Profile user = new Profile(login, passwordEncoder.encode(password), new ArrayList<>(), new ArrayList<>(), UserRole.USER);
if (!Objects.equals(password, passwordConfirm)) {
throw new Exception("Passwords not equals");
}
return profileRepository.save(user);
}
@Transactional
public Profile createUser(String login, String password, String passwordConfirm, UserRole role) throws Exception {
if (findByLogin(login) != null) {
throw new Exception("User " + login + " already exists");
}
final Profile user = new Profile(login, passwordEncoder.encode(password), new ArrayList<>(), new ArrayList<>(), role);
if (!Objects.equals(password, passwordConfirm)) {
throw new Exception("Passwords not equals");
}
return profileRepository.save(user); return profileRepository.save(user);
} }
@ -70,4 +108,17 @@ public class ProfileService {
public void deleteAllUsers() { public void deleteAllUsers() {
profileRepository.deleteAll(); profileRepository.deleteAll();
} }
public Profile findByLogin(String login) {
return profileRepository.findOneByLoginIgnoreCase(login);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
final Profile userEntity = findByLogin(username);
if (userEntity == null) {
throw new UsernameNotFoundException(username);
}
return new org.springframework.security.core.userdetails.User(
userEntity.getLogin(), userEntity.getPassword(), Collections.singleton(userEntity.getRole()));
}
} }

View File

@ -9,3 +9,5 @@ spring.jpa.hibernate.ddl-auto=update
spring.h2.console.enabled=true spring.h2.console.enabled=true
spring.h2.console.settings.trace=false spring.h2.console.settings.trace=false
spring.h2.console.settings.web-allow-others=false spring.h2.console.settings.web-allow-others=false
# Security
spring.security.user.password=user

View File

@ -1,7 +1,9 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="ru" <html lang="ru"
xmlns:th="http://www.thymeleaf.org" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"> xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
>
<head> <head>
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<title>Лабораторная работа 5</title> <title>Лабораторная работа 5</title>
@ -21,8 +23,11 @@
</div> </div>
<div> <div>
<p class='h4 text-center'> <p class='h4 text-center'>
<a href="/profiles" class="text-decoration-none m-3">Профили</a> <a sec:authorize="hasRole('ROLE_ADMIN')" href="/profiles" class="text-decoration-none m-3">Профили</a>
<a href="/feed" class="text-decoration-none m-3">Лента</a> <a sec:authorize="isAuthenticated()" href="/feed/" class="text-decoration-none m-3">Лента</a>
<a sec:authorize="isAuthenticated()" href="/logout" class="text-decoration-none m-3">
Выход
</a>
</p> </p>
</div> </div>

View File

@ -8,19 +8,19 @@
<div layout:fragment="content"> <div layout:fragment="content">
<div class="text-center"> <div class="text-center">
<div class="dropdown text-center mx-auto w-25 "> <!-- <div class="dropdown text-center mx-auto w-25 ">-->
<div class="text-end mt-3"> <!-- <div class="text-end mt-3">-->
<button class="btn btn-secondary dropdown-toggle ms-3 mb-3 " type="button" data-bs-toggle="dropdown" aria-expanded="false"> <!-- <button class="btn btn-secondary dropdown-toggle ms-3 mb-3 " type="button" data-bs-toggle="dropdown" aria-expanded="false">-->
Выбор пользователя <!-- Выбор пользователя-->
</button> <!-- </button>-->
<ul class="dropdown-menu " > <!-- <ul class="dropdown-menu " >-->
<li th:each="profile: ${profiles}"> <!-- <li th:each="profile: ${profiles}">-->
<a class="dropdown-item" th:href="@{/feed/{id}(id=${profile.id})}" th:text="${profile.login}"> <!-- <a class="dropdown-item" th:href="@{/feed/{id}(id=${profile.id})}" th:text="${profile.login}">-->
</a> <!-- </a>-->
</li> <!-- </li>-->
</ul> <!-- </ul>-->
</div> <!-- </div>-->
</div> <!-- </div>-->
<div layout:fragment="contentFeed"></div> <div layout:fragment="contentFeed"></div>

View File

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html lang="en"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{default}">
<body>
<div class="container" layout:fragment="content">
<div th:if="${param.error}" class="alert alert-danger margin-bottom">
Пользователь не найден или пароль указан не верно
</div>
<div th:if="${param.logout}" class="alert alert-success margin-bottom">
Выход успешно произведен
</div>
<div th:if="${param.created}" class="alert alert-success margin-bottom">
Пользователь '<span th:text="${param.created}"></span>' успешно создан
</div>
<form th:action="@{/login}" method="post" class="container-padding">
<div class="mb-3">
<input type="text" name="username" id="username" class="form-control"
placeholder="Логин" required="true" autofocus="true"/>
</div>
<div class="mb-3">
<input type="password" name="password" id="password" class="form-control"
placeholder="Пароль" required="true"/>
</div>
<button type="submit" class="btn btn-success button-fixed">Войти</button>
<a class="btn btn-primary button-fixed" href="/signup">Регистрация</a>
</form>
</div>
</body>
</html>

View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{default}">
<body>
<div class="container container-padding" layout:fragment="content">
<div th:if="${errors}" th:text="${errors}" class="margin-bottom alert alert-danger"></div>
<form action="#" th:action="@{/signup}" th:object="${userDto}" method="post">
<div class="mb-3">
<input type="text" class="form-control" th:field="${userDto.login}"
placeholder="Логин" required="true" autofocus="true" maxlength="64"/>
</div>
<div class="mb-3">
<input type="password" class="form-control" th:field="${userDto.password}"
placeholder="Пароль" required="true" minlength="6" maxlength="64"/>
</div>
<div class="mb-3">
<input type="password" class="form-control" th:field="${userDto.passwordConfirm}"
placeholder="Пароль (подтверждение)" required="true" minlength="6" maxlength="64"/>
</div>
<div class="mb-3">
<button type="submit" class="btn btn-success button-fixed">Создать</button>
<a class="btn btn-primary button-fixed" href="/login">Назад</a>
</div>
</form>
</div>
</body>
</html>