implement login/quit, PendingStore.ts
This commit is contained in:
parent
f80db06adf
commit
96ffb3ad41
3
server/.gitignore
vendored
3
server/.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
|
/logs/app.log
|
||||||
|
|||||||
@ -3,26 +3,22 @@ package ru.tubryansk.tdms.config;
|
|||||||
|
|
||||||
import jakarta.servlet.http.HttpSessionEvent;
|
import jakarta.servlet.http.HttpSessionEvent;
|
||||||
import jakarta.servlet.http.HttpSessionListener;
|
import jakarta.servlet.http.HttpSessionListener;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.beans.factory.annotation.Qualifier;
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Profile;
|
import org.springframework.core.env.Environment;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.security.authentication.AuthenticationManager;
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
import org.springframework.security.authentication.AuthenticationProvider;
|
import org.springframework.security.authentication.AuthenticationProvider;
|
||||||
import org.springframework.security.authentication.ProviderManager;
|
import org.springframework.security.authentication.ProviderManager;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
||||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||||
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
|
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
|
||||||
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
|
|
||||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.security.core.context.SecurityContext;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||||
@ -34,10 +30,9 @@ import org.springframework.web.cors.CorsConfigurationSource;
|
|||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY;
|
|
||||||
|
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
|
@Slf4j
|
||||||
public class SecurityConfiguration {
|
public class SecurityConfiguration {
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain securityFilterChain(
|
public SecurityFilterChain securityFilterChain(
|
||||||
@ -50,40 +45,49 @@ public class SecurityConfiguration {
|
|||||||
.csrf(AbstractHttpConfigurer::disable)
|
.csrf(AbstractHttpConfigurer::disable)
|
||||||
.cors(a -> a.configurationSource(cors))
|
.cors(a -> a.configurationSource(cors))
|
||||||
.authenticationManager(authenticationManager)
|
.authenticationManager(authenticationManager)
|
||||||
.sessionManagement(this::configureSessionManagement)
|
.sessionManagement(cfg -> {
|
||||||
|
cfg.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
|
||||||
|
cfg.maximumSessions(1);
|
||||||
|
})
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Profile("dev")
|
|
||||||
@Qualifier("corsConfig")
|
@Qualifier("corsConfig")
|
||||||
public CorsConfigurationSource corsConfigurationDev() {
|
public CorsConfigurationSource corsConfigurationProd(
|
||||||
|
@Value("${application.domain}") String domain,
|
||||||
|
@Value("${application.port}") String port,
|
||||||
|
@Value("${application.protocol}") String protocol,
|
||||||
|
Environment environment
|
||||||
|
) {
|
||||||
return request -> {
|
return request -> {
|
||||||
|
String url = StringUtils.join(protocol, "://", domain, ":", port);
|
||||||
CorsConfiguration corsConfiguration = new CorsConfiguration();
|
CorsConfiguration corsConfiguration = new CorsConfiguration();
|
||||||
corsConfiguration.applyPermitDefaultValues();
|
corsConfiguration.setMaxAge(Duration.ofDays(1));
|
||||||
corsConfiguration.addAllowedMethod("DELETE");
|
corsConfiguration.addAllowedOrigin(url);
|
||||||
corsConfiguration.addAllowedMethod("PUT");
|
if (environment.matchesProfiles("dev")) {
|
||||||
corsConfiguration.addAllowedMethod("PATCH");
|
corsConfiguration.addAllowedOrigin("http://localhost:8081");
|
||||||
|
}
|
||||||
|
|
||||||
|
corsConfiguration.setAllowedMethods(List.of(HttpMethod.GET.name(), HttpMethod.POST.name(), HttpMethod.OPTIONS.name()));
|
||||||
|
corsConfiguration.setAllowedHeaders(List.of("Authorization", "Content-Type"));
|
||||||
|
corsConfiguration.setAllowCredentials(true);
|
||||||
return corsConfiguration;
|
return corsConfiguration;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@Profile("!dev")
|
public HttpSessionListener httpSessionListener() {
|
||||||
@Qualifier("corsConfig")
|
return new HttpSessionListener() {
|
||||||
public CorsConfigurationSource corsConfigurationProd(
|
@Override
|
||||||
@Value("${application.domain}") String domain,
|
public void sessionCreated(HttpSessionEvent se) {
|
||||||
@Value("${application.port}") String port,
|
log.debug("Session created: {}, user {}", se.getSession().getId(), SecurityContextHolder.getContext().getAuthentication().getName());
|
||||||
@Value("${application.protocol}") String protocol
|
}
|
||||||
) {
|
|
||||||
return request -> {
|
@Override
|
||||||
String url = StringUtils.join(protocol, "://", domain, ":", port);
|
public void sessionDestroyed(HttpSessionEvent se) {
|
||||||
CorsConfiguration corsConfiguration = new CorsConfiguration();
|
log.debug("Session destroyed: {}, user: {}", se.getSession().getId(), SecurityContextHolder.getContext().getAuthentication().getName());
|
||||||
corsConfiguration.setMaxAge(Duration.ofHours(1));
|
}
|
||||||
corsConfiguration.addAllowedOrigin(url);
|
|
||||||
corsConfiguration.setAllowedMethods(List.of(HttpMethod.GET.name(), HttpMethod.POST.name()));
|
|
||||||
// corsConfiguration.setAllowedHeaders();
|
|
||||||
return corsConfiguration;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,29 +101,6 @@ public class SecurityConfiguration {
|
|||||||
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Bean
|
|
||||||
// @Profile("dev")
|
|
||||||
// public HttpSessionListener autoAuthenticateUnderAdmin(AuthenticationManager authenticationManager) {
|
|
||||||
// return new HttpSessionListener() {
|
|
||||||
// @Override
|
|
||||||
// public void sessionCreated(HttpSessionEvent se) {
|
|
||||||
// String username = "admin";
|
|
||||||
// LoggerFactory.getLogger(this.getClass()).info("Session created {}. Authenticated, as {}", se.getSession().getId(), username);
|
|
||||||
// SecurityContext context = SecurityContextHolder.createEmptyContext();
|
|
||||||
// UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(username, "1");
|
|
||||||
// Authentication authenticated = authenticationManager.authenticate(authentication);
|
|
||||||
// context.setAuthentication(authenticated);
|
|
||||||
// SecurityContextHolder.setContext(context);
|
|
||||||
// se.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
// }
|
|
||||||
|
|
||||||
// todo: remove when login/logout is implemented, since we do not need automatically created session with no authentication
|
|
||||||
private void configureSessionManagement(SessionManagementConfigurer<HttpSecurity> sessionManagement) {
|
|
||||||
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.ALWAYS);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void configureHttpAuthorization(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry httpAuthorization) {
|
private void configureHttpAuthorization(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry httpAuthorization) {
|
||||||
/* API ROUTES */
|
/* API ROUTES */
|
||||||
httpAuthorization.requestMatchers("/api/v1/user/logout").authenticated();
|
httpAuthorization.requestMatchers("/api/v1/user/logout").authenticated();
|
||||||
|
|||||||
@ -3,10 +3,10 @@ package ru.tubryansk.tdms.controller;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import ru.tubryansk.tdms.dto.LoginDTO;
|
||||||
import ru.tubryansk.tdms.dto.UserDTO;
|
import ru.tubryansk.tdms.dto.UserDTO;
|
||||||
import ru.tubryansk.tdms.service.AuthenticationService;
|
import ru.tubryansk.tdms.service.AuthenticationService;
|
||||||
import ru.tubryansk.tdms.service.CallerService;
|
import ru.tubryansk.tdms.service.CallerService;
|
||||||
import ru.tubryansk.tdms.service.UserService;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/user")
|
@RequestMapping("/api/v1/user")
|
||||||
@ -28,7 +28,7 @@ public class UserController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/login")
|
@PostMapping("/login")
|
||||||
public void login(@RequestParam String username, @RequestParam String password) {
|
public void login(@RequestBody LoginDTO loginDTO) {
|
||||||
authenticationService.login(username, password);
|
authenticationService.login(loginDTO.username(), loginDTO.password());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
server/src/main/java/ru/tubryansk/tdms/dto/LoginDTO.java
Normal file
6
server/src/main/java/ru/tubryansk/tdms/dto/LoginDTO.java
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package ru.tubryansk.tdms.dto;
|
||||||
|
|
||||||
|
public record LoginDTO(
|
||||||
|
String username,
|
||||||
|
String password) {
|
||||||
|
}
|
||||||
@ -15,7 +15,7 @@ import org.springframework.security.core.GrantedAuthority;
|
|||||||
@Setter
|
@Setter
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Table(name = "role")
|
@Table(name = "`role`")
|
||||||
public class Role implements GrantedAuthority {
|
public class Role implements GrantedAuthority {
|
||||||
@Id
|
@Id
|
||||||
@Column(name = "id")
|
@Column(name = "id")
|
||||||
|
|||||||
@ -20,7 +20,7 @@ import java.util.List;
|
|||||||
@Setter
|
@Setter
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Table(name = "user")
|
@Table(name = "`user`")
|
||||||
public class User implements UserDetails {
|
public class User implements UserDetails {
|
||||||
@Id
|
@Id
|
||||||
@Column(name = "id")
|
@Column(name = "id")
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package ru.tubryansk.tdms.service;
|
|||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpSession;
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.security.authentication.AuthenticationManager;
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
@ -12,6 +13,7 @@ import ru.tubryansk.tdms.entity.User;
|
|||||||
import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY;
|
import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
@Slf4j
|
||||||
public class AuthenticationService {
|
public class AuthenticationService {
|
||||||
@Autowired
|
@Autowired
|
||||||
private HttpServletRequest request;
|
private HttpServletRequest request;
|
||||||
@ -31,10 +33,17 @@ public class AuthenticationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void login(String username, String password) {
|
public void login(String username, String password) {
|
||||||
var context = SecurityContextHolder.createEmptyContext();
|
try {
|
||||||
var token = new UsernamePasswordAuthenticationToken(username, password);
|
var context = SecurityContextHolder.createEmptyContext();
|
||||||
var authenticated = authenticationManager.authenticate(token);
|
var token = new UsernamePasswordAuthenticationToken(username, password);
|
||||||
context.setAuthentication(authenticated);
|
var authenticated = authenticationManager.authenticate(token);
|
||||||
request.getSession(true).setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
|
context.setAuthentication(authenticated);
|
||||||
|
SecurityContextHolder.setContext(context);
|
||||||
|
request.getSession(true).setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("Failed to log in user: {}. {}", username, e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
log.debug("User {} logged in", username);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,27 @@
|
|||||||
|
package ru.tubryansk.tdms.web;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.filter.AbstractRequestLoggingFilter;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class RequestLogger extends AbstractRequestLoggingFilter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void beforeRequest(HttpServletRequest request, String message) {
|
||||||
|
String logMessage = message +
|
||||||
|
", method=" + request.getMethod() +
|
||||||
|
", uri=" + request.getRequestURI() +
|
||||||
|
", query=" + request.getQueryString() +
|
||||||
|
", remote=" + request.getRemoteAddr() +
|
||||||
|
", user=" + request.getRemoteUser();
|
||||||
|
log.debug(logMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void afterRequest(HttpServletRequest request, String message) {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,7 +18,7 @@ class DefaultPage extends Component<any> {
|
|||||||
static contextType = RootStoreContext;
|
static contextType = RootStoreContext;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let isLoading = this.context.userStore.isLoading;
|
let isLoading = this.context.pendingStore.isThinking();
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<Header/>
|
<Header/>
|
||||||
|
|||||||
@ -2,60 +2,99 @@ import {Container, Nav, Navbar, NavDropdown} from "react-bootstrap";
|
|||||||
import {Component} from "react";
|
import {Component} from "react";
|
||||||
import {RouterLink} from "mobx-state-router";
|
import {RouterLink} from "mobx-state-router";
|
||||||
import {IAuthenticated} from "../../../models/user";
|
import {IAuthenticated} from "../../../models/user";
|
||||||
import {makeObservable} from "mobx";
|
|
||||||
import {RootStoreContext, RootStoreContextType} from "../../../context/RootStoreContext";
|
import {RootStoreContext, RootStoreContextType} from "../../../context/RootStoreContext";
|
||||||
import {observer} from "mobx-react";
|
import {observer} from "mobx-react";
|
||||||
|
import {post} from "../../../utils/request";
|
||||||
|
import {LoginModal} from "../../user/LoginModal";
|
||||||
|
import {ModalState} from "../../../state/ModalState";
|
||||||
|
import {action, makeObservable} from "mobx";
|
||||||
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
class Header extends Component {
|
class Header extends Component {
|
||||||
declare context: RootStoreContextType;
|
declare context: RootStoreContextType;
|
||||||
static contextType = RootStoreContext;
|
static contextType = RootStoreContext;
|
||||||
|
|
||||||
|
loginModalState = new ModalState();
|
||||||
|
|
||||||
|
constructor(props: any) {
|
||||||
|
super(props);
|
||||||
|
makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
get loginThink() {
|
||||||
|
return this.context.pendingStore.isThinking('updateCurrentUser');
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const userStore = this.context.userStore;
|
||||||
|
const user = userStore.user;
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<header>
|
||||||
|
<Navbar className="bg-body-tertiary" fixed="top">
|
||||||
|
<Container>
|
||||||
|
<Navbar.Brand>
|
||||||
|
<Nav.Link as={RouterLink} routeName='root'>TDMS</Nav.Link>
|
||||||
|
</Navbar.Brand>
|
||||||
|
<Nav>
|
||||||
|
<NavDropdown title="Группы">
|
||||||
|
<NavDropdown.Item>Список</NavDropdown.Item>
|
||||||
|
<NavDropdown.Item>Редактировать</NavDropdown.Item>
|
||||||
|
</NavDropdown>
|
||||||
|
</Nav>
|
||||||
|
|
||||||
|
<Nav className="ms-auto">
|
||||||
|
{
|
||||||
|
this.loginThink &&
|
||||||
|
<FontAwesomeIcon icon='gear' spin/>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
user.authenticated && !this.loginThink &&
|
||||||
|
<AuthenticatedItems/>
|
||||||
|
}
|
||||||
|
{
|
||||||
|
!user.authenticated && !this.loginThink &&
|
||||||
|
<>
|
||||||
|
<Nav.Link onClick={this.loginModalState.open}>Войти</Nav.Link>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
</Nav>
|
||||||
|
</Container>
|
||||||
|
</Navbar>
|
||||||
|
</header>
|
||||||
|
<LoginModal modalState={this.loginModalState}/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthenticatedItems extends Component {
|
||||||
|
declare context: RootStoreContextType;
|
||||||
|
static contextType = RootStoreContext;
|
||||||
|
|
||||||
constructor(props: any) {
|
constructor(props: any) {
|
||||||
super(props);
|
super(props);
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
logout() {
|
||||||
|
post('user/logout').then(() => this.context.userStore.updateCurrentUser());
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const userStore = this.context.userStore;
|
const userStore = this.context.userStore;
|
||||||
const routerStore = this.context.routerStore;
|
|
||||||
const user = userStore.user;
|
const user = userStore.user;
|
||||||
|
|
||||||
return <header>
|
return <>
|
||||||
<Navbar className="bg-body-tertiary" fixed="top">
|
<Navbar.Text>Пользователь:</Navbar.Text>
|
||||||
<Container>
|
<NavDropdown
|
||||||
<Navbar.Brand>
|
title={(user as IAuthenticated).fullName}>
|
||||||
<Nav.Link as={RouterLink} routeName='root'>TDMS</Nav.Link>
|
<NavDropdown.Item as={RouterLink} routeName='profile'>Моя страница</NavDropdown.Item>
|
||||||
</Navbar.Brand>
|
<NavDropdown.Divider/>
|
||||||
<Nav>
|
<NavDropdown.Item onClick={this.logout}>Выйти</NavDropdown.Item>
|
||||||
<NavDropdown title="Группы">
|
</NavDropdown>
|
||||||
<NavDropdown.Item>Список</NavDropdown.Item>
|
</>
|
||||||
<NavDropdown.Item>Редактировать</NavDropdown.Item>
|
|
||||||
</NavDropdown>
|
|
||||||
</Nav>
|
|
||||||
|
|
||||||
<Nav className="ms-auto">
|
|
||||||
{
|
|
||||||
user.authenticated &&
|
|
||||||
<>
|
|
||||||
<Navbar.Text>Пользователь:</Navbar.Text>
|
|
||||||
<NavDropdown
|
|
||||||
title={(user as IAuthenticated).fullName}>
|
|
||||||
<NavDropdown.Item onClick={() => {routerStore.goTo('profile')}}>Моя страница</NavDropdown.Item>
|
|
||||||
<NavDropdown.Divider/>
|
|
||||||
<NavDropdown.Item>Выйти</NavDropdown.Item>
|
|
||||||
</NavDropdown>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
!user.authenticated &&
|
|
||||||
<Nav.Link as={RouterLink} routeName='login'>Войти</Nav.Link>
|
|
||||||
}
|
|
||||||
</Nav>
|
|
||||||
</Container>
|
|
||||||
</Navbar>
|
|
||||||
</header>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
121
web/src/components/user/LoginModal.tsx
Normal file
121
web/src/components/user/LoginModal.tsx
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
import {ChangeEvent, Component} from "react";
|
||||||
|
import {Button, FormControl, FormGroup, FormLabel, FormText, Modal} from "react-bootstrap";
|
||||||
|
import {ModalState} from "../../state/ModalState";
|
||||||
|
import {observer} from "mobx-react";
|
||||||
|
import {action, computed, makeObservable, observable, reaction} from "mobx";
|
||||||
|
import {post} from "../../utils/request";
|
||||||
|
import {RootStoreContext, RootStoreContextType} from "../../context/RootStoreContext";
|
||||||
|
|
||||||
|
interface LoginModalProps {
|
||||||
|
modalState: ModalState;
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class LoginModal extends Component<LoginModalProps> {
|
||||||
|
declare context: RootStoreContextType;
|
||||||
|
static contextType = RootStoreContext;
|
||||||
|
modalState = this.props.modalState;
|
||||||
|
@observable login = '';
|
||||||
|
@observable loginError = '';
|
||||||
|
@observable password = '';
|
||||||
|
@observable passwordError = '';
|
||||||
|
@observable rememberMe = false;
|
||||||
|
|
||||||
|
constructor(props: LoginModalProps) {
|
||||||
|
super(props);
|
||||||
|
makeObservable(this);
|
||||||
|
reaction(() => this.login, this.validateLogin);
|
||||||
|
reaction(() => this.password, this.validatePassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
validateLogin() {
|
||||||
|
if (!this.login) {
|
||||||
|
this.loginError = 'Имя пользователя не может быть пустым';
|
||||||
|
} else if (this.login.length < 5) {
|
||||||
|
this.loginError = 'Имя пользователя должно быть не менее 5 символов';
|
||||||
|
} else if (this.login.length > 50) {
|
||||||
|
this.loginError = 'Имя пользователя должно быть не более 50 символов';
|
||||||
|
} else if (!/^[a-zA-Z0-9_]+$/.test(this.login)) {
|
||||||
|
this.loginError = 'Имя пользователя должно содержать только латинские буквы, цифры и знак подчеркивания';
|
||||||
|
} else {
|
||||||
|
this.loginError = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
validatePassword() {
|
||||||
|
if (!this.password) {
|
||||||
|
this.passwordError = 'Пароль не может быть пустым';
|
||||||
|
} else if (this.password.length < 5) {
|
||||||
|
this.passwordError = 'Пароль должен быть не менее 5 символов';
|
||||||
|
} else if (this.password.length > 32) {
|
||||||
|
this.passwordError = 'Пароль должен быть не более 32 символов';
|
||||||
|
} else if (!/^[a-zA-Z0-9!@#$%^&*()_+]+$/.test(this.password)) {
|
||||||
|
this.passwordError = 'Пароль должен содержать только латинские буквы, цифры и специальные символы';
|
||||||
|
} else {
|
||||||
|
this.passwordError = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed
|
||||||
|
get loginButtonDisabled() {
|
||||||
|
return !this.login || !this.password || !!this.loginError || !!this.passwordError;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
onLoginInput(event: ChangeEvent<HTMLInputElement>) {
|
||||||
|
this.login = event.target.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
onPasswordInput(event: ChangeEvent<HTMLInputElement>) {
|
||||||
|
this.password = event.target.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
onLogin() {
|
||||||
|
if (this.loginButtonDisabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
post('user/login', {
|
||||||
|
username: this.login,
|
||||||
|
password: this.password
|
||||||
|
}).then(() => {
|
||||||
|
this.context.userStore.updateCurrentUser();
|
||||||
|
this.modalState.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return <Modal show={this.modalState.isOpen} centered>
|
||||||
|
<Modal.Header>
|
||||||
|
<Modal.Title>Вход</Modal.Title>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Body>
|
||||||
|
<FormGroup className={'mb-3'}>
|
||||||
|
<FormLabel>Имя пользователя</FormLabel>
|
||||||
|
<FormControl type="text" onChange={this.onLoginInput}/>
|
||||||
|
{
|
||||||
|
this.loginError &&
|
||||||
|
<FormText className={'text-danger'}>{this.loginError}</FormText>
|
||||||
|
}
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup className={'mb-3'}>
|
||||||
|
<FormLabel>Пароль</FormLabel>
|
||||||
|
<FormControl type="password" onChange={this.onPasswordInput}/>
|
||||||
|
{
|
||||||
|
this.passwordError &&
|
||||||
|
<FormText className={'text-danger'}>{this.passwordError}</FormText>
|
||||||
|
}
|
||||||
|
</FormGroup>
|
||||||
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
|
<Button variant={'primary'} onClick={this.onLogin} disabled={this.loginButtonDisabled}>Войти</Button>
|
||||||
|
<Button variant={'secondary'} onClick={this.modalState.close}>Закрыть</Button>
|
||||||
|
</Modal.Footer>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import {DefaultPage} from "./layout/DefaultPage";
|
import {DefaultPage} from "../page/layout/DefaultPage";
|
||||||
import {Col, Form, Row} from "react-bootstrap";
|
import {Col, Form, Row} from "react-bootstrap";
|
||||||
import {observer} from "mobx-react";
|
import {observer} from "mobx-react";
|
||||||
import {RootStoreContext, type RootStoreContextType} from "../../context/RootStoreContext";
|
import {RootStoreContext, type RootStoreContextType} from "../../context/RootStoreContext";
|
||||||
@ -151,7 +151,7 @@ class StudentInfo extends Component<{student: IStudent}> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class UserProfile extends DefaultPage {
|
export default class UserProfilePage extends DefaultPage {
|
||||||
declare context: RootStoreContextType;
|
declare context: RootStoreContextType;
|
||||||
static contextType = RootStoreContext;
|
static contextType = RootStoreContext;
|
||||||
|
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
// todo: update
|
||||||
export enum Role {
|
export enum Role {
|
||||||
STUDENT = 'ROLE_STUDENT',
|
STUDENT = 'ROLE_STUDENT',
|
||||||
TUTOR = 'ROLE_TUTOR',
|
TUTOR = 'ROLE_TUTOR',
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import {ViewMap} from "mobx-state-router";
|
import {ViewMap} from "mobx-state-router";
|
||||||
import Home from "../components/page/Home";
|
import Home from "../components/page/Home";
|
||||||
import Error from "../components/page/Error";
|
import Error from "../components/page/Error";
|
||||||
import UserProfile from "../components/page/UserProfile";
|
import UserProfilePage from "../components/user/UserProfilePage";
|
||||||
|
|
||||||
export const viewMap: ViewMap = {
|
export const viewMap: ViewMap = {
|
||||||
root: <Home/>,
|
root: <Home/>,
|
||||||
profile: <UserProfile/>,
|
profile: <UserProfilePage/>,
|
||||||
error: <Error/>,
|
error: <Error/>,
|
||||||
}
|
}
|
||||||
20
web/src/state/ModalState.ts
Normal file
20
web/src/state/ModalState.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import {action, makeObservable, observable} from "mobx";
|
||||||
|
|
||||||
|
export class ModalState {
|
||||||
|
@observable isOpen: boolean;
|
||||||
|
|
||||||
|
constructor(isOpen: boolean = false) {
|
||||||
|
makeObservable(this);
|
||||||
|
this.isOpen = isOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
open = () => {
|
||||||
|
this.isOpen = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
close = () => {
|
||||||
|
this.isOpen = false;
|
||||||
|
};
|
||||||
|
}
|
||||||
41
web/src/store/PendingStore.ts
Normal file
41
web/src/store/PendingStore.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import {action, makeObservable, observable} from "mobx";
|
||||||
|
import {RootStore} from "./RootStore";
|
||||||
|
|
||||||
|
export class PendingStore {
|
||||||
|
rootStore: RootStore;
|
||||||
|
@observable pendingMap = new Map<string, number>();
|
||||||
|
|
||||||
|
constructor(rootStore: RootStore) {
|
||||||
|
this.rootStore = rootStore;
|
||||||
|
makeObservable(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
think(key: string = '$default') {
|
||||||
|
const count = this.pendingMap.get(key) || 0;
|
||||||
|
this.pendingMap.set(key, count + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
completeOne(key: string) {
|
||||||
|
const count = this.pendingMap.get(key);
|
||||||
|
if (count === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pendingMap.set(key, count - 1);
|
||||||
|
if (this.pendingMap.get(key) === 0) {
|
||||||
|
this.pendingMap.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
completeAll(key: string) {
|
||||||
|
this.pendingMap.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@action.bound
|
||||||
|
isThinking(key: string = '$default') {
|
||||||
|
return this.pendingMap.get(key) !== undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,9 @@
|
|||||||
import {MyRouterStore} from "./MyRouterStore";
|
import {MyRouterStore} from "./MyRouterStore";
|
||||||
import {UserStore} from "./UserStore";
|
import {UserStore} from "./UserStore";
|
||||||
|
import {PendingStore} from "./PendingStore";
|
||||||
|
|
||||||
export class RootStore {
|
export class RootStore {
|
||||||
|
pendingStore = new PendingStore(this);
|
||||||
userStore = new UserStore(this);
|
userStore = new UserStore(this);
|
||||||
routerStore = new MyRouterStore(this);
|
routerStore = new MyRouterStore(this);
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import {get} from "../utils/request";
|
import {get} from "../utils/request";
|
||||||
import {makeObservable, observable, runInAction} from "mobx";
|
import {action, makeObservable, observable, runInAction} from "mobx";
|
||||||
import {RootStore} from "./RootStore";
|
import {RootStore} from "./RootStore";
|
||||||
import type {IUser} from "../models/user";
|
import type {IUser} from "../models/user";
|
||||||
import {IStudent} from "../models/student";
|
import {IStudent} from "../models/student";
|
||||||
@ -7,25 +7,23 @@ import {Role} from "../models/role";
|
|||||||
|
|
||||||
export class UserStore {
|
export class UserStore {
|
||||||
rootStore: RootStore;
|
rootStore: RootStore;
|
||||||
@observable
|
@observable user: IUser = {authenticated: false};
|
||||||
user: IUser = {authenticated: false};
|
@observable student?: IStudent;
|
||||||
@observable
|
|
||||||
student: IStudent | undefined;
|
|
||||||
@observable
|
|
||||||
isLoading: boolean = true;
|
|
||||||
|
|
||||||
constructor(rootStore: RootStore) {
|
constructor(rootStore: RootStore) {
|
||||||
makeObservable(this);
|
makeObservable(this);
|
||||||
this.rootStore = rootStore;
|
this.rootStore = rootStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchCurrentUserData() {
|
@action.bound
|
||||||
|
updateCurrentUser() {
|
||||||
// todo: store token in localStorage
|
// todo: store token in localStorage
|
||||||
|
this.rootStore.pendingStore.think('updateCurrentUser');
|
||||||
get<IUser>('/user/current').then((response) => {
|
get<IUser>('/user/current').then((response) => {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.user = response;
|
this.user = response;
|
||||||
});
|
});
|
||||||
if(response.authenticated && response.authorities.some(a => a.authority === Role.STUDENT)) {
|
if (response.authenticated && response.authorities.some(a => a.authority === Role.STUDENT)) {
|
||||||
get<IStudent>('/student/current').then((student) => {
|
get<IStudent>('/student/current').then((student) => {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.student = student;
|
this.student = student;
|
||||||
@ -34,13 +32,13 @@ export class UserStore {
|
|||||||
}
|
}
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
this.isLoading = false;
|
this.rootStore.pendingStore.completeOne('updateCurrentUser');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this.fetchCurrentUserData();
|
this.updateCurrentUser();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -21,7 +21,7 @@ export const post = async <T,> (url: string, data?: any) => {
|
|||||||
export const request = async <T,> (config: AxiosRequestConfig<any>) => {
|
export const request = async <T,> (config: AxiosRequestConfig<any>) => {
|
||||||
return new Promise<T>((resolve, reject) => {
|
return new Promise<T>((resolve, reject) => {
|
||||||
console.debug(`${config.method} ${config.url} request: ${config.method === 'GET' ? JSON.stringify(config.params) : JSON.stringify(config.data)}`);
|
console.debug(`${config.method} ${config.url} request: ${config.method === 'GET' ? JSON.stringify(config.params) : JSON.stringify(config.data)}`);
|
||||||
axios.request({...config, baseURL: apiUrl}).then((response) => {
|
axios.request({...config, baseURL: apiUrl, withCredentials: true}).then((response) => {
|
||||||
console.debug(`${config.method} ${config.url} response: ${JSON.stringify(response.data)}`);
|
console.debug(`${config.method} ${config.url} response: ${JSON.stringify(response.data)}`);
|
||||||
resolve(response.data);
|
resolve(response.data);
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user