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:
2026-10-17 19:33:38 +02:00
co-authored by Claude Opus 5.5
parent 1d38a95f48
commit 514e1fd0c7
118 changed files with 9134 additions and 79218 deletions
@@ -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) {
+6 -5
View File
@@ -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;