Add custom error page, add profile settings page '/profile/'

This commit is contained in:
Dmitry Isaenko 2023-10-06 22:07:15 +03:00
parent 147f590410
commit bb6059d63e
28 changed files with 401 additions and 35 deletions

View file

@ -1,8 +1,10 @@
package ru.redrise.marinesco;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
//@PreAuthorize("hasRole('USER')")
@Controller
public class RootController {

View file

@ -7,6 +7,7 @@ import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
import lombok.extern.slf4j.Slf4j;
import ru.redrise.marinesco.data.RolesRepository;
@ -25,13 +26,14 @@ public class ShinyApplicationRunner {
}
@Bean
public ApplicationRunner appRunner() {
public ApplicationRunner appRunner(PasswordEncoder encoder) {
return args -> {
if (isFirstRun()) {
log.info("Application first run");
setRoles();
setAdmin(args);
setAdmin(args, encoder);
} else
log.info("NOT FIRST APPLICATION RUN; DB Already set up");
log.info("Regular run");
};
}
@ -45,7 +47,7 @@ public class ShinyApplicationRunner {
new UserRole(null, "User", UserRole.Type.USER)));
}
private void setAdmin(ApplicationArguments args) {
private void setAdmin(ApplicationArguments args, PasswordEncoder encoder) {
List<String> login = args.getOptionValues("admin_login");
List<String> password = args.getOptionValues("admin_password");
@ -53,10 +55,11 @@ public class ShinyApplicationRunner {
if (login == null || login.size() == 0 || password == null || password.size() == 0) {
log.warn("No administrator credentials provided, using defaults:\n * Login: root\n * Password: root\n Expected: --admin_login LOGIN --admin_password PASSWORD "); // TODO: Move into properties i18n
var adminUser = new User("root", "root", "SuperAdmin", adminRoleOnlyAthority);
var adminUser = new User("root", encoder.encode("root"), "SuperAdmin", adminRoleOnlyAthority);
users.save(adminUser);
return;
}
users.save(new User(login.get(0), password.get(0), "SuperAdmin", adminRoleOnlyAthority));
log.info("SuperAdmin role created\n * Login: {}", login.get(0));
users.save(new User(login.get(0), encoder.encode(password.get(0)), "SuperAdmin", adminRoleOnlyAthority));
}
}

View file

@ -1,11 +1,12 @@
package ru.redrise.marinesco;
import java.util.ArrayList;
import java.util.List;
import org.springframework.security.core.userdetails.UserDetails;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@ -14,14 +15,12 @@ import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import ru.redrise.marinesco.security.UserRole;
@Data
@Entity
@Table(name = "\"USER\"")
@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
@RequiredArgsConstructor
public class User implements UserDetails{
private static final long serialVersionUID = 1L;
@ -31,12 +30,19 @@ public class User implements UserDetails{
private Long id;
private final String username;
private final String password;
private final String displayname;
private String password;
private String displayname;
@ManyToMany
@ManyToMany(cascade = CascadeType.REMOVE, fetch = FetchType.EAGER)
private final List<UserRole> authorities;
public User(String username, String password, String displayname, List<UserRole> authorities){
this.username = username;
this.password = password;
this.displayname = displayname;
this.authorities = authorities;
}
@Override
public boolean isAccountNonExpired() {
return true;

View file

@ -0,0 +1,23 @@
package ru.redrise.marinesco;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class UserSettingsForm {
@NotNull
@NotEmpty(message = "Display name could not be blank")
public String displayname;
public String newPassword;
public boolean isNewPasswordSet(){
return ! newPassword.isBlank();
}
public boolean isNewPasswordValid(){
final int newPasswordLength = newPassword.length();
return newPasswordLength > 8 && newPasswordLength < 32;
}
}

View file

@ -2,10 +2,14 @@ package ru.redrise.marinesco.security;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
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 jakarta.validation.Valid;
import ru.redrise.marinesco.data.RolesRepository;
import ru.redrise.marinesco.data.UserRepository;
@ -16,20 +20,33 @@ public class RegistrationController {
private RolesRepository rolesRepo;
private PasswordEncoder passwordEncoder;
public RegistrationController(UserRepository userRepo, RolesRepository rolesRepo, PasswordEncoder passwordEncoder){
public RegistrationController(UserRepository userRepo, RolesRepository rolesRepo, PasswordEncoder passwordEncoder) {
this.userRepo = userRepo;
this.rolesRepo = rolesRepo;
this.passwordEncoder = passwordEncoder;
}
@ModelAttribute(name = "registrationForm")
public RegistrationForm form() {
return new RegistrationForm();
}
@GetMapping
public String registerForm(){
public String registerForm() {
return "registration";
}
@PostMapping
public String postMethodName(RegistrationForm registrationForm) {
userRepo.save(registrationForm.toUser(passwordEncoder, rolesRepo));
public String postMethodName(@Valid RegistrationForm registerForm, Errors errors, Model model) {
if (registerForm.isPasswordsNotEqual()){
model.addAttribute("passwordsMismatch", "Passwords must be the same.");
return "registration";
}
if (errors.hasErrors()) {
return "registration";
}
userRepo.save(registerForm.toUser(passwordEncoder, rolesRepo));
return "redirect:/login";
}
}

View file

@ -2,20 +2,28 @@ package ru.redrise.marinesco.security;
import org.springframework.security.crypto.password.PasswordEncoder;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data;
import ru.redrise.marinesco.User;
import ru.redrise.marinesco.data.RolesRepository;
@Data
public class RegistrationForm {
@NotNull
@Size(min=3, max=32, message="Username must be at least 3 characters long. Should not exceed 32 characters.")
private String username;
@NotNull
@Size(min=8, max = 32, message="Password must be at least 8 characters long. Should not exceed 32 characters.")
private String password;
private String passwordConfirm;
@NotNull
private String fullname;
@NotNull
@NotEmpty(message = "Display name could not be blank")
private String displayname;
public User toUser(PasswordEncoder passwordEncoder, RolesRepository rolesRepo){
@ -25,4 +33,7 @@ public class RegistrationForm {
displayname,
rolesRepo.findByType(UserRole.Type.USER));
}
public boolean isPasswordsNotEqual(){
return ! password.equals(passwordConfirm);
}
}

View file

@ -45,19 +45,25 @@ public class SecurityConfig {
public SecurityFilterChain filterChain(HttpSecurity http, MvcRequestMatcher.Builder mvc) throws Exception {
return http
.authorizeHttpRequests(autorize -> autorize
.requestMatchers(mvc.pattern("/favicon.ico")).permitAll()
.requestMatchers(mvc.pattern("/jquery.js")).permitAll()
.requestMatchers(mvc.pattern("/styles/**")).permitAll()
.requestMatchers(mvc.pattern("/images/*")).permitAll()
.requestMatchers(mvc.pattern("/register")).permitAll()
.requestMatchers(mvc.pattern("/login")).permitAll()
.requestMatchers(mvc.pattern("/error")).permitAll()
.requestMatchers(mvc.pattern("/")).hasAnyRole("ADMIN", "USER")
.requestMatchers(mvc.pattern("/profile/**")).hasAnyRole("ADMIN", "USER")
.requestMatchers(PathRequest.toH2Console()).permitAll()
//.requestMatchers(mvc.pattern("/design/**")).hasRole("USER")
.anyRequest().denyAll())
//.anyRequest().permitAll())
.formLogin(formLoginConfigurer -> formLoginConfigurer
.loginPage("/login")
.loginProcessingUrl("/auth")
//.loginProcessingUrl("/auth")
.usernameParameter("login")
.passwordParameter("pwd")
//.defaultSuccessUrl("/", true)
.defaultSuccessUrl("/")
)
// .formLogin(Customizer.withDefaults())
//.oauth2Login(c -> c.loginPage("/login"))

View file

@ -0,0 +1,82 @@
package ru.redrise.marinesco.security;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
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 jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import ru.redrise.marinesco.User;
import ru.redrise.marinesco.UserSettingsForm;
import ru.redrise.marinesco.data.UserRepository;
@Slf4j
@Controller
@RequestMapping("/profile")
public class UserSettingsController {
private final UserRepository userRepo;
private final PasswordEncoder passwordEncoder;
public UserSettingsController(UserRepository userRepo, PasswordEncoder passwordEncoder){
this.userRepo = userRepo;
this.passwordEncoder = passwordEncoder;
}
@ModelAttribute
public void addMisc(Model model, @AuthenticationPrincipal User user){
final String displayName = user.getDisplayname();
model.addAttribute("header_text", "Welcome " + displayName);
UserSettingsForm form = new UserSettingsForm();
form.setDisplayname(displayName);
model.addAttribute( "userSettingsForm", form);
}
@ModelAttribute
public UserSettingsForm addSettingsForm(){
return new UserSettingsForm();
}
@GetMapping
public String getPage(){
return "redirect:/profile/settings";
}
@GetMapping("/settings")
public String getSettingsFirstPage(){
return "user_settings";
}
@PostMapping("/settings")
public String getSettingsPage(@Valid UserSettingsForm userSettingsForm,
Errors errors,
@AuthenticationPrincipal User user,
Model model){
if (errors.hasErrors())
return "user_settings";
if (! user.getDisplayname().equals(userSettingsForm.getDisplayname()))
user.setDisplayname(userSettingsForm.getDisplayname());
if (userSettingsForm.isNewPasswordSet()){
if (userSettingsForm.isNewPasswordValid()){
user.setPassword(passwordEncoder.encode(userSettingsForm.getNewPassword()));
}
else{
model.addAttribute("password_incorrect", "Password must be at least 8 characters long. Should not exceed 32 characters.");
return "user_settings";
}
}
log.info("{} {}", userSettingsForm.getDisplayname(), userSettingsForm.getNewPassword());
userRepo.save(user);
return "user_settings";
}
}

View file

@ -0,0 +1,24 @@
package ru.redrise.marinesco.web;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.http.HttpServletRequest;
@Controller
public class HandyErrorController implements ErrorController{
@ModelAttribute(name = "code")
public String addMisc(HttpServletRequest request){
return request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE).toString();
}
@RequestMapping("/error")
public String handleError(){
return "error";
}
}

2
src/main/resources/static/jquery.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style type=text/css>
html {
height: 100%;
}
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #212121;
color: #cfcfcf;
font-family: Terminus;
}
.error {
border-width: 3px;
border-style: double;
padding: 8px;
margin: auto;
}
.bli {
padding: 3px;
background-color: #D00000;
animation: blinker 2.5s ease infinite;
}
@keyframes blinker {
50% {
opacity: 0.0;
}
}
</style>
<title th:text="${code}">title</title>
</head>
<body>
<div class='error'>
ОШИБКА: <span class='bli' th:text="${code}"></span>
</div>
</body>
</html>

View file

@ -16,6 +16,7 @@
<br /><a href="/h2">H2</a>
<form class="form-signin" method="post" action="/login">
<h2 class="form-signin-heading">Please sign in</h2>
<br /><span class="validationError" th:if="${param.error}">Unable to login. Check your username and password.</span>
<p>
<label for="username" class="sr-only">Username</label>
<input type="text" id="username" name="login" class="form-control" placeholder="Username" required autofocus>

View file

@ -5,20 +5,34 @@
<title>Marinesco - registration form</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="stylesheet" th:href="@{/styles/styles.css}" />
<script src="@{/jquery.js}"></script>
</head>
<body>
<h1>Register</h1>
<img th:src="@{/images/logo.svg}" />
<form method="POST" th:action="@{/register}" id="registerForm">
<form class="regForm" method="POST" th:action="@{/register}" th:object="${registrationForm}">
<span class="validationError" th:if="${#fields.hasErrors('username')}" th:errors="*{username}">Error</span>
<br />
<label for="username">Username: </label>
<input type="text" name="login" /><br />
<input type="text" name="username" id="username"/><br />
<span class="validationError" th:if="${#fields.hasErrors('password')}" th:errors="*{password}">Error</span>
<br />
<label for="password">Password: </label>
<input type="password" name="pwd" /><br />
<input type="password" name="password" id="password"/><br />
<span class="validationError" th:if="${passwordsMismatch} != null" th:text="${passwordsMismatch}">false</span>
<br />
<label for="confirm">Confirm password: </label>
<input type="password" name="confirm" /><br />
<input type="password" name="passwordConfirm" id="passwordConfirm" /><br />
<span class="validationError" th:if="${#fields.hasErrors('displayname')}" th:errors="*{displayname}">Error</span>
<br />
<label for="displayname">Displayed name: </label>
<input type="text" name="fullname" /><br />
<input type="text" name="displayname" id="displayname"/><br />
<input type="submit" value="Register" />
</form>
</body>

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Marinesco - Profile settings</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="stylesheet" th:href="@{/styles/styles.css}" />
</head>
<body>
<h1 th:text="${header_text}">welcome</h1>
<form method="POST" th:action="@{/profile/settings}" th:object="${userSettingsForm}"> <!-- -->
<span class="validationError" th:if="${#fields.hasErrors('displayname')}" th:errors="*{displayname}">Error</span>
<br />
<label for="displayname">Displayed name: </label>
<input type="text" name="displayname" id="displayname" th:value="${userSettingsForm.displayname}" /><br />
<span class="validationError" th:if="${password_incorrect} != null" th:text="${password_incorrect}">false</span>
<br />
<label for="password">New password: </label>
<input type="password" name="newPassword" id="newPassword" /><br />
<input type="submit" value="Save Changes" />
</form>
</body>
</html>

2
target/classes/static/jquery.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style type=text/css>
html {
height: 100%;
}
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #212121;
color: #cfcfcf;
font-family: Terminus;
}
.error {
border-width: 3px;
border-style: double;
padding: 8px;
margin: auto;
}
.bli {
padding: 3px;
background-color: #D00000;
animation: blinker 2.5s ease infinite;
}
@keyframes blinker {
50% {
opacity: 0.0;
}
}
</style>
<title th:text="${code}">title</title>
</head>
<body>
<div class='error'>
ОШИБКА: <span class='bli' th:text="${code}"></span>
</div>
</body>
</html>

View file

@ -16,6 +16,7 @@
<br /><a href="/h2">H2</a>
<form class="form-signin" method="post" action="/login">
<h2 class="form-signin-heading">Please sign in</h2>
<br /><span class="validationError" th:if="${param.error}">Unable to login. Check your username and password.</span>
<p>
<label for="username" class="sr-only">Username</label>
<input type="text" id="username" name="login" class="form-control" placeholder="Username" required autofocus>

View file

@ -5,20 +5,34 @@
<title>Marinesco - registration form</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="stylesheet" th:href="@{/styles/styles.css}" />
<script src="@{/jquery.js}"></script>
</head>
<body>
<h1>Register</h1>
<img th:src="@{/images/logo.svg}" />
<form method="POST" th:action="@{/register}" id="registerForm">
<form class="regForm" method="POST" th:action="@{/register}" th:object="${registrationForm}">
<span class="validationError" th:if="${#fields.hasErrors('username')}" th:errors="*{username}">Error</span>
<br />
<label for="username">Username: </label>
<input type="text" name="login" /><br />
<input type="text" name="username" id="username"/><br />
<span class="validationError" th:if="${#fields.hasErrors('password')}" th:errors="*{password}">Error</span>
<br />
<label for="password">Password: </label>
<input type="password" name="pwd" /><br />
<input type="password" name="password" id="password"/><br />
<span class="validationError" th:if="${passwordsMismatch} != null" th:text="${passwordsMismatch}">false</span>
<br />
<label for="confirm">Confirm password: </label>
<input type="password" name="confirm" /><br />
<input type="password" name="passwordConfirm" id="passwordConfirm" /><br />
<span class="validationError" th:if="${#fields.hasErrors('displayname')}" th:errors="*{displayname}">Error</span>
<br />
<label for="displayname">Displayed name: </label>
<input type="text" name="fullname" /><br />
<input type="text" name="displayname" id="displayname"/><br />
<input type="submit" value="Register" />
</form>
</body>

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Marinesco - Profile settings</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="stylesheet" th:href="@{/styles/styles.css}" />
</head>
<body>
<h1 th:text="${header_text}">welcome</h1>
<form method="POST" th:action="@{/profile/settings}" th:object="${userSettingsForm}"> <!-- -->
<span class="validationError" th:if="${#fields.hasErrors('displayname')}" th:errors="*{displayname}">Error</span>
<br />
<label for="displayname">Displayed name: </label>
<input type="text" name="displayname" id="displayname" th:value="${userSettingsForm.displayname}" /><br />
<span class="validationError" th:if="${password_incorrect} != null" th:text="${password_incorrect}">false</span>
<br />
<label for="password">New password: </label>
<input type="password" name="newPassword" id="newPassword" /><br />
<input type="submit" value="Save Changes" />
</form>
</body>
</html>