implement login/quit, PendingStore.ts

This commit is contained in:
Maksim Skobaro 2025-02-02 09:44:29 +03:00
parent f80db06adf
commit 96ffb3ad41
19 changed files with 364 additions and 118 deletions

3
server/.gitignore vendored
View File

@ -1 +1,2 @@
/target /target
/logs/app.log

View File

@ -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();

View File

@ -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());
} }
} }

View File

@ -0,0 +1,6 @@
package ru.tubryansk.tdms.dto;
public record LoginDTO(
String username,
String password) {
}

View File

@ -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")

View File

@ -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")

View File

@ -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);
} }
} }

View File

@ -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
}
}

View File

@ -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/>

View File

@ -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>
} }
} }

View 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>
}
}

View File

@ -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;

View File

@ -1,3 +1,4 @@
// todo: update
export enum Role { export enum Role {
STUDENT = 'ROLE_STUDENT', STUDENT = 'ROLE_STUDENT',
TUTOR = 'ROLE_TUTOR', TUTOR = 'ROLE_TUTOR',

View File

@ -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/>,
} }

View 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;
};
}

View 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;
}
}

View File

@ -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);

View File

@ -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;
} }
} }

View File

@ -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) => {