mirror of
https://github.com/LOBSTERVOVA/Tennis-Site.git
synced 2026-04-17 17:40:49 +03:00
пытаюсь сделать авторизацию
This commit is contained in:
@@ -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();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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() : "";
|
||||
}
|
||||
}
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
// }
|
||||
}
|
||||
@@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
27
src/main/java/com/example/dateplanner/utils/CookieUtil.java
Normal file
27
src/main/java/com/example/dateplanner/utils/CookieUtil.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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 ;
|
||||
@@ -82,4 +121,4 @@
|
||||
|
||||
.social-auth-btn.vk { color: #4C75A3; }
|
||||
.social-auth-btn.google { color: #DB4437; }
|
||||
.social-auth-btn.yandex { color: #FF0000; }
|
||||
.social-auth-btn.yandex { color: #FF0000; }
|
||||
|
||||
@@ -126,4 +126,4 @@ footer {
|
||||
.social-icon:hover {
|
||||
background: var(--love-red);
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
@import "fonts.css";
|
||||
@import "main.css";
|
||||
@import "header.css";
|
||||
@import "header.css";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user