feat: finish demo — port demo-monolith cleanup, separate UI and API containers
Frontend
- Bring spring-angular-ui back under version control with the Angular 22 +
Material UI from demo-monolith; stop committing the built static/ folder
- Serve the UI from nginx, which proxies /api to the backend so the JWT
cookie works on a single origin, and falls back to index.html for routes
Backend
- Add PUT /api/users/{id}/roles (admin only) that keeps at least one admin
and does not let an admin drop their own admin role
- Tighten security: read tutorials publicly, change them only when logged
in, users/roles API for admins only; hide password hashes in JSON
- Remove unused Settings and Test endpoints, DTO mappers, commented code
- Flyway V2 aligns the schema with the entities; Hibernate now runs with
ddl-auto: validate
- JWT lifetime matches the 24h cookie; fix the cookie name typo
- Keep CORS only for calling the API directly (ng serve without proxy)
Docker
- Backend Dockerfile: Gradle build + JRE runtime with layer caching,
non-root user and healthcheck; add .dockerignore
- UI Dockerfile: Node 24 build + nginx with healthcheck
- docker-compose runs its own Postgres, starts services in health order
and always rebuilds images
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
package com.example.demo_decoupled.DTOs;
|
||||
|
||||
|
||||
import com.example.demo_decoupled.model.ERole;
|
||||
import com.example.demo_decoupled.model.Role;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
@@ -34,12 +33,6 @@ public class RoleDto {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
// Mapping helpers
|
||||
public static RoleDto fromEntity(Role role) {
|
||||
if (role == null) return null;
|
||||
return new RoleDto(role.getId(), role.getName());
|
||||
}
|
||||
|
||||
public Role toEntity() {
|
||||
Role role = new Role();
|
||||
role.setId(this.id);
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
package com.example.demo_decoupled.DTOs;
|
||||
|
||||
|
||||
import com.example.demo_decoupled.model.User;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class UserDto {
|
||||
@JsonIgnore
|
||||
@@ -69,35 +65,4 @@ public class UserDto {
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
// Mapping helpers
|
||||
public static UserDto fromEntity(User user) {
|
||||
if (user == null) return null;
|
||||
return new UserDto(
|
||||
user.getId(),
|
||||
user.getUsername(),
|
||||
user.getEmail(),
|
||||
user.getPassword(),
|
||||
user.getRoles() == null ? null :
|
||||
user.getRoles().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(RoleDto::fromEntity)
|
||||
.collect(Collectors.toSet())
|
||||
);
|
||||
}
|
||||
|
||||
public User toEntity() {
|
||||
User user = new User();
|
||||
user.setId(this.id);
|
||||
user.setUsername(this.username);
|
||||
user.setEmail(this.email);
|
||||
user.setPassword(this.password); // Assuming password is already hashed
|
||||
if (this.roles != null) {
|
||||
user.setRoles(this.roles.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(RoleDto::toEntity)
|
||||
.collect(Collectors.toSet()));
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,8 +27,7 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
//@CrossOrigin(origins = "*", maxAge = 3600)
|
||||
@CrossOrigin(origins = "http://localhost:4200", maxAge = 3600, allowCredentials="true")
|
||||
@CrossOrigin(origins = "${app.origin}", maxAge = 3600, allowCredentials="true")
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
public class AuthController {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.example.demo_decoupled.controllers;
|
||||
|
||||
|
||||
|
||||
import com.example.demo_decoupled.model.Role;
|
||||
import com.example.demo_decoupled.repository.RoleRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -24,7 +23,6 @@ public class RolesController {
|
||||
try {
|
||||
List<Role> role = new ArrayList<Role>();
|
||||
role = roleRepository.findAll();
|
||||
//userRepository.findAll().forEach(users::add);
|
||||
|
||||
|
||||
if (role.isEmpty()) {
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
package com.example.demo_decoupled.controllers;
|
||||
|
||||
|
||||
|
||||
import com.example.demo_decoupled.model.Setting;
|
||||
import com.example.demo_decoupled.repository.SettingsRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class SettingsController {
|
||||
@Autowired
|
||||
private SettingsRepository settingsRepository;
|
||||
|
||||
@GetMapping("/settings")
|
||||
public ResponseEntity<List<Setting>> getSettings(@RequestParam(required = false) String title) {
|
||||
try {
|
||||
List<Setting> settings = settingsRepository.findAll();
|
||||
//userRepository.findAll().forEach(users::add);
|
||||
|
||||
|
||||
if (settings.isEmpty()) {
|
||||
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
|
||||
}
|
||||
|
||||
return new ResponseEntity<>(settings, HttpStatus.OK);
|
||||
} catch (Exception e) {
|
||||
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package com.example.demo_decoupled.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;
|
||||
|
||||
@CrossOrigin(origins = "*", maxAge = 3600)
|
||||
@RestController
|
||||
@RequestMapping("/api/test")
|
||||
public class TestController {
|
||||
@GetMapping("/all")
|
||||
public String allAccess() {
|
||||
return "Public Content.";
|
||||
}
|
||||
|
||||
@GetMapping("/user")
|
||||
@PreAuthorize("hasRole('USER') or hasRole('MODERATOR') or hasRole('ADMIN')")
|
||||
public String userAccess() {
|
||||
return "User Content.";
|
||||
}
|
||||
|
||||
@GetMapping("/mod")
|
||||
@PreAuthorize("hasRole('MODERATOR')")
|
||||
public String moderatorAccess() {
|
||||
return "Moderator Board.";
|
||||
}
|
||||
|
||||
@GetMapping("/admin")
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public String adminAccess() {
|
||||
return "Admin Board.";
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.example.demo_decoupled.controllers;
|
||||
|
||||
|
||||
|
||||
import com.example.demo_decoupled.model.Tutorial;
|
||||
import com.example.demo_decoupled.repository.TutorialRepository;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -56,8 +55,11 @@ public class TutorialController {
|
||||
@PostMapping("/tutorials")
|
||||
public ResponseEntity<Tutorial> createTutorial(@RequestBody Tutorial tutorial) {
|
||||
try {
|
||||
Tutorial _tutorial = tutorialRepository
|
||||
.save(new Tutorial(tutorial.getTitle(), tutorial.getDescription(), false));
|
||||
Tutorial _tutorial = new Tutorial();
|
||||
_tutorial.setTitle(tutorial.getTitle());
|
||||
_tutorial.setDescription(tutorial.getDescription());
|
||||
_tutorial.setPublished(false);
|
||||
_tutorial = tutorialRepository.save(_tutorial);
|
||||
return new ResponseEntity<>(_tutorial, HttpStatus.CREATED);
|
||||
} catch (Exception e) {
|
||||
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
|
||||
@@ -1,16 +1,27 @@
|
||||
package com.example.demo_decoupled.controllers;
|
||||
|
||||
|
||||
|
||||
import com.example.demo_decoupled.model.ERole;
|
||||
import com.example.demo_decoupled.model.Role;
|
||||
import com.example.demo_decoupled.model.User;
|
||||
import com.example.demo_decoupled.payload.request.UpdateRolesRequest;
|
||||
import com.example.demo_decoupled.payload.response.MessageResponse;
|
||||
import com.example.demo_decoupled.repository.RoleRepository;
|
||||
import com.example.demo_decoupled.repository.UserRepository;
|
||||
import com.example.demo_decoupled.security.services.UserDetailsImpl;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@CrossOrigin(origins = "${app.origin}", maxAge = 3600, allowCredentials="true")
|
||||
@RestController
|
||||
@@ -19,12 +30,14 @@ public class UsersController {
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
private RoleRepository roleRepository;
|
||||
|
||||
@GetMapping("/users")
|
||||
public ResponseEntity<List<User>> getUsers(@RequestParam(required = false) String title) {
|
||||
try {
|
||||
List<User> users = new ArrayList<User>();
|
||||
users = userRepository.findAll();
|
||||
//userRepository.findAll().forEach(users::add);
|
||||
|
||||
|
||||
if (users.isEmpty()) {
|
||||
@@ -36,4 +49,36 @@ public class UsersController {
|
||||
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
@PutMapping("/users/{id}/roles")
|
||||
@Transactional
|
||||
public ResponseEntity<?> updateRoles(@PathVariable("id") long id,
|
||||
@Valid @RequestBody UpdateRolesRequest request,
|
||||
@AuthenticationPrincipal UserDetailsImpl currentUser) {
|
||||
Optional<User> userData = userRepository.findById(id);
|
||||
if (userData.isEmpty()) {
|
||||
return new ResponseEntity<>(new MessageResponse("User not found"), HttpStatus.NOT_FOUND);
|
||||
}
|
||||
User user = userData.get();
|
||||
|
||||
boolean isAdmin = user.getRoles().stream().anyMatch(role -> role.getName() == ERole.ROLE_ADMIN);
|
||||
boolean staysAdmin = request.getRoles().contains(ERole.ROLE_ADMIN);
|
||||
if (isAdmin && !staysAdmin) {
|
||||
// Иначе администратор может случайно запереть себя или всех снаружи админки
|
||||
if (user.getId().equals(currentUser.getId())) {
|
||||
return ResponseEntity.badRequest().body(new MessageResponse("You cannot remove the admin role from yourself"));
|
||||
}
|
||||
if (userRepository.countByRolesName(ERole.ROLE_ADMIN) <= 1) {
|
||||
return ResponseEntity.badRequest().body(new MessageResponse("At least one admin must remain"));
|
||||
}
|
||||
}
|
||||
|
||||
Set<Role> roles = request.getRoles().stream()
|
||||
.map(name -> roleRepository.findByName(name)
|
||||
.orElseThrow(() -> new RuntimeException("Error: Role is not found.")))
|
||||
.collect(Collectors.toSet());
|
||||
user.setRoles(roles);
|
||||
|
||||
return ResponseEntity.ok(userRepository.save(user));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
//@ConditionalOnProperty(name = "app.init.data.enabled", havingValue = "true", matchIfMissing = true)
|
||||
public class InitDataConfiguration {
|
||||
|
||||
@Autowired
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.example.demo_decoupled.initData;
|
||||
|
||||
|
||||
import com.example.demo_decoupled.DTOs.RoleDto;
|
||||
import com.example.demo_decoupled.DTOs.UserDto;
|
||||
import com.example.demo_decoupled.model.ERole;
|
||||
@@ -16,7 +15,6 @@ import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@@ -37,8 +35,6 @@ public class InitDataService {
|
||||
|
||||
protected ObjectMapper _mapper;
|
||||
|
||||
@Autowired
|
||||
private ResourceLoader _resources;
|
||||
@Autowired
|
||||
private RoleRepository _roleRepository;
|
||||
|
||||
@@ -77,13 +73,13 @@ public class InitDataService {
|
||||
Path targetPath = _initDataPath.resolve(fileName);
|
||||
if (!Files.exists(targetPath)) {
|
||||
try (InputStream inputStream = resource.getInputStream()) {
|
||||
Files.copy(resource.getInputStream(), targetPath);
|
||||
Files.copy(inputStream, targetPath);
|
||||
}
|
||||
|
||||
_logger.info("Copied {} to {}", fileName, targetPath.toAbsolutePath());
|
||||
} else {
|
||||
try (InputStream inputStream = resource.getInputStream()) {
|
||||
Files.copy(resource.getInputStream(), targetPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
Files.copy(inputStream, targetPath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
_logger.warn("{} already exists, REPLACE", targetPath.toAbsolutePath());
|
||||
@@ -100,9 +96,6 @@ public class InitDataService {
|
||||
List<RoleDto> items = readList(jsonFile, new TypeReference<List<RoleDto>>() {});
|
||||
for (RoleDto dto : items) {
|
||||
Role roleEntity = dto.toEntity();
|
||||
// If a role with same name exists, reuse its ID to upsert
|
||||
// _roleRepository.findByName(entity.getName())
|
||||
// .ifPresent(existing -> entity.setId(existing.getId()));
|
||||
|
||||
if(!_roleRepository.existsByName(roleEntity.getName())) {
|
||||
_roleRepository.save(roleEntity);
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package com.example.demo_decoupled.model;
|
||||
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "settings")
|
||||
public class Setting {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private long id;
|
||||
|
||||
@Column(name = "useremaileUniq")
|
||||
private Boolean useremaileUniq;
|
||||
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import jakarta.persistence.*;
|
||||
public class Tutorial {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private long id;
|
||||
|
||||
@Column(name = "title")
|
||||
@@ -21,16 +21,12 @@ public class Tutorial {
|
||||
@Column(name = "published")
|
||||
private boolean published;
|
||||
|
||||
// Единственный конструктор: иначе Jackson 3 выбирает конструктор с параметрами
|
||||
// и падает на запросах без поля published ("Cannot map `null` into type `boolean`").
|
||||
public Tutorial() {
|
||||
|
||||
}
|
||||
|
||||
public Tutorial(String title, String description, boolean published) {
|
||||
this.title = title;
|
||||
this.description = description;
|
||||
this.published = published;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.example.demo_decoupled.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import jakarta.persistence.*;
|
||||
import jakarta.validation.constraints.Email;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
@@ -18,7 +19,7 @@ import java.util.Set;
|
||||
})
|
||||
public class User {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private long id;
|
||||
|
||||
@Column(name = "username")
|
||||
@@ -31,6 +32,7 @@ public class User {
|
||||
@Email
|
||||
private String email;
|
||||
|
||||
@JsonIgnore
|
||||
@NotBlank
|
||||
@Size(max = 120)
|
||||
private String password;
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
|
||||
INSERT INTO roles (id, name) VALUES (1, 'ROLE_USER');
|
||||
INSERT INTO roles (id, name) VALUES (2, 'ROLE_MODERATOR');
|
||||
INSERT INTO roles (id, name) VALUES (3, 'ROLE_ADMIN');
|
||||
|
||||
INSERT INTO users (id, email, password, username)
|
||||
VALUES (1, ''liosha84@gmail.com'', ''$2a$10$qyoKXYSukha6XCjorTzoweF4Os1pwmwyzbaSsb3RCVB0LK6WLQKPC'', ''Admin'');
|
||||
INSERT INTO users (id, email, password, username)
|
||||
VALUES (4, ''Moderator@gmail.com'', ''$2a$10$zLo5th8Xbfq.MM7y/dCRQu3Ud7HHyAwm.7.yS08ytJtkHMKrbOJlu'', ''Moderator'');
|
||||
INSERT INTO users (id, email, password, username)
|
||||
VALUES (5, ''user@gmail.com'', ''$2a$10$2EcwYffteBF3GhVaf5qPB.I7XiHepDEauU5D4fx9fpBXMTZI/QnnC'', ''User'');
|
||||
|
||||
|
||||
INSERT INTO user_roles (user_id, role_id) VALUES (5, 1);
|
||||
INSERT INTO user_roles (user_id, role_id) VALUES (1, 3);
|
||||
INSERT INTO user_roles (user_id, role_id) VALUES (4, 2);
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.example.demo_decoupled.payload.request;
|
||||
|
||||
import com.example.demo_decoupled.model.ERole;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class UpdateRolesRequest {
|
||||
@NotEmpty
|
||||
private Set<ERole> roles;
|
||||
|
||||
public Set<ERole> getRoles() {
|
||||
return roles;
|
||||
}
|
||||
|
||||
public void setRoles(Set<ERole> roles) {
|
||||
this.roles = roles;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package com.example.demo_decoupled.repository;
|
||||
|
||||
|
||||
|
||||
import com.example.demo_decoupled.model.Setting;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface SettingsRepository extends JpaRepository<Setting, Long> {
|
||||
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package com.example.demo_decoupled.repository;
|
||||
|
||||
|
||||
|
||||
import com.example.demo_decoupled.model.ERole;
|
||||
import com.example.demo_decoupled.model.User;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
@@ -15,4 +15,6 @@ public interface UserRepository extends JpaRepository<User, Long> {
|
||||
Boolean existsByUsername(String username);
|
||||
|
||||
Boolean existsByEmail(String email);
|
||||
|
||||
long countByRolesName(ERole name);
|
||||
}
|
||||
|
||||
@@ -6,27 +6,24 @@ import com.example.demo_decoupled.security.services.UserDetailsServiceImpl;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
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.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
//import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@EnableMethodSecurity
|
||||
//@EnableWebSecurity
|
||||
//@EnableGlobalMethodSecurity(
|
||||
// securedEnabled = true,
|
||||
// jsr250Enabled = true,
|
||||
//prePostEnabled = true)
|
||||
public class WebSecurityConfig {// extends WebSecurityConfigurerAdapter {
|
||||
public class WebSecurityConfig {
|
||||
@Autowired
|
||||
UserDetailsServiceImpl userDetailsService;
|
||||
|
||||
@@ -38,27 +35,15 @@ public class WebSecurityConfig {// extends WebSecurityConfigurerAdapter {
|
||||
return new AuthTokenFilter();
|
||||
}
|
||||
|
||||
// @Override
|
||||
// public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
|
||||
// authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
|
||||
// }
|
||||
|
||||
@Bean
|
||||
public DaoAuthenticationProvider authenticationProvider() {
|
||||
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(userDetailsService);
|
||||
|
||||
//authProvider.setUserDetailsService(userDetailsService);
|
||||
authProvider.setPasswordEncoder(passwordEncoder());
|
||||
|
||||
return authProvider;
|
||||
}
|
||||
|
||||
// @Bean
|
||||
// @Override
|
||||
// public AuthenticationManager authenticationManagerBean() throws Exception {
|
||||
// return super.authenticationManagerBean();
|
||||
// }
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
|
||||
return authConfig.getAuthenticationManager();
|
||||
@@ -69,56 +54,25 @@ public class WebSecurityConfig {// extends WebSecurityConfigurerAdapter {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
// @Override
|
||||
// protected void configure(HttpSecurity http) throws Exception {
|
||||
// http
|
||||
// .csrf()
|
||||
// .disable()
|
||||
// .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
|
||||
// .authorizeRequests()
|
||||
//
|
||||
//
|
||||
// // //Доступ только для пользователей с ролью Администратор
|
||||
// //.antMatchers("/api/users").hasRole("ADMIN")
|
||||
// // .antMatchers("/news").hasRole("USER")
|
||||
// //Доступ разрешен всем пользователей
|
||||
// .antMatchers("/*", "/home", "/resources/**").permitAll()
|
||||
// .antMatchers("/api/auth/**").permitAll()
|
||||
// .antMatchers("/api/tutorials").permitAll()
|
||||
// .antMatchers("/api/tutorials/**").permitAll()
|
||||
// //.antMatchers("/api/test/**").permitAll()
|
||||
// //Все остальные страницы требуют аутентификации
|
||||
// .anyRequest().authenticated()
|
||||
// .and()
|
||||
// .sessionManagement()
|
||||
// .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
// .and()
|
||||
// .addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
|
||||
//
|
||||
//
|
||||
// //http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
|
||||
// }
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource())) // Включаем CORS
|
||||
http.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.exceptionHandling(exception -> exception.authenticationEntryPoint(unauthorizedHandler))
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth ->
|
||||
auth.requestMatchers("/api/auth/**").permitAll()
|
||||
.requestMatchers("/*", "/home", "/resources/**").permitAll()
|
||||
.requestMatchers("/api/test/**").permitAll()
|
||||
.requestMatchers("/api/tutorials").permitAll()
|
||||
.requestMatchers("/actuator/health").permitAll()
|
||||
|
||||
.requestMatchers("/api/users").permitAll()
|
||||
.requestMatchers("/api/users/**").permitAll()
|
||||
// Читать туториалы может любой, изменять — только вошедший пользователь
|
||||
.requestMatchers(HttpMethod.GET, "/api/tutorials", "/api/tutorials/**").permitAll()
|
||||
.requestMatchers(HttpMethod.DELETE, "/api/tutorials").hasRole("ADMIN")
|
||||
.requestMatchers("/api/tutorials", "/api/tutorials/**").authenticated()
|
||||
|
||||
.requestMatchers("/api/roles").permitAll()
|
||||
.requestMatchers("/api/roles/**").permitAll()
|
||||
.requestMatchers("/api/users", "/api/users/**").hasRole("ADMIN")
|
||||
.requestMatchers("/api/roles", "/api/roles/**").hasRole("ADMIN")
|
||||
|
||||
.requestMatchers("/api/tutorials/**").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
);
|
||||
|
||||
@@ -129,17 +83,17 @@ public class WebSecurityConfig {// extends WebSecurityConfigurerAdapter {
|
||||
return http.build();
|
||||
}
|
||||
|
||||
// Добавляем метод конфигурации CORS источников и разрешенных методов
|
||||
// Нужен, только если UI обращается к API напрямую на :8080 (ng serve без proxy.conf.json).
|
||||
// В docker-compose nginx фронта проксирует /api, и запросы идут с того же origin
|
||||
@Bean
|
||||
public org.springframework.web.cors.CorsConfigurationSource corsConfigurationSource() {
|
||||
org.springframework.web.cors.CorsConfiguration configuration = new org.springframework.web.cors.CorsConfiguration();
|
||||
// Укажите порты, с которых разрешено ходить (например, ваш Angular на 8081 и дефолтный 4200)
|
||||
configuration.setAllowedOrigins(java.util.List.of("http://localhost:8081", "http://localhost:4200", "http://localhost"));
|
||||
configuration.setAllowedMethods(java.util.List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
|
||||
configuration.setAllowedHeaders(java.util.List.of("*"));
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
configuration.setAllowedOrigins(List.of("http://localhost:8081", "http://localhost:4200"));
|
||||
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
|
||||
configuration.setAllowedHeaders(List.of("*"));
|
||||
configuration.setAllowCredentials(true);
|
||||
|
||||
org.springframework.web.cors.UrlBasedCorsConfigurationSource source = new org.springframework.web.cors.UrlBasedCorsConfigurationSource();
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", configuration);
|
||||
return source;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.example.demo_decoupled.security.jwt;
|
||||
|
||||
|
||||
|
||||
import com.example.demo_decoupled.security.services.UserDetailsImpl;
|
||||
import io.jsonwebtoken.*;
|
||||
import io.jsonwebtoken.io.Decoders;
|
||||
@@ -56,8 +57,7 @@ public class JwtUtils {
|
||||
}
|
||||
|
||||
private Key key() {
|
||||
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret));//java.util.Base64.getDecoder().decode(jwtSecret.getBytes()));
|
||||
//Decoders.BASE64.decode(jwtSecret));
|
||||
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret));
|
||||
}
|
||||
|
||||
public boolean validateJwtToken(String authToken) {
|
||||
|
||||
@@ -15,12 +15,13 @@ spring:
|
||||
jdbc:
|
||||
lob:
|
||||
non_contextual_creation: true
|
||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
# Схемой управляет Flyway (db/migration), Hibernate только проверяет соответствие сущностям
|
||||
ddl-auto: validate
|
||||
|
||||
app:
|
||||
jwtSecret: ======================spring=back====================
|
||||
jwtExpirationMs: 30000
|
||||
jwtCookieName: demo-deoupled
|
||||
jwtSecret: ${JWT_SECRET:======================spring=back====================}
|
||||
# Совпадает со сроком жизни cookie (24 ч), иначе UI считает пользователя вошедшим, а API отвечает 401
|
||||
jwtExpirationMs: 86400000
|
||||
jwtCookieName: demo-decoupled
|
||||
origin: http://localhost:4200
|
||||
@@ -0,0 +1,32 @@
|
||||
-- Выравнивает схему под JPA-сущности, чтобы Hibernate работал в режиме ddl-auto: validate.
|
||||
-- Должна проходить и на чистой базе (только V1), и на базе, где раньше работал ddl-auto: update
|
||||
-- и успел создать свои последовательности и таблицу settings.
|
||||
|
||||
-- 1. Идентификаторы users и roles: integer -> bigint (в сущностях long/Long)
|
||||
ALTER TABLE public.user_roles ALTER COLUMN role_id TYPE bigint;
|
||||
ALTER TABLE public.roles ALTER COLUMN id TYPE bigint;
|
||||
ALTER TABLE public.roles ALTER COLUMN id SET MAXVALUE 9223372036854775807;
|
||||
ALTER TABLE public.users ALTER COLUMN id TYPE bigint;
|
||||
ALTER TABLE public.users ALTER COLUMN id SET MAXVALUE 9223372036854775807;
|
||||
|
||||
-- 2. У tutorials в V1 не было первичного ключа
|
||||
ALTER TABLE public.tutorials ADD CONSTRAINT tutorials_pkey PRIMARY KEY (id);
|
||||
|
||||
-- 3. Сущности переведены на GenerationType.IDENTITY. Если раньше id выдавал Hibernate из своих
|
||||
-- последовательностей, identity-последовательности отстают от данных — подтягиваем их к MAX(id)
|
||||
SELECT setval(pg_get_serial_sequence('public.users', 'id'),
|
||||
COALESCE((SELECT MAX(id) FROM public.users), 0) + 1, false);
|
||||
SELECT setval(pg_get_serial_sequence('public.roles', 'id'),
|
||||
COALESCE((SELECT MAX(id) FROM public.roles), 0) + 1, false);
|
||||
SELECT setval(pg_get_serial_sequence('public.tutorials', 'id'),
|
||||
COALESCE((SELECT MAX(id) FROM public.tutorials), 0) + 1, false);
|
||||
|
||||
-- 4. Остатки ddl-auto: update — больше не используются.
|
||||
-- Hibernate 6 называет последовательности по имени сущности (user_seq), старые версии — по таблице
|
||||
DROP SEQUENCE IF EXISTS public.user_seq;
|
||||
DROP SEQUENCE IF EXISTS public.users_seq;
|
||||
DROP SEQUENCE IF EXISTS public.tutorial_seq;
|
||||
DROP SEQUENCE IF EXISTS public.tutorials_seq;
|
||||
DROP TABLE IF EXISTS public.settings;
|
||||
DROP SEQUENCE IF EXISTS public.setting_seq;
|
||||
DROP SEQUENCE IF EXISTS public.settings_seq;
|
||||
Reference in New Issue
Block a user