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.HttpSessionListener;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
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.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
|
||||
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.AuthorizeHttpRequestsConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.SessionManagementConfigurer;
|
||||
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.userdetails.UserDetailsService;
|
||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||
@ -34,10 +30,9 @@ import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY;
|
||||
|
||||
|
||||
@Configuration
|
||||
@Slf4j
|
||||
public class SecurityConfiguration {
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(
|
||||
@ -50,40 +45,49 @@ public class SecurityConfiguration {
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.cors(a -> a.configurationSource(cors))
|
||||
.authenticationManager(authenticationManager)
|
||||
.sessionManagement(this::configureSessionManagement)
|
||||
.sessionManagement(cfg -> {
|
||||
cfg.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
|
||||
cfg.maximumSessions(1);
|
||||
})
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Profile("dev")
|
||||
@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 -> {
|
||||
String url = StringUtils.join(protocol, "://", domain, ":", port);
|
||||
CorsConfiguration corsConfiguration = new CorsConfiguration();
|
||||
corsConfiguration.applyPermitDefaultValues();
|
||||
corsConfiguration.addAllowedMethod("DELETE");
|
||||
corsConfiguration.addAllowedMethod("PUT");
|
||||
corsConfiguration.addAllowedMethod("PATCH");
|
||||
corsConfiguration.setMaxAge(Duration.ofDays(1));
|
||||
corsConfiguration.addAllowedOrigin(url);
|
||||
if (environment.matchesProfiles("dev")) {
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Profile("!dev")
|
||||
@Qualifier("corsConfig")
|
||||
public CorsConfigurationSource corsConfigurationProd(
|
||||
@Value("${application.domain}") String domain,
|
||||
@Value("${application.port}") String port,
|
||||
@Value("${application.protocol}") String protocol
|
||||
) {
|
||||
return request -> {
|
||||
String url = StringUtils.join(protocol, "://", domain, ":", port);
|
||||
CorsConfiguration corsConfiguration = new CorsConfiguration();
|
||||
corsConfiguration.setMaxAge(Duration.ofHours(1));
|
||||
corsConfiguration.addAllowedOrigin(url);
|
||||
corsConfiguration.setAllowedMethods(List.of(HttpMethod.GET.name(), HttpMethod.POST.name()));
|
||||
// corsConfiguration.setAllowedHeaders();
|
||||
return corsConfiguration;
|
||||
public HttpSessionListener httpSessionListener() {
|
||||
return new HttpSessionListener() {
|
||||
@Override
|
||||
public void sessionCreated(HttpSessionEvent se) {
|
||||
log.debug("Session created: {}, user {}", se.getSession().getId(), SecurityContextHolder.getContext().getAuthentication().getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sessionDestroyed(HttpSessionEvent se) {
|
||||
log.debug("Session destroyed: {}, user: {}", se.getSession().getId(), SecurityContextHolder.getContext().getAuthentication().getName());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -97,29 +101,6 @@ public class SecurityConfiguration {
|
||||
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) {
|
||||
/* API ROUTES */
|
||||
httpAuthorization.requestMatchers("/api/v1/user/logout").authenticated();
|
||||
|
||||
@ -3,10 +3,10 @@ package ru.tubryansk.tdms.controller;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import ru.tubryansk.tdms.dto.LoginDTO;
|
||||
import ru.tubryansk.tdms.dto.UserDTO;
|
||||
import ru.tubryansk.tdms.service.AuthenticationService;
|
||||
import ru.tubryansk.tdms.service.CallerService;
|
||||
import ru.tubryansk.tdms.service.UserService;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/user")
|
||||
@ -28,7 +28,7 @@ public class UserController {
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public void login(@RequestParam String username, @RequestParam String password) {
|
||||
authenticationService.login(username, password);
|
||||
public void login(@RequestBody LoginDTO loginDTO) {
|
||||
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
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Table(name = "role")
|
||||
@Table(name = "`role`")
|
||||
public class Role implements GrantedAuthority {
|
||||
@Id
|
||||
@Column(name = "id")
|
||||
|
||||
@ -20,7 +20,7 @@ import java.util.List;
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Table(name = "user")
|
||||
@Table(name = "`user`")
|
||||
public class User implements UserDetails {
|
||||
@Id
|
||||
@Column(name = "id")
|
||||
|
||||
@ -2,6 +2,7 @@ package ru.tubryansk.tdms.service;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
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;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class AuthenticationService {
|
||||
@Autowired
|
||||
private HttpServletRequest request;
|
||||
@ -31,10 +33,17 @@ public class AuthenticationService {
|
||||
}
|
||||
|
||||
public void login(String username, String password) {
|
||||
var context = SecurityContextHolder.createEmptyContext();
|
||||
var token = new UsernamePasswordAuthenticationToken(username, password);
|
||||
var authenticated = authenticationManager.authenticate(token);
|
||||
context.setAuthentication(authenticated);
|
||||
request.getSession(true).setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
|
||||
try {
|
||||
var context = SecurityContextHolder.createEmptyContext();
|
||||
var token = new UsernamePasswordAuthenticationToken(username, password);
|
||||
var authenticated = authenticationManager.authenticate(token);
|
||||
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;
|
||||
|
||||
render() {
|
||||
let isLoading = this.context.userStore.isLoading;
|
||||
let isLoading = this.context.pendingStore.isThinking();
|
||||
|
||||
return <>
|
||||
<Header/>
|
||||
|
||||
@ -2,60 +2,99 @@ import {Container, Nav, Navbar, NavDropdown} from "react-bootstrap";
|
||||
import {Component} from "react";
|
||||
import {RouterLink} from "mobx-state-router";
|
||||
import {IAuthenticated} from "../../../models/user";
|
||||
import {makeObservable} from "mobx";
|
||||
import {RootStoreContext, RootStoreContextType} from "../../../context/RootStoreContext";
|
||||
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
|
||||
class Header extends Component {
|
||||
declare context: RootStoreContextType;
|
||||
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) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
@action.bound
|
||||
logout() {
|
||||
post('user/logout').then(() => this.context.userStore.updateCurrentUser());
|
||||
}
|
||||
|
||||
render() {
|
||||
const userStore = this.context.userStore;
|
||||
const routerStore = this.context.routerStore;
|
||||
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">
|
||||
{
|
||||
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>
|
||||
return <>
|
||||
<Navbar.Text>Пользователь:</Navbar.Text>
|
||||
<NavDropdown
|
||||
title={(user as IAuthenticated).fullName}>
|
||||
<NavDropdown.Item as={RouterLink} routeName='profile'>Моя страница</NavDropdown.Item>
|
||||
<NavDropdown.Divider/>
|
||||
<NavDropdown.Item onClick={this.logout}>Выйти</NavDropdown.Item>
|
||||
</NavDropdown>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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 {observer} from "mobx-react";
|
||||
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;
|
||||
static contextType = RootStoreContext;
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
// todo: update
|
||||
export enum Role {
|
||||
STUDENT = 'ROLE_STUDENT',
|
||||
TUTOR = 'ROLE_TUTOR',
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import {ViewMap} from "mobx-state-router";
|
||||
import Home from "../components/page/Home";
|
||||
import Error from "../components/page/Error";
|
||||
import UserProfile from "../components/page/UserProfile";
|
||||
import UserProfilePage from "../components/user/UserProfilePage";
|
||||
|
||||
export const viewMap: ViewMap = {
|
||||
root: <Home/>,
|
||||
profile: <UserProfile/>,
|
||||
profile: <UserProfilePage/>,
|
||||
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 {UserStore} from "./UserStore";
|
||||
import {PendingStore} from "./PendingStore";
|
||||
|
||||
export class RootStore {
|
||||
pendingStore = new PendingStore(this);
|
||||
userStore = new UserStore(this);
|
||||
routerStore = new MyRouterStore(this);
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import {get} from "../utils/request";
|
||||
import {makeObservable, observable, runInAction} from "mobx";
|
||||
import {action, makeObservable, observable, runInAction} from "mobx";
|
||||
import {RootStore} from "./RootStore";
|
||||
import type {IUser} from "../models/user";
|
||||
import {IStudent} from "../models/student";
|
||||
@ -7,25 +7,23 @@ import {Role} from "../models/role";
|
||||
|
||||
export class UserStore {
|
||||
rootStore: RootStore;
|
||||
@observable
|
||||
user: IUser = {authenticated: false};
|
||||
@observable
|
||||
student: IStudent | undefined;
|
||||
@observable
|
||||
isLoading: boolean = true;
|
||||
@observable user: IUser = {authenticated: false};
|
||||
@observable student?: IStudent;
|
||||
|
||||
constructor(rootStore: RootStore) {
|
||||
makeObservable(this);
|
||||
this.rootStore = rootStore;
|
||||
}
|
||||
|
||||
fetchCurrentUserData() {
|
||||
@action.bound
|
||||
updateCurrentUser() {
|
||||
// todo: store token in localStorage
|
||||
this.rootStore.pendingStore.think('updateCurrentUser');
|
||||
get<IUser>('/user/current').then((response) => {
|
||||
runInAction(() => {
|
||||
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) => {
|
||||
runInAction(() => {
|
||||
this.student = student;
|
||||
@ -34,13 +32,13 @@ export class UserStore {
|
||||
}
|
||||
}).finally(() => {
|
||||
runInAction(() => {
|
||||
this.isLoading = false;
|
||||
this.rootStore.pendingStore.completeOne('updateCurrentUser');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
init() {
|
||||
this.fetchCurrentUserData();
|
||||
this.updateCurrentUser();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@ -21,7 +21,7 @@ export const post = async <T,> (url: string, data?: any) => {
|
||||
export const request = async <T,> (config: AxiosRequestConfig<any>) => {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
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)}`);
|
||||
resolve(response.data);
|
||||
}).catch((error) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user