пытаюсь сделать авторизацию

This commit is contained in:
Lobstervova
2026-02-03 22:20:01 +03:00
parent a8e895139a
commit d7a0cbb7a4
22 changed files with 1479 additions and 66 deletions

View File

@@ -0,0 +1,51 @@
package com.example.dateplanner.configurations;
import com.example.dateplanner.models.entities.AppUser;
import com.example.dateplanner.models.enums.Role;
import com.example.dateplanner.repositories.AppUserRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
import reactor.core.publisher.Mono;
@Slf4j
@Configuration
public class AppConfig {
private final String username = "morgan";
private final String password = "Admin_123!";
@Bean
public CommandLineRunner prepare(AppUserRepository userRepository, PasswordEncoder encoder) {
return args -> {
log.info("***************** environments start *******************");
for (String key : System.getenv().keySet()) {
String[] p = key.split("\\.");
if (p.length > 1) {
log.info("\"{}\"=\"{}\"", key, System.getenv().get(key));
}
}
log.info("****************** environments end ********************");
userRepository.findByPhone(username).flatMap(existingUser -> {
log.info("User {} exist.", existingUser.getPhone());
return Mono.just(existingUser);
}).switchIfEmpty(
Mono.defer(() -> {
log.info("User not found! Create default admin user: {}, password: {}", username,password);
AppUser user = new AppUser();
user.setPhone(username);
user.setPassword(encoder.encode(password));
user.setLastName("Admin");
user.setFirstName("Master");
user.setRole(Role.ADMIN);
return userRepository.save(user);
})
).flatMap(u -> {
log.info("User in db: [{}]", u);
return Mono.empty();
}).subscribe();
};
}
}

View File

@@ -1,15 +1,38 @@
package com.example.dateplanner.configurations;
import com.example.dateplanner.configurations.filters.ErrorHandlingFilter;
import com.example.dateplanner.configurations.handlers.JwtAuthenticationConverter;
import com.example.dateplanner.configurations.handlers.JwtAuthenticationManager;
import com.example.dateplanner.configurations.handlers.JwtAuthenticationSuccessHandler;
import com.example.dateplanner.configurations.handlers.JwtLogoutSuccessHandler;
import com.example.dateplanner.repositories.AppUserRepository;
import com.example.dateplanner.services.JwtService;
import com.example.dateplanner.services.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableReactiveMethodSecurity;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.authentication.AuthenticationWebFilter;
import org.springframework.security.web.server.csrf.CookieServerCsrfTokenRepository;
import org.springframework.security.web.server.csrf.ServerCsrfTokenRequestAttributeHandler;
import org.springframework.security.web.server.savedrequest.ServerRequestCache;
import org.springframework.security.web.server.savedrequest.WebSessionServerRequestCache;
import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher;
import reactor.core.publisher.Mono;
import java.net.URI;
@Slf4j
@Configuration
@@ -17,49 +40,53 @@ import org.springframework.security.web.server.util.matcher.PathPatternParserSer
@RequiredArgsConstructor
@EnableReactiveMethodSecurity
public class SecurityConfig {
private final AppUserRepository userRepository;
private final JwtService jwtService;
// Цепочка для API (Basic Auth) - CSRF отключаем
// @Bean
// @Order(1)
// public SecurityWebFilterChain apiFilterChain(ServerHttpSecurity http) {
// return http
// .securityMatcher(new PathPatternParserServerWebExchangeMatcher("/api/**"))
// .authorizeExchange(exchange -> exchange
// .pathMatchers("/api/mobile/login","/api/news/paged", "/api/documents/paged").permitAll()
// .anyExchange().authenticated()
// )
// .httpBasic(Customizer.withDefaults())
// .formLogin(ServerHttpSecurity.FormLoginSpec::disable)
// .csrf(ServerHttpSecurity.CsrfSpec::disable) // CSRF отключаем для API
// .authenticationManager(reactiveAuthenticationManager())
// .build();
// }
//
// // Цепочка для веб-интерфейса (Form Login) - CSRF включаем
// @Bean
// @Order(2)
// public SecurityWebFilterChain webFilterChain(ServerHttpSecurity http) {
// ServerCsrfTokenRequestAttributeHandler requestHandler = new ServerCsrfTokenRequestAttributeHandler();
// requestHandler.setTokenFromMultipartDataEnabled(true);
//
// AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(authenticationManager());
// authenticationWebFilter.setServerAuthenticationConverter(authenticationConverter());
//
// ErrorHandlingFilter errorHandlingFilter = new ErrorHandlingFilter();
//
// return http
// .securityMatcher(new PathPatternParserServerWebExchangeMatcher("/account/**"))
// .csrf(csrf -> csrf.csrfTokenRequestHandler(requestHandler).csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()))
// .addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION)
// .addFilterAfter(errorHandlingFilter, SecurityWebFiltersOrder.AUTHENTICATION)
// .authorizeExchange(exchange -> exchange
// .pathMatchers("/account/login").permitAll()
// .anyExchange().authenticated()
// )
// .formLogin(loginSpec -> loginSpec.loginPage("/account/login").authenticationSuccessHandler(authenticationSuccessHandler()))
// .logout(logoutSpec -> logoutSpec.logoutSuccessHandler(logoutSuccessHandler()))
// .requestCache(requestCacheSpec -> requestCacheSpec.requestCache(serverRequestCache()))
// .build();
// }
@Bean
@Order(1)
public SecurityWebFilterChain apiFilterChain(ServerHttpSecurity http) {
return http
.securityMatcher(new PathPatternParserServerWebExchangeMatcher("/api/**"))
.authorizeExchange(exchange -> exchange
.pathMatchers("/api/mobile/login").permitAll()
.anyExchange().authenticated()
)
.httpBasic(Customizer.withDefaults())
.formLogin(ServerHttpSecurity.FormLoginSpec::disable)
.csrf(ServerHttpSecurity.CsrfSpec::disable) // CSRF отключаем для API
.authenticationManager(reactiveAuthenticationManager())
.build();
}
// Цепочка для веб-интерфейса (Form Login) - CSRF включаем
@Bean
@Order(2)
public SecurityWebFilterChain webFilterChain(ServerHttpSecurity http) {
ServerCsrfTokenRequestAttributeHandler requestHandler = new ServerCsrfTokenRequestAttributeHandler();
requestHandler.setTokenFromMultipartDataEnabled(true);
AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(authenticationManager());
authenticationWebFilter.setServerAuthenticationConverter(authenticationConverter());
ErrorHandlingFilter errorHandlingFilter = new ErrorHandlingFilter();
return http
.securityMatcher(new PathPatternParserServerWebExchangeMatcher("/account/**"))
.csrf(csrf -> csrf.csrfTokenRequestHandler(requestHandler).csrfTokenRepository(CookieServerCsrfTokenRepository.withHttpOnlyFalse()))
.addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.addFilterAfter(errorHandlingFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.authorizeExchange(exchange -> exchange
.pathMatchers("/account/login").permitAll()
.anyExchange().authenticated()
)
.formLogin(loginSpec -> loginSpec.loginPage("/account/login").authenticationSuccessHandler(authenticationSuccessHandler()))
.logout(logoutSpec -> logoutSpec.logoutSuccessHandler(logoutSuccessHandler()))
.requestCache(requestCacheSpec -> requestCacheSpec.requestCache(serverRequestCache()))
.build();
}
// Цепочка для всех остальных путей (публичные)
@Bean
@@ -73,4 +100,62 @@ public class SecurityConfig {
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.build();
}
@Bean
public ServerRequestCache serverRequestCache() {
return new WebSessionServerRequestCache();
}
@Bean
public JwtAuthenticationManager authenticationManager() {
return new JwtAuthenticationManager(jwtService, userService(), passwordEncoder());
}
@Bean
public JwtAuthenticationSuccessHandler authenticationSuccessHandler(){
return new JwtAuthenticationSuccessHandler(jwtService,serverRequestCache());
}
@Bean
public JwtLogoutSuccessHandler logoutSuccessHandler(){
JwtLogoutSuccessHandler logoutSuccessHandler = new JwtLogoutSuccessHandler();
logoutSuccessHandler.setLogoutSuccessUrl(URI.create("/"));
return logoutSuccessHandler;
}
@Bean
public JwtAuthenticationConverter authenticationConverter(){
return new JwtAuthenticationConverter();
}
@Bean
public ReactiveAuthenticationManager reactiveAuthenticationManager() {
UserDetailsRepositoryReactiveAuthenticationManager manager = new UserDetailsRepositoryReactiveAuthenticationManager(userService());
manager.setPasswordEncoder(passwordEncoder());
manager.setUserDetailsPasswordService(userDetailsPasswordService());
return manager;
}
@Bean
public ReactiveUserDetailsPasswordService userDetailsPasswordService() {
return new ReactiveUserDetailsPasswordService() {
@Override
public Mono<UserDetails> updatePassword(UserDetails user, String newPassword) {
return userRepository.findByPhone(user.getUsername()).flatMap(appUser -> {
appUser.setPassword(passwordEncoder().encode(newPassword));
return userRepository.save(appUser);
});
}
};
}
@Bean
public UserService userService(){
return new UserService(userRepository,passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}

View File

@@ -0,0 +1,50 @@
package com.example.dateplanner.configurations.filters;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.net.URI;
@Slf4j
public class ErrorHandlingFilter implements WebFilter {
@Override
public @NotNull Mono<Void> filter(@NotNull ServerWebExchange exchange, WebFilterChain chain) {
//log.info("error handler!");
return chain.filter(exchange)
.onErrorResume(error -> handleError(exchange, error))
.then(Mono.defer(() -> handleNotFound(exchange)));
}
private Mono<Void> handleError(ServerWebExchange exchange, Throwable error) {
HttpStatusCode status = determineStatusCode(error);
return prepareRedirect(exchange, status);
}
private Mono<Void> handleNotFound(ServerWebExchange exchange) {
if (exchange.getResponse().getStatusCode() == null) {
return prepareRedirect(exchange, HttpStatus.NOT_FOUND);
}
return Mono.empty();
}
private Mono<Void> prepareRedirect(ServerWebExchange exchange, HttpStatusCode status) {
exchange.getResponse().setStatusCode(HttpStatus.FOUND); // 302 Redirect
String redirectUrl = "/error?status=" + status.value();
exchange.getResponse().getHeaders().setLocation(URI.create(redirectUrl));
return exchange.getResponse().setComplete();
}
private HttpStatusCode determineStatusCode(Throwable error) {
if (error instanceof ResponseStatusException) {
return ((ResponseStatusException) error).getStatusCode();
}
return HttpStatus.INTERNAL_SERVER_ERROR;
}
}

View File

@@ -0,0 +1,34 @@
package com.example.dateplanner.configurations.handlers;
import com.example.dateplanner.utils.CookieUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpCookie;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.authentication.ServerAuthenticationConverter;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Slf4j
public class JwtAuthenticationConverter implements ServerAuthenticationConverter {
@Override
public Mono<Authentication> convert(ServerWebExchange exchange) {
//log.info("start auth converter");
String accessToken = extractTokenFromCookie(exchange.getRequest(), CookieUtil.getInstance().getACCESS());
String refreshToken = extractTokenFromCookie(exchange.getRequest(), CookieUtil.getInstance().getREFRESH());
//log.info("access and refresh tokens: {},{}",accessToken,refreshToken);
if(accessToken.equals("") && refreshToken.equals("")){
//log.info("token not found");
return Mono.empty();
}else{
//log.info("found access: {}, and refresh: {} tokens", accessToken,refreshToken);
return Mono.just(new UsernamePasswordAuthenticationToken(accessToken, refreshToken));
}
}
private String extractTokenFromCookie(ServerHttpRequest request, String cookieName) {
HttpCookie cookie = request.getCookies().getFirst(cookieName);
return cookie != null ? cookie.getValue() : "";
}
}

View File

@@ -0,0 +1,119 @@
package com.example.dateplanner.configurations.handlers;
import com.example.dateplanner.services.JwtService;
import com.example.dateplanner.services.UserService;
import com.example.dateplanner.utils.CookieUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseCookie;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.time.Duration;
@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationManager implements ReactiveAuthenticationManager {
private final JwtService jwt;
private final UserService userService;
private final PasswordEncoder encoder;
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
//log.info("start auth manager");
String accessToken = authentication.getPrincipal().toString();
String refreshToken = authentication.getCredentials().toString();
log.info("refresh is {},{}", accessToken, refreshToken);
if(jwt.validateToken(accessToken)){
//log.info("access token is valid");
return Mono.deferContextual(contextView -> {
ServerWebExchange exchange = contextView.get(ServerWebExchange.class);
String digitalSignature = exchange.getRequest().getHeaders().getFirst("User-Agent");
assert digitalSignature != null;
if(digitalSignature.equals(jwt.getDigitalSignatureFromToken(accessToken))) {
//log.info("the correct digital signature has been presented");
return userService.findByUsername(jwt.getUsernameFromToken(accessToken)).flatMap(user -> Mono.just(new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities())));
}else{
//log.error("the digital signature does not match the signature in the token [{}] | [{}]", digitalSignature, jwt.getDigitalSignatureFromToken(accessToken));
ServerHttpResponse response = exchange.getResponse();
response.addCookie(ResponseCookie.from(CookieUtil.getInstance().getACCESS(), "")
.httpOnly(true)
.path("/")
.maxAge(0)
.build());
response.addCookie(ResponseCookie.from(CookieUtil.getInstance().getREFRESH(), "")
.httpOnly(true)
.path("/")
.maxAge(0)
.build());
response.addCookie(ResponseCookie.from(CookieUtil.getInstance().getSESSION(), "")
.httpOnly(true)
.path("/")
.maxAge(0)
.build());
return Mono.error(new BadCredentialsException("Authentication failed"));
}
});
}else if(jwt.validateToken(refreshToken)){
//log.info("access token is not valid, but refresh token is ok");
return Mono.deferContextual(contextView -> {
ServerWebExchange exchange = contextView.get(ServerWebExchange.class);
String digitalSignature = exchange.getRequest().getHeaders().getFirst("User-Agent");
assert digitalSignature != null;
if(digitalSignature.equals(jwt.getDigitalSignatureFromToken(refreshToken))) {
//log.info("digital signature of refresh token is ok - update jwt");
return userService.findByUsername(jwt.getUsernameFromToken(refreshToken)).flatMap(user -> {
String username = user.getUsername();
String newAccessToken = jwt.generateAccessToken(username, digitalSignature);
String newRefreshToken = jwt.generateRefreshToken(username, digitalSignature);
ResponseCookie accessCookie = ResponseCookie.from(CookieUtil.getInstance().getACCESS(), newAccessToken)
.httpOnly(true)
.maxAge(Duration.ofMillis(jwt.getAccessExpiration()))
.path("/")
.build();
ResponseCookie refreshCookie = ResponseCookie.from(CookieUtil.getInstance().getREFRESH(), newRefreshToken)
.httpOnly(true)
.maxAge(Duration.ofMillis(jwt.getRefreshExpiration()))
.path("/")
.build();
exchange.getResponse().addCookie(accessCookie);
exchange.getResponse().addCookie(refreshCookie);
return Mono.just(new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()));
}).switchIfEmpty(Mono.error(new BadCredentialsException("Authentication failed")));
}else{
//log.error("the digital signature of refresh token does not match the signature in the token [{}] | [{}]", digitalSignature, jwt.getDigitalSignatureFromToken(refreshToken));
ResponseCookie refreshCookie = ResponseCookie.from(CookieUtil.getInstance().getREFRESH(), "")
.httpOnly(true)
.path("/")
.maxAge(0)
.build();
exchange.getResponse().addCookie(refreshCookie);
return baseAuth(authentication);
}
});
}else{
return baseAuth(authentication);
}
}
private Mono<Authentication> baseAuth(Authentication authentication){
log.info("access and refresh token is not valid - try authenticate by basic login");
String username = authentication.getPrincipal().toString();
String password = authentication.getCredentials().toString();
return userService.findByUsername(username).flatMap(user -> {
if(encoder.matches(password,user.getPassword())){
return Mono.just(new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities()));
} else{
return Mono.error(new BadCredentialsException("Authentication failed"));
}
}).cast(Authentication.class).switchIfEmpty(Mono.error(new BadCredentialsException("Authentication failed")));
}
}

View File

@@ -0,0 +1,56 @@
package com.example.dateplanner.configurations.handlers;
import com.example.dateplanner.services.JwtService;
import com.example.dateplanner.utils.CookieUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.ServerAuthenticationSuccessHandler;
import org.springframework.security.web.server.savedrequest.ServerRequestCache;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.time.Duration;
@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationSuccessHandler implements ServerAuthenticationSuccessHandler {
private final JwtService jwt;
private final ServerRequestCache requestCache;
@Override
public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
//log.info("auth login success");
String username = authentication.getName();
String digitalSignature = webFilterExchange.getExchange().getRequest().getHeaders().getFirst("User-Agent");
String accessToken = jwt.generateAccessToken(username, digitalSignature);
String refreshToken = jwt.generateRefreshToken(username, digitalSignature);
ResponseCookie accessCookie = ResponseCookie.from(CookieUtil.getInstance().getACCESS(), accessToken)
.httpOnly(true)
.maxAge(Duration.ofMillis(jwt.getAccessExpiration()))
.path("/")
.build();
ResponseCookie refreshCookie = ResponseCookie.from(CookieUtil.getInstance().getREFRESH(), refreshToken)
.httpOnly(true)
.maxAge(Duration.ofMillis(jwt.getRefreshExpiration()))
.path("/")
.build();
ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
response.addCookie(accessCookie);
response.addCookie(refreshCookie);
return requestCache.getRedirectUri(webFilterExchange.getExchange())
.defaultIfEmpty(URI.create("/"))
.flatMap(redirectUri -> {
response.setStatusCode(HttpStatus.FOUND);
webFilterExchange.getExchange().getResponse().getHeaders().setLocation(redirectUri);
return webFilterExchange.getExchange().getResponse().setComplete();
});
}
}

View File

@@ -0,0 +1,30 @@
package com.example.dateplanner.configurations.handlers;
import com.example.dateplanner.utils.CookieUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseCookie;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.logout.RedirectServerLogoutSuccessHandler;
import reactor.core.publisher.Mono;
@Slf4j
public class JwtLogoutSuccessHandler extends RedirectServerLogoutSuccessHandler {
@Override
public Mono<Void> onLogoutSuccess(WebFilterExchange webFilterExchange, Authentication authentication) {
//log.info("auth logout success");
ServerHttpResponse response = webFilterExchange.getExchange().getResponse();
response.addCookie(ResponseCookie.from(CookieUtil.getInstance().getACCESS(), "")
.httpOnly(true)
.path("/")
.maxAge(0)
.build());
response.addCookie(ResponseCookie.from(CookieUtil.getInstance().getREFRESH(), "")
.httpOnly(true)
.path("/")
.maxAge(0)
.build());
return super.onLogoutSuccess(webFilterExchange, authentication);
}
}

View File

@@ -0,0 +1,43 @@
package com.example.dateplanner.controllers.advice;
import com.example.dateplanner.models.entities.AppUser;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@ControllerAdvice
@PropertySource("classpath:application.properties")
public class SecurityAdviceController {
@Value("${minio.cdn}")
private String cdn;
@ModelAttribute(name = "cdn")
public Mono<String> cdnUrl() {
return Mono.just(cdn);
}
@ModelAttribute(name = "auth")
public Mono<Boolean> isAuthenticate(@AuthenticationPrincipal AppUser user){
if(user != null){
return Mono.just(true);
}else{
return Mono.just(false);
}
}
@ModelAttribute(name = "user")
public Mono<AppUser> provideUser(@AuthenticationPrincipal AppUser user){
return Mono.justOrEmpty(user);
}
// @ModelAttribute(name = "_csrf")
// public Mono<CsrfToken> provideCsrfToken(ServerWebExchange exchange) {
// return exchange.getAttribute(CsrfToken.class.getName());
// }
}

View File

@@ -0,0 +1,34 @@
package com.example.dateplanner.controllers.web;
import com.example.dateplanner.models.entities.AppUser;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
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.reactive.result.view.Rendering;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@Controller
@RequiredArgsConstructor
@RequestMapping("/account")
public class AccountController {
@GetMapping("/login")
public Mono<Rendering> loginPage() {
Map<String, String> model = new HashMap<>();
model.put("title", "Login");
model.put("index", "login");
return Mono.just(
Rendering.view("template")
.model(model)
.build()
);
}
}

View File

@@ -2,9 +2,11 @@ package com.example.dateplanner.repositories;
import com.example.dateplanner.models.entities.AppUser;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import reactor.core.publisher.Mono;
import java.util.UUID;
public interface AppUserRepository extends R2dbcRepository<AppUser, UUID> {
Mono<AppUser> findByPhone(String phone);
}

View File

@@ -1,12 +0,0 @@
package com.example.dateplanner.services;
import com.example.dateplanner.repositories.AppUserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class AppUserService {
private final AppUserRepository appUserRepository;
}

View File

@@ -0,0 +1,82 @@
package com.example.dateplanner.services;
import com.example.dateplanner.utils.PasswordGenerator;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
@Slf4j
@Getter
@Service
public class JwtService {
private final long accessExpiration;
private final long refreshExpiration;
private final SecretKey key;
public JwtService(@Value("${jwt.secret}") String secret, @Value("${jwt.expiration.access}") long accessExpiration, @Value("${jwt.expiration.refresh}") long refreshExpiration){
this.accessExpiration = accessExpiration;
this.refreshExpiration = refreshExpiration;
String secretKey = PasswordGenerator.generatePassword(64, PasswordGenerator.Complexity.HARD);
key = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
public String generateAccessToken(String username, String digitalSignature){
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + accessExpiration))
.signWith(key, SignatureAlgorithm.HS512)
.claim("digital_signature",digitalSignature)
.compact();
}
public String generateRefreshToken(String username, String digitalSignature) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + refreshExpiration))
.signWith(key, SignatureAlgorithm.HS512)
.claim("digital_signature",digitalSignature)
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
public String getUsernameFromToken(String token) {
try {
Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
return claims.getSubject();
} catch (JwtException | IllegalArgumentException e) {
return null;
}
}
public String getDigitalSignatureFromToken(String token) {
try {
Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
if (claims.containsKey("digital_signature")) {
return (String) claims.get("digital_signature");
} else {
throw new IllegalArgumentException("Token does not contain digital_signature claim");
}
} catch (JwtException | IllegalArgumentException e) {
return null;
}
}
}

View File

@@ -0,0 +1,21 @@
package com.example.dateplanner.services;
import com.example.dateplanner.repositories.AppUserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
@Service
@RequiredArgsConstructor
public class UserService implements ReactiveUserDetailsService {
private final AppUserRepository appUserRepository;
private final PasswordEncoder passwordEncoder;
@Override
public Mono<UserDetails> findByUsername(String username) {
return appUserRepository.findByPhone(username).flatMap(Mono::just).cast(UserDetails.class);
}
}

View File

@@ -0,0 +1,27 @@
package com.example.dateplanner.utils;
import lombok.Getter;
@Getter
public class CookieUtil {
private static CookieUtil instance = null;
private final String REFRESH;
private final String ACCESS;
private final String SESSION;
protected CookieUtil(){
String appName = "pstweb";
REFRESH = appName + "-refresh";
ACCESS = appName + "-access";
SESSION = appName + "-session";
}
public static CookieUtil getInstance(){
if(instance == null){
instance = new CookieUtil();
}
return instance;
}
}

View File

@@ -0,0 +1,49 @@
package com.example.dateplanner.utils;
import java.util.Random;
public class PasswordGenerator {
private static final String LOWERCASE_LETTERS = "abcdefghijklmnopqrstuvwxyz";
private static final String UPPERCASE_LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final String NUMBERS = "0123456789";
private static final String SPECIAL_CHARACTERS = "!@#$%^&*()-_=+[]{}|;:,.<>?";
private static final String EASY_CHARACTERS = LOWERCASE_LETTERS + UPPERCASE_LETTERS;
private static final String MEDIUM_CHARACTERS = EASY_CHARACTERS + NUMBERS;
private static final String HARD_CHARACTERS = MEDIUM_CHARACTERS + SPECIAL_CHARACTERS;
private static final Random RANDOM = new Random();
public enum Complexity {
EASY, MEDIUM, HARD
}
public static String generatePassword(int length, Complexity complexity) {
if (length <= 0) {
throw new IllegalArgumentException("Password length must be greater than zero");
}
StringBuilder password = new StringBuilder(length);
String characters;
switch (complexity) {
case EASY:
characters = EASY_CHARACTERS;
break;
case MEDIUM:
characters = MEDIUM_CHARACTERS;
break;
case HARD:
characters = HARD_CHARACTERS;
break;
default:
throw new IllegalArgumentException("Invalid complexity level");
}
for (int i = 0; i < length; i++) {
password.append(characters.charAt(RANDOM.nextInt(characters.length())));
}
return password.toString();
}
}

View File

@@ -16,8 +16,10 @@ CREATE TABLE organizations (
-- Таблица пользователей
CREATE TABLE app_users (
uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username VARCHAR(100) NOT NULL UNIQUE,
phone VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
role VARCHAR(50) NOT NULL,
dating_profile_uuid UUID,
balance INTEGER NOT NULL DEFAULT 0,
@@ -120,7 +122,7 @@ CREATE TRIGGER update_organizations_updated_at
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- Индексы для производительности
CREATE INDEX idx_app_users_username ON app_users(username);
CREATE INDEX idx_app_users_phone ON app_users(phone);
CREATE INDEX idx_app_users_dating_profile_uuid ON app_users(dating_profile_uuid);
CREATE INDEX idx_dating_places_organization_uuid ON dating_places(organization_uuid);
CREATE INDEX idx_dating_places_dating_type ON dating_places(dating_type);

View File

@@ -1,3 +1,42 @@
.user-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: #e74c3c;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 14px;
border: 2px solid white;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.user-info {
padding: 15px;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
gap: 10px;
}
.dropdown-item {
padding: 10px 15px;
display: flex;
align-items: center;
gap: 10px;
transition: all 0.2s;
}
.dropdown-item:hover {
background: rgba(231, 76, 60, 0.1) !important;
color: #e74c3c !important;
}
/* Модальное окно авторизации */
.auth-tabs {
border: none ;

View File

@@ -1,5 +1,46 @@
function initHeader($header){
console.log("init header date")
let authSection
if(!auth) {
authSection = `
<div id="authButtonContainer" class="d-flex align-items-center ms-0 ms-lg-3 mt-2 mt-lg-0">
<button class="auth-btn" data-bs-toggle="modal" data-bs-target="#authModal" style="padding-top: 12px; padding-bottom: 12px;">
<i class="fas fa-user"></i>
<span class="auth-btn-text">Войти</span>
</button>
</div>
`;
} else {
authSection = `
<div id="userProfileContainer" class="dropdown ms-0 ms-lg-3 mt-2 mt-lg-0">
<a href="#" class="d-flex align-items-center text-decoration-none dropdown-toggle"
data-bs-toggle="dropdown" aria-expanded="false">
<div class="user-avatar" id="userAvatar">А</div>
<span class="ms-2 d-md-inline" id="userName">Алексей</span>
</a>
<ul class="dropdown-menu dropdown-user">
<li>
<div class="user-info">
<div class="user-avatar" id="dropdownUserAvatar">А</div>
<div class="user-details">
<h6 id="dropdownUserName">Алексей Петров</h6>
<small id="dropdownUserEmail">alexey@example.com</small>
</div>
</div>
</li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#"><i class="fas fa-heart"></i> Мои избранные</a></li>
<li><a class="dropdown-item" href="#"><i class="fas fa-calendar-alt"></i> Мои бронирования</a></li>
<li><a class="dropdown-item" href="#"><i class="fas fa-star"></i> Мои отзывы</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="showAddPlaceModal()"><i class="fas fa-plus-circle"></i> Добавить место</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#"><i class="fas fa-cog"></i> Настройки</a></li>
<li><a class="dropdown-item text-danger" href="#" onclick="logout()"><i class="fas fa-sign-out-alt"></i> Выйти</a></li>
</ul>
</div>
`;
}
$header.append(`
<nav class="navbar navbar-expand-lg navbar-light bg-white shadow-sm fixed-top">
<div class="container">
@@ -27,12 +68,11 @@ function initHeader($header){
<button class="btn btn-heart ms-0 ms-lg-3" onclick="showLoginModal()">
<i class="fas fa-plus me-2"></i>Добавить место
</button>
<div id="authButtonContainer" class="d-flex align-items-center ms-0 ms-lg-3 mt-2 mt-lg-0">
<button class="auth-btn" data-bs-toggle="modal" data-bs-target="#authModal" style="padding-top: 12px; padding-bottom: 12px;">
<i class="fas fa-user"></i>
<span class="auth-btn-text">Войти</span>
</button>
</div>
<!-- Кнопка авторизации (показывается когда пользователь не авторизован) -->
<!-- или -->
<!-- Профиль пользователя (показывается после авторизации) -->
${authSection}
</div>
</div>
</nav>
@@ -62,7 +102,7 @@ function initHeader($header){
<div class="tab-content" id="authTabContent">
<!-- Форма входа -->
<div class="tab-pane fade show active" id="login" role="tabpanel">
<form id="loginForm">
<form id="loginForm" method="POST" action="/account/login">
<div class="mb-3">
<label class="form-label">Email или телефон</label>
<input type="text" class="form-control" id="loginEmail" placeholder="example@mail.ru" required>

View File

@@ -0,0 +1,652 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DatePlanner - Идеальные места для свиданий</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<style>
/* Оставляем все существующие стили */
:root {
--love-red: #e74c3c;
--romantic-pink: #fd79a8;
--cozy-orange: #e67e22;
--intellectual-blue: #3498db;
--adventure-green: #2ecc71;
--mystery-purple: #9b59b6;
--dark-mode: #2d3436;
--light-bg: #f9f7f7;
}
/* ... все остальные стили без изменений ... */
/* Добавляем новые стили для иконки авторизации */
.auth-btn {
background: linear-gradient(135deg, var(--love-red) 0%, var(--romantic-pink) 100%);
color: white;
border: none;
border-radius: 50px;
padding: 8px 20px;
font-weight: 600;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 8px;
text-decoration: none;
}
.auth-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(231, 76, 60, 0.3);
color: white;
}
.auth-btn-outline {
background: transparent;
border: 2px solid var(--love-red);
color: var(--love-red);
padding: 8px 20px;
border-radius: 50px;
font-weight: 600;
transition: all 0.3s;
display: flex;
align-items: center;
gap: 8px;
text-decoration: none;
}
.auth-btn-outline:hover {
background: var(--love-red);
color: white;
transform: translateY(-2px);
}
.user-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, var(--love-red) 0%, var(--romantic-pink) 100%);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 14px;
border: 2px solid white;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.dropdown-user {
min-width: 250px;
}
.user-info {
padding: 15px;
border-bottom: 1px solid #eee;
display: flex;
align-items: center;
gap: 10px;
}
.user-details h6 {
margin: 0;
font-weight: 600;
}
.user-details small {
color: #666;
font-size: 0.85rem;
}
.dropdown-item {
padding: 10px 15px;
display: flex;
align-items: center;
gap: 10px;
transition: all 0.2s;
}
.dropdown-item:hover {
background: rgba(231, 76, 60, 0.1);
color: var(--love-red);
}
.dropdown-divider {
margin: 5px 0;
}
/* Модальное окно авторизации */
.auth-tabs {
border: none;
margin-bottom: 20px;
}
.auth-tabs .nav-link {
border: none;
color: #666;
font-weight: 500;
padding: 10px 0;
margin: 0 15px;
position: relative;
}
.auth-tabs .nav-link.active {
color: var(--love-red);
background: none;
}
.auth-tabs .nav-link.active:after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background: var(--love-red);
border-radius: 3px;
}
.social-auth-btn {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 8px;
background: white;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin-bottom: 10px;
transition: all 0.3s;
font-weight: 500;
}
.social-auth-btn:hover {
border-color: var(--love-red);
background: rgba(231, 76, 60, 0.05);
}
.social-auth-btn.vk { color: #4C75A3; }
.social-auth-btn.google { color: #DB4437; }
.social-auth-btn.yandex { color: #FF0000; }
.auth-divider {
text-align: center;
position: relative;
margin: 20px 0;
color: #666;
}
.auth-divider:before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: #eee;
z-index: 1;
}
.auth-divider span {
background: white;
padding: 0 15px;
position: relative;
z-index: 2;
}
.form-check-label {
font-size: 0.9rem;
}
.forgot-password {
font-size: 0.9rem;
color: var(--love-red);
text-decoration: none;
}
.forgot-password:hover {
text-decoration: underline;
}
/* Адаптивность */
@media (max-width: 768px) {
.auth-btn-text {
display: none;
}
.auth-btn, .auth-btn-outline {
padding: 8px 12px;
}
.dropdown-user {
min-width: 200px;
}
}
/* Сообщение об успешной авторизации */
.auth-success {
display: none;
background: #d4edda;
color: #155724;
padding: 15px;
border-radius: 10px;
margin-bottom: 20px;
border: 1px solid #c3e6cb;
}
.auth-success.show {
display: block;
animation: fadeIn 0.5s;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
</head>
<body>
<!-- Навигация с добавленной кнопкой авторизации -->
<nav class="navbar navbar-expand-lg navbar-light bg-white shadow-sm fixed-top">
<div class="container">
<a class="navbar-brand" href="#">
<i class="fas fa-heart heart-icon me-2"></i>DatePlanner
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
<li class="nav-item">
<a class="nav-link" href="#places">Места</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#map-section">Карта</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#ideas">Идеи</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#admin">Для организаций</a>
</li>
</ul>
<!-- Кнопка авторизации (показывается когда пользователь не авторизован) -->
<div id="authButtonContainer" class="d-flex align-items-center ms-3">
<button class="auth-btn" onclick="showAuthModal()">
<i class="fas fa-user"></i>
<span class="auth-btn-text">Войти</span>
</button>
</div>
<!-- Профиль пользователя (показывается после авторизации) -->
<div id="userProfileContainer" class="dropdown ms-3" style="display: none;">
<a href="#" class="d-flex align-items-center text-decoration-none dropdown-toggle"
data-bs-toggle="dropdown" aria-expanded="false">
<div class="user-avatar" id="userAvatar">А</div>
<span class="ms-2 d-none d-md-inline" id="userName">Алексей</span>
</a>
<ul class="dropdown-menu dropdown-user">
<li>
<div class="user-info">
<div class="user-avatar" id="dropdownUserAvatar">А</div>
<div class="user-details">
<h6 id="dropdownUserName">Алексей Петров</h6>
<small id="dropdownUserEmail">alexey@example.com</small>
</div>
</div>
</li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#"><i class="fas fa-heart"></i> Мои избранные</a></li>
<li><a class="dropdown-item" href="#"><i class="fas fa-calendar-alt"></i> Мои бронирования</a></li>
<li><a class="dropdown-item" href="#"><i class="fas fa-star"></i> Мои отзывы</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#" onclick="showAddPlaceModal()"><i class="fas fa-plus-circle"></i> Добавить место</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="#"><i class="fas fa-cog"></i> Настройки</a></li>
<li><a class="dropdown-item text-danger" href="#" onclick="logout()"><i class="fas fa-sign-out-alt"></i> Выйти</a></li>
</ul>
</div>
<button class="btn btn-heart ms-3" onclick="checkAuthBeforeAddPlace()">
<i class="fas fa-plus me-2"></i>Добавить место
</button>
</div>
</div>
</nav>
<!-- Герой секция (оставляем без изменений) -->
<section class="love-gradient hero-section">
<!-- ... существующий код герой секции без изменений ... -->
<div class="heart-animation" style="top:10%;left:5%">❤️</div>
<div class="heart-animation" style="top:30%;right:10%">❤️</div>
<div class="heart-animation" style="bottom:20%;left:15%">❤️</div>
<div class="heart-animation" style="bottom:40%;right:20%">❤️</div>
<div class="container">
<div class="row align-items-center">
<div class="col-lg-6">
<h1 class="hero-title mb-4">Найдите идеальное место для свидания</h1>
<p class="lead mb-4">Более 500 проверенных локаций: от романтических ужинов до экстремальных приключений. Подберите свидание по настроению, бюджету и интересам.</p>
<div class="d-flex flex-wrap gap-3">
<a href="#places" class="btn btn-heart">
<i class="fas fa-search me-2"></i>Найти место
</a>
<a href="#map-section" class="btn btn-outline-light">
<i class="fas fa-map-marked-alt me-2"></i>Посмотреть на карте
</a>
</div>
</div>
<div class="col-lg-6">
<img src="https://images.unsplash.com/photo-1518568814500-bf0f8d125f46?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80"
class="img-fluid rounded-3 shadow-lg"
alt="Свидание в парке">
</div>
</div>
</div>
</section>
<!-- Остальные секции остаются без изменений -->
<!-- Фильтры -->
<section class="py-5 bg-light">
<!-- ... существующий код фильтров ... -->
</section>
<!-- Модальное окно авторизации -->
<div class="modal fade" id="authModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header border-0">
<h5 class="modal-title">Вход в DatePlanner</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-4">
<div id="authSuccess" class="auth-success">
<i class="fas fa-check-circle me-2"></i>
Вы успешно вошли в систему!
</div>
<ul class="nav nav-tabs auth-tabs" id="authTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="login-tab" data-bs-toggle="tab" data-bs-target="#login" type="button">
Вход
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="register-tab" data-bs-toggle="tab" data-bs-target="#register" type="button">
Регистрация
</button>
</li>
</ul>
<div class="tab-content" id="authTabContent">
<!-- Форма входа -->
<div class="tab-pane fade show active" id="login" role="tabpanel">
<form id="loginForm">
<div class="mb-3">
<label class="form-label">Email или телефон</label>
<input type="text" class="form-control" id="loginEmail" placeholder="example@mail.ru" required>
</div>
<div class="mb-3">
<label class="form-label">Пароль</label>
<input type="password" class="form-control" id="loginPassword" required>
<div class="text-end mt-1">
<a href="#" class="forgot-password" onclick="showForgotPassword()">Забыли пароль?</a>
</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="rememberMe">
<label class="form-check-label" for="rememberMe">Запомнить меня</label>
</div>
<button type="submit" class="btn btn-heart w-100">Войти</button>
</form>
<div class="auth-divider">
<span>или войдите через</span>
</div>
<button class="social-auth-btn vk" onclick="loginWithVK()">
<i class="fab fa-vk"></i> ВКонтакте
</button>
<button class="social-auth-btn google" onclick="loginWithGoogle()">
<i class="fab fa-google"></i> Google
</button>
<button class="social-auth-btn yandex" onclick="loginWithYandex()">
<i class="fab fa-yandex"></i> Яндекс
</button>
</div>
<!-- Форма регистрации -->
<div class="tab-pane fade" id="register" role="tabpanel">
<form id="registerForm">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Имя</label>
<input type="text" class="form-control" id="registerFirstName" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Фамилия</label>
<input type="text" class="form-control" id="registerLastName">
</div>
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<input type="email" class="form-control" id="registerEmail" required>
</div>
<div class="mb-3">
<label class="form-label">Пароль</label>
<input type="password" class="form-control" id="registerPassword" required>
<small class="text-muted">Минимум 6 символов</small>
</div>
<div class="mb-3">
<label class="form-label">Подтвердите пароль</label>
<input type="password" class="form-control" id="registerPasswordConfirm" required>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="acceptTerms" required>
<label class="form-check-label" for="acceptTerms">
Я принимаю <a href="#" class="forgot-password">условия использования</a> и
<a href="#" class="forgot-password">политику конфиденциальности</a>
</label>
</div>
<button type="submit" class="btn btn-heart w-100">Зарегистрироваться</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Скрипты -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
// Инициализация модального окна авторизации
const authModal = new bootstrap.Modal(document.getElementById('authModal'));
let isAuthenticated = false; // Флаг авторизации
// Показать модальное окно авторизации
function showAuthModal() {
authModal.show();
}
// Проверка авторизации перед добавлением места
function checkAuthBeforeAddPlace() {
if (isAuthenticated) {
showAddPlaceModal();
} else {
showAuthModal();
// Переключаем на вкладку регистрации, если пользователь хочет добавить место
setTimeout(() => {
const registerTab = document.getElementById('register-tab');
if (registerTab) {
registerTab.click();
}
}, 500);
}
}
// Обработка формы входа
document.getElementById('loginForm').addEventListener('submit', function(e) {
e.preventDefault();
const email = document.getElementById('loginEmail').value;
const password = document.getElementById('loginPassword').value;
// Здесь должна быть реальная проверка на сервере
// Для демо просто имитируем успешный вход
if (email && password) {
// Симуляция успешного входа
loginUser({
firstName: 'Алексей',
lastName: 'Петров',
email: email,
avatarLetter: 'А'
});
// Показать сообщение об успехе
document.getElementById('authSuccess').classList.add('show');
// Закрыть модальное окно через 2 секунды
setTimeout(() => {
authModal.hide();
document.getElementById('authSuccess').classList.remove('show');
this.reset();
}, 2000);
} else {
alert('Пожалуйста, заполните все поля');
}
});
// Обработка формы регистрации
document.getElementById('registerForm').addEventListener('submit', function(e) {
e.preventDefault();
const firstName = document.getElementById('registerFirstName').value;
const lastName = document.getElementById('registerLastName').value;
const email = document.getElementById('registerEmail').value;
const password = document.getElementById('registerPassword').value;
const passwordConfirm = document.getElementById('registerPasswordConfirm').value;
if (password !== passwordConfirm) {
alert('Пароли не совпадают');
return;
}
if (password.length < 6) {
alert('Пароль должен содержать минимум 6 символов');
return;
}
// Здесь должна быть реальная регистрация на сервере
// Для демо просто имитируем успешную регистрацию и вход
loginUser({
firstName: firstName,
lastName: lastName,
email: email,
avatarLetter: firstName.charAt(0).toUpperCase()
});
// Показать сообщение об успехе
document.getElementById('authSuccess').classList.add('show');
// Закрыть модальное окно через 2 секунды
setTimeout(() => {
authModal.hide();
document.getElementById('authSuccess').classList.remove('show');
this.reset();
}, 2000);
});
// Функция входа пользователя
function loginUser(userData) {
isAuthenticated = true;
// Обновляем интерфейс
document.getElementById('authButtonContainer').style.display = 'none';
document.getElementById('userProfileContainer').style.display = 'block';
// Заполняем данные пользователя
const fullName = userData.firstName + (userData.lastName ? ' ' + userData.lastName : '');
document.getElementById('userName').textContent = userData.firstName;
document.getElementById('userAvatar').textContent = userData.avatarLetter;
document.getElementById('dropdownUserName').textContent = fullName;
document.getElementById('dropdownUserEmail').textContent = userData.email;
document.getElementById('dropdownUserAvatar').textContent = userData.avatarLetter;
// Сохраняем в localStorage (в реальном приложении используйте токены)
localStorage.setItem('datePlannerUser', JSON.stringify(userData));
// Показать приветственное сообщение
showWelcomeMessage(userData.firstName);
}
// Функция выхода
function logout() {
isAuthenticated = false;
// Обновляем интерфейс
document.getElementById('authButtonContainer').style.display = 'block';
document.getElementById('userProfileContainer').style.display = 'none';
// Очищаем localStorage
localStorage.removeItem('datePlannerUser');
// Показать сообщение о выходе
alert('Вы успешно вышли из системы');
}
// Проверка авторизации при загрузке страницы
function checkAuthOnLoad() {
const savedUser = localStorage.getItem('datePlannerUser');
if (savedUser) {
loginUser(JSON.parse(savedUser));
}
}
// Показать приветственное сообщение
function showWelcomeMessage(firstName) {
// Создаем и показываем тост-уведомление
const toast = document.createElement('div');
toast.className = 'position-fixed top-0 end-0 p-3';
toast.style.zIndex = '1060';
toast.innerHTML = `
<div class="toast show" role="alert">
<div class="toast-header" style="background: linear-gradient(135deg, #e74c3c 0%, #fd79a8 100%); color: white;">
<i class="fas fa-heart me-2"></i>
<strong class="me-auto">DatePlanner</strong>
<button type="button" class="btn-close btn-close-white" onclick="this.closest('.toast').remove()"></button>
</div>
<div class="toast-body">
Добро пожаловать, ${firstName}! 🎉<br>
Теперь вы можете добавлять места в избранное и бронировать свидания.
</div>
</div>
`;
document.body.appendChild(toast);
// Автоматически удаляем через 5 секунд
setTimeout(() => {
if (toast.parentNode) {
toast.remove();
}
}, 5000);
}
// Инициализация при загрузке страницы
document.addEventListener('DOMContentLoaded', function() {
checkAuthOnLoad();
// Инициализация карты и остальной функциональности
// ... существующий код инициализации ...
// Ваш существующий код здесь
});
</script>
</body>
</html>

View File

@@ -10,6 +10,15 @@
<script src="/js/fw/jquery/dist/jquery.js"></script>
<script src="/js/fw/apexcharts/loader.js"></script>
<script src="/js/fw/apexcharts/apex.js"></script>
<script th:inline="javascript">
let csrf = [[${_csrf}]];
let auth = [[${auth}]];
let user = [[${user}]];
let cdn = [[${cdn}]];
console.log(cdn);
console.log(csrf);
console.log('auth is: ', auth);
</script>
</head>
<body>