From 4a229376fbd458d524649b5e8534a0f1600a395d Mon Sep 17 00:00:00 2001 From: Thibaut Mouton Date: Tue, 16 Apr 2024 23:03:22 +0200 Subject: [PATCH] chore():multiple refactos --- .github/workflows/npm.yml | 2 +- README.md | 4 +- .../config/JwtAuthenticationEntryPoint.java | 19 --- .../java/com/mercure/config/JwtWebConfig.java | 17 +-- .../com/mercure/config/SecurityConfig.java | 65 ++++----- .../com/mercure/controller/ApiController.java | 1 + .../mercure/controller/PingController.java | 6 + .../main/java/com/mercure/utils/JwtUtil.java | 1 + .../create-group/create-group-component.tsx | 6 +- frontend-web/src/components/home.tsx | 9 +- ...login-component.tsx => LoginComponent.tsx} | 8 +- .../CreateMessageComponent.tsx | 4 +- .../messages/DisplayMessagesComponent.tsx | 105 ++++++++++++++ .../components/partials/HeaderComponent.tsx | 131 +++--------------- .../components/partials/alert-component.tsx | 2 +- .../src/components/register/register-user.tsx | 6 +- .../user-account/UseAccountComponent.tsx | 118 ++++++++++++++++ .../components/websocket/video-component.tsx | 4 +- .../websocket/websocket-chat-component.tsx | 113 ++------------- .../websocket-group-actions-component.tsx | 9 +- .../websocket/websocket-groups-component.tsx | 11 +- .../websocket/websocket-main-component.tsx | 28 ++-- .../components/websocket/websocketStyle.css | 2 +- frontend-web/src/config/websocket-config.ts | 4 +- frontend-web/src/context/AuthContext.tsx | 15 +- frontend-web/src/context/WebsocketContext.tsx | 38 ++--- frontend-web/src/index.tsx | 32 ++++- ...{http-service.ts => http-group-service.ts} | 4 +- frontend-web/src/service/http-main.service.ts | 7 +- 29 files changed, 410 insertions(+), 361 deletions(-) delete mode 100644 backend/src/main/java/com/mercure/config/JwtAuthenticationEntryPoint.java rename frontend-web/src/components/login/{login-component.tsx => LoginComponent.tsx} (96%) rename frontend-web/src/components/{message => messages}/CreateMessageComponent.tsx (97%) create mode 100644 frontend-web/src/components/messages/DisplayMessagesComponent.tsx create mode 100644 frontend-web/src/components/user-account/UseAccountComponent.tsx rename frontend-web/src/service/{http-service.ts => http-group-service.ts} (96%) diff --git a/.github/workflows/npm.yml b/.github/workflows/npm.yml index 9813166..0f8dd21 100644 --- a/.github/workflows/npm.yml +++ b/.github/workflows/npm.yml @@ -11,4 +11,4 @@ jobs: with: node-version: 20.x - name: build - run: npm run build --prefix frontend-web + run: npm run build --prefix ./frontend-web diff --git a/README.md b/README.md index 56637a1..566b77e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -![maven build status](https://github.com/Thibaut-Mouton/react-spring-messenger-project/workflows/maven/badge.svg?branch=master) -![npm build status](https://github.com/Thibaut-Mouton/react-spring-messenger-project/workflows/npm/badge.svg?branch=master) +![maven build status](https://github.com/Thibaut-Mouton/react-spring-messenger-project/workflows/build-back/badge.svg?branch=master) +![npm build status](https://github.com/Thibaut-Mouton/react-spring-messenger-project/workflows/build-front/badge.svg?branch=master)

React logo diff --git a/backend/src/main/java/com/mercure/config/JwtAuthenticationEntryPoint.java b/backend/src/main/java/com/mercure/config/JwtAuthenticationEntryPoint.java deleted file mode 100644 index 46ee0ac..0000000 --- a/backend/src/main/java/com/mercure/config/JwtAuthenticationEntryPoint.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.mercure.config; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.springframework.security.core.AuthenticationException; -import org.springframework.security.web.AuthenticationEntryPoint; -import org.springframework.stereotype.Component; - -import java.io.IOException; -import java.io.Serializable; - -@Component -public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint, Serializable { - - @Override - public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException { - httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED,"Unauthorized"); - } -} diff --git a/backend/src/main/java/com/mercure/config/JwtWebConfig.java b/backend/src/main/java/com/mercure/config/JwtWebConfig.java index 0fbafe1..6446b7b 100644 --- a/backend/src/main/java/com/mercure/config/JwtWebConfig.java +++ b/backend/src/main/java/com/mercure/config/JwtWebConfig.java @@ -28,7 +28,7 @@ public class JwtWebConfig extends OncePerRequestFilter { private JwtUtil jwtUtil; @Override - protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException, ServletException { + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { String jwtToken = null; String username; Cookie cookie = WebUtils.getCookie(request, StaticVariable.SECURE_COOKIE); @@ -38,17 +38,10 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } if (jwtToken != null) { username = jwtUtil.getUserNameFromJwtToken(jwtToken); - try { - UserDetails userDetails = userDetailsService.loadUserByUsername(username); - if (jwtUtil.validateToken(jwtToken, userDetails)) { - UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); - SecurityContextHolder.getContext().setAuthentication(auth); - } - } catch (Exception ex) { - //this is very important, since it guarantees the user is not authenticated at all - filterChain.doFilter(request, response); - SecurityContextHolder.clearContext(); - return; + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + if (jwtUtil.validateToken(jwtToken, userDetails)) { + UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(auth); } } filterChain.doFilter(request, response); diff --git a/backend/src/main/java/com/mercure/config/SecurityConfig.java b/backend/src/main/java/com/mercure/config/SecurityConfig.java index 7858396..559bbfd 100644 --- a/backend/src/main/java/com/mercure/config/SecurityConfig.java +++ b/backend/src/main/java/com/mercure/config/SecurityConfig.java @@ -1,24 +1,56 @@ package com.mercure.config; +import com.mercure.service.CustomUserDetailsService; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration -@EnableWebSecurity public class SecurityConfig { + @Autowired + public JwtWebConfig jwtWebConfig; + @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) +// .csrf(httpSecurityCsrfConfigurer -> httpSecurityCsrfConfigurer.ignoringRequestMatchers("/api/csrf")) + .cors(Customizer.withDefaults()) + .authorizeHttpRequests((request) -> request + .requestMatchers("/api").permitAll() + .requestMatchers("/api/csrf").permitAll() + .requestMatchers("/api/auth").permitAll() + .requestMatchers("/api/**").authenticated()) + .sessionManagement(httpSecuritySessionManagementConfigurer -> httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .authenticationProvider(authenticationProvider()) + .addFilterBefore(jwtWebConfig, UsernamePasswordAuthenticationFilter.class); + return http.build(); + } + + @Bean + public AuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); + authenticationProvider.setUserDetailsService(new CustomUserDetailsService()); + authenticationProvider.setPasswordEncoder(passwordEncoder()); + return authenticationProvider; + } + // @Bean // public CorsConfigurationSource corsConfigurationSource() { // CorsConfiguration configuration = new CorsConfiguration(); @@ -30,34 +62,5 @@ public PasswordEncoder passwordEncoder() { // UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); // source.registerCorsConfiguration("/**", configuration); // return source; -// } - - @Bean - public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { - http - .cors(Customizer.withDefaults()) - .authorizeHttpRequests((auth) -> auth.anyRequest().permitAll()); - return http.build(); - } - -// protected void configure(HttpSecurity http) throws Exception { -// http.cors().and() -// .csrf() -// .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) -// .and() -// .authorizeRequests() -// .antMatchers("/api/auth").permitAll() -// .antMatchers("/api/user/register").permitAll() -// .antMatchers("/ws").permitAll() -// .antMatchers("/static/**").permitAll() -// .antMatchers("/images/**").permitAll() -// .antMatchers("/").permitAll() -// .anyRequest().authenticated() -// .and() -// .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint) -// .and() -// .sessionManagement() -// .sessionCreationPolicy(SessionCreationPolicy.STATELESS); -// http.addFilterBefore(jwtWebConfig, UsernamePasswordAuthenticationFilter.class); // } } diff --git a/backend/src/main/java/com/mercure/controller/ApiController.java b/backend/src/main/java/com/mercure/controller/ApiController.java index 0290cdf..f9fb317 100644 --- a/backend/src/main/java/com/mercure/controller/ApiController.java +++ b/backend/src/main/java/com/mercure/controller/ApiController.java @@ -25,6 +25,7 @@ import java.util.*; @RestController +@CrossOrigin public class ApiController { private final Logger log = LoggerFactory.getLogger(ApiController.class); diff --git a/backend/src/main/java/com/mercure/controller/PingController.java b/backend/src/main/java/com/mercure/controller/PingController.java index 398f8e8..112ff53 100644 --- a/backend/src/main/java/com/mercure/controller/PingController.java +++ b/backend/src/main/java/com/mercure/controller/PingController.java @@ -1,5 +1,7 @@ package com.mercure.controller; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -8,8 +10,12 @@ @RequestMapping(value = "/") public class PingController { + private final Logger log = LoggerFactory.getLogger(PingController.class); + + @GetMapping public String testRoute() { + log.debug("Ping base route"); return "Server status OK"; } } diff --git a/backend/src/main/java/com/mercure/utils/JwtUtil.java b/backend/src/main/java/com/mercure/utils/JwtUtil.java index a9a1e9e..64347bc 100644 --- a/backend/src/main/java/com/mercure/utils/JwtUtil.java +++ b/backend/src/main/java/com/mercure/utils/JwtUtil.java @@ -17,6 +17,7 @@ public class JwtUtil implements Serializable { public static final long JWT_TOKEN_VALIDITY = 1000 * 3600 * 365; + // TODO generate key public static final String JWT_TOKEN = "d95d7dc9-0d56-4ef3-8d03-263c23b5bce5"; // retrieve username from jwt token diff --git a/frontend-web/src/components/create-group/create-group-component.tsx b/frontend-web/src/components/create-group/create-group-component.tsx index 9beb238..894480e 100644 --- a/frontend-web/src/components/create-group/create-group-component.tsx +++ b/frontend-web/src/components/create-group/create-group-component.tsx @@ -2,13 +2,13 @@ import {Button, Container, CssBaseline, Grid, Typography} from "@mui/material" import React, {useContext, useEffect, useState} from "react" import {useThemeContext} from "../../context/theme-context" import {CustomTextField} from "../partials/custom-material-textfield" -import {HttpService} from "../../service/http-service" +import {HttpGroupService} from "../../service/http-group-service" import {AlertAction, AlertContext} from "../../context/AlertContext" export const CreateGroupComponent = () => { const [groupName, setGroupName] = useState("") const {theme} = useThemeContext() - const httpService = new HttpService() + const httpService = new HttpGroupService() const {dispatch} = useContext(AlertContext)! useEffect(() => { @@ -65,7 +65,7 @@ export const CreateGroupComponent = () => { return (

diff --git a/frontend-web/src/components/home.tsx b/frontend-web/src/components/home.tsx index 21849b9..899ff1e 100644 --- a/frontend-web/src/components/home.tsx +++ b/frontend-web/src/components/home.tsx @@ -2,9 +2,9 @@ import {Box, Card, CardContent, Grid, Typography} from "@mui/material" import React, {useContext, useEffect} from "react" import {generateColorMode} from "./utils/enable-dark-mode" import {useThemeContext} from "../context/theme-context" -import {LoginComponent} from "./login/login-component" import {FooterComponent} from "./partials/footer-component" import {AuthUserContext} from "../context/AuthContext" +import {LoginComponent} from "./login/LoginComponent" export const HomeComponent = (): React.JSX.Element => { const {theme} = useThemeContext() @@ -18,7 +18,7 @@ export const HomeComponent = (): React.JSX.Element => {
@@ -35,7 +35,10 @@ export const HomeComponent = (): React.JSX.Element => { Simple, fast and secure -
FastLiteMessage allow to communicate with other people, create groups, make serverless video calls in an easy way. Log into your account or register to start using FastLiteMessage.
+
FastLiteMessage allow to communicate with other people, create groups, make + serverless video calls in an easy way. Log into your account or register to start + using FastLiteMessage. +
diff --git a/frontend-web/src/components/login/login-component.tsx b/frontend-web/src/components/login/LoginComponent.tsx similarity index 96% rename from frontend-web/src/components/login/login-component.tsx rename to frontend-web/src/components/login/LoginComponent.tsx index 9c1226e..15bd2d8 100644 --- a/frontend-web/src/components/login/login-component.tsx +++ b/frontend-web/src/components/login/LoginComponent.tsx @@ -5,18 +5,18 @@ import {Link, redirect} from "react-router-dom" import {useThemeContext} from "../../context/theme-context" import {generateIconColorMode, generateLinkColorMode} from "../utils/enable-dark-mode" import {CustomTextField} from "../partials/custom-material-textfield" -import {HttpService} from "../../service/http-service" +import {HttpGroupService} from "../../service/http-group-service" import {LoaderContext} from "../../context/loader-context" import {AlertAction, AlertContext} from "../../context/AlertContext" -export const LoginComponent: React.FunctionComponent = () => { +export function LoginComponent(): React.JSX.Element { const [username, setUsername] = useState("") const [password, setPassword] = useState("") const {dispatch} = useContext(AlertContext)! const {setLoading} = useContext(LoaderContext) const {theme} = useThemeContext() - const httpService = new HttpService() + const httpService = new HttpGroupService() useEffect(() => { document.title = "Login | FLM" @@ -73,7 +73,7 @@ export const LoginComponent: React.FunctionComponent = () => { return (
diff --git a/frontend-web/src/components/message/CreateMessageComponent.tsx b/frontend-web/src/components/messages/CreateMessageComponent.tsx similarity index 97% rename from frontend-web/src/components/message/CreateMessageComponent.tsx rename to frontend-web/src/components/messages/CreateMessageComponent.tsx index 210c4a5..6c88097 100644 --- a/frontend-web/src/components/message/CreateMessageComponent.tsx +++ b/frontend-web/src/components/messages/CreateMessageComponent.tsx @@ -4,7 +4,7 @@ import React, {useContext, useState} from "react" import {getPayloadSize} from "../../utils/string-size-calculator" import {TransportModel} from "../../interface-contract/transport-model" import {TransportActionEnum} from "../../utils/transport-action-enum" -import {HttpService} from "../../service/http-service" +import {HttpGroupService} from "../../service/http-group-service" import HighlightOffIcon from "@mui/icons-material/HighlightOff" import {WebSocketContext} from "../../context/WebsocketContext" import {AuthUserContext} from "../../context/AuthContext" @@ -74,7 +74,7 @@ export function CreateMessageComponent({groupUrl}: CreateMessageComponentProps): setMessage("") } if (file !== null) { - const httpService = new HttpService() + const httpService = new HttpGroupService() const formData = new FormData() formData.append("file", file) formData.append("userId", String(user?.id || 0)) diff --git a/frontend-web/src/components/messages/DisplayMessagesComponent.tsx b/frontend-web/src/components/messages/DisplayMessagesComponent.tsx new file mode 100644 index 0000000..e3d8676 --- /dev/null +++ b/frontend-web/src/components/messages/DisplayMessagesComponent.tsx @@ -0,0 +1,105 @@ +import {Tooltip} from "@mui/material" +import {TypeMessageEnum} from "../../utils/type-message-enum" +import React from "react" +import {FullMessageModel} from "../../interface-contract/full-message-model" +import {GroupActionEnum} from "../websocket/group-action-enum" +import {ImagePreviewComponent} from "../partials/image-preview" + +interface DisplayMessagesProps { + messages: FullMessageModel[] +} + +export function DisplayMessagesComponent({messages}: DisplayMessagesProps) { + const [imgSrc, setImgSrc] = React.useState("") + const [isPreviewImageOpen, setPreviewImageOpen] = React.useState(false) + + function handlePopupState(isOpen: boolean) { + setPreviewImageOpen(isOpen) + } + + function handleImagePreview(action: string, src: string) { + switch (action) { + case GroupActionEnum.OPEN: + setImgSrc(src) + handlePopupState(true) + break + case GroupActionEnum.CLOSE: + handlePopupState(false) + break + default: + throw new Error("handleImagePreview failed") + } + } + + function generateImageRender(message: FullMessageModel) { + if (message.fileUrl === undefined) { + return null + } + return ( +
+ {message.name} handleImagePreview(GroupActionEnum.OPEN, message.fileUrl)} + style={{ + border: "1px solid #c8c8c8", + borderRadius: "7%" + }}/> +
+ ) + } + + return
+ + {messages.map((messageModel, index, array) => ( + +
+ {index >= 1 && array[index - 1].userId === array[index].userId + ?
+ :
+
{messageModel.initials}
+
+ } +
+ {index >= 1 && array[index - 1].userId === array[index].userId + ?
+ :
+ {messageModel.sender} +
+ } + { + messageModel.type === TypeMessageEnum.TEXT + ?
+ {messageModel.message} +
+ :
+ {generateImageRender(messageModel)} +
+ } +
+
+ + ))} +
+} diff --git a/frontend-web/src/components/partials/HeaderComponent.tsx b/frontend-web/src/components/partials/HeaderComponent.tsx index 9f2b783..70a486f 100644 --- a/frontend-web/src/components/partials/HeaderComponent.tsx +++ b/frontend-web/src/components/partials/HeaderComponent.tsx @@ -1,130 +1,45 @@ import ClearAllIcon from "@mui/icons-material/ClearAll" -import {Button, FormControlLabel, Skeleton, Switch, Toolbar, Typography} from "@mui/material" -import React, {useContext} from "react" -import {HttpService} from "../../service/http-service" -import {AuthUserContext} from "../../context/AuthContext" +import {TextField, Toolbar, Typography} from "@mui/material" +import React from "react" +import {AccountMenu} from "../user-account/UseAccountComponent" export function HeaderComponent(): React.JSX.Element { - const {user, setUser} = useContext(AuthUserContext)! - const { - authLoading, - } = {authLoading: true} - - const httpService = new HttpService() - const theme = "light" // useEffect(() => { // setCookie("pref-theme", theme) // }, [theme]) - function toggleThemeMode() { - // toggleTheme() - } - - async function logoutUser(event: React.MouseEvent) { - event.preventDefault() - await httpService.logout() - setUser(undefined) - // dispatch(setAlerts({ - // alert: { - // text: "You log out successfully", - // alert: "success", - // isOpen: true - // } - // })) - // history.push("/") - } - - function generateLoading() { - return [1, 2, 3, 4].map((index) => ( -
- -
- )) - } + // async function logoutUser(event: React.MouseEvent) { + // event.preventDefault() + // await httpService.logout() + // setUser(undefined) + // dispatch(setAlerts({ + // alert: { + // text: "You log out successfully", + // alert: "success", + // isOpen: true + // } + // })) + // history.push("/") + // } return ( <>
- + - {/**/} FastLiteMessage - {/**/} - + +
diff --git a/frontend-web/src/components/partials/alert-component.tsx b/frontend-web/src/components/partials/alert-component.tsx index 8144837..ffc8e19 100644 --- a/frontend-web/src/components/partials/alert-component.tsx +++ b/frontend-web/src/components/partials/alert-component.tsx @@ -1,6 +1,6 @@ import {Alert, AlertTitle, Collapse} from "@mui/material" import React, {useContext} from "react" -import {AlertAction, AlertContext, AlertContextProvider} from "../../context/AlertContext" +import {AlertAction, AlertContext} from "../../context/AlertContext" export const AlertComponent: React.FunctionComponent = () => { const {alerts, dispatch} = useContext(AlertContext)! diff --git a/frontend-web/src/components/register/register-user.tsx b/frontend-web/src/components/register/register-user.tsx index 9e087f6..c6556d2 100644 --- a/frontend-web/src/components/register/register-user.tsx +++ b/frontend-web/src/components/register/register-user.tsx @@ -4,7 +4,7 @@ import { Alert, Button, Collapse, Grid, IconButton, Typography } from "@mui/mate import React from "react" import { Link } from "react-router-dom" import { useThemeContext } from "../../context/theme-context" -import { HttpService } from "../../service/http-service" +import { HttpGroupService } from "../../service/http-group-service" import "./register-form.css" import { CustomTextField } from "../partials/custom-material-textfield" import { generateColorMode, generateIconColorMode, generateLinkColorMode } from "../utils/enable-dark-mode" @@ -22,7 +22,7 @@ export const RegisterFormComponent = (): JSX.Element => { const [displayEmailNotValid, setDisplayEmailNotValid] = React.useState(false) const [errorArray, setErrorArray] = React.useState([]) const refWrapper = React.useRef() - const httpService = new HttpService() + const httpService = new HttpGroupService() function checkFormValidation (): string[] { const validationErrors: string[] = [] @@ -141,7 +141,7 @@ export const RegisterFormComponent = (): JSX.Element => { return (
diff --git a/frontend-web/src/components/user-account/UseAccountComponent.tsx b/frontend-web/src/components/user-account/UseAccountComponent.tsx new file mode 100644 index 0000000..3ac0e09 --- /dev/null +++ b/frontend-web/src/components/user-account/UseAccountComponent.tsx @@ -0,0 +1,118 @@ +import * as React from "react" +import {useContext} from "react" +import Box from "@mui/material/Box" +import Avatar from "@mui/material/Avatar" +import Menu from "@mui/material/Menu" +import MenuItem from "@mui/material/MenuItem" +import ListItemIcon from "@mui/material/ListItemIcon" +import Divider from "@mui/material/Divider" +import IconButton from "@mui/material/IconButton" +import Tooltip from "@mui/material/Tooltip" +import PersonAdd from "@mui/icons-material/PersonAdd" +import Settings from "@mui/icons-material/Settings" +import Logout from "@mui/icons-material/Logout" +import {HttpGroupService} from "../../service/http-group-service" +import {AlertAction, AlertContext} from "../../context/AlertContext" + +export function AccountMenu() { + const {dispatch} = useContext(AlertContext)! + const [anchorEl, setAnchorEl] = React.useState(null) + const open = Boolean(anchorEl) + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget) + } + + const handleClose = () => { + setAnchorEl(null) + } + + async function logout() { + const http = new HttpGroupService() + await http.logout() + dispatch({ + type: AlertAction.ADD_ALERT, + payload: {alert: "success", id: crypto.randomUUID(), isOpen: true, text: "You have successfully logged out"} + }) + handleClose() + } + + return ( + + + + + TM + + + + + + Profile + + + My account + + + + + + + Add another account + + + + + + Settings + + + + + + Logout + + + + ) +} diff --git a/frontend-web/src/components/websocket/video-component.tsx b/frontend-web/src/components/websocket/video-component.tsx index 324a964..ebf546a 100644 --- a/frontend-web/src/components/websocket/video-component.tsx +++ b/frontend-web/src/components/websocket/video-component.tsx @@ -10,7 +10,7 @@ import {VideoControl} from "../partials/video/video-control" import {EmptyRoom} from "../partials/video/empty-room" import {HangUpControl} from "../partials/video/hang-up-control" import {CallEnded} from "../partials/video/call-ended" -import {HttpService} from "../../service/http-service" +import {HttpGroupService} from "../../service/http-group-service" const getUuid = (location: string): string => { const temp = location.split("/") @@ -33,7 +33,7 @@ export const VideoComponent = (): React.JSX.Element => { const isUserInitiateSession = location.search.split("=")[1] const roomUrl = getUuid(location.pathname) const groupUrlFromParent = (window as any).groupUrl - const http = new HttpService() + const http = new HttpGroupService() peerConnection.addEventListener("connectionstatechange", () => { switch (peerConnection.connectionState) { diff --git a/frontend-web/src/components/websocket/websocket-chat-component.tsx b/frontend-web/src/components/websocket/websocket-chat-component.tsx index a15ce2f..f482d73 100644 --- a/frontend-web/src/components/websocket/websocket-chat-component.tsx +++ b/frontend-web/src/components/websocket/websocket-chat-component.tsx @@ -1,29 +1,21 @@ -import {Box, CircularProgress, Tooltip} from "@mui/material" -import React, {useContext, useEffect, useState} from "react" -import {GroupActionEnum} from "./group-action-enum" -import {useThemeContext} from "../../context/theme-context" -import {FullMessageModel} from "../../interface-contract/full-message-model" +import {Box, CircularProgress} from "@mui/material" +import React, {useContext, useEffect} from "react" import {TransportActionEnum} from "../../utils/transport-action-enum" -import {TypeMessageEnum} from "../../utils/type-message-enum" import {TransportModel} from "../../interface-contract/transport-model" -import {ImagePreviewComponent} from "../partials/image-preview" import {ActiveVideoCall} from "../partials/video/active-video-call" import {GroupModel} from "../../interface-contract/group-model" import {NoDataComponent} from "../partials/NoDataComponent" import {HttpMessageService} from "../../service/http-message.service" import {AlertAction, AlertContext} from "../../context/AlertContext" -import {CreateMessageComponent} from "../message/CreateMessageComponent" +import {CreateMessageComponent} from "../messages/CreateMessageComponent" import {WebSocketContext} from "../../context/WebsocketContext" +import {DisplayMessagesComponent} from "../messages/DisplayMessagesComponent" export const WebSocketChatComponent: React.FunctionComponent<{ groupUrl?: string }> = ({groupUrl}) => { - const {theme} = useThemeContext() const {dispatch} = useContext(AlertContext)! - const {ws} = useContext(WebSocketContext)! - const [isPreviewImageOpen, setPreviewImageOpen] = React.useState(false) + const {ws, messages, setMessages} = useContext(WebSocketContext)! const [messageId, setLastMessageId] = React.useState(0) const [loadingOldMessages, setLoadingOldMessages] = React.useState(false) - const [messages, setMessages] = useState([]) - const [imgSrc, setImgSrc] = React.useState("") let messageEnd: HTMLDivElement | null const { @@ -67,48 +59,10 @@ export const WebSocketChatComponent: React.FunctionComponent<{ groupUrl?: string } }, [messages]) - function styleSelectedMessage() { - return theme === "dark" ? "hover-msg-dark" : "hover-msg-light" - } - - function generateImageRender(message: FullMessageModel) { - if (message.fileUrl === undefined) { - return null - } - return ( -
- {message.name} handleImagePreview(GroupActionEnum.OPEN, message.fileUrl)} - style={{ - border: "1px solid #c8c8c8", - borderRadius: "7%" - }}/> -
- ) - } - function scrollToEnd() { messageEnd?.scrollIntoView({behavior: "auto"}) } - function handlePopupState(isOpen: boolean) { - setPreviewImageOpen(isOpen) - } - - function handleImagePreview(action: string, src: string) { - switch (action) { - case GroupActionEnum.OPEN: - setImgSrc(src) - handlePopupState(true) - break - case GroupActionEnum.CLOSE: - handlePopupState(false) - break - default: - throw new Error("handleImagePreview failed") - } - } - function handleScroll(event: any) { if (event.target.scrollTop === 0) { if (!allMessagesFetched && ws) { @@ -144,7 +98,7 @@ export const WebSocketChatComponent: React.FunctionComponent<{ groupUrl?: string }}>
@@ -193,59 +147,8 @@ export const WebSocketChatComponent: React.FunctionComponent<{ groupUrl?: string
} - - {messages.map((messageModel, index, array) => ( - -
- {index >= 1 && array[index - 1].userId === array[index].userId - ?
- :
-
{messageModel.initials}
-
- } -
- {index >= 1 && array[index - 1].userId === array[index].userId - ?
- :
- {messageModel.sender} -
- } - { - messageModel.type === TypeMessageEnum.TEXT - ?
- {messageModel.message} -
- :
- {generateImageRender(messageModel)} -
- } -
-
- - ))} + + {/*
= ({groupUrl}) => { const [paramsOpen, setParamsOpen] = useState(false) @@ -40,10 +41,8 @@ export const WebSocketGroupActionComponent: React.FunctionComponent<{ groupUrl?: const [openTooltipId, setToolTipId] = useState(null) const {theme} = useThemeContext() const {ws} = useContext(WebSocketContext)! - const httpService = new HttpService() - const {user} = {} as any // TODO remove any - - const groups = [] + const httpService = new HttpGroupService() + const {user, groups} = useContext(AuthUserContext)! function handleTooltipAction(event: any, action: string) { event.preventDefault() diff --git a/frontend-web/src/components/websocket/websocket-groups-component.tsx b/frontend-web/src/components/websocket/websocket-groups-component.tsx index 7a0c866..65110b6 100644 --- a/frontend-web/src/components/websocket/websocket-groups-component.tsx +++ b/frontend-web/src/components/websocket/websocket-groups-component.tsx @@ -12,6 +12,7 @@ import {dateParser} from "../../utils/date-formater" import {SkeletonLoader} from "../partials/skeleten-loader" import {AuthUserContext} from "../../context/AuthContext" import NoteAddOutlinedIcon from "@mui/icons-material/NoteAddOutlined" +import {WebSocketContext} from "../../context/WebsocketContext" interface IClockType { date: string @@ -46,7 +47,7 @@ export const WebsocketGroupsComponent: React.FunctionComponent group.url === url) @@ -92,9 +93,11 @@ export const WebsocketGroupsComponent: React.FunctionComponent - - Application is currently unavailable - + + + Application is currently unavailable + +
{ const {theme} = useThemeContext() @@ -17,17 +18,20 @@ export const WebSocketMainComponent: React.FunctionComponent = (): React.JSX.Ele }, []) return ( -
- - - - - -
+ <> + +
+ + + + + +
+ ) } diff --git a/frontend-web/src/components/websocket/websocketStyle.css b/frontend-web/src/components/websocket/websocketStyle.css index 2fcfa64..33ea468 100644 --- a/frontend-web/src/components/websocket/websocketStyle.css +++ b/frontend-web/src/components/websocket/websocketStyle.css @@ -1,5 +1,5 @@ .sidebar { - width: 420px; + width: 340px; } .light-t { diff --git a/frontend-web/src/config/websocket-config.ts b/frontend-web/src/config/websocket-config.ts index 0863aec..b3505a6 100644 --- a/frontend-web/src/config/websocket-config.ts +++ b/frontend-web/src/config/websocket-config.ts @@ -1,5 +1,5 @@ import {Client} from "@stomp/stompjs" -import {HttpService} from "../service/http-service" +import {HttpGroupService} from "../service/http-group-service" const WS_URL = process.env.NODE_ENV === "development" ? "localhost:9090/api/" : "localhost:9090/api/" @@ -7,7 +7,7 @@ const WS_BROKER = process.env.NODE_ENV === "development" ? "ws" : "wss" export async function initWebSocket(userToken: string): Promise { console.log("Initiating WS connection...") - const service = new HttpService() + const service = new HttpGroupService() const {data} = await service.getCsrfToken() const {headerName, token} = data return new Client({ diff --git a/frontend-web/src/context/AuthContext.tsx b/frontend-web/src/context/AuthContext.tsx index a316d9f..11110c9 100644 --- a/frontend-web/src/context/AuthContext.tsx +++ b/frontend-web/src/context/AuthContext.tsx @@ -1,7 +1,8 @@ import React, {createContext, useEffect, useState} from "react" import {IUser} from "../interface-contract/user/user-model" -import {HttpService} from "../service/http-service" +import {HttpGroupService} from "../service/http-group-service" import {GroupModel} from "../interface-contract/group-model" +import {redirect} from "react-router-dom" type AuthUserContextType = { user: IUser | undefined; @@ -15,11 +16,17 @@ const AuthUserContext = createContext(null) const AuthUserContextProvider: React.FC<{ children: React.ReactNode }> = ({children}) => { const [user, setUser] = useState(undefined) const [groups, setGroups] = useState([]) + useEffect(() => { const getUserData = async () => { - const userData = await new HttpService().pingRoute() - setUser(userData.data.user) - setGroups(userData.data.groupsWrapper.map((group => group.group))) + try { + const userData = await new HttpGroupService().pingRoute() + setUser(userData.data.user) + setGroups(userData.data.groupsWrapper.map((group => group.group))) + } catch (error) { + console.warn(`User not connected : ${error}`) + redirect("/login") + } } getUserData() }, []) diff --git a/frontend-web/src/context/WebsocketContext.tsx b/frontend-web/src/context/WebsocketContext.tsx index d87b040..d920304 100644 --- a/frontend-web/src/context/WebsocketContext.tsx +++ b/frontend-web/src/context/WebsocketContext.tsx @@ -10,8 +10,10 @@ import {AuthUserContext} from "./AuthContext" type WebSocketContextType = { ws: Client | undefined + messages: FullMessageModel[] isWsConnected: boolean setWsClient: (ws: Client) => void + setMessages: (messages: FullMessageModel[]) => void setWsConnected: (isConnected: boolean) => void } @@ -19,6 +21,7 @@ const WebSocketContext = createContext(undefin const WebsocketContextProvider: React.FC<{ children: ReactNode }> = ({children}) => { const [ws, setWsClient] = useState(undefined) + const [messages, setMessages] = useState([]) const [isWsConnected, setWsConnected] = useState(false) const {user} = useContext(AuthUserContext)! @@ -33,10 +36,10 @@ const WebsocketContextProvider: React.FC<{ children: ReactNode }> = ({children}) setWsClient(wsObj) wsObj.onConnect = () => { console.log("WS connected") - // dispatch(wsHealthCheckConnected({ isWsConnected: true })) - // setLoading(false) + setWsConnected(true) wsObj.subscribe(`/topic/user/${user.id}`, (res: IMessage) => { const data = JSON.parse(res.body) as OutputTransportDTO + console.log("RECEIVE SUBSCRIBIG", data) switch (data.action) { case TransportActionEnum.FETCH_GROUP_MESSAGES: { // const result = data.object as WrapperMessageModel @@ -46,38 +49,17 @@ const WebsocketContextProvider: React.FC<{ children: ReactNode }> = ({children}) // })) break } - case TransportActionEnum.LEAVE_GROUP: { - // const { - // groupUrl, - // groupName - // } = data.object as ILeaveGroupModel - // dispatch(removeUserFromGroup({ groupUrl })) - // dispatch(setAlerts({ - // alert: { - // alert: "success", - // isOpen: true, - // text: `you left the group ${groupName}` - // } - // })) - break - } - case TransportActionEnum.ADD_CHAT_HISTORY: { - // const wrapper = data.object as WrapperMessageModel - // dispatch(setAllMessagesFetched({ allMessagesFetched: wrapper.lastMessage })) - // const messages = wrapper.messages - // dispatch(setGroupMessages({ messages })) - } - break + case TransportActionEnum.SEND_GROUP_MESSAGE: break case TransportActionEnum.NOTIFICATION_MESSAGE: { const message = data.object as FullMessageModel // dispatch(updateGroupsWithLastMessageSent({ - // userId: user.id, - // message + // userId: user.id, + // message // })) // updateGroupsWithLastMessageSent(dispatch, groups, message, user.id) - // dispatch(addChatHistory({ newMessage: message })) + setMessages(state => [...state, message]) if (message.userId !== user.id) { playNotificationSound() } @@ -119,8 +101,10 @@ const WebsocketContextProvider: React.FC<{ children: ReactNode }> = ({children}) return ( {children} diff --git a/frontend-web/src/index.tsx b/frontend-web/src/index.tsx index 69f1ec3..2076fd3 100644 --- a/frontend-web/src/index.tsx +++ b/frontend-web/src/index.tsx @@ -1,4 +1,4 @@ -import React from "react" +import React, {useEffect} from "react" import "./index.css" import * as serviceWorker from "./serviceWorker" import {createBrowserRouter, redirect, RouterProvider} from "react-router-dom" @@ -10,16 +10,15 @@ import {VideoComponent} from "./components/websocket/video-component" import {LoaderProvider} from "./context/loader-context" import {AlertComponent} from "./components/partials/alert-component" import {LoaderComponent} from "./components/partials/loader/LoaderComponent" -import {HttpService} from "./service/http-service" +import {HttpGroupService} from "./service/http-group-service" import {AlertContextProvider} from "./context/AlertContext" import {AuthUserContextProvider} from "./context/AuthContext" -import {HeaderComponent} from "./components/partials/HeaderComponent" const router = createBrowserRouter([ { path: "/", loader: async () => { - return new HttpService().pingRoute().catch(() => redirect("/login")) + return new HttpGroupService().pingRoute().catch(() => redirect("/login")) }, }, { @@ -44,19 +43,38 @@ const router = createBrowserRouter([ } ]) -createRoot(document.getElementById("root")!) - .render( +function RootComponent() { + + useEffect(() => { + const getCsrfToken = async () => { + const http = new HttpGroupService() + try { + const {data} = await http.getCsrfToken() + localStorage.setItem("csrf", JSON.stringify(data)) + } catch (error) { + console.log("ERROR", error) + } + } + getCsrfToken() + }, []) + + return ( - ) +} + +createRoot(document.getElementById("root")!) + .render( + + ) // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. diff --git a/frontend-web/src/service/http-service.ts b/frontend-web/src/service/http-group-service.ts similarity index 96% rename from frontend-web/src/service/http-service.ts rename to frontend-web/src/service/http-group-service.ts index ff5e322..611bb91 100644 --- a/frontend-web/src/service/http-service.ts +++ b/frontend-web/src/service/http-group-service.ts @@ -7,7 +7,7 @@ import {GroupUserModel} from "../interface-contract/group-user-model" import {HttpMainService} from "./http-main.service" import {Csrf} from "../interface-contract/csrf/csrf.type" -export class HttpService extends HttpMainService { +export class HttpGroupService extends HttpMainService { public constructor() { super() @@ -42,7 +42,7 @@ export class HttpService extends HttpMainService { } public fetchAllUsersInConversation(groupUrl: string): Promise> { - return this.instance.get("users/group/" + groupUrl, {}) + return this.instance.get(`users/group/${groupUrl}`) } public fetchAllUsersWithoutAlreadyInGroup(groupUrl: string): Promise> { diff --git a/frontend-web/src/service/http-main.service.ts b/frontend-web/src/service/http-main.service.ts index d6d115d..7ddf986 100644 --- a/frontend-web/src/service/http-main.service.ts +++ b/frontend-web/src/service/http-main.service.ts @@ -1,4 +1,5 @@ import axios, {AxiosInstance} from "axios" +import {Csrf} from "../interface-contract/csrf/csrf.type" export abstract class HttpMainService { @@ -11,9 +12,13 @@ export abstract class HttpMainService { baseURL }) this.instance.interceptors.response.use((response) => { + const csrf = localStorage.getItem("csrf") + if (csrf) { + const {headerName, token} = JSON.parse(csrf) as Csrf + response.config.headers[headerName] = token + } return response }, (error) => { - console.log("ERROR", error) return Promise.reject(error) }) }