Merge pull request '11-projects' (#12) from 11-projects into master

Reviewed-on: #12
This commit is contained in:
romanov73 2025-02-15 14:12:42 +04:00
commit 5f111a9671
39 changed files with 1302 additions and 24 deletions

View File

@ -24,11 +24,13 @@ dependencies {
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-web' implementation group: 'org.springframework.boot', name: 'spring-boot-starter-web'
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-jetty' implementation group: 'org.springframework.boot', name: 'spring-boot-starter-jetty'
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-thymeleaf' implementation group: 'org.springframework.boot', name: 'spring-boot-starter-thymeleaf'
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-validation'
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa' implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa'
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-webflux' implementation group: 'org.springframework.boot', name: 'spring-boot-starter-security'
implementation group: 'org.json', name: 'json', version: '20220320' implementation group: 'org.json', name: 'json', version: '20220320'
implementation group: 'nz.net.ultraq.thymeleaf', name: 'thymeleaf-layout-dialect' implementation group: 'nz.net.ultraq.thymeleaf', name: 'thymeleaf-layout-dialect'
implementation group: 'org.thymeleaf.extras', name: 'thymeleaf-extras-springsecurity6'
implementation group: 'com.fasterxml.jackson.module', name: 'jackson-module-afterburner' implementation group: 'com.fasterxml.jackson.module', name: 'jackson-module-afterburner'
implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-hibernate5' implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-hibernate5'
implementation group: 'com.h2database', name:'h2' implementation group: 'com.h2database', name:'h2'

View File

@ -2,11 +2,28 @@ package ru.ulstu.fc;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.event.EventListener;
import ru.ulstu.fc.user.service.UserService;
@SpringBootApplication @SpringBootApplication
@EnableConfigurationProperties
public class FuzzyControllerApplication { public class FuzzyControllerApplication {
private final UserService userService;
public FuzzyControllerApplication(UserService userService) {
this.userService = userService;
}
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(FuzzyControllerApplication.class, args); SpringApplication.run(FuzzyControllerApplication.class, args);
} }
@EventListener(ApplicationReadyEvent.class)
public void doSomethingAfterStartup() {
System.out.println("hello world, I have just started up");
userService.initDefaultAdmin();
}
} }

View File

@ -0,0 +1,12 @@
package ru.ulstu.fc.config;
public class Constants {
public static final int MIN_PASSWORD_LENGTH = 6;
public static final String LOGIN_REGEX = "^[_'.@A-Za-z0-9-]*$";
public static final String COOKIES_NAME = "JSESSIONID";
public static final String LOGOUT_URL = "/login?logout";
public static final String SESSION_ID_ATTR = "sessionId";
public static final int SESSION_TIMEOUT_SECONDS = 30 * 60;
}

View File

@ -1,9 +1,3 @@
/*
* Copyright (C) 2021 Anton Romanov - All Rights Reserved
* You may use, distribute and modify this code, please write to: romanov73@gmail.com.
*
*/
package ru.ulstu.fc.config; package ru.ulstu.fc.config;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -11,12 +5,19 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver; import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.CookieLocaleResolver; import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
@Configuration @Configuration
public class MvcConfiguration implements WebMvcConfigurer { public class MvcConfiguration implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login");
registry.addViewController("/loginError");
registry.addViewController("/index");
}
@Override @Override
public void addResourceHandlers(ResourceHandlerRegistry registry) { public void addResourceHandlers(ResourceHandlerRegistry registry) {

View File

@ -0,0 +1,13 @@
package ru.ulstu.fc.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
public class PasswordEncoderConfiguration {
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}

View File

@ -0,0 +1,52 @@
package ru.ulstu.fc.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import ru.ulstu.fc.user.model.UserRoleConstants;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration {
private final Logger log = LoggerFactory.getLogger(SecurityConfiguration.class);
private final String[] permittedUrls = new String[]{
"/login", "/index",
"/public/**", "/organizers", "/webjars/**",
"/h2-console/*", "/h2-console",
"/css/**", "/js/**", "/img/**",
"/templates/**", "/webjars/**"};
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
log.debug("Security enabled");
http
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable))
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth ->
auth.requestMatchers("/").permitAll()
.requestMatchers(permittedUrls).permitAll()
.requestMatchers("/swagger-ui.html").hasAuthority(UserRoleConstants.ADMIN)
.anyRequest().authenticated())
.formLogin(form ->
form.loginPage("/login")
.failureUrl("/loginError")
.permitAll())
.logout(logout ->
logout
.logoutSuccessUrl(Constants.LOGOUT_URL)
.invalidateHttpSession(false)
.clearAuthentication(true)
.deleteCookies(Constants.COOKIES_NAME)
.permitAll());
return http.build();
}
}

View File

@ -1,13 +0,0 @@
package ru.ulstu.fc.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class WebClientConfiguration {
@Bean
public WebClient webClient(WebClient.Builder webClientBuilder) {
return webClientBuilder.build();
}
}

View File

@ -0,0 +1,84 @@
package ru.ulstu.fc.core.model;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.Version;
import jakarta.validation.constraints.NotNull;
import java.io.Serializable;
@MappedSuperclass
public abstract class BaseEntity implements Serializable, Comparable<BaseEntity> {
@Id
@GeneratedValue(strategy = GenerationType.TABLE)
private Integer id;
@Version
private Integer version;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getVersion() {
return version;
}
public void setVersion(Integer version) {
this.version = version;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (!getClass().isAssignableFrom(obj.getClass())) {
return false;
}
BaseEntity other = (BaseEntity) obj;
if (id == null) {
if (other.id != null) {
return false;
}
} else if (!id.equals(other.id)) {
return false;
}
return true;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (id == null ? 0 : id.hashCode());
return result;
}
@Override
public String toString() {
return getClass().getSimpleName() + "{" +
"id=" + id +
", version=" + version +
'}';
}
@Override
public int compareTo(@NotNull BaseEntity o) {
return id != null ? id.compareTo(o.getId()) : -1;
}
public void reset() {
this.id = null;
this.version = null;
}
}

View File

@ -0,0 +1,119 @@
package ru.ulstu.fc.core.model;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import java.io.Serializable;
import java.util.Optional;
public class OffsetablePageRequest implements Pageable, Serializable {
private final int offset;
private final int count;
private final Sort sort;
public OffsetablePageRequest(int page, long pageSize) {
this(pageSize * page, pageSize, Sort.by("id"));
}
public OffsetablePageRequest(long offset, long count, Sort sort) {
if (offset < 0) {
throw new IllegalArgumentException("Offset value must not be less than zero!");
}
if (count < 1) {
throw new IllegalArgumentException("Count value must not be less than one!");
}
this.offset = (int) offset;
this.count = (int) count;
this.sort = sort;
}
@Override
public Sort getSort() {
return sort;
}
@Override
public Sort getSortOr(Sort sort) {
return Pageable.super.getSortOr(sort);
}
@Override
public int getPageSize() {
return count;
}
@Override
public boolean isPaged() {
return Pageable.super.isPaged();
}
@Override
public boolean isUnpaged() {
return Pageable.super.isUnpaged();
}
@Override
public int getPageNumber() {
return offset / count;
}
@Override
public long getOffset() {
return offset;
}
@Override
public boolean hasPrevious() {
return offset > 0;
}
@Override
public Optional<Pageable> toOptional() {
return Pageable.super.toOptional();
}
@Override
public Pageable next() {
return new OffsetablePageRequest(getOffset() + getPageSize(), getPageSize(), getSort());
}
@Override
public Pageable previousOrFirst() {
return hasPrevious() ? previous() : first();
}
public Pageable previous() {
return getOffset() == 0 ? this : new OffsetablePageRequest(getOffset() - getPageSize(), getPageSize(), getSort());
}
@Override
public Pageable first() {
return new OffsetablePageRequest(0, getPageSize(), getSort());
}
@Override
public Pageable withPage(int pageNumber) {
return null;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
final OffsetablePageRequest other = (OffsetablePageRequest) obj;
return this.offset == other.offset && this.count == other.count;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + offset;
result = prime * result + count;
return result;
}
}

View File

@ -0,0 +1,55 @@
package ru.ulstu.fc.project.controller;
import io.swagger.v3.oas.annotations.Hidden;
import org.springframework.security.access.annotation.Secured;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import ru.ulstu.fc.project.model.Project;
import ru.ulstu.fc.project.model.ProjectForm;
import ru.ulstu.fc.project.service.ProjectService;
import ru.ulstu.fc.user.model.UserRoleConstants;
@Controller
@Hidden
@RequestMapping("project")
@Secured({UserRoleConstants.ADMIN})
public class ProjectController {
private final ProjectService projectService;
public ProjectController(ProjectService projectService) {
this.projectService = projectService;
}
@GetMapping("list")
public String getProjects(Model model) {
model.addAttribute("projects", projectService.getCurrentUserProjects());
return "project/list";
}
@GetMapping("/edit/{projectId}")
public String edit(@PathVariable(value = "projectId") Integer id, Model model) {
model.addAttribute("project",
new ProjectForm((id != null && id != 0)
? projectService.getById(id)
: new Project()));
return "project/edit";
}
@PostMapping(value = "save", params = "save")
public String save(ProjectForm projectForm, Model model) {
model.addAttribute("project", projectService.save(projectForm));
return "redirect:/project/list";
}
@PostMapping(value = "save", params = "delete")
public String delete(ProjectForm projectForm) {
if (projectForm != null && projectForm.getId() != null) {
projectService.delete(projectForm);
}
return "redirect:/project/list";
}
}

View File

@ -0,0 +1,41 @@
package ru.ulstu.fc.project.controller;
import org.springframework.security.access.annotation.Secured;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import ru.ulstu.fc.project.model.Project;
import ru.ulstu.fc.project.model.ProjectForm;
import ru.ulstu.fc.project.service.ProjectService;
import ru.ulstu.fc.user.model.UserRoleConstants;
import java.util.List;
@RestController
@RequestMapping("projectRest")
@Secured({UserRoleConstants.ADMIN})
public class ProjectRestController {
private final ProjectService projectService;
public ProjectRestController(ProjectService projectService) {
this.projectService = projectService;
}
@GetMapping("list")
public List<Project> getProjects() {
return projectService.getCurrentUserProjects();
}
@PostMapping("save")
public Project save(Project project) {
return projectService.save(project);
}
@DeleteMapping("delete")
public String delete(ProjectForm projectForm) {
projectService.delete(projectForm);
return "redirect:/list";
}
}

View File

@ -0,0 +1,52 @@
package ru.ulstu.fc.project.model;
import jakarta.persistence.Entity;
import jakarta.persistence.ManyToOne;
import jakarta.validation.constraints.NotEmpty;
import ru.ulstu.fc.core.model.BaseEntity;
import ru.ulstu.fc.user.model.User;
import java.util.Date;
@Entity
public class Project extends BaseEntity {
@NotEmpty(message = "Текст новости не может быть пустым")
private String name;
private Date createDate = new Date();
@ManyToOne
private User user;
public Project() {
}
public Project(ProjectForm projectForm) {
if (projectForm.getId() != null) {
setId(projectForm.getId());
}
this.name = projectForm.getName();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getCreateDate() {
return createDate;
}
public void setCreateDate(Date createDate) {
this.createDate = createDate;
}
public User getUser() {
return user;
}
public void setUser(User user) {
this.user = user;
}
}

View File

@ -0,0 +1,38 @@
package ru.ulstu.fc.project.model;
import java.util.Date;
public class ProjectForm {
private Integer id;
private String name;
private Date createDate;
public ProjectForm() {
}
public ProjectForm(Project project) {
this.id = project.getId();
this.name = project.getName();
this.createDate = project.getCreateDate();
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Date getCreateDate() {
return createDate;
}
}

View File

@ -0,0 +1,14 @@
package ru.ulstu.fc.project.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import ru.ulstu.fc.project.model.Project;
import java.util.List;
@Repository
public interface ProjectRepository extends JpaRepository<Project, Integer> {
List<Project> findAllByUserId(@Param("userId") Integer userId);
}

View File

@ -0,0 +1,64 @@
package ru.ulstu.fc.project.service;
import org.springframework.stereotype.Service;
import ru.ulstu.fc.project.model.Project;
import ru.ulstu.fc.project.model.ProjectForm;
import ru.ulstu.fc.project.repository.ProjectRepository;
import ru.ulstu.fc.user.model.User;
import ru.ulstu.fc.user.service.UserService;
import java.util.List;
@Service
public class ProjectService {
private final ProjectRepository projectRepository;
private final UserService userService;
public ProjectService(ProjectRepository projectRepository,
UserService userService) {
this.projectRepository = projectRepository;
this.userService = userService;
}
public List<Project> getCurrentUserProjects() {
return projectRepository.findAllByUserId(userService.getCurrentUser().getId());
}
public Project getById(Integer id) {
Project project = projectRepository
.findById(id)
.orElseThrow(() -> new RuntimeException("Project not found by id"));
checkUserProjectWithThrow(project, userService.getCurrentUser());
return project;
}
public Project save(ProjectForm projectForm) {
return save(new Project(projectForm));
}
public Project save(Project projectToSave) {
User currentUser = userService.getCurrentUser();
if (projectToSave.getId() == null) {
projectToSave.setUser(currentUser);
return projectRepository.save(projectToSave);
}
Project dbProject = getById(projectToSave.getId());
dbProject.setName(projectToSave.getName());
return projectRepository.save(dbProject);
}
public void delete(ProjectForm projectForm) {
getById(projectForm.getId());
projectRepository.deleteById(projectForm.getId());
}
private void checkUserProjectWithThrow(Project project, User currentUser) {
if (!isUserProject(project, currentUser)) {
throw new RuntimeException("User can not get access to project");
}
}
private boolean isUserProject(Project project, User currentUser) {
return (currentUser.equals(project.getUser()));
}
}

View File

@ -11,7 +11,7 @@ import ru.ulstu.fc.rule.service.FuzzyInferenceService;
import java.util.List; import java.util.List;
@RestController @RestController
@RequestMapping("rest") @RequestMapping("inferenceRest")
public class InferenceRestController { public class InferenceRestController {
private final FuzzyInferenceService fuzzyInferenceService; private final FuzzyInferenceService fuzzyInferenceService;

View File

@ -0,0 +1,72 @@
package ru.ulstu.fc.user.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import ru.ulstu.fc.config.Constants;
import ru.ulstu.fc.core.model.BaseEntity;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "is_users")
public class User extends BaseEntity {
@NotNull
@Pattern(regexp = Constants.LOGIN_REGEX)
@Size(min = 1, max = 50)
@Column(length = 50, unique = true, nullable = false)
private String login;
@NotNull
@Size(min = 60, max = 60)
@Column(name = "password_hash", length = 60, nullable = false)
private String password;
@ManyToMany
@JoinTable(
name = "is_user_role",
joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},
inverseJoinColumns = {@JoinColumn(name = "user_role_name", referencedColumnName = "name")})
private Set<UserRole> roles;
public User() {
roles = new HashSet<>();
}
public User(String login, String password, Set<UserRole> roles) {
this.login = login;
this.password = password;
this.roles = roles;
}
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login.toLowerCase();
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Set<UserRole> getRoles() {
return roles;
}
public void setRoles(Set<UserRole> roles) {
this.roles = roles;
}
}

View File

@ -0,0 +1,7 @@
package ru.ulstu.fc.user.model;
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}

View File

@ -0,0 +1,50 @@
package ru.ulstu.fc.user.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
@Entity
@Table(name = "is_user_roles")
public class UserRole {
@Id
@NotNull
@Size(max = 50)
@Column(length = 50, nullable = false)
private String name;
public UserRole() {
}
public UserRole(String name) {
this.name = name;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
UserRole role = (UserRole) o;
return !(name != null ? !name.equals(role.name) : role.name != null);
}
public void setName(String name) {
this.name = name;
}
@Override
public int hashCode() {
return name != null ? name.hashCode() : 0;
}
}

View File

@ -0,0 +1,6 @@
package ru.ulstu.fc.user.model;
public class UserRoleConstants {
public static final String ADMIN = "ROLE_ADMIN";
public static final String USER = "ROLE_USER";
}

View File

@ -0,0 +1,105 @@
package ru.ulstu.fc.user.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import jakarta.persistence.Temporal;
import jakarta.persistence.TemporalType;
import jakarta.validation.constraints.NotNull;
import ru.ulstu.fc.core.model.BaseEntity;
import java.util.Date;
@Entity
@Table(name = "is_user_sessions")
public class UserSession extends BaseEntity {
@NotNull
@Column(name = "session_id", nullable = false, unique = true)
private String sessionId;
@NotNull
@Column(name = "ip_address", nullable = false)
private String ipAddress;
@NotNull
@Column(nullable = false)
private String host;
@NotNull
@Column(name = "login_time", nullable = false)
@Temporal(TemporalType.TIMESTAMP)
private Date loginTime;
@Column(name = "logout_time")
@Temporal(TemporalType.TIMESTAMP)
private Date logoutTime;
@ManyToOne(optional = false)
@JoinColumn(name = "user_id")
private User user;
public UserSession() {
}
public UserSession(String sessionId, String ipAddress, String host, User user) {
this.sessionId = sessionId;
this.ipAddress = ipAddress;
this.host = host;
this.loginTime = new Date();
this.user = user;
}
public String getSessionId() {
return sessionId;
}
public String getIpAddress() {
return ipAddress;
}
public String getHost() {
return host;
}
public Date getLoginTime() {
return loginTime;
}
public Date getLogoutTime() {
return logoutTime;
}
public User getUser() {
return user;
}
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
public void setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
}
public void setHost(String host) {
this.host = host;
}
public void setLoginTime(Date loginTime) {
this.loginTime = loginTime;
}
public void setLogoutTime(Date logoutTime) {
this.logoutTime = logoutTime;
}
public void setUser(User user) {
this.user = user;
}
public void close() {
this.logoutTime = new Date();
}
}

View File

@ -0,0 +1,15 @@
package ru.ulstu.fc.user.repository;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import ru.ulstu.fc.user.model.User;
public interface UserRepository extends JpaRepository<User, Integer> {
User findOneByLoginIgnoreCase(String login);
@EntityGraph(attributePaths = "roles")
User findOneWithRolesById(int id);
@EntityGraph(attributePaths = "roles")
User findOneWithRolesByLogin(String login);
}

View File

@ -0,0 +1,7 @@
package ru.ulstu.fc.user.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import ru.ulstu.fc.user.model.UserRole;
public interface UserRoleRepository extends JpaRepository<UserRole, String> {
}

View File

@ -0,0 +1,13 @@
package ru.ulstu.fc.user.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import ru.ulstu.fc.user.model.UserSession;
import java.util.Date;
import java.util.List;
public interface UserSessionRepository extends JpaRepository<UserSession, Integer> {
UserSession findOneBySessionId(String sessionId);
List<UserSession> findAllByLogoutTimeIsNullAndLoginTimeBefore(Date date);
}

View File

@ -0,0 +1,22 @@
package ru.ulstu.fc.user.service;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.util.StringUtils;
public final class IpAddressResolver {
private static final String CLIENT_IP_HEADER = "Client-IP";
private static final String FORWARDED_FOR_HEADER = "X-Forwarded-For";
public static String getRemoteAddr(HttpServletRequest request) {
String headerClientIp = request.getHeader("");
String headerXForwardedFor = request.getHeader(HttpServletRequest.FORM_AUTH);
if (StringUtils.isEmpty(request.getRemoteAddr()) && !StringUtils.isEmpty(headerClientIp)) {
return headerClientIp;
}
if (!StringUtils.isEmpty(headerXForwardedFor)) {
return headerXForwardedFor;
}
return request.getRemoteAddr();
}
}

View File

@ -0,0 +1,93 @@
package ru.ulstu.fc.user.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.ulstu.fc.user.model.User;
import ru.ulstu.fc.user.model.UserNotFoundException;
import ru.ulstu.fc.user.model.UserRole;
import ru.ulstu.fc.user.model.UserRoleConstants;
import ru.ulstu.fc.user.repository.UserRepository;
import ru.ulstu.fc.user.repository.UserRoleRepository;
import ru.ulstu.fc.user.utils.UserUtils;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@Transactional
public class UserService implements UserDetailsService {
private final Logger log = LoggerFactory.getLogger(UserService.class);
private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;
private final UserRoleRepository userRoleRepository;
@Value("${admin-password}")
private String adminPassword;
public UserService(PasswordEncoder passwordEncoder,
UserRepository userRepository,
UserRoleRepository userRoleRepository) {
this.passwordEncoder = passwordEncoder;
this.userRepository = userRepository;
this.userRoleRepository = userRoleRepository;
}
public User getUserByLogin(String login) {
return userRepository.findOneByLoginIgnoreCase(login);
}
@Override
public UserDetails loadUserByUsername(String username) {
final User user = userRepository.findOneByLoginIgnoreCase(username);
if (user == null) {
throw new UserNotFoundException(username);
}
return new org.springframework.security.core.userdetails.User(user.getLogin(),
user.getPassword(),
Optional.ofNullable(user.getRoles()).orElse(Collections.emptySet()).stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList()));
}
public User createUser(User user) {
if (getUserByLogin(user.getLogin()) != null) {
throw new RuntimeException(user.getLogin());
}
User dbUser = (user.getId() == null)
? user
: getUserById(user.getId());
//user.setRoles(Collections.singleton(new UserRole(UserRoleConstants.USER)));
dbUser.setPassword(passwordEncoder.encode(user.getPassword()));
dbUser.setLogin(user.getLogin());
dbUser = userRepository.save(dbUser);
log.debug("Created Information for User: {}", dbUser.getLogin());
return dbUser;
}
public User getUserById(Integer id) {
return userRepository.findById(id).orElseThrow(() -> new RuntimeException("User not found by id"));
}
private void createDefaultUser(String login, String userRole) {
if (getUserByLogin(login) == null) {
UserRole role = userRoleRepository.save(new UserRole(userRole.toString()));
createUser(new User(login, login.equals("admin") ? adminPassword : login, Set.of(role)));
}
}
public void initDefaultAdmin() {
createDefaultUser("admin", UserRoleConstants.ADMIN);
}
public User getCurrentUser() {
return getUserByLogin(UserUtils.getCurrentUserLogin());
}
}

View File

@ -0,0 +1,44 @@
package ru.ulstu.fc.user.service;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import ru.ulstu.fc.config.Constants;
import java.io.IOException;
@Component
public class UserSessionLoginHandler extends SavedRequestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final Logger log = LoggerFactory.getLogger(UserSessionLoginHandler.class);
private final UserSessionService userSessionService;
public UserSessionLoginHandler(UserSessionService userSessionService) {
super();
this.userSessionService = userSessionService;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
super.onAuthenticationSuccess(request, response, authentication);
final String login = authentication.getName();
final String ipAddress = IpAddressResolver.getRemoteAddr(request);
final String host = request.getRemoteHost();
log.debug("Authentication Success for {}@{} ({})", login, ipAddress, host);
HttpSession session = request.getSession(false);
if (session != null) {
final String sessionId = session.getId();
userSessionService.createUserSession(sessionId, login, ipAddress, host);
session.setAttribute(Constants.SESSION_ID_ATTR, sessionId);
session.setMaxInactiveInterval(Constants.SESSION_TIMEOUT_SECONDS);
}
}
}

View File

@ -0,0 +1,48 @@
package ru.ulstu.fc.user.service;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import org.springframework.stereotype.Component;
import ru.ulstu.fc.config.Constants;
import java.io.IOException;
@Component
public class UserSessionLogoutHandler extends SimpleUrlLogoutSuccessHandler implements LogoutSuccessHandler {
private final Logger log = LoggerFactory.getLogger(UserSessionLogoutHandler.class);
private final UserSessionService userSessionService;
public UserSessionLogoutHandler(UserSessionService userSessionService) {
this.userSessionService = userSessionService;
setDefaultTargetUrl(Constants.LOGOUT_URL);
}
@Override
public void onLogoutSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
if (authentication == null) {
super.onLogoutSuccess(request, response, authentication);
return;
}
final String login = authentication.getName();
final String ipAddress = IpAddressResolver.getRemoteAddr(request);
final String host = request.getRemoteHost();
log.debug("Logout Success for {}@{} ({})", login, ipAddress, host);
HttpSession session = request.getSession(false);
if (session != null) {
final String sessionId = session.getAttribute(Constants.SESSION_ID_ATTR).toString();
userSessionService.closeUserSession(sessionId);
session.removeAttribute(Constants.SESSION_ID_ATTR);
session.invalidate();
}
super.onLogoutSuccess(request, response, authentication);
}
}

View File

@ -0,0 +1,42 @@
package ru.ulstu.fc.user.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import ru.ulstu.fc.user.model.User;
import ru.ulstu.fc.user.model.UserNotFoundException;
import ru.ulstu.fc.user.model.UserSession;
import ru.ulstu.fc.user.repository.UserSessionRepository;
@Service
@Transactional
public class UserSessionService {
private final Logger log = LoggerFactory.getLogger(UserSessionService.class);
private final UserSessionRepository userSessionRepository;
private final UserService userService;
public UserSessionService(UserSessionRepository userSessionRepository, UserService userService) {
this.userSessionRepository = userSessionRepository;
this.userService = userService;
}
public void createUserSession(String sessionId, String login, String ipAddress, String host) {
final User user = userService.getUserByLogin(login);
if (user == null) {
throw new UserNotFoundException(login);
}
userSessionRepository.save(new UserSession(sessionId, ipAddress, host, user));
log.debug("User session {} created for user {}@{} ({})", sessionId, login, ipAddress, host);
}
public void closeUserSession(String sessionId) {
final UserSession userSession = userSessionRepository.findOneBySessionId(sessionId);
if (userSession == null) {
throw new IllegalArgumentException(String.format("User session %s not found", sessionId));
}
userSession.close();
userSessionRepository.save(userSession);
log.debug("User session {} closed", sessionId);
}
}

View File

@ -0,0 +1,24 @@
package ru.ulstu.fc.user.utils;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
public class UserUtils {
public static String getCurrentUserLogin() {
final SecurityContext securityContext = SecurityContextHolder.getContext();
if (securityContext == null) {
return null;
}
final Authentication authentication = securityContext.getAuthentication();
if (authentication.getPrincipal() instanceof UserDetails) {
final UserDetails springSecurityUser = (UserDetails) authentication.getPrincipal();
return springSecurityUser.getUsername();
}
if (authentication.getPrincipal() instanceof String) {
return (String) authentication.getPrincipal();
}
return null;
}
}

View File

@ -1,6 +1,7 @@
spring.main.banner-mode=off spring.main.banner-mode=off
server.port=8080 server.port=8080
server.jetty.connection-idle-timeout=1000s server.jetty.connection-idle-timeout=1000s
admin-password=admin
# Available levels are: TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF # Available levels are: TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF
logging.level.ru.ulstu=DEBUG logging.level.ru.ulstu=DEBUG
logging.level.sun.rmi.transport=off logging.level.sun.rmi.transport=off

View File

@ -21,11 +21,15 @@
</button> </button>
<div class="collapse navbar-collapse" id="navbarSupportedContent"> <div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav mr-auto"> <ul class="navbar-nav mr-auto">
<li class="nav-item"> <li class="nav-item dropdown">
<a class="nav-link" href="/swagger-ui/index.html">API</a> <a class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="false">Проекты</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="/project/list">Список проектов</a>
</div>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/listRules">Правила</a> <a class="nav-link" href="/swagger-ui/index.html">API</a>
</li> </li>
</ul> </ul>
</div> </div>

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{default}">
<head>
</head>
<body>
<div class="container" layout:fragment="content">
<h5>Доступ запрещён</h5>
<a href="/"><h6>Вернуться на главную</h6></a>
</div>
</body>
</html>

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{default}">
<head>
</head>
<body>
<div class="container" layout:fragment="content">
<h5>Страница не найдена</h5>
<a href="/"><h6>Вернуться на главную</h6></a>
</div>
</body>
</html>

View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{default}">
<head>
</head>
<body>
<div class="container" layout:fragment="content">
<h5>Ошибка сервера</h5>
<a href="/"><h6>Вернуться на главную</h6></a>
</div>
</body>
</html>

View File

@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{default}">
<head>
</head>
<body>
<nav layout:fragment="navbar">
<div class="navbar-header">
<a class="navbar-brand" href="/"><span class="ui-menuitem-text">Нечеткий контроллер</span></a>
</div>
</nav>
<div class="container" layout:fragment="content">
<div class="tab-content">
<div id="signin" class="tab-pane active">
<form th:action="@{/login}" method="post" class="margined-top-10">
<fieldset>
<div class="form-group">
<input type="text" name="username" id="username" class="form-control"
placeholder="Логин" required="true" autofocus="true"/>
</div>
<div class="form-group">
<input type="password" name="password" id="password" class="form-control"
placeholder="Пароль" required="true"/>
</div>
<button type="submit" class="btn btn-outline-dark btn-block">Войти</button>
</fieldset>
</form>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{default}">
<head>
</head>
<body>
<nav layout:fragment="navbar">
<div class="navbar-header">
<a class="navbar-brand" href="/"><span class="ui-menuitem-text"><i
class="fa fa-plane fa-4" aria-hidden="true"></i> Balance</span></a>
</div>
</nav>
<div class="container" layout:fragment="content">
<div class="container-fluid">
<ul id="messages" class="feedback-panel">
<div class="alert alert-danger" role="alert">Ошибка входа</div>
</ul>
</div>
<div class="tab-content">
<div id="signin" class="tab-pane active">
<form th:action="@{/login}" method="post" class="margined-top-10">
<fieldset>
<div class="form-group">
<input type="text" name="username" id="username" class="form-control"
placeholder="Логин" required="true" autofocus="true"/>
</div>
<div class="form-group">
<input type="password" name="password" id="password" class="form-control"
placeholder="Пароль" required="true"/>
</div>
<button type="submit" class="btn btn-outline-dark btn-block">Войти</button>
</fieldset>
</form>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,39 @@
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:th="http://www.w3.org/1999/xhtml"
layout:decorate="~{default}">
<head>
<title>Редактирование проекта</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<div class="container" layout:fragment="content">
<h3>Редактирование проекта:</h3>
<form th:action="@{/project/save}" th:object="${project}" method="post">
<input type="hidden" th:field="*{id}">
<div class="form-group">
<label for="name">Название</label>
<input th:field="*{name}"
id="name"
type="text"
required
class="form-control"
placeholder="Название">
<p th:if="${#fields.hasErrors('name')}"
th:class="${#fields.hasErrors('name')}? error">
Не может быть пустым
</p>
</div>
<div class="form-group">
<label th:text="'Дата создания: ' + ${#dates.format(project.createDate, 'dd.MM.yyyy HH:mm')}"></label>
</div>
<button name="save" type="submit" class="btn btn-outline-dark">Сохранить</button>
<button name="delete"
type="submit"
class="btn btn-outline-dark"
onclick="return confirm('Удалить запись?')">
Удалить
</button>
<a href="/project/list" class="btn btn-outline-dark">Отмена</a>
</form>
</div>
</html>

View File

@ -0,0 +1,22 @@
<!DOCTYPE html SYSTEM "http://www.thymeleaf.org/dtd/xhtml1-strict-thymeleaf-spring4-4.dtd">
<html
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:th="http://www.w3.org/1999/xhtml"
layout:decorate="~{default}">
<head>
<title>Список правил</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<div class="container" layout:fragment="content">
<a href="/project/edit/0" class="btn btn-outline-dark">
<i class="fa fa-plus-square" aria-hidden="true">Добавить проект</i>
</a>
<ul>
<li th:each="p : ${projects}">
<a th:href="@{'/project/edit/' + ${p.id}}">
<span th:text="${p.name} + ' от ' + ${#dates.format(p.createDate, 'dd.MM.yyyy HH:mm')}"></span>
</a>
</li>
</ul>
</div>
</html>