├── .gitattributes ├── frontend-web ├── .nvmrc ├── .eslintignore ├── jest.setup.ts ├── .npmrc ├── src │ ├── react-app-env.d.ts │ ├── components │ │ ├── messages │ │ │ ├── VideoCallMessageComponent.css │ │ │ ├── SearchMessageComponent.css │ │ │ ├── VideoCallMessageComponent.tsx │ │ │ └── SearchMessageComponent.tsx │ │ ├── websocket │ │ │ ├── group-action-enum.ts │ │ │ ├── websocketStyle.css │ │ │ ├── websocket-chat-wrapper.component.tsx │ │ │ ├── CallWindowComponent.tsx │ │ │ └── websocket-main-component.tsx │ │ ├── utils │ │ │ ├── play-sound-notification.ts │ │ │ └── enable-dark-mode.ts │ │ ├── register │ │ │ ├── register-form.css │ │ │ └── RegisterUserWrapper.tsx │ │ ├── partials │ │ │ ├── list-items │ │ │ │ ├── MultimediaListStyle.css │ │ │ │ └── MultimediaListComponent.tsx │ │ │ ├── FooterComponent.test.tsx │ │ │ ├── video │ │ │ │ ├── hang-up-control.tsx │ │ │ │ ├── sound-control.tsx │ │ │ │ ├── live-call.svg │ │ │ │ ├── call-ended.tsx │ │ │ │ ├── empty-room.tsx │ │ │ │ ├── video-control.tsx │ │ │ │ └── active-video-call.tsx │ │ │ ├── loader │ │ │ │ └── LoaderComponent.tsx │ │ │ ├── NoDataComponent.tsx │ │ │ ├── skeleten-loader.tsx │ │ │ ├── FooterComponent.tsx │ │ │ ├── HeaderComponent.tsx │ │ │ ├── alert-component.tsx │ │ │ └── all-users-dialog.tsx │ │ ├── login │ │ │ └── LoginWrapperComponent.tsx │ │ ├── reset-password │ │ │ └── ResetPasswordComponent.tsx │ │ ├── welcome │ │ │ └── WelcomeComponent.tsx │ │ └── search │ │ │ └── SearchComponent.tsx │ ├── utils │ │ ├── type-group-enum.ts │ │ ├── type-message-enum.ts │ │ ├── string-size-calculator.ts │ │ ├── date-formater.spec.ts │ │ ├── rtc-action-enum.ts │ │ ├── transport-action-enum.ts │ │ ├── redux-constants.ts │ │ └── date-formater.ts │ ├── interface-contract │ │ ├── jwt-model.ts │ │ ├── leave-group-model.ts │ │ ├── user │ │ │ ├── group-call-model.ts │ │ │ ├── user-wrapper.ts │ │ │ ├── group-wrapper-model.ts │ │ │ └── user-model.ts │ │ ├── csrf │ │ │ └── csrf.type.ts │ │ ├── message-model.ts │ │ ├── group-user-model.ts │ │ ├── wrapper-message-model.ts │ │ ├── group-model.ts │ │ ├── feedback-model.ts │ │ ├── input-transport-model.ts │ │ ├── full-message-model.ts │ │ ├── search-text │ │ │ └── search-text.type.ts │ │ ├── rtc-transport-model.ts │ │ ├── redux-model.ts │ │ └── transport-model.ts │ ├── service │ │ ├── http-message.service.ts │ │ └── http-main.service.ts │ ├── context │ │ ├── loader-context.tsx │ │ ├── theme-context.tsx │ │ ├── SearchContext.tsx │ │ ├── UserContext.tsx │ │ ├── AlertContext.tsx │ │ └── GroupContext.tsx │ ├── config │ │ └── websocket-config.ts │ ├── actions │ │ └── web-rtc-actions.ts │ ├── index.css │ └── index.tsx ├── public │ ├── config.json │ ├── robots.txt │ ├── favicon.png │ ├── assets │ │ ├── sounds │ │ │ ├── new_message.mp3 │ │ │ └── incoming-call-audio.mp3 │ │ └── icons │ │ │ ├── live.svg │ │ │ ├── landing_logo2.svg │ │ │ └── landing_logo.svg │ ├── manifest.json │ └── index.html ├── devcontainer.json ├── jest.config.js ├── Dockerfile ├── tsconfig.json ├── .eslintrc.js └── package.json ├── assets ├── flm.PNG └── flm2.PNG ├── .dockerignore ├── .devcontainer ├── devcontainer.json └── Dockerfile ├── backend ├── commons │ ├── src │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── mercure │ │ │ └── commons │ │ │ ├── utils │ │ │ ├── GroupTypeEnum.java │ │ │ ├── MessageTypeEnum.java │ │ │ ├── RtcActionEnum.java │ │ │ ├── StaticVariable.java │ │ │ ├── TransportActionEnum.java │ │ │ ├── ColorsUtils.java │ │ │ ├── ComparatorListGroupDTO.java │ │ │ └── ComparatorListWrapperGroupDTO.java │ │ │ ├── dto │ │ │ ├── search │ │ │ │ ├── FullTextSearchDTO.java │ │ │ │ ├── FullTextSearchDatabaseResponse.java │ │ │ │ ├── FullTextSearchResponseDTO.java │ │ │ │ └── FullTextSearchDatabaseResponseDTO.java │ │ │ ├── CreateGroupDTO.java │ │ │ ├── LeaveGroupDTO.java │ │ │ ├── user │ │ │ │ ├── GroupCallDTO.java │ │ │ │ ├── GroupWrapperDTO.java │ │ │ │ ├── InitUserDTO.java │ │ │ │ └── GroupDTO.java │ │ │ ├── JwtDTO.java │ │ │ ├── OutputTransportDTO.java │ │ │ ├── GroupMemberDTO.java │ │ │ ├── AuthenticationUserDTO.java │ │ │ ├── LightUserDTO.java │ │ │ ├── WrapperMessageDTO.java │ │ │ ├── RtcTransportDTO.java │ │ │ ├── AuthUserDTO.java │ │ │ ├── InputTransportDTO.java │ │ │ ├── MessageDTO.java │ │ │ └── NotificationDTO.java │ │ │ ├── entity │ │ │ ├── FileEntity.java │ │ │ ├── GroupRoleKey.java │ │ │ ├── MessageUserKey.java │ │ │ ├── MessageEntity.java │ │ │ ├── MessageUserEntity.java │ │ │ ├── GroupUser.java │ │ │ ├── GroupEntity.java │ │ │ └── UserEntity.java │ │ │ ├── service │ │ │ └── FileService.java │ │ │ └── repository │ │ │ └── FileRepository.java │ └── pom.xml ├── .devcontainer │ ├── devcontainer.json │ ├── docker-compose.yml │ └── Dockerfile ├── .idea │ ├── vcs.xml │ ├── .gitignore │ ├── jpa-buddy.xml │ ├── inspectionProfiles │ │ └── Project_Default.xml │ ├── misc.xml │ ├── encodings.xml │ ├── jarRepositories.xml │ ├── dataSources.xml │ └── compiler.xml ├── storage │ ├── src │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── mercure │ │ │ ├── storage │ │ │ ├── config │ │ │ │ ├── StorageOptions.java │ │ │ │ └── StorageConfiguration.java │ │ │ ├── service │ │ │ │ ├── GcpStorage.java │ │ │ │ ├── LocalStorage.java │ │ │ │ └── StorageService.java │ │ │ └── controller │ │ │ │ └── WsFileController.java │ │ │ └── StorageApplication.java │ └── pom.xml ├── core │ └── src │ │ ├── main │ │ ├── java │ │ │ └── com │ │ │ │ └── mercure │ │ │ │ ├── CoreApplication.java │ │ │ │ └── core │ │ │ │ ├── repository │ │ │ │ ├── UserSeenMessageRepository.java │ │ │ │ ├── GroupRepository.java │ │ │ │ ├── GroupUserJoinRepository.java │ │ │ │ ├── UserRepository.java │ │ │ │ └── MessageRepository.java │ │ │ │ ├── config │ │ │ │ ├── ResourceConfig.java │ │ │ │ ├── CacheConfig.java │ │ │ │ ├── HandShakeInterceptor.java │ │ │ │ ├── WebSocketSecurityConfig.java │ │ │ │ ├── WsConfig.java │ │ │ │ ├── JwtWebConfig.java │ │ │ │ └── SecurityConfig.java │ │ │ │ ├── controller │ │ │ │ ├── PingController.java │ │ │ │ ├── SearchController.java │ │ │ │ └── MessageController.java │ │ │ │ ├── service │ │ │ │ ├── CustomUserDetailsService.java │ │ │ │ ├── cache │ │ │ │ │ ├── RoomCacheService.java │ │ │ │ │ └── CallsCacheService.java │ │ │ │ ├── UserSeenMessageService.java │ │ │ │ ├── DbInit.java │ │ │ │ ├── JwtUtil.java │ │ │ │ └── UserService.java │ │ │ │ └── mapper │ │ │ │ └── GroupCallMapper.java │ │ └── resources │ │ │ ├── banner │ │ │ └── banner.txt │ │ │ └── db │ │ │ ├── changelog-master.xml │ │ │ └── changelog │ │ │ ├── addTimestampOnGroupTable.xml │ │ │ ├── deleteSeenColumnInMessage.xml │ │ │ ├── addMessageSeenFlagGroupUser.xml │ │ │ ├── addGroupColumn.xml │ │ │ ├── createGroupTable.xml │ │ │ ├── createJoinTableGroupUser.xml │ │ │ ├── createFileBlobTable.xml │ │ │ ├── createJoinTableUserGroup.xml │ │ │ ├── createJoinTableMessageUserSeen.xml │ │ │ ├── createMessageWsTable.xml │ │ │ └── createUserTable.xml │ │ └── test │ │ └── java │ │ └── com │ │ └── mercure │ │ ├── AppTest.java │ │ └── mapper │ │ └── GroupCallMapperTest.java ├── rtc │ └── src │ │ ├── main │ │ └── java │ │ │ └── com │ │ │ └── mercure │ │ │ └── RtcApplication.java │ │ └── test │ │ └── java │ │ └── com │ │ └── mercure │ │ └── AppTest.java ├── gateway │ ├── src │ │ └── main │ │ │ ├── java │ │ │ └── com │ │ │ │ └── mercure │ │ │ │ ├── GatewayApplication.java │ │ │ │ └── gateway │ │ │ │ ├── controller │ │ │ │ └── HealthController.java │ │ │ │ └── configuration │ │ │ │ └── GatewayConfig.java │ │ │ └── resources │ │ │ └── application.yml │ └── pom.xml └── cache │ └── src │ ├── main │ └── java │ │ └── com │ │ └── mercure │ │ ├── CacheApplication.java │ │ └── cache │ │ └── controller │ │ └── HealthController.java │ └── test │ └── java │ └── com │ └── mercure │ └── AppTest.java ├── Dockerfile-front ├── Dockerfile-back ├── .github └── workflows │ ├── test-back.yml │ ├── build-back.yml │ └── front.yml ├── docker-compose.yml ├── .gitignore ├── LICENSE └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /frontend-web/.nvmrc: -------------------------------------------------------------------------------- 1 | 20.12.1 2 | -------------------------------------------------------------------------------- /frontend-web/.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | public 3 | -------------------------------------------------------------------------------- /frontend-web/jest.setup.ts: -------------------------------------------------------------------------------- 1 | import "@testing-library/jest-dom" -------------------------------------------------------------------------------- /frontend-web/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | registry=https://registry.npmjs.org/ 3 | -------------------------------------------------------------------------------- /frontend-web/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend-web/public/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "apiUrl": "http://localhost:9090/api/" 3 | } 4 | -------------------------------------------------------------------------------- /assets/flm.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thibaut-Mouton/react-spring-messenger-project/HEAD/assets/flm.PNG -------------------------------------------------------------------------------- /assets/flm2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thibaut-Mouton/react-spring-messenger-project/HEAD/assets/flm2.PNG -------------------------------------------------------------------------------- /frontend-web/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend-web/src/components/messages/VideoCallMessageComponent.css: -------------------------------------------------------------------------------- 1 | .call-container { 2 | cursor: pointer; 3 | } 4 | -------------------------------------------------------------------------------- /frontend-web/src/utils/type-group-enum.ts: -------------------------------------------------------------------------------- 1 | export enum TypeGroupEnum { 2 | GROUP = "GROUP", 3 | SINGLE = "SINGLE" 4 | } 5 | -------------------------------------------------------------------------------- /frontend-web/src/interface-contract/jwt-model.ts: -------------------------------------------------------------------------------- 1 | export interface JwtModel { 2 | username: string 3 | password: string 4 | } 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | uploads 3 | frontend-web/build 4 | backend/target 5 | backend/mvnw 6 | backend/mvnw.cmd 7 | backend/.idea -------------------------------------------------------------------------------- /frontend-web/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "node:20", 3 | "name": "flm-frontend-web", 4 | "forwardPorts": [ 5 | 3000, 6 | ] 7 | } -------------------------------------------------------------------------------- /frontend-web/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thibaut-Mouton/react-spring-messenger-project/HEAD/frontend-web/public/favicon.png -------------------------------------------------------------------------------- /frontend-web/src/components/websocket/group-action-enum.ts: -------------------------------------------------------------------------------- 1 | export enum GroupActionEnum { 2 | OPEN = "OPEN", CLOSE = "CLOSE", PARAM = "PARAM" 3 | } 4 | -------------------------------------------------------------------------------- /frontend-web/src/utils/type-message-enum.ts: -------------------------------------------------------------------------------- 1 | export enum TypeMessageEnum { 2 | TEXT = "TEXT", 3 | FILE = "FILE", 4 | CALL = "CALL" 5 | } 6 | -------------------------------------------------------------------------------- /frontend-web/src/interface-contract/leave-group-model.ts: -------------------------------------------------------------------------------- 1 | export interface ILeaveGroupModel { 2 | groupUrl: string 3 | 4 | groupName: string 5 | } 6 | -------------------------------------------------------------------------------- /frontend-web/src/utils/string-size-calculator.ts: -------------------------------------------------------------------------------- 1 | export const getPayloadSize = (value: string): number => { 2 | return new Blob([value]).size 3 | } 4 | -------------------------------------------------------------------------------- /frontend-web/src/interface-contract/user/group-call-model.ts: -------------------------------------------------------------------------------- 1 | export interface IGroupCall { 2 | anyCallActive: boolean; 3 | activeCallUrl?: string; 4 | } 5 | -------------------------------------------------------------------------------- /frontend-web/src/interface-contract/csrf/csrf.type.ts: -------------------------------------------------------------------------------- 1 | export type Csrf = { 2 | parameterName: string, 3 | token: string, 4 | headerName: string, 5 | } 6 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "dockerfile": "Dockerfile" 4 | }, 5 | "forwardPorts": [ 6 | 3000, 7 | 8080 8 | ] 9 | } -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/utils/GroupTypeEnum.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.utils; 2 | 3 | public enum GroupTypeEnum { 4 | GROUP, SINGLE 5 | } 6 | -------------------------------------------------------------------------------- /frontend-web/src/components/messages/SearchMessageComponent.css: -------------------------------------------------------------------------------- 1 | .simple-highlight { 2 | background-color: rgba(255, 184, 0, 0.62); 3 | padding: 0.1em 0.2em; 4 | } 5 | -------------------------------------------------------------------------------- /frontend-web/public/assets/sounds/new_message.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thibaut-Mouton/react-spring-messenger-project/HEAD/frontend-web/public/assets/sounds/new_message.mp3 -------------------------------------------------------------------------------- /frontend-web/src/components/utils/play-sound-notification.ts: -------------------------------------------------------------------------------- 1 | export const playNotificationSound = (): void => { 2 | new Audio("/assets/sounds/new_message.mp3").play() 3 | } 4 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/utils/MessageTypeEnum.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.utils; 2 | 3 | public enum MessageTypeEnum { 4 | TEXT, FILE, CALL 5 | } 6 | -------------------------------------------------------------------------------- /frontend-web/src/interface-contract/message-model.ts: -------------------------------------------------------------------------------- 1 | export interface MessageModel { 2 | userId: number | string | undefined 3 | groupUrl: string | null 4 | message: string 5 | } 6 | -------------------------------------------------------------------------------- /backend/.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flm-backend", 3 | "dockerComposeFile": "docker-compose.yml", 4 | "service": "flm-back", 5 | "workspaceFolder": "/workspace" 6 | } 7 | -------------------------------------------------------------------------------- /frontend-web/public/assets/sounds/incoming-call-audio.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Thibaut-Mouton/react-spring-messenger-project/HEAD/frontend-web/public/assets/sounds/incoming-call-audio.mp3 -------------------------------------------------------------------------------- /frontend-web/src/interface-contract/group-user-model.ts: -------------------------------------------------------------------------------- 1 | export interface GroupUserModel { 2 | userId: number | string 3 | firstName: string 4 | lastName: string 5 | admin: boolean 6 | } 7 | -------------------------------------------------------------------------------- /backend/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /backend/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /frontend-web/src/interface-contract/user/user-wrapper.ts: -------------------------------------------------------------------------------- 1 | import { IUser } from "./user-model" 2 | import { IGroupWrapper } from "./group-wrapper-model" 3 | 4 | export interface IUserWrapper { 5 | user: IUser 6 | groupsWrapper: IGroupWrapper[] 7 | } 8 | -------------------------------------------------------------------------------- /frontend-web/src/interface-contract/user/group-wrapper-model.ts: -------------------------------------------------------------------------------- 1 | import { GroupModel } from "../group-model" 2 | import { IGroupCall } from "./group-call-model" 3 | 4 | export interface IGroupWrapper { 5 | group: GroupModel 6 | groupCall: IGroupCall 7 | } 8 | -------------------------------------------------------------------------------- /Dockerfile-front: -------------------------------------------------------------------------------- 1 | FROM node:16.14.2 2 | 3 | MAINTAINER Thibaut MOUTON 4 | 5 | RUN mkdir -p "/product/front" 6 | 7 | COPY frontend-web /product/front 8 | 9 | WORKDIR /product/front 10 | RUN npm i 11 | 12 | CMD ["npm", "start"] -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/utils/RtcActionEnum.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.utils; 2 | 3 | public enum RtcActionEnum { 4 | INIT_ROOM, 5 | LEAVE_ROOM, 6 | JOIN_ROOM, 7 | RECEIVE_ANSWER, 8 | ICE_CANDIDATE, 9 | SEND_ANSWER 10 | } 11 | -------------------------------------------------------------------------------- /backend/.idea/jpa-buddy.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/dto/search/FullTextSearchDTO.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.dto.search; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | @Getter 7 | @Setter 8 | public class FullTextSearchDTO { 9 | 10 | private String text; 11 | } 12 | -------------------------------------------------------------------------------- /frontend-web/src/interface-contract/wrapper-message-model.ts: -------------------------------------------------------------------------------- 1 | import { FullMessageModel } from "./full-message-model" 2 | 3 | export interface WrapperMessageModel { 4 | lastMessage: boolean 5 | activeCall: boolean 6 | callUrl: string 7 | messages: FullMessageModel[] 8 | groupName: string 9 | } 10 | -------------------------------------------------------------------------------- /frontend-web/src/interface-contract/group-model.ts: -------------------------------------------------------------------------------- 1 | export interface GroupModel { 2 | id: number 3 | url: string 4 | name: string 5 | groupType: string 6 | lastMessage: string 7 | lastMessageDate: string 8 | lastMessageSeen: boolean 9 | lastMessageSender?: string 10 | } 11 | -------------------------------------------------------------------------------- /backend/storage/src/main/java/com/mercure/storage/config/StorageOptions.java: -------------------------------------------------------------------------------- 1 | package com.mercure.storage.config; 2 | 3 | import java.io.File; 4 | 5 | public interface StorageOptions { 6 | 7 | void deleteFile(); 8 | 9 | void uploadFile(File file); 10 | 11 | void downloadFile(File file); 12 | } 13 | -------------------------------------------------------------------------------- /frontend-web/src/interface-contract/user/user-model.ts: -------------------------------------------------------------------------------- 1 | import {GroupModel} from "../group-model" 2 | 3 | export interface IUser { 4 | id: number 5 | firstName: string 6 | lastName: string 7 | firstGroupUrl: string 8 | wsToken: string 9 | color: string 10 | groups: GroupModel[] 11 | } 12 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/dto/search/FullTextSearchDatabaseResponse.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.dto.search; 2 | 3 | public interface FullTextSearchDatabaseResponse { 4 | int getId(); 5 | 6 | String getGroupUrl(); 7 | 8 | String getMessage(); 9 | 10 | String getGroupName(); 11 | } 12 | -------------------------------------------------------------------------------- /frontend-web/src/components/register/register-form.css: -------------------------------------------------------------------------------- 1 | .main-register-form { 2 | text-align: center; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | @media (max-width: 1250px) { 8 | /*.main-register-form {*/ 9 | /* margin-left: 20%;*/ 10 | /* margin-right: 20%;*/ 11 | /*}*/ 12 | } 13 | -------------------------------------------------------------------------------- /Dockerfile-back: -------------------------------------------------------------------------------- 1 | FROM maven:3.8.5-openjdk-17 2 | 3 | MAINTAINER Thibaut MOUTON 4 | 5 | RUN mkdir -p "/product/backend" && \ 6 | mkdir uploads 7 | 8 | WORKDIR /product/backend 9 | 10 | COPY backend ./ 11 | 12 | RUN mvn clean install 13 | 14 | CMD ["mvn", "spring-boot:run","-Dspring-boot.run.profiles=dev"] 15 | -------------------------------------------------------------------------------- /frontend-web/src/components/partials/list-items/MultimediaListStyle.css: -------------------------------------------------------------------------------- 1 | .display { 2 | display: grid; 3 | grid-template-rows: repeat(4, 100px); 4 | grid-template-columns: repeat(4, 1fr); 5 | } 6 | 7 | .content { 8 | background-repeat: no-repeat; 9 | background-position: center; 10 | background-size: cover; 11 | } 12 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/utils/StaticVariable.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.utils; 2 | 3 | public class StaticVariable { 4 | 5 | public static String SECURE_COOKIE = "_Secure-45SFAWZ"; 6 | 7 | public static String FILE_STORAGE_PATH = "./uploads"; 8 | 9 | public static int MAX_RANDOM_STRING_GEN = 25; 10 | } 11 | -------------------------------------------------------------------------------- /frontend-web/src/interface-contract/feedback-model.ts: -------------------------------------------------------------------------------- 1 | export interface FeedbackModel { 2 | id?: string 3 | text: string 4 | alert: "warning" | "error" | "info" | "success" 5 | isOpen: boolean 6 | } 7 | 8 | export interface IPartialFeedBack { 9 | text: string 10 | alert: "warning" | "error" | "info" | "success" 11 | isOpen: boolean 12 | } 13 | -------------------------------------------------------------------------------- /frontend-web/src/utils/date-formater.spec.ts: -------------------------------------------------------------------------------- 1 | import {dateParser} from "./date-formater" 2 | 3 | describe("dateFormater", () => { 4 | describe(dateParser.name, () => { 5 | it("should format date correctly", () => { 6 | const result = dateParser("24/04/2024") 7 | expect(result).toBeDefined() 8 | }) 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /frontend-web/src/components/websocket/websocketStyle.css: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | width: 22%; 3 | background-color: #f6f8fc; 4 | } 5 | 6 | .light-t { 7 | color: rgba(0, 0, 0, 0.54); 8 | } 9 | 10 | .dark-t { 11 | color: rgba(255, 255, 255, 0.54); 12 | } 13 | 14 | @media (max-width: 1250px) { 15 | .sidebar { 16 | width: 200px; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend-web/src/interface-contract/input-transport-model.ts: -------------------------------------------------------------------------------- 1 | import { TransportActionEnum } from "../utils/transport-action-enum" 2 | 3 | export class OutputTransportDTO { 4 | public action: TransportActionEnum 5 | 6 | public object: object 7 | 8 | constructor (action: TransportActionEnum, object: object) { 9 | this.action = action 10 | this.object = object 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /frontend-web/src/interface-contract/full-message-model.ts: -------------------------------------------------------------------------------- 1 | export interface FullMessageModel { 2 | id: number 3 | type: string 4 | message: string 5 | userId: number 6 | groupId: number 7 | groupUrl: string 8 | sender: string 9 | time: string 10 | initials: string 11 | color: string 12 | name: string 13 | fileUrl: string 14 | lastMessage: boolean 15 | isMessageSeen: boolean 16 | } 17 | -------------------------------------------------------------------------------- /backend/core/src/main/java/com/mercure/CoreApplication.java: -------------------------------------------------------------------------------- 1 | package com.mercure; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class CoreApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(CoreApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/rtc/src/main/java/com/mercure/RtcApplication.java: -------------------------------------------------------------------------------- 1 | package com.mercure; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class RtcApplication { 8 | public static void main(String[] args) { 9 | SpringApplication.run(RtcApplication.class, args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /frontend-web/src/interface-contract/search-text/search-text.type.ts: -------------------------------------------------------------------------------- 1 | type FullTextSearchResponseType = { 2 | matchingText: string 3 | matchingMessages: FullTextSearchResponseGroupType[]; 4 | } 5 | 6 | type FullTextSearchResponseGroupType = { 7 | id: number 8 | groupUrl: string 9 | messages: string[] 10 | groupName: string 11 | } 12 | 13 | export type {FullTextSearchResponseType} 14 | -------------------------------------------------------------------------------- /backend/gateway/src/main/java/com/mercure/GatewayApplication.java: -------------------------------------------------------------------------------- 1 | package com.mercure; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication() 7 | public class GatewayApplication { 8 | public static void main(String[] args) { 9 | SpringApplication.run(GatewayApplication.class); 10 | } 11 | } -------------------------------------------------------------------------------- /backend/core/src/test/java/com/mercure/AppTest.java: -------------------------------------------------------------------------------- 1 | package com.mercure; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.assertTrue; 6 | 7 | 8 | /** 9 | * Unit test for simple App. 10 | */ 11 | public class AppTest { 12 | 13 | /** 14 | * Rigorous Test :-) 15 | */ 16 | @Test 17 | public void shouldAnswerWithTrue() { 18 | assertTrue(true); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/cache/src/main/java/com/mercure/CacheApplication.java: -------------------------------------------------------------------------------- 1 | package com.mercure; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class CacheApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(CacheApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/dto/CreateGroupDTO.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter 9 | @Setter 10 | @NoArgsConstructor 11 | @AllArgsConstructor 12 | public class CreateGroupDTO { 13 | 14 | private Long id1; 15 | 16 | private Long id2; 17 | } 18 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/dto/search/FullTextSearchResponseDTO.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.dto.search; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.util.List; 7 | 8 | @Getter 9 | @Setter 10 | public class FullTextSearchResponseDTO { 11 | 12 | private String matchingText; 13 | 14 | private List matchingMessages; 15 | } 16 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/dto/search/FullTextSearchDatabaseResponseDTO.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.dto.search; 2 | 3 | import lombok.*; 4 | 5 | import java.util.List; 6 | 7 | @Data 8 | public class FullTextSearchDatabaseResponseDTO { 9 | 10 | private Integer id; 11 | 12 | private String groupUrl; 13 | 14 | private List messages; 15 | 16 | private String groupName; 17 | } 18 | -------------------------------------------------------------------------------- /backend/cache/src/test/java/com/mercure/AppTest.java: -------------------------------------------------------------------------------- 1 | package com.mercure; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertTrue; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | /** 8 | * Unit test for simple App. 9 | */ 10 | public class AppTest { 11 | 12 | /** 13 | * Rigorous Test :-) 14 | */ 15 | @Test 16 | public void shouldAnswerWithTrue() { 17 | assertTrue(true); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/dto/LeaveGroupDTO.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter 9 | @Setter 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | public class LeaveGroupDTO { 13 | 14 | public String groupUrl; 15 | 16 | public String groupName; 17 | } 18 | -------------------------------------------------------------------------------- /backend/rtc/src/test/java/com/mercure/AppTest.java: -------------------------------------------------------------------------------- 1 | package com.mercure; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertTrue; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | /** 8 | * Unit test for simple App. 9 | */ 10 | public class AppTest { 11 | 12 | /** 13 | * Rigorous Test :-) 14 | */ 15 | @Test 16 | public void shouldAnswerWithTrue() { 17 | assertTrue(true); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/test-back.yml: -------------------------------------------------------------------------------- 1 | name: test-back 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v3 11 | - name: Set up JDK 17 12 | uses: actions/setup-java@v3 13 | with: 14 | java-version: '17' 15 | distribution: 'temurin' 16 | cache: maven 17 | - name: build 18 | run: mvn clean test -f backend/pom.xml 19 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/dto/user/GroupCallDTO.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.dto.user; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Setter 9 | @Getter 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | public class GroupCallDTO { 13 | 14 | private Boolean anyCallActive; 15 | 16 | private String activeCallUrl; 17 | } 18 | -------------------------------------------------------------------------------- /.github/workflows/build-back.yml: -------------------------------------------------------------------------------- 1 | name: build-back 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v3 11 | - name: Set up JDK 17 12 | uses: actions/setup-java@v3 13 | with: 14 | java-version: '17' 15 | distribution: 'temurin' 16 | cache: maven 17 | - name: build 18 | run: mvn -Dmaven.test.skip=true compile -f backend/core/pom.xml 19 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/dto/user/GroupWrapperDTO.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.dto.user; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | 9 | @Setter 10 | @Getter 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | public class GroupWrapperDTO { 14 | 15 | private GroupDTO group; 16 | 17 | private GroupCallDTO groupCall; 18 | } 19 | -------------------------------------------------------------------------------- /frontend-web/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('jest').Config} */ 2 | const config = { 3 | testEnvironment: "jsdom", 4 | coveragePathIgnorePatterns: [ 5 | "/node_modules/", 6 | "/coverage", 7 | "package.json", 8 | "package-lock.json", 9 | "reportWebVitals.ts", 10 | "setupTests.ts", 11 | "index.tsx" 12 | ], 13 | setupFilesAfterEnv: ["/jest.setup.ts"], 14 | } 15 | 16 | module.exports = config -------------------------------------------------------------------------------- /frontend-web/src/components/partials/FooterComponent.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import {render, screen} from "@testing-library/react" 3 | import {FooterComponent} from "./FooterComponent" 4 | 5 | describe(FooterComponent.name, () => { 6 | test("should render footer component", () => { 7 | render() 8 | expect(screen.getByTestId("footer-title").textContent).toEqual("FastLiteMessage - Open source software") 9 | }) 10 | }) -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/dto/JwtDTO.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | import java.io.Serializable; 9 | 10 | @Setter 11 | @Getter 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | public class JwtDTO implements Serializable { 15 | 16 | private String username; 17 | 18 | private String password; 19 | } 20 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/utils/TransportActionEnum.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.utils; 2 | 3 | public enum TransportActionEnum { 4 | SEND_GROUP_MESSAGE, 5 | FETCH_GROUP_MESSAGES, 6 | CALL_INCOMING, 7 | ADD_CHAT_HISTORY, 8 | NOTIFICATION_MESSAGE, 9 | GRANT_USER_ADMIN, 10 | MARK_MESSAGE_AS_SEEN, 11 | LEAVE_GROUP, 12 | CHECK_EXISTING_CALL, 13 | CALL_IN_PROGRESS, 14 | END_CALL, 15 | NO_CALL_IN_PROGRESS, 16 | } 17 | -------------------------------------------------------------------------------- /frontend-web/src/utils/rtc-action-enum.ts: -------------------------------------------------------------------------------- 1 | export enum RtcActionEnum { 2 | INIT_ROOM = "INIT_ROOM", 3 | 4 | LEAVE_ROOM = "LEAVE_ROOM", 5 | 6 | JOIN_ROOM = "JOIN_ROOM", 7 | 8 | SEND_ANSWER = "SEND_ANSWER", 9 | 10 | RECEIVE_ANSWER = "RECEIVE_ANSWER", 11 | 12 | ICE_CANDIDATE = "ICE_CANDIDATE", 13 | 14 | CHECK_EXISTING_CALL = "CHECK_EXISTING_CALL", 15 | 16 | CALL_IN_PROGRESS = "CALL_IN_PROGRESS", 17 | 18 | NO_CALL_IN_PROGRESS = "NO_CALL_IN_PROGRESS", 19 | } 20 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/dto/OutputTransportDTO.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.dto; 2 | 3 | import com.mercure.commons.utils.TransportActionEnum; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | @Getter 10 | @Setter 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | public class OutputTransportDTO { 14 | 15 | private TransportActionEnum action; 16 | 17 | private Object object; 18 | } -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/dto/GroupMemberDTO.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter 9 | @Setter 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | public class GroupMemberDTO { 13 | 14 | private int userId; 15 | 16 | private String firstName; 17 | 18 | private String lastName; 19 | 20 | private boolean isAdmin; 21 | } 22 | -------------------------------------------------------------------------------- /frontend-web/src/components/login/LoginWrapperComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from "react" 2 | import {WelcomeComponent} from "../welcome/WelcomeComponent" 3 | import {LoginComponent} from "./LoginComponent" 4 | 5 | export function LoginWrapperComponent(): React.JSX.Element { 6 | 7 | useEffect(() => { 8 | document.title = "Login | FLM" 9 | }, []) 10 | 11 | return <> 12 | 13 | 14 | 15 | 16 | } -------------------------------------------------------------------------------- /frontend-web/src/components/register/RegisterUserWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from "react" 2 | import {WelcomeComponent} from "../welcome/WelcomeComponent" 3 | import {RegisterUserComponent} from "./RegisterUser" 4 | 5 | export function RegisterUserWrapper(): React.JSX.Element { 6 | 7 | useEffect(() => { 8 | document.title = "Register | FLM" 9 | }, []) 10 | 11 | return <> 12 | 13 | 14 | 15 | 16 | } -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/dto/AuthenticationUserDTO.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter 9 | @Setter 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | public class AuthenticationUserDTO { 13 | 14 | private String firstname; 15 | 16 | private String lastname; 17 | 18 | private String password; 19 | 20 | private String email; 21 | } 22 | -------------------------------------------------------------------------------- /frontend-web/src/components/reset-password/ResetPasswordComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import {Box, TextField, Typography} from "@mui/material" 3 | 4 | export function ResetPasswordComponent(): React.JSX.Element { 5 | return 6 | 7 | Forgot your password ? Type your mail below. We will send to you a recovery mail. 8 | 9 | 10 | 11 | } -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/dto/LightUserDTO.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter 9 | @Setter 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | public class LightUserDTO { 13 | 14 | public int id; 15 | 16 | public String firstName; 17 | 18 | public String lastName; 19 | 20 | private int groupRole; 21 | 22 | private String wsToken; 23 | } 24 | -------------------------------------------------------------------------------- /frontend-web/src/utils/transport-action-enum.ts: -------------------------------------------------------------------------------- 1 | export enum TransportActionEnum { 2 | FETCH_GROUP_MESSAGES = "FETCH_GROUP_MESSAGES", 3 | SEND_GROUP_MESSAGE = "SEND_GROUP_MESSAGE", 4 | NOTIFICATION_MESSAGE = "NOTIFICATION_MESSAGE", 5 | GRANT_USER_ADMIN = "GRANT_USER_ADMIN", 6 | ADD_CHAT_HISTORY = "ADD_CHAT_HISTORY", 7 | MARK_MESSAGE_AS_SEEN = "MARK_MESSAGE_AS_SEEN", 8 | LEAVE_GROUP = "LEAVE_GROUP", 9 | CALL_INCOMING = "CALL_INCOMING", 10 | END_CALL = "END_CALL", 11 | CHECK_EXISTING_CALL = "CHECK_EXISTING_CALL", 12 | } 13 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/dto/user/InitUserDTO.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.dto.user; 2 | 3 | import com.mercure.commons.dto.AuthUserDTO; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | import java.util.List; 10 | 11 | @Getter 12 | @Setter 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | public class InitUserDTO { 16 | 17 | private AuthUserDTO user; 18 | 19 | private List groupsWrapper; 20 | } 21 | -------------------------------------------------------------------------------- /backend/storage/src/main/java/com/mercure/StorageApplication.java: -------------------------------------------------------------------------------- 1 | package com.mercure; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.annotation.ComponentScan; 6 | 7 | @SpringBootApplication() 8 | @ComponentScan({"com.mercure.core", "com.mercure.commons"}) 9 | public class StorageApplication { 10 | public static void main(String[] args) { 11 | SpringApplication.run(StorageApplication.class, args); 12 | } 13 | } -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | services: 3 | db: 4 | image: "mysql:8.0.29" 5 | ports: 6 | - "3306:3306" 7 | environment: 8 | MYSQL_DATABASE: fastlitemessage 9 | MYSQL_ROOT_PASSWORD: password 10 | back: 11 | restart: on-failure 12 | build: 13 | context: . 14 | dockerfile: Dockerfile-back 15 | ports: 16 | - "9090:9090" 17 | depends_on: 18 | - "db" 19 | web: 20 | build: 21 | context: . 22 | dockerfile: Dockerfile-front 23 | ports: 24 | - "3000:3000" -------------------------------------------------------------------------------- /frontend-web/src/components/partials/video/hang-up-control.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Box, IconButton } from "@mui/material" 3 | import CallEndIcon from "@mui/icons-material/CallEnd" 4 | 5 | export const HangUpControl: React.FunctionComponent<{ hangOnRoom: () => void }> = ({ hangOnRoom }) => { 6 | 7 | return ( 8 | 9 | hangOnRoom()}> 11 | 12 | 13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /backend/core/src/main/resources/banner/banner.txt: -------------------------------------------------------------------------------- 1 | _____ _ _ _ _ __ __ 2 | | ___|_ _ ___| |_| | (_) |_ ___| \/ | ___ ___ ___ ___ _ __ __ _ ___ _ __ 3 | | |_ / _` / __| __| | | | __/ _ \ |\/| |/ _ \/ __/ __|/ _ \ '_ \ / _` |/ _ \ '__| 4 | | _| (_| \__ \ |_| |___| | || __/ | | | __/\__ \__ \ __/ | | | (_| | __/ | 5 | |_| \__,_|___/\__|_____|_|\__\___|_| |_|\___||___/___/\___|_| |_|\__, |\___|_| 6 | |___/ -------------------------------------------------------------------------------- /backend/.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | flm-back: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | volumes: 7 | - .:/workspace:cached 8 | command: sleep infinity 9 | ports: 10 | - "9090:9090" 11 | - "3306:3306" 12 | 13 | flm-db: 14 | image: mysql:8.0.40-debian 15 | restart: unless-stopped 16 | network_mode: service:flm-back 17 | volumes: 18 | - mysql-data:/var/lib/mysql 19 | environment: 20 | MYSQL_ROOT_PASSWORD: root 21 | 22 | volumes: 23 | mysql-data: 24 | -------------------------------------------------------------------------------- /frontend-web/src/components/partials/loader/LoaderComponent.tsx: -------------------------------------------------------------------------------- 1 | import {LinearProgress} from "@mui/material" 2 | import React, {useContext} from "react" 3 | import {LoaderContext} from "../../../context/loader-context" 4 | 5 | export function LoaderComponent() { 6 | const {loading} = useContext(LoaderContext)! 7 | return <> 8 | { 9 | loading && 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /frontend-web/src/interface-contract/rtc-transport-model.ts: -------------------------------------------------------------------------------- 1 | import { RtcActionEnum } from "../utils/rtc-action-enum" 2 | 3 | export class RtcTransportDTO { 4 | 5 | constructor (public userId: number, public groupUrl: string, public action: RtcActionEnum, public offer?: RTCSessionDescriptionInit, public answer?: RTCSessionDescriptionInit, public iceCandidate?: RTCIceCandidateInit) { 6 | this.userId = userId 7 | this.groupUrl = groupUrl 8 | this.action = action 9 | this.offer = offer 10 | this.answer = answer 11 | this.iceCandidate = iceCandidate 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:latest 2 | 3 | MAINTAINER Thibaut MOUTON 4 | 5 | # replace shell with bash so we can source files 6 | RUN rm /bin/sh && ln -s /bin/bash /bin/sh 7 | 8 | RUN apt-get update && apt-get install -y curl \ 9 | && apt-get -y autoclean \ 10 | && apt-get -y install procps \ 11 | && apt-get -y install git \ 12 | && apt-get -y install openjdk-17-jdk \ 13 | && rm -rf /var/lib/apt/lists/* 14 | 15 | # confirm installation 16 | RUN java --version 17 | 18 | ENTRYPOINT ["tail", "-f", "/dev/null"] 19 | -------------------------------------------------------------------------------- /frontend-web/src/service/http-message.service.ts: -------------------------------------------------------------------------------- 1 | import {HttpMainService} from "./http-main.service" 2 | import {WrapperMessageModel} from "../interface-contract/wrapper-message-model" 3 | import {AxiosResponse} from "axios" 4 | 5 | export class HttpMessageService extends HttpMainService { 6 | public constructor() { 7 | super() 8 | } 9 | 10 | public getMessages(groupUrl: string, offset: number): Promise> { 11 | return this.instance.get(`/messages/${offset}/group/${groupUrl}`) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/core/src/main/java/com/mercure/core/repository/UserSeenMessageRepository.java: -------------------------------------------------------------------------------- 1 | package com.mercure.core.repository; 2 | 3 | import com.mercure.commons.entity.MessageUserEntity; 4 | import com.mercure.commons.entity.MessageUserKey; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.stereotype.Repository; 7 | 8 | @Repository 9 | public interface UserSeenMessageRepository extends JpaRepository { 10 | 11 | MessageUserEntity findAllByMessageIdAndUserId(int messageId, int userId); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /backend/storage/src/main/java/com/mercure/storage/service/GcpStorage.java: -------------------------------------------------------------------------------- 1 | package com.mercure.storage.service; 2 | 3 | import com.mercure.storage.config.StorageOptions; 4 | import org.springframework.stereotype.Service; 5 | 6 | import java.io.File; 7 | 8 | @Service 9 | public class GcpStorage implements StorageOptions { 10 | 11 | @Override 12 | public void deleteFile() { 13 | 14 | } 15 | 16 | @Override 17 | public void uploadFile(File file) { 18 | 19 | } 20 | 21 | @Override 22 | public void downloadFile(File file) { 23 | 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/storage/src/main/java/com/mercure/storage/service/LocalStorage.java: -------------------------------------------------------------------------------- 1 | package com.mercure.storage.service; 2 | 3 | import com.mercure.storage.config.StorageOptions; 4 | import org.springframework.stereotype.Service; 5 | 6 | import java.io.File; 7 | 8 | @Service 9 | public class LocalStorage implements StorageOptions { 10 | 11 | @Override 12 | public void deleteFile() { 13 | 14 | } 15 | 16 | @Override 17 | public void uploadFile(File file) { 18 | 19 | } 20 | 21 | @Override 22 | public void downloadFile(File file) { 23 | 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend-web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:mainline-alpine3.18-slim 2 | 3 | RUN rm /etc/nginx/conf.d/default.conf 4 | RUN echo 'server { \ 5 | listen 80; \ 6 | server_name localhost; \ 7 | \ 8 | location / { \ 9 | root /usr/share/nginx/html; \ 10 | index index.html index.htm; \ 11 | try_files $uri $uri/ /index.html; \ 12 | } \ 13 | }' >> /etc/nginx/conf.d/default.conf 14 | RUN rm -rf /usr/share/nginx/html/* 15 | 16 | COPY dist /usr/share/nginx/html 17 | -------------------------------------------------------------------------------- /frontend-web/src/components/utils/enable-dark-mode.ts: -------------------------------------------------------------------------------- 1 | export const generateColorMode = (isDarkMode: string): string => { 2 | return isDarkMode === "dark" ? "dark" : "light" 3 | } 4 | 5 | export const generateIconColorMode = (isDarkMode: string): string => { 6 | return isDarkMode !== "dark" ? "#dcdcdc" : "#4A4A4A" 7 | } 8 | 9 | export const generateLinkColorMode = (isDarkMode: string): string => { 10 | return isDarkMode === "dark" ? "white" : "black" 11 | } 12 | 13 | export const generateClassName = (isDarkMode: string): string => { 14 | return isDarkMode === "dark" ? "dark-t" : "light-t" 15 | } 16 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/dto/WrapperMessageDTO.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | import java.util.List; 9 | 10 | @Getter 11 | @Setter 12 | @AllArgsConstructor 13 | @NoArgsConstructor 14 | public class WrapperMessageDTO { 15 | 16 | private boolean isLastMessage; 17 | 18 | private String groupName; 19 | 20 | private boolean isActiveCall; 21 | 22 | private String callUrl; 23 | 24 | private List messages; 25 | } 26 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/utils/ColorsUtils.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.utils; 2 | 3 | import java.util.Random; 4 | 5 | public class ColorsUtils { 6 | 7 | private final String[] colorsArray = 8 | { 9 | "#FFC194", "#9CE03F", "#62C555", "#3AD079", 10 | "#44CEC3", "#F772EE", "#FFAFD2", "#FFB4AF", 11 | "#FF9207", "#E3D530", "#D2FFAF", "FF5733" 12 | }; 13 | 14 | public String getRandomColor() { 15 | return this.colorsArray[new Random().nextInt(colorsArray.length)]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /backend/core/src/main/java/com/mercure/core/config/ResourceConfig.java: -------------------------------------------------------------------------------- 1 | package com.mercure.core.config; 2 | 3 | import org.springframework.stereotype.Component; 4 | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; 5 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 6 | 7 | @Component 8 | public class ResourceConfig implements WebMvcConfigurer { 9 | 10 | @Override 11 | public void addResourceHandlers(ResourceHandlerRegistry registry) { 12 | registry.addResourceHandler("/uploads/**").addResourceLocations("file:uploads/"); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /backend/gateway/src/main/java/com/mercure/gateway/controller/HealthController.java: -------------------------------------------------------------------------------- 1 | package com.mercure.gateway.controller; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | @Slf4j 9 | @RequestMapping() 10 | @RestController 11 | public class HealthController { 12 | 13 | @GetMapping 14 | public String healthCheck() { 15 | log.info("Health check Gateway OK"); 16 | return "OK - gateway"; 17 | } 18 | } -------------------------------------------------------------------------------- /backend/cache/src/main/java/com/mercure/cache/controller/HealthController.java: -------------------------------------------------------------------------------- 1 | package com.mercure.cache.controller; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | @Slf4j 9 | @RequestMapping() 10 | @RestController 11 | public class HealthController { 12 | 13 | @GetMapping("health") 14 | public String healthCheck() { 15 | log.info("Health check cache OK"); 16 | return "OK - cache"; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /backend/.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /backend/core/src/main/java/com/mercure/core/config/CacheConfig.java: -------------------------------------------------------------------------------- 1 | package com.mercure.core.config; 2 | 3 | import org.springframework.cache.CacheManager; 4 | import org.springframework.cache.annotation.EnableCaching; 5 | import org.springframework.cache.concurrent.ConcurrentMapCacheManager; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @Configuration 10 | @EnableCaching 11 | public class CacheConfig { 12 | 13 | @Bean 14 | public CacheManager cacheManager() { 15 | return new ConcurrentMapCacheManager("rooms", "calls"); 16 | } 17 | } -------------------------------------------------------------------------------- /frontend-web/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/dto/RtcTransportDTO.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.dto; 2 | 3 | import com.mercure.commons.utils.RtcActionEnum; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | @Setter 10 | @Getter 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | public class RtcTransportDTO { 14 | 15 | private int userId; 16 | 17 | private String groupUrl; 18 | 19 | private RtcActionEnum action; 20 | 21 | private Object offer; 22 | 23 | private Object answer; 24 | 25 | private Object iceCandidate; 26 | } 27 | -------------------------------------------------------------------------------- /frontend-web/src/components/partials/NoDataComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import CommentsDisabledIcon from "@mui/icons-material/CommentsDisabled" 3 | import { Box } from "@mui/material" 4 | 5 | export function NoDataComponent(): React.JSX.Element { 6 | return ( 7 | 9 |
10 | 11 |
12 |

No conversation selected. You can open one or create a new conversation.

13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/dto/user/GroupDTO.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.dto.user; 2 | 3 | import lombok.Getter; 4 | import lombok.NoArgsConstructor; 5 | import lombok.Setter; 6 | import lombok.AllArgsConstructor; 7 | 8 | @Setter 9 | @Getter 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | public class GroupDTO { 13 | 14 | private int id; 15 | 16 | private String url; 17 | 18 | private String name; 19 | 20 | private String groupType; 21 | 22 | private String lastMessageSender; 23 | 24 | private String lastMessage; 25 | 26 | private String lastMessageDate; 27 | 28 | private boolean isLastMessageSeen; 29 | } 30 | -------------------------------------------------------------------------------- /backend/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 | 16 | -------------------------------------------------------------------------------- /frontend-web/src/context/loader-context.tsx: -------------------------------------------------------------------------------- 1 | import React, {createContext, useState} from "react" 2 | 3 | type LoaderContextType = { 4 | loading: boolean; 5 | setLoading: (isAppLoading: boolean) => void 6 | }; 7 | 8 | const LoaderContext = createContext( 9 | {} as LoaderContextType 10 | ) 11 | 12 | const LoaderProvider: React.FC<{children: React.ReactNode}> = ({children}) => { 13 | const [loading, setLoading] = useState(false) 14 | return ( 15 | 16 | {children} 17 | 18 | ) 19 | } 20 | 21 | 22 | export {LoaderContext, LoaderProvider} 23 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/dto/AuthUserDTO.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.dto; 2 | 3 | import com.mercure.commons.dto.user.GroupDTO; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | import java.util.List; 10 | 11 | @Getter 12 | @Setter 13 | @AllArgsConstructor 14 | @NoArgsConstructor 15 | public class AuthUserDTO { 16 | 17 | private int id; 18 | 19 | private String firstName; 20 | 21 | private String lastName; 22 | 23 | private String firstGroupUrl; 24 | 25 | private String wsToken; 26 | 27 | private String color; 28 | 29 | private List groups; 30 | } 31 | -------------------------------------------------------------------------------- /frontend-web/src/components/partials/video/sound-control.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | import { Box, IconButton } from "@mui/material" 3 | import MicIcon from "@mui/icons-material/Mic" 4 | import MicOffIcon from "@mui/icons-material/MicOff" 5 | 6 | export const SoundControl = (): JSX.Element => { 7 | const [isMicMuted, setMicMuted] = useState(false) 8 | 9 | return ( 10 | 11 | setMicMuted(!isMicMuted)}> 13 | {isMicMuted ? : 14 | 15 | } 16 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /frontend-web/src/interface-contract/redux-model.ts: -------------------------------------------------------------------------------- 1 | import { Client } from "@stomp/stompjs" 2 | 3 | export class ReduxModel { 4 | public client: Client | undefined 5 | 6 | public userToken: string | undefined 7 | 8 | public groupUrl: string | undefined 9 | 10 | public userId: number | undefined 11 | 12 | public message: string | undefined 13 | 14 | public messageId: number | undefined 15 | 16 | constructor (client?: Client, userToken?: string, groupUrl?: string, userId?: number, message?: string, messageId?: number) { 17 | this.client = client 18 | this.userToken = userToken 19 | this.groupUrl = groupUrl 20 | this.userId = userId 21 | this.message = message 22 | this.messageId = messageId 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /backend/core/src/main/java/com/mercure/core/controller/PingController.java: -------------------------------------------------------------------------------- 1 | package com.mercure.core.controller; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RestController; 8 | 9 | @RestController 10 | @RequestMapping(value = "/") 11 | public class PingController { 12 | 13 | private final Logger log = LoggerFactory.getLogger(PingController.class); 14 | 15 | @GetMapping("health-check") 16 | public String testRoute() { 17 | log.debug("Ping base route"); 18 | return "Server status OK"; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /backend/core/src/main/java/com/mercure/core/config/HandShakeInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.mercure.core.config; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import org.springframework.context.ApplicationListener; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.socket.messaging.SessionDisconnectEvent; 8 | 9 | @Component 10 | public class HandShakeInterceptor implements ApplicationListener { 11 | 12 | private final Logger log = LoggerFactory.getLogger(HandShakeInterceptor.class); 13 | 14 | @Override 15 | public void onApplicationEvent(SessionDisconnectEvent event) { 16 | log.warn("Test {}", event.getCloseStatus()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend-web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | Fast Lite Message | FLM 15 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /frontend-web/src/components/websocket/websocket-chat-wrapper.component.tsx: -------------------------------------------------------------------------------- 1 | import React, {useContext} from "react" 2 | import {WebSocketChatComponent} from "./websocket-chat-component" 3 | import {SearchMessageComponent} from "../messages/SearchMessageComponent" 4 | import {SearchContext} from "../../context/SearchContext" 5 | 6 | interface WebsocketChatWrapperComponentProps { 7 | groupUrl?: string 8 | } 9 | 10 | export function WebsocketChatWrapperComponent({groupUrl}: WebsocketChatWrapperComponentProps) { 11 | const {searchText} = useContext(SearchContext)! 12 | 13 | return <> 14 | { 15 | searchText === "" ? : 16 | } 17 | 18 | 19 | } 20 | -------------------------------------------------------------------------------- /frontend-web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "outDir": "dist", 5 | "module": "esnext", 6 | "strict": true, 7 | "allowJs": true, 8 | "strictNullChecks": true, 9 | "lib": [ 10 | "dom", 11 | "dom.iterable", 12 | "esnext" 13 | ], 14 | "isolatedModules": true, 15 | "skipLibCheck": true, 16 | "esModuleInterop": true, 17 | "allowSyntheticDefaultImports": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "moduleResolution": "node", 20 | "resolveJsonModule": true, 21 | "noEmit": true, 22 | "jsx": "react-jsx", 23 | "noFallthroughCasesInSwitch": true 24 | }, 25 | "include": [ 26 | "src", 27 | "./jest.setup.ts" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/dto/InputTransportDTO.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.dto; 2 | 3 | import com.mercure.commons.utils.MessageTypeEnum; 4 | import com.mercure.commons.utils.TransportActionEnum; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.Setter; 9 | 10 | @Setter 11 | @Getter 12 | @AllArgsConstructor 13 | @NoArgsConstructor 14 | public class InputTransportDTO { 15 | 16 | private int userId; 17 | 18 | private TransportActionEnum action; 19 | 20 | private String wsToken; 21 | 22 | private String groupUrl; 23 | 24 | private String message; 25 | 26 | private MessageTypeEnum messageType; 27 | 28 | private int messageId; 29 | } 30 | -------------------------------------------------------------------------------- /frontend-web/src/utils/redux-constants.ts: -------------------------------------------------------------------------------- 1 | export const GRANT_USER_ADMIN = "GRANT_USER_ADMIN" 2 | 3 | export const FETCH_GROUP_MESSAGES = "FETCH_GROUP_MESSAGES" 4 | 5 | export const HANDLE_RTC_ACTIONS = "HANDLE_RTC_ACTIONS" 6 | 7 | export const HANDLE_RTC_OFFER = "HANDLE_RTC_OFFER" 8 | 9 | export const HANDLE_RTC_ANSWER = "HANDLE_RTC_ANSWER" 10 | 11 | export const SET_RTC_OFFER = "SET_RTC_OFFER" 12 | 13 | export const SET_RTC_ANSWER = "SET_RTC_OFFER" 14 | 15 | export const HANDLE_RTC_CANDIDATE = "HANDLE_RTC_CANDIDATE" 16 | 17 | export const SEND_TO_SERVER = "SEND_TO_SERVER" 18 | 19 | export const SEND_GROUP_MESSAGE = "SEND_GROUP_MESSAGE" 20 | 21 | export const ADD_CHAT_HISTORY = "ADD_CHAT_HISTORY" 22 | 23 | export const MARK_MESSAGE_AS_SEEN = "MARK_MESSAGE_AS_SEEN" 24 | -------------------------------------------------------------------------------- /backend/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/dto/MessageDTO.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import lombok.Setter; 7 | 8 | @Getter 9 | @Setter 10 | @AllArgsConstructor 11 | @NoArgsConstructor 12 | public class MessageDTO { 13 | 14 | private int id; 15 | 16 | private String type; 17 | 18 | private String message; 19 | 20 | private int userId; 21 | 22 | private int groupId; 23 | 24 | private String groupUrl; 25 | 26 | private String sender; 27 | 28 | private String time; 29 | 30 | private String initials; 31 | 32 | private String color; 33 | 34 | private String fileUrl; 35 | 36 | private boolean isMessageSeen; 37 | } 38 | -------------------------------------------------------------------------------- /frontend-web/src/config/websocket-config.ts: -------------------------------------------------------------------------------- 1 | import {Client} from "@stomp/stompjs" 2 | 3 | const WS_URL = process.env.REACT_APP_WS_URL ?? "" 4 | 5 | const WS_BROKER = process.env.NODE_ENV === "development" ? "ws" : "wss" 6 | 7 | export async function initWebSocket(userToken: string): Promise { 8 | // const {headerName, token} = JSON.parse(localStorage.getItem("csrf") || "") 9 | return new Client({ 10 | brokerURL: `${WS_BROKER}://${WS_URL}/messenger/websocket?token=${userToken}`, 11 | // connectHeaders: {clientSessionId: crypto.randomUUID(), [headerName]: token}, 12 | connectHeaders: {clientSessionId: crypto.randomUUID()}, 13 | reconnectDelay: 5000, 14 | heartbeatIncoming: 4000, 15 | heartbeatOutgoing: 4000 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/dto/NotificationDTO.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.dto; 2 | 3 | import com.mercure.commons.utils.MessageTypeEnum; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | @Getter 10 | @Setter 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | public class NotificationDTO { 14 | 15 | private int fromUserId; 16 | 17 | private String senderName; 18 | 19 | private MessageTypeEnum type; 20 | 21 | private String message; 22 | 23 | private String lastMessageDate; 24 | 25 | private String groupUrl; 26 | 27 | private int groupId; 28 | 29 | private String fileUrl; 30 | 31 | private String fileName; 32 | 33 | private boolean isMessageSeen; 34 | } 35 | -------------------------------------------------------------------------------- /frontend-web/src/components/partials/skeleten-loader.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, ListItem, ListItemText, Skeleton } from "@mui/material" 2 | import React from "react" 3 | 4 | export function SkeletonLoader () { 5 | const toLoad: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9] 6 | return ( 7 | <> 8 | { 9 | toLoad.map((index) => ( 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | } 19 | secondary={ 20 | 21 | 22 | 23 | }/> 24 | 25 | )) 26 | } 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /frontend-web/src/context/theme-context.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext, useState } from "react" 2 | 3 | type Theme = "light" | "dark"; 4 | type ThemeContextType = { theme: Theme; toggleTheme: () => void }; 5 | 6 | export const ThemeContext = React.createContext( 7 | {} as ThemeContextType 8 | ) 9 | 10 | export const ThemeProvider: React.FunctionComponent = ({ children }) => { 11 | const [theme, setTheme] = useState("dark") 12 | const toggleTheme = () => { 13 | setTheme(theme === "light" ? "dark" : "light") 14 | } 15 | 16 | return ( 17 | 21 | {children} 22 | 23 | ) 24 | } 25 | 26 | export const useThemeContext = (): ThemeContextType => useContext(ThemeContext) 27 | -------------------------------------------------------------------------------- /frontend-web/public/assets/icons/live.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /frontend-web/src/components/websocket/CallWindowComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import CallIcon from "@mui/icons-material/Call" 3 | import {IconButton, Tooltip} from "@mui/material" 4 | 5 | interface CallWindowComponentProps { 6 | sendCallMessage: (id: string) => void 7 | } 8 | 9 | export function CallWindowComponent({sendCallMessage}: CallWindowComponentProps) { 10 | 11 | function initCall() { 12 | const url = crypto.randomUUID() 13 | sendCallMessage(url) 14 | } 15 | 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /frontend-web/src/components/partials/video/live-call.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /backend/core/src/test/java/com/mercure/mapper/GroupCallMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.mercure.mapper; 2 | 3 | import com.mercure.commons.dto.user.GroupCallDTO; 4 | import com.mercure.commons.entity.GroupEntity; 5 | import com.mercure.core.mapper.GroupCallMapper; 6 | import com.mercure.core.service.cache.RoomCacheService; 7 | import org.junit.Test; 8 | 9 | import static org.junit.Assert.assertNotEquals; 10 | 11 | 12 | public class GroupCallMapperTest { 13 | 14 | @Test 15 | public void compare() { 16 | RoomCacheService roomCacheService = new RoomCacheService(); 17 | GroupCallMapper groupCallMapper = new GroupCallMapper(roomCacheService); 18 | GroupEntity groupEntity = new GroupEntity(); 19 | GroupCallDTO groupCallDTO = groupCallMapper.toGroupCall(groupEntity); 20 | assertNotEquals("", groupCallDTO.getActiveCallUrl()); 21 | } 22 | } -------------------------------------------------------------------------------- /frontend-web/src/components/partials/video/call-ended.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { Box, Button } from "@mui/material" 3 | import SendIcon from "@mui/icons-material/Send" 4 | import { Link as RouterLink } from "react-router-dom" 5 | import CheckCircleOutlineIcon from "@mui/icons-material/CheckCircleOutline" 6 | 7 | export const CallEnded = (): JSX.Element => { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 |

You leave the room meeting

15 | 16 | 19 | 20 |
21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /frontend-web/src/service/http-main.service.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosError, AxiosInstance } from "axios" 2 | 3 | export abstract class HttpMainService { 4 | 5 | protected instance: AxiosInstance 6 | 7 | protected constructor () { 8 | this.instance = axios.create({ 9 | withCredentials: true, 10 | baseURL: process.env.REACT_APP_API_URL ?? "", 11 | }) 12 | this.instance.interceptors.request.use((config) => { 13 | config.headers["X-CSRF-TOKEN"] = document.cookie.replace(/(?:^|.*;\s*)XSRF-TOKEN\s*=\s*([^;]*).*$|^.*$/, "$1") 14 | return config 15 | }) 16 | this.instance.interceptors.response.use((response) => { 17 | return response 18 | }, (error: AxiosError) => { 19 | if (error.response && error.response.status === 403) { 20 | if (window.location.pathname !== "/login") { 21 | window.location.pathname = "/login" 22 | } 23 | } 24 | return Promise.reject(error) 25 | }) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /frontend-web/src/components/partials/video/empty-room.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import NoMeetingRoomIcon from "@mui/icons-material/NoMeetingRoom" 3 | import { Box, Button } from "@mui/material" 4 | import SendIcon from "@mui/icons-material/Send" 5 | import { Link as RouterLink } from "react-router-dom" 6 | 7 | export const EmptyRoom = (): JSX.Element => { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 |

No meeting room were found with this URL

15 |

Please check your room link and try again

16 | 17 | 20 | 21 |
22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/entity/FileEntity.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.entity; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | import org.hibernate.annotations.CreationTimestamp; 9 | 10 | import java.sql.Timestamp; 11 | 12 | @Entity 13 | @Table(name = "file_storage") 14 | @Getter 15 | @Setter 16 | @AllArgsConstructor 17 | @NoArgsConstructor 18 | public class FileEntity { 19 | 20 | @Id 21 | @GeneratedValue(strategy = GenerationType.IDENTITY) 22 | private int Id; 23 | 24 | @Column(name = "fk_message_id") 25 | private int messageId; 26 | 27 | @Column(name = "filename") 28 | private String filename; 29 | 30 | @Column(name = "url") 31 | private String url; 32 | 33 | @Column(name = "created_at") 34 | @CreationTimestamp 35 | private Timestamp createdAt; 36 | } 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/** 2 | /frontend-web/build/ 3 | /frontend-web/.idea/** 4 | livraison.sh 5 | 6 | # dependencies 7 | /frontend-web/node_modules 8 | /frontend-web/.pnp 9 | /frontend-web/.pnp.js 10 | 11 | # testing 12 | /frontend-web/coverage 13 | 14 | # production 15 | /build 16 | 17 | # misc 18 | /frontend-web/.DS_Store 19 | /frontend-web/.env 20 | /frontend-web/.env.local 21 | /frontend-web/.env.development.local 22 | /frontend-web/.env.test.local 23 | /frontend-web/.env.production.local 24 | 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | 29 | *.jar 30 | target/ 31 | uploads/ 32 | *.properties 33 | 34 | ### IntelliJ IDEA ### 35 | .idea/** 36 | .mvn/** 37 | *.iws 38 | *.iml 39 | *.ipr 40 | 41 | ### NetBeans ### 42 | nbproject/private/ 43 | nbbuild/ 44 | dist/ 45 | nbdist/ 46 | .nb-gradle/ 47 | build/ 48 | !**/src/main/**/build/ 49 | !**/src/test/**/build/ 50 | 51 | ### VS Code ### 52 | .vscode/ 53 | application-prod.properties 54 | static/** 55 | ssl/** -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/service/FileService.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.service; 2 | 3 | import com.mercure.commons.entity.FileEntity; 4 | import com.mercure.commons.repository.FileRepository; 5 | import lombok.AllArgsConstructor; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.util.List; 9 | 10 | @Service 11 | @AllArgsConstructor 12 | public class FileService { 13 | 14 | private FileRepository fileRepository; 15 | 16 | public FileEntity save(FileEntity f) { 17 | return fileRepository.save(f); 18 | } 19 | 20 | public FileEntity findByFkMessageId(int id) { 21 | return fileRepository.findByMessageId(id); 22 | } 23 | 24 | public List getFilesUrlByGroupId(int groupId) { 25 | return fileRepository.findFilesUrlsByGroupId(groupId); 26 | } 27 | 28 | public String findFileUrlByMessageId(int id) { 29 | return fileRepository.findFileUrlByMessageId(id); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /backend/.idea/dataSources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mysql.8 6 | true 7 | com.mysql.cj.jdbc.Driver 8 | jdbc:mysql://localhost:3306 9 | 10 | 11 | 12 | 13 | 14 | 15 | $ProjectFileDir$ 16 | 17 | 18 | -------------------------------------------------------------------------------- /frontend-web/src/interface-contract/transport-model.ts: -------------------------------------------------------------------------------- 1 | import {TransportActionEnum} from "../utils/transport-action-enum" 2 | import {TypeMessageEnum} from "../utils/type-message-enum" 3 | 4 | export class TransportModel { 5 | private userId: number 6 | 7 | private action: TransportActionEnum 8 | 9 | private wsToken: string | undefined 10 | 11 | private groupUrl: string | undefined 12 | 13 | private message: string | undefined 14 | 15 | private messageType: TypeMessageEnum | undefined 16 | 17 | private messageId: number | undefined 18 | 19 | constructor(userId: number, action: TransportActionEnum, wsToken?: string, groupUrl?: string, message?: string, messageType?: TypeMessageEnum, messageId?: number) { 20 | this.userId = userId 21 | this.action = action 22 | this.wsToken = wsToken 23 | this.groupUrl = groupUrl 24 | this.message = message 25 | this.messageType = messageType 26 | this.messageId = messageId 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/gateway/src/main/java/com/mercure/gateway/configuration/GatewayConfig.java: -------------------------------------------------------------------------------- 1 | package com.mercure.gateway.configuration; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.Primary; 6 | import org.springframework.web.reactive.socket.client.TomcatWebSocketClient; 7 | import org.springframework.web.reactive.socket.client.WebSocketClient; 8 | import org.springframework.web.reactive.socket.server.RequestUpgradeStrategy; 9 | import org.springframework.web.reactive.socket.server.upgrade.TomcatRequestUpgradeStrategy; 10 | 11 | @Configuration 12 | public class GatewayConfig { 13 | 14 | @Bean 15 | @Primary 16 | public WebSocketClient tomcatWebSocketClient() { 17 | return new TomcatWebSocketClient(); 18 | } 19 | 20 | @Bean 21 | @Primary 22 | public RequestUpgradeStrategy requestUpgradeStrategy() { 23 | return new TomcatRequestUpgradeStrategy(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /frontend-web/src/components/messages/VideoCallMessageComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import "./VideoCallMessageComponent.css" 3 | import {Card, CardHeader, IconButton} from "@mui/material" 4 | import PhoneInTalkIcon from "@mui/icons-material/PhoneInTalk" 5 | 6 | interface VideoCallMessageProps { 7 | url: string 8 | groupUrl: string 9 | } 10 | 11 | export function VideoCallMessageComponent({url, groupUrl}: VideoCallMessageProps) { 12 | function openPage() { 13 | window.open(`${url}?u=${groupUrl}`, "_blank") 14 | } 15 | 16 | return <> 17 | 18 | 21 | 22 | 23 | } 24 | title="Video call in progress" 25 | subheader="Thibaut, Mark and John" 26 | /> 27 | 28 | 29 | } 30 | -------------------------------------------------------------------------------- /frontend-web/src/actions/web-rtc-actions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HANDLE_RTC_ACTIONS, 3 | HANDLE_RTC_ANSWER, 4 | HANDLE_RTC_CANDIDATE, 5 | HANDLE_RTC_OFFER, SEND_TO_SERVER, SET_RTC_ANSWER, 6 | SET_RTC_OFFER 7 | } from "../utils/redux-constants" 8 | 9 | export const initCallWebRTC = (data: any) => ({ 10 | type: HANDLE_RTC_ACTIONS, 11 | payload: data 12 | }) 13 | 14 | export const createOffer = (data: any) => ({ 15 | type: HANDLE_RTC_OFFER, 16 | payload: data 17 | }) 18 | 19 | export const createAnswer = (data: any) => ({ 20 | type: HANDLE_RTC_ANSWER, 21 | payload: data 22 | }) 23 | 24 | export const handleOffer = (data: any) => ({ 25 | type: SET_RTC_OFFER, 26 | payload: data 27 | }) 28 | 29 | export const handleAnswer = (data: any) => ({ 30 | type: SET_RTC_ANSWER, 31 | payload: data 32 | }) 33 | 34 | export const sendToServer = (data: any) => ({ 35 | type: SEND_TO_SERVER, 36 | payload: data 37 | }) 38 | 39 | export const handleRtCandidate = (data: any) => ({ 40 | type: HANDLE_RTC_CANDIDATE, 41 | payload: data 42 | }) 43 | -------------------------------------------------------------------------------- /frontend-web/src/components/partials/video/video-control.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react" 2 | import { Box, IconButton } from "@mui/material" 3 | import VideocamIcon from "@mui/icons-material/Videocam" 4 | import VideocamOffIcon from "@mui/icons-material/VideocamOff" 5 | 6 | export const VideoControl: React.FunctionComponent<{ changeVideoStatus: (stopVideo: boolean) => void }> = ({ changeVideoStatus }) => { 7 | const [isWebCamOff, setWebCam] = useState(false) 8 | 9 | const stopVideo = () => { 10 | if (isWebCamOff) { 11 | setWebCam(false) 12 | changeVideoStatus(false) 13 | } else { 14 | setWebCam(true) 15 | changeVideoStatus(true) 16 | } 17 | } 18 | 19 | return ( 20 | 21 | stopVideo()}> 23 | {isWebCamOff ? : 24 | 25 | } 26 | 27 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/repository/FileRepository.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.repository; 2 | 3 | import com.mercure.commons.entity.FileEntity; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.jpa.repository.Query; 6 | import org.springframework.data.repository.query.Param; 7 | import org.springframework.stereotype.Repository; 8 | 9 | import java.util.List; 10 | 11 | @Repository 12 | public interface FileRepository extends JpaRepository { 13 | 14 | FileEntity findByMessageId(int id); 15 | 16 | @Query(value = "SELECT storage.url FROM file_storage storage WHERE storage.fk_message_id = :id", nativeQuery = true) 17 | String findFileUrlByMessageId(@Param(value = "id") int id); 18 | 19 | @Query(value = "SELECT url FROM file_storage file INNER JOIN message m ON m.id = file.fk_message_id WHERE m.msg_group_id = :groupId", nativeQuery = true) 20 | List findFilesUrlsByGroupId(@Param(value = "groupId") int groupId); 21 | } 22 | -------------------------------------------------------------------------------- /backend/core/src/main/java/com/mercure/core/config/WebSocketSecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.mercure.core.config; 2 | 3 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.messaging.Message; 7 | import org.springframework.security.authorization.AuthorizationManager; 8 | import org.springframework.security.config.annotation.web.socket.EnableWebSocketSecurity; 9 | import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager; 10 | 11 | @Configuration 12 | @EnableWebSocketSecurity 13 | @ConditionalOnProperty(name = "websocket.csrf.enable", havingValue = "1") 14 | public class WebSocketSecurityConfig { 15 | 16 | @Bean 17 | public AuthorizationManager> authorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) { 18 | messages.anyMessage().permitAll(); 19 | return messages.build(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend-web/src/context/SearchContext.tsx: -------------------------------------------------------------------------------- 1 | import React, {createContext, useState} from "react" 2 | import {FullTextSearchResponseType} from "../interface-contract/search-text/search-text.type" 3 | 4 | type SearchContextType = { 5 | searchText: string; 6 | searchResponse: FullTextSearchResponseType | undefined 7 | setSearchText: (searchQuery: string) => void 8 | setSearchResponse: (response: FullTextSearchResponseType) => void 9 | }; 10 | 11 | const SearchContext = createContext( 12 | {} as SearchContextType 13 | ) 14 | 15 | const SearchProvider: React.FC<{ children: React.ReactNode }> = ({children}) => { 16 | const [searchText, setSearchText] = useState("") 17 | const [searchResponse, setSearchResponse] = useState() 18 | return ( 19 | 20 | {children} 21 | 22 | ) 23 | } 24 | 25 | 26 | export {SearchContext, SearchProvider} 27 | -------------------------------------------------------------------------------- /backend/core/src/main/resources/db/changelog-master.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /frontend-web/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "node": true, 4 | "browser": true, 5 | "es2021": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:react/recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "ignorePatterns": [ 13 | ".js" 14 | ], 15 | "parser": "@typescript-eslint/parser", 16 | "parserOptions": { 17 | "ecmaFeatures": { 18 | "jsx": true 19 | }, 20 | "ecmaVersion": "latest", 21 | "sourceType": "module" 22 | }, 23 | "plugins": [ 24 | "react", 25 | "@typescript-eslint" 26 | ], 27 | "rules": { 28 | "@typescript-eslint/no-explicit-any": "off", 29 | "no-mixed-spaces-and-tabs": 0, 30 | "linebreak-style": ["error", (process.platform === "win32" ? "windows" : "unix")], 31 | "quotes": [ 32 | "error", 33 | "double" 34 | ], 35 | "semi": [ 36 | "error", 37 | "never" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /frontend-web/public/assets/icons/landing_logo2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/entity/GroupRoleKey.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.entity; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Embeddable; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.Setter; 9 | 10 | import java.util.Objects; 11 | 12 | @Embeddable 13 | @Getter 14 | @Setter 15 | @AllArgsConstructor 16 | @NoArgsConstructor 17 | public class GroupRoleKey { 18 | 19 | @Column(name = "group_id") 20 | private int groupId; 21 | 22 | @Column(name = "user_id") 23 | private int userId; 24 | 25 | @Override 26 | public int hashCode() { 27 | return Objects.hash(groupId, userId); 28 | } 29 | 30 | @Override 31 | public boolean equals(Object obj) { 32 | if (this == obj) return true; 33 | if (obj == null || getClass() != obj.getClass()) return false; 34 | GroupRoleKey groupRoleKey = (GroupRoleKey) obj; 35 | return groupId == groupRoleKey.groupId && 36 | userId == groupRoleKey.userId; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/entity/MessageUserKey.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.entity; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.Embeddable; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.Setter; 9 | 10 | import java.util.Objects; 11 | 12 | @Embeddable 13 | @Getter 14 | @Setter 15 | @NoArgsConstructor 16 | @AllArgsConstructor 17 | public class MessageUserKey { 18 | 19 | @Column(name = "message_id") 20 | private int messageId; 21 | 22 | @Column(name = "user_id") 23 | private int userId; 24 | 25 | @Override 26 | public int hashCode() { 27 | return Objects.hash(messageId, userId); 28 | } 29 | 30 | @Override 31 | public boolean equals(Object obj) { 32 | if (this == obj) return true; 33 | if (obj == null || getClass() != obj.getClass()) return false; 34 | MessageUserKey groupRoleKey = (MessageUserKey) obj; 35 | return messageId == groupRoleKey.messageId && 36 | userId == groupRoleKey.userId; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend-web/src/utils/date-formater.ts: -------------------------------------------------------------------------------- 1 | import moment from "moment" 2 | 3 | export function dateParser(date: string): string { 4 | const messageDate = moment(date, "YYYY-MM-DD HH:mm:ss").fromNow() 5 | if (messageDate.includes("year")) { 6 | return moment(date, "YYYY-MM-DD HH:mm:ss").fromNow(true) 7 | } 8 | if (messageDate.includes("month")) { 9 | return moment(date, "YYYY-MM-DD HH:mm:ss").fromNow(true).replace("a", "1") 10 | } 11 | if (messageDate.includes("day")) { 12 | return moment(date, "YYYY-MM-DD HH:mm:ss").fromNow(true) 13 | } 14 | if (messageDate.includes("hour")) { 15 | if (messageDate.includes("hours")) { 16 | return moment(date, "YYYY-MM-DD HH:mm:ss").fromNow(true) 17 | } 18 | return moment(date, "YYYY-MM-DD HH:mm:ss").fromNow(true).replace("an", "1") 19 | } 20 | if (messageDate.includes("minutes") || messageDate.includes("minute")) { 21 | return moment(date, "YYYY-MM-DD HH:mm:ss").fromNow(true) 22 | } 23 | if (messageDate.includes("seconds")) { 24 | return "now" 25 | } 26 | return "" 27 | } 28 | -------------------------------------------------------------------------------- /backend/core/src/main/resources/db/changelog/addTimestampOnGroupTable.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Thibaut 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:latest 2 | 3 | MAINTAINER Thibaut MOUTON 4 | 5 | # replace shell with bash so we can source files 6 | RUN rm /bin/sh && ln -s /bin/bash /bin/sh 7 | 8 | RUN apt-get update && apt-get install -y curl \ 9 | && apt-get -y autoclean \ 10 | && apt-get -y install procps \ 11 | && apt-get -y install git \ 12 | && apt-get -y install openjdk-17-jdk \ 13 | && rm -rf /var/lib/apt/lists/* 14 | 15 | # nvm environment variables 16 | ENV NVM_DIR /root/.nvm 17 | ENV NODE_VERSION 20.12.1 18 | 19 | RUN curl --silent -o- https://raw.githubusercontent.com/creationix/nvm/v0.39.7/install.sh | bash 20 | 21 | # install node and npm 22 | RUN source $NVM_DIR/nvm.sh \ 23 | && nvm install $NODE_VERSION \ 24 | && nvm alias default $NODE_VERSION \ 25 | && nvm use default 26 | 27 | # add node and npm to path so the commands are available 28 | ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modules 29 | ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH 30 | 31 | # confirm installation 32 | RUN node -v 33 | RUN npm -v 34 | RUN java --version 35 | 36 | ENTRYPOINT ["tail", "-f", "/dev/null"] -------------------------------------------------------------------------------- /backend/core/src/main/java/com/mercure/core/config/WsConfig.java: -------------------------------------------------------------------------------- 1 | package com.mercure.core.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 5 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 6 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 7 | import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; 8 | 9 | @Configuration 10 | @EnableWebSocketMessageBroker 11 | public class WsConfig implements WebSocketMessageBrokerConfigurer { 12 | 13 | @Override 14 | public void configureMessageBroker(MessageBrokerRegistry registry) { 15 | registry.enableSimpleBroker("/topic"); 16 | } 17 | 18 | @Override 19 | public void registerStompEndpoints(StompEndpointRegistry registry) { 20 | registry.addEndpoint("/messenger").setAllowedOrigins("http://localhost:3000"); 21 | registry.addEndpoint("/messenger", "/call") 22 | .setAllowedOrigins("http://localhost:3000") 23 | .withSockJS(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /backend/core/src/main/java/com/mercure/core/controller/SearchController.java: -------------------------------------------------------------------------------- 1 | package com.mercure.core.controller; 2 | 3 | import com.mercure.commons.dto.search.FullTextSearchDTO; 4 | import com.mercure.commons.dto.search.FullTextSearchResponseDTO; 5 | import com.mercure.commons.entity.UserEntity; 6 | import com.mercure.core.service.MessageService; 7 | import com.mercure.core.service.UserService; 8 | import lombok.AllArgsConstructor; 9 | import org.springframework.security.core.Authentication; 10 | import org.springframework.web.bind.annotation.*; 11 | 12 | @RestController 13 | @RequestMapping(value = "/search") 14 | @AllArgsConstructor 15 | public class SearchController { 16 | 17 | private UserService userService; 18 | 19 | private MessageService messageService; 20 | 21 | @PostMapping() 22 | public FullTextSearchResponseDTO searchInDiscussions(@RequestBody FullTextSearchDTO search, Authentication authentication) { 23 | String name = authentication.getName(); 24 | UserEntity user = this.userService.findByNameOrEmail(name, name); 25 | return this.messageService.searchMessages(user.getId(), search.getText()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /backend/core/src/main/java/com/mercure/core/repository/GroupRepository.java: -------------------------------------------------------------------------------- 1 | package com.mercure.core.repository; 2 | 3 | import com.mercure.commons.entity.GroupEntity; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.jpa.repository.Query; 6 | import org.springframework.data.repository.query.Param; 7 | import org.springframework.stereotype.Repository; 8 | 9 | @Repository 10 | public interface GroupRepository extends JpaRepository { 11 | 12 | @Query(value = "SELECT g.id FROM chat_group g WHERE g.url = :url", nativeQuery = true) 13 | int findGroupByUrl(@Param(value = "url") String url); 14 | 15 | @Query(value = "SELECT g.name FROM chat_group g WHERE g.url = :url", nativeQuery = true) 16 | String getGroupEntitiesBy(@Param(value = "url") String url); 17 | 18 | @Query(value = "SELECT * FROM chat_group g WHERE g.url = :url", nativeQuery = true) 19 | GroupEntity getGroupByUrl(@Param(value = "url") String url); 20 | 21 | @Query(value = "SELECT g.url FROM chat_group g WHERE g.id = :id", nativeQuery = true) 22 | String getGroupUrlById(@Param(value = "id") Integer id); 23 | } 24 | -------------------------------------------------------------------------------- /backend/commons/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | commons 6 | jar 7 | 8 | 9 | flm 10 | com.mercure 11 | 1.3.0 12 | 13 | 14 | 15 | 16 | UTF-8 17 | 17 18 | 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-maven-plugin 25 | 26 | true 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /backend/core/src/main/resources/db/changelog/deleteSeenColumnInMessage.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/entity/MessageEntity.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.entity; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | import org.hibernate.annotations.CreationTimestamp; 9 | 10 | import java.sql.Timestamp; 11 | 12 | @Entity 13 | @Table(name = "message") 14 | @Getter 15 | @Setter 16 | @AllArgsConstructor 17 | @NoArgsConstructor 18 | public class MessageEntity { 19 | 20 | public MessageEntity(int userId, int groupId, String type, String message) { 21 | this.user_id = userId; 22 | this.group_id = groupId; 23 | this.type = type; 24 | this.message = message; 25 | } 26 | 27 | @Id 28 | @GeneratedValue(strategy = GenerationType.IDENTITY) 29 | private int id; 30 | 31 | private String message; 32 | 33 | @Column(name = "msg_group_id") 34 | private int group_id; 35 | 36 | @Column(name = "msg_user_id") 37 | private int user_id; 38 | 39 | @Column(name = "type") 40 | private String type; 41 | 42 | @Column(name = "created_at") 43 | @CreationTimestamp 44 | private Timestamp createdAt; 45 | } 46 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/entity/MessageUserEntity.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.entity; 2 | 3 | import jakarta.persistence.Entity; 4 | import jakarta.persistence.Id; 5 | import jakarta.persistence.IdClass; 6 | import jakarta.persistence.Table; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Getter; 9 | import lombok.NoArgsConstructor; 10 | import lombok.Setter; 11 | 12 | import java.util.Objects; 13 | 14 | @Entity 15 | @Table(name = "message_user") 16 | @IdClass(MessageUserKey.class) 17 | @Getter 18 | @Setter 19 | @AllArgsConstructor 20 | @NoArgsConstructor 21 | public class MessageUserEntity { 22 | 23 | @Id 24 | private int messageId; 25 | 26 | @Id 27 | private int userId; 28 | 29 | @Override 30 | public int hashCode() { 31 | return Objects.hash(messageId, userId); 32 | } 33 | 34 | @Override 35 | public boolean equals(Object obj) { 36 | if (this == obj) return true; 37 | if (obj == null || getClass() != obj.getClass()) return false; 38 | MessageUserEntity groupRoleKey = (MessageUserEntity) obj; 39 | return messageId == groupRoleKey.messageId && 40 | userId == groupRoleKey.userId; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /backend/core/src/main/resources/db/changelog/addMessageSeenFlagGroupUser.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /backend/gateway/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | main: 3 | web-application-type: reactive 4 | liquibase: 5 | enabled: false 6 | datasource: 7 | url: jdbc:mysql://localhost:3306/fastlitemessage_dev 8 | driver-class-name: com.mysql.cj.jdbc.Driver 9 | username: root 10 | password: 11 | cloud: 12 | gateway: 13 | globalcors: 14 | cors-configurations: 15 | '[/**]': 16 | allowedOrigins: "http://localhost:3000" 17 | allowCredentials: true 18 | allowedHeaders: "*" 19 | allowedMethods: 20 | - GET 21 | - POST 22 | add-to-simple-url-handler-mapping: true 23 | routes: 24 | - id: authentication 25 | uri: http://localhost:9090 26 | predicates: 27 | - Path=/core/** 28 | - id: websocket_route 29 | uri: ws://localhost:9090 30 | predicates: 31 | - Path=/core/messenger/websocket/** 32 | - id: cache 33 | uri: http://localhost:9092 34 | predicates: 35 | - Path=/service1/** 36 | - id: service2 37 | uri: http://localhost:8082 38 | predicates: 39 | - Path=/service2/** -------------------------------------------------------------------------------- /backend/storage/src/main/java/com/mercure/storage/config/StorageConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.mercure.storage.config; 2 | 3 | import com.mercure.storage.service.GcpStorage; 4 | import com.mercure.storage.service.LocalStorage; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.context.annotation.Primary; 10 | 11 | @Configuration 12 | @Slf4j 13 | public class StorageConfiguration { 14 | 15 | @Bean 16 | @ConditionalOnProperty( 17 | value = "storage.provider", 18 | havingValue = "local", 19 | matchIfMissing = true 20 | ) 21 | @Primary 22 | public StorageOptions getLocalStorage() { 23 | log.debug("Local storage provider is configured"); 24 | return new LocalStorage(); 25 | } 26 | 27 | @Bean 28 | @ConditionalOnProperty( 29 | value = "storage.provider", 30 | havingValue = "gcp", 31 | matchIfMissing = true 32 | ) 33 | public StorageOptions getGcpStorage() { 34 | log.info("GCP storage provider is configured"); 35 | return new GcpStorage(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /frontend-web/src/components/partials/FooterComponent.tsx: -------------------------------------------------------------------------------- 1 | import {Box, Typography} from "@mui/material" 2 | import React from "react" 3 | import {useThemeContext} from "../../context/theme-context" 4 | import {generateColorMode} from "../utils/enable-dark-mode" 5 | 6 | export function FooterComponent(): React.JSX.Element { 7 | const {theme} = useThemeContext() 8 | 9 | return ( 10 | 11 | 12 | 14 | FastLiteMessage - Open source software 15 | 16 | {" | "} 17 | {new Date().getFullYear()} 18 | 19 | 20 | 22 | GCU 23 | 24 | 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /backend/core/src/main/java/com/mercure/core/service/CustomUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package com.mercure.core.service; 2 | 3 | 4 | import com.mercure.commons.entity.UserEntity; 5 | import lombok.AllArgsConstructor; 6 | import org.springframework.security.authentication.DisabledException; 7 | import org.springframework.security.core.userdetails.UserDetails; 8 | import org.springframework.security.core.userdetails.UserDetailsService; 9 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 10 | import org.springframework.stereotype.Service; 11 | 12 | @Service 13 | @AllArgsConstructor 14 | public class CustomUserDetailsService implements UserDetailsService { 15 | 16 | private UserService userService; 17 | 18 | @Override 19 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 20 | UserEntity user = userService.findByNameOrEmail(username, username); 21 | if (user == null) { 22 | throw new UsernameNotFoundException(username); 23 | } 24 | if (!user.isEnabled()) { 25 | throw new DisabledException("Account is not enabled"); 26 | } 27 | return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), user.getAuthorities()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /backend/core/src/main/java/com/mercure/core/controller/MessageController.java: -------------------------------------------------------------------------------- 1 | package com.mercure.core.controller; 2 | 3 | import com.mercure.commons.dto.WrapperMessageDTO; 4 | import com.mercure.core.service.GroupUserJoinService; 5 | import com.mercure.core.service.MessageService; 6 | import lombok.AllArgsConstructor; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | @RestController 11 | @RequestMapping(value = "/messages") 12 | @AllArgsConstructor 13 | @Slf4j 14 | public class MessageController { 15 | 16 | private MessageService messageService; 17 | 18 | private GroupUserJoinService groupUserJoinService; 19 | 20 | @GetMapping(value = "{offset}/group/{groupUrl}") 21 | public WrapperMessageDTO fetchGroupMessages(@PathVariable String groupUrl, @PathVariable int offset) throws Exception { 22 | log.debug("Fetching messages from conversation"); 23 | return this.messageService.getConversationMessage(groupUrl, offset); 24 | } 25 | 26 | @GetMapping(value = "seen/group/{groupUrl}/user/{userId}") 27 | public void markMessageAsSeen(@PathVariable String groupUrl, @PathVariable int userId) { 28 | log.debug("Mark message as seen"); 29 | this.groupUserJoinService.saveLastMessageDate(userId, groupUrl); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /backend/core/src/main/resources/db/changelog/addGroupColumn.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /backend/.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 29 | 30 | -------------------------------------------------------------------------------- /backend/core/src/main/java/com/mercure/core/repository/GroupUserJoinRepository.java: -------------------------------------------------------------------------------- 1 | package com.mercure.core.repository; 2 | 3 | import com.mercure.commons.entity.GroupRoleKey; 4 | import com.mercure.commons.entity.GroupUser; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.data.jpa.repository.Query; 7 | import org.springframework.data.repository.query.Param; 8 | import org.springframework.stereotype.Repository; 9 | 10 | import java.util.List; 11 | 12 | @Repository 13 | public interface GroupUserJoinRepository extends JpaRepository { 14 | 15 | @Query(value = "SELECT * FROM group_user WHERE group_id=:groupId", nativeQuery = true) 16 | List getAllByGroupId(@Param("groupId") int groupId); 17 | 18 | @Query(value = "SELECT group_id FROM group_user WHERE user_id= :userId", nativeQuery = true) 19 | List getGroupUserByUserId(@Param("userId") int userId); 20 | 21 | @Query(value = "SELECT * FROM group_user WHERE group_id=:groupId and user_id = :userId", nativeQuery = true) 22 | GroupUser getGroupUser(@Param("userId") int userId, @Param("groupId") int groupId); 23 | 24 | @Query(value = "SELECT g.user_id FROM group_user g WHERE g.group_id = :groupId", nativeQuery = true) 25 | List getUsersIdInGroup(@Param("groupId") int groupId); 26 | } 27 | -------------------------------------------------------------------------------- /frontend-web/src/components/websocket/websocket-main-component.tsx: -------------------------------------------------------------------------------- 1 | import "./websocketStyle.css" 2 | import React, {useEffect} from "react" 3 | import {WebSocketGroupActionComponent} from "./websocket-group-actions-component" 4 | import {WebsocketGroupsComponent} from "./websocket-groups-component" 5 | import {useParams} from "react-router-dom" 6 | import {WebsocketContextProvider} from "../../context/WebsocketContext" 7 | import {HeaderComponent} from "../partials/HeaderComponent" 8 | import {WebsocketChatWrapperComponent} from "./websocket-chat-wrapper.component" 9 | 10 | export const WebSocketMainComponent: React.FunctionComponent = (): React.JSX.Element => { 11 | const {groupId} = useParams() 12 | 13 | useEffect(() => { 14 | document.title = "Messages | FLM" 15 | }, []) 16 | 17 | return ( 18 | <> 19 | 20 |
24 | 25 | 26 | 27 | 28 | 29 |
30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/utils/ComparatorListGroupDTO.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.utils; 2 | 3 | import com.mercure.commons.dto.user.GroupDTO; 4 | 5 | import java.text.ParseException; 6 | import java.text.SimpleDateFormat; 7 | import java.util.Comparator; 8 | 9 | public class ComparatorListGroupDTO implements Comparator { 10 | 11 | @Override 12 | public int compare(GroupDTO group1, GroupDTO group2) { 13 | if (group1.getLastMessageDate() == null) { 14 | return -1; 15 | } 16 | if (group2.getLastMessageDate() == null) { 17 | return 1; 18 | } 19 | if (group2.getLastMessageDate() == null && group1.getLastMessageDate() == null) { 20 | return group1.getName().compareTo(group2.getName()); 21 | } 22 | try { 23 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); 24 | if (sdf.parse(group1.getLastMessageDate()).before(sdf.parse(group2.getLastMessageDate()))) { 25 | return 1; 26 | } else if (sdf.parse(group1.getLastMessageDate()).after(sdf.parse(group2.getLastMessageDate()))) { 27 | return -1; 28 | } else { 29 | return 0; 30 | } 31 | } catch (ParseException e) { 32 | e.printStackTrace(); 33 | } 34 | return 0; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/core/src/main/java/com/mercure/core/mapper/GroupCallMapper.java: -------------------------------------------------------------------------------- 1 | package com.mercure.core.mapper; 2 | 3 | import com.mercure.commons.dto.user.GroupCallDTO; 4 | import com.mercure.commons.entity.GroupEntity; 5 | import com.mercure.core.service.cache.RoomCacheService; 6 | import lombok.AllArgsConstructor; 7 | import org.springframework.stereotype.Service; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.Optional; 12 | 13 | @Service 14 | @AllArgsConstructor 15 | public class GroupCallMapper { 16 | 17 | private RoomCacheService roomCacheService; 18 | 19 | public GroupCallDTO toGroupCall(GroupEntity group) { 20 | // TODO cache room 21 | // List keys = roomCacheService.getAllKeys(); 22 | List keys = new ArrayList<>(); 23 | GroupCallDTO groupCallDTO = new GroupCallDTO(); 24 | Optional actualRoomKey = 25 | keys.stream().filter((key) -> { 26 | String[] roomKey = key.split("_"); 27 | return group.getUrl().equals(roomKey[0]); 28 | }).findFirst(); 29 | if (actualRoomKey.isPresent()) { 30 | groupCallDTO.setAnyCallActive(true); 31 | groupCallDTO.setActiveCallUrl(actualRoomKey.get().split("_")[1]); 32 | } else { 33 | groupCallDTO.setAnyCallActive(false); 34 | } 35 | return groupCallDTO; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /backend/core/src/main/java/com/mercure/core/service/cache/RoomCacheService.java: -------------------------------------------------------------------------------- 1 | package com.mercure.core.service.cache; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.NoArgsConstructor; 5 | import org.springframework.cache.Cache; 6 | import org.springframework.cache.CacheManager; 7 | import org.springframework.stereotype.Service; 8 | 9 | import java.util.*; 10 | import java.util.concurrent.ConcurrentHashMap; 11 | 12 | @Service 13 | @AllArgsConstructor 14 | @NoArgsConstructor 15 | public class RoomCacheService { 16 | 17 | private CacheManager cacheManager; 18 | 19 | private Cache getCache() { 20 | return cacheManager.getCache("rooms"); 21 | } 22 | 23 | public void setNewRoom(String roomUrl, Object object) { 24 | Cache roomsCache = this.getCache(); 25 | roomsCache.put(roomUrl, object); 26 | } 27 | 28 | public List getAllKeys() { 29 | ConcurrentHashMap>> map = (ConcurrentHashMap) this.getCache().getNativeCache(); 30 | return Collections.list(map.keys()); 31 | } 32 | 33 | public Object getRoomByKey(String groupUrl) { 34 | Cache roomsCache = this.getCache(); 35 | if (roomsCache != null) { 36 | Cache.ValueWrapper valueWrapper = roomsCache.get(groupUrl); 37 | if (valueWrapper != null) { 38 | return valueWrapper.get(); 39 | } 40 | } 41 | return null; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /frontend-web/src/components/partials/video/active-video-call.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import {Box, Button, Icon} from "@mui/material" 3 | 4 | export const ActiveVideoCall: React.FunctionComponent<{ 5 | isAnyCallActive: boolean 6 | }> = ({isAnyCallActive}): React.JSX.Element => { 7 | 8 | return ( 9 | <> 10 | { 11 | isAnyCallActive && 12 | 19 | 35 | 36 | } 37 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /frontend-web/src/components/partials/HeaderComponent.tsx: -------------------------------------------------------------------------------- 1 | import ClearAllIcon from "@mui/icons-material/ClearAll" 2 | import {Toolbar, Typography} from "@mui/material" 3 | import React, {useContext} from "react" 4 | import {AccountMenu} from "../user-account/UseAccountComponent" 5 | import {UserContext} from "../../context/UserContext" 6 | import {SearchComponent} from "../search/SearchComponent" 7 | 8 | export function HeaderComponent(): React.JSX.Element { 9 | const theme = "light" 10 | const {user} = useContext(UserContext)! 11 | // useEffect(() => { 12 | // setCookie("pref-theme", theme) 13 | // }, [theme]) 14 | 15 | return ( 16 | <> 17 |
18 | 22 | 23 | FastLiteMessage 28 | 29 | 30 | {user && } 31 | 32 |
33 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /backend/core/src/main/resources/db/changelog/createGroupTable.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/entity/GroupUser.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.entity; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | 9 | import java.sql.Timestamp; 10 | import java.util.Objects; 11 | 12 | @Entity 13 | @Table(name = "group_user") 14 | @IdClass(GroupRoleKey.class) 15 | @Getter 16 | @Setter 17 | @AllArgsConstructor 18 | @NoArgsConstructor 19 | public class GroupUser { 20 | 21 | @Id 22 | private int groupId; 23 | 24 | @Id 25 | private int userId; 26 | 27 | @ManyToOne 28 | @MapsId("groupId") 29 | @JoinColumn(name = "group_id") 30 | GroupEntity groupUsers; 31 | 32 | @ManyToOne 33 | @MapsId("userId") 34 | @JoinColumn(name = "user_id") 35 | UserEntity userEntities; 36 | 37 | private int role; 38 | 39 | @Temporal(TemporalType.TIMESTAMP) 40 | @Column(name = "last_message_seen_date") 41 | private Timestamp lastMessageSeenDate; 42 | 43 | @Override 44 | public int hashCode() { 45 | return Objects.hash(groupId, userId); 46 | } 47 | 48 | @Override 49 | public boolean equals(Object obj) { 50 | if (this == obj) return true; 51 | if (obj == null || getClass() != obj.getClass()) return false; 52 | GroupUser groupRoleKey = (GroupUser) obj; 53 | return groupId == groupRoleKey.groupId && 54 | userId == groupRoleKey.userId; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/utils/ComparatorListWrapperGroupDTO.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.utils; 2 | 3 | import com.mercure.commons.dto.user.GroupWrapperDTO; 4 | 5 | import java.text.ParseException; 6 | import java.text.SimpleDateFormat; 7 | import java.util.Comparator; 8 | 9 | public class ComparatorListWrapperGroupDTO implements Comparator { 10 | 11 | @Override 12 | public int compare(GroupWrapperDTO group1, GroupWrapperDTO group2) { 13 | if (group1.getGroup().getLastMessageDate() == null) { 14 | return -1; 15 | } 16 | if (group2.getGroup().getLastMessageDate() == null) { 17 | return 1; 18 | } 19 | if (group2.getGroup().getLastMessageDate() == null && group1.getGroup().getLastMessageDate() == null) { 20 | return group1.getGroup().getName().compareTo(group2.getGroup().getName()); 21 | } 22 | try { 23 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); 24 | if (sdf.parse(group1.getGroup().getLastMessageDate()).before(sdf.parse(group2.getGroup().getLastMessageDate()))) { 25 | return 1; 26 | } else if (sdf.parse(group1.getGroup().getLastMessageDate()).after(sdf.parse(group2.getGroup().getLastMessageDate()))) { 27 | return -1; 28 | } else { 29 | return 0; 30 | } 31 | } catch (ParseException e) { 32 | e.printStackTrace(); 33 | } 34 | return 0; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /backend/core/src/main/resources/db/changelog/createJoinTableGroupUser.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /backend/gateway/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.mercure 8 | flm 9 | 1.3.0 10 | 11 | 12 | gateway 13 | 14 | 15 | 17 16 | 17 17 | 2024.0.0 18 | UTF-8 19 | 20 | 21 | 22 | 23 | org.springframework.cloud 24 | spring-cloud-starter-gateway 25 | 4.2.0 26 | 27 | 28 | 29 | 30 | 31 | 32 | org.springframework.cloud 33 | spring-cloud-dependencies 34 | ${spring-cloud.version} 35 | pom 36 | import 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /backend/core/src/main/java/com/mercure/core/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.mercure.core.repository; 2 | 3 | import com.mercure.commons.entity.UserEntity; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.jpa.repository.Query; 6 | import org.springframework.data.repository.query.Param; 7 | import org.springframework.stereotype.Repository; 8 | 9 | import java.util.List; 10 | 11 | @Repository 12 | public interface UserRepository extends JpaRepository { 13 | 14 | UserEntity getUserByFirstNameOrMail(String firstName, String mail); 15 | 16 | @Query(value = "SELECT u.firstname, u.lastname FROM users u WHERE u.id = :userId", nativeQuery = true) 17 | String getUsernameByUserId(@Param(value = "userId") int id); 18 | 19 | @Query(value = "SELECT u.firstname FROM users u WHERE u.id = :userId", nativeQuery = true) 20 | String getFirstNameByUserId(@Param(value = "userId") int id); 21 | 22 | @Query(value = "SELECT u.firstname FROM users u WHERE u.wstoken = :token", nativeQuery = true) 23 | String getUsernameWithWsToken(@Param(value = "token") String token); 24 | 25 | @Query(value = "SELECT u.id FROM users u WHERE u.wstoken = :token", nativeQuery = true) 26 | int getUserIdWithWsToken(@Param(value = "token") String token); 27 | 28 | @Query(value = "SELECT * FROM users u WHERE u.id NOT IN :ids", nativeQuery = true) 29 | List getAllUsersNotAlreadyInConversation(@Param(value = "ids") int[] ids); 30 | 31 | int countAllByMail(String mail); 32 | 33 | int countAllByShortUrl(String shortUrl); 34 | } 35 | -------------------------------------------------------------------------------- /backend/core/src/main/java/com/mercure/core/service/UserSeenMessageService.java: -------------------------------------------------------------------------------- 1 | package com.mercure.core.service; 2 | 3 | import com.mercure.commons.entity.GroupEntity; 4 | import com.mercure.commons.entity.MessageEntity; 5 | import com.mercure.commons.entity.MessageUserEntity; 6 | import com.mercure.core.repository.UserSeenMessageRepository; 7 | import lombok.AllArgsConstructor; 8 | import org.springframework.stereotype.Service; 9 | import org.springframework.transaction.annotation.Transactional; 10 | 11 | import java.util.Optional; 12 | 13 | @Service 14 | @AllArgsConstructor 15 | public class UserSeenMessageService { 16 | 17 | private UserSeenMessageRepository seenMessageRepository; 18 | 19 | private GroupService groupService; 20 | 21 | @Transactional 22 | public void saveMessageNotSeen(MessageEntity msg, int groupId) { 23 | Optional group = groupService.findById(groupId); 24 | 25 | group.ifPresent(groupEntity -> 26 | groupEntity.getUserEntities().forEach((user) -> { 27 | MessageUserEntity message = new MessageUserEntity(); 28 | message.setMessageId(msg.getId()); 29 | message.setUserId(user.getId()); 30 | seenMessageRepository.save(message); 31 | })); 32 | } 33 | 34 | public MessageUserEntity findByMessageId(int messageId, int userId) { 35 | return seenMessageRepository.findAllByMessageIdAndUserId(messageId, userId); 36 | } 37 | 38 | public void saveMessageUserEntity(MessageUserEntity toSave) { 39 | seenMessageRepository.save(toSave); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /frontend-web/src/context/UserContext.tsx: -------------------------------------------------------------------------------- 1 | import React, {createContext, useContext, useEffect, useState} from "react" 2 | import {IUser} from "../interface-contract/user/user-model" 3 | import {HttpGroupService} from "../service/http-group-service" 4 | import {GroupContext, GroupContextAction} from "./GroupContext" 5 | 6 | type UserContextType = { 7 | user: IUser | undefined; 8 | setUser: (user: IUser) => void 9 | }; 10 | 11 | const UserContext = createContext(undefined) 12 | 13 | const UserContextProvider: React.FC<{ children: React.ReactNode }> = ({children}) => { 14 | const [user, setUser] = useState(undefined) 15 | const {changeGroupState} = useContext(GroupContext)! 16 | 17 | useEffect(() => { 18 | const getUserData = async () => { 19 | try { 20 | if (window.location.pathname !== "/register") { 21 | const {data} = await new HttpGroupService().pingRoute() 22 | setUser(data.user) 23 | changeGroupState({ 24 | type: GroupContextAction.SET_GROUPS, 25 | payload: data.groupsWrapper.map((group) => group.group) 26 | }) 27 | } 28 | } catch (error) { 29 | if (window.location.pathname !== "/login") { 30 | window.location.pathname = "/login" 31 | } 32 | } 33 | } 34 | getUserData() 35 | }, []) 36 | return ( 37 | 38 | {children} 39 | 40 | ) 41 | } 42 | 43 | 44 | export {UserContext, UserContextProvider} 45 | -------------------------------------------------------------------------------- /backend/storage/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | storage 7 | jar 8 | 9 | 10 | com.mercure 11 | flm 12 | 1.3.0 13 | 14 | 15 | 16 | 17 | 17 18 | 17 19 | UTF-8 20 | 21 | 22 | 23 | 24 | com.google.cloud 25 | google-cloud-storage 26 | 2.45.0 27 | 28 | 29 | 30 | com.azure 31 | azure-storage-blob 32 | 12.29.0 33 | 34 | 35 | 36 | com.mercure 37 | commons 38 | ${project.version} 39 | 40 | 41 | 42 | com.mercure 43 | core 44 | ${project.version} 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /backend/core/src/main/java/com/mercure/core/service/cache/CallsCacheService.java: -------------------------------------------------------------------------------- 1 | package com.mercure.core.service.cache; 2 | 3 | import lombok.AllArgsConstructor; 4 | import org.springframework.cache.Cache; 5 | import org.springframework.cache.CacheManager; 6 | import org.springframework.stereotype.Service; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | @Service 12 | @AllArgsConstructor 13 | public class CallsCacheService { 14 | 15 | private CacheManager cacheManager; 16 | 17 | private Cache getCache() { 18 | return cacheManager.getCache("calls"); 19 | } 20 | 21 | public Object getAll() { 22 | return this.getCache(); 23 | } 24 | 25 | public void setUser(String callUrl, int userId) { 26 | Cache cache = this.getCache(); 27 | Cache.ValueWrapper value = cache.get(callUrl); 28 | if (value != null && value.get() != null) { 29 | ArrayList userIds = (ArrayList) value.get(); 30 | assert userIds != null; 31 | if (!userIds.contains(userId)) { 32 | userIds.add(userId); 33 | cache.put(callUrl, userIds); 34 | } 35 | } else { 36 | cache.put(callUrl, new ArrayList<>(List.of(userId))); 37 | } 38 | } 39 | 40 | public boolean removeUser(String callUrl, int userId) { 41 | Cache cache = this.getCache(); 42 | Cache.ValueWrapper value = cache.get(callUrl); 43 | if (value != null && value.get() != null) { 44 | ArrayList userIds = (ArrayList) value.get(); 45 | assert userIds != null; 46 | userIds.remove(userId); 47 | return userIds.isEmpty(); 48 | } 49 | return false; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /frontend-web/src/context/AlertContext.tsx: -------------------------------------------------------------------------------- 1 | import React, {createContext, Dispatch, ReactNode, useReducer} from "react" 2 | 3 | enum AlertAction { 4 | DELETE_ALERT = "DELETE_ALERT", 5 | ADD_ALERT = "ADD_ALERT" 6 | } 7 | 8 | type AlertSeverityType = "success" | "info" | "warning" | "error" | undefined 9 | 10 | type AlertType = { 11 | id: string, 12 | isOpen: boolean, 13 | text: string, 14 | alert: AlertSeverityType 15 | } 16 | 17 | export type AlertActionType = 18 | | { type: AlertAction.ADD_ALERT; payload: AlertType } 19 | | { type: AlertAction.DELETE_ALERT; payload: string }; 20 | 21 | export const AlertContext = createContext<{ 22 | alerts: AlertType[]; 23 | dispatch: Dispatch; 24 | } | null>(null) 25 | 26 | export const alertsReducer = (state: AlertType[], action: AlertActionType): AlertType[] => { 27 | switch (action.type) { 28 | case AlertAction.ADD_ALERT: { 29 | return [...state, action.payload] 30 | } 31 | 32 | case AlertAction.DELETE_ALERT: { 33 | const indexToDelete = state.findIndex((alert) => alert.id === action.payload) 34 | const eltToDelete = state[indexToDelete] 35 | eltToDelete.isOpen = false 36 | state[indexToDelete] = eltToDelete 37 | return state.filter((alert) => alert.id !== action.payload) 38 | } 39 | 40 | default: 41 | return state 42 | } 43 | } 44 | 45 | const AlertContextProvider: React.FC<{ children: ReactNode }> = ({children}) => { 46 | const [alerts, dispatch] = useReducer(alertsReducer, []) 47 | return ( 48 | 49 | {children} 50 | 51 | ) 52 | } 53 | 54 | export {AlertContextProvider, AlertAction, type AlertSeverityType} 55 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/entity/GroupEntity.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.entity; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.mercure.commons.utils.GroupTypeEnum; 5 | import jakarta.persistence.*; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import lombok.Setter; 10 | import org.hibernate.annotations.CreationTimestamp; 11 | 12 | import java.sql.Timestamp; 13 | import java.util.HashSet; 14 | import java.util.Set; 15 | 16 | @Entity 17 | @Table(name = "chat_group") 18 | @Getter 19 | @Setter 20 | @AllArgsConstructor 21 | @NoArgsConstructor 22 | public class GroupEntity { 23 | 24 | public GroupEntity(String name) { 25 | this.name = name; 26 | } 27 | 28 | public GroupEntity(int id, String name, String url) { 29 | this.id = id; 30 | this.name = name; 31 | this.url = url; 32 | } 33 | 34 | @Id 35 | @GeneratedValue(strategy = GenerationType.IDENTITY) 36 | private int id; 37 | 38 | @Column(name = "name") 39 | private String name; 40 | 41 | private String url; 42 | 43 | @Column(name = "active_call") 44 | private boolean activeCall; 45 | 46 | @Column(name = "call_url") 47 | private String callUrl; 48 | 49 | @Column(name = "type") 50 | @Enumerated(value = EnumType.STRING) 51 | private GroupTypeEnum groupTypeEnum; 52 | 53 | @Column(name = "created_at") 54 | @CreationTimestamp 55 | private Timestamp createdAt; 56 | 57 | @ManyToMany(fetch = FetchType.LAZY) 58 | @JoinTable( 59 | name = "group_user", 60 | joinColumns = @JoinColumn(name = "group_id"), 61 | inverseJoinColumns = @JoinColumn(name = "user_id")) 62 | @JsonIgnore 63 | private Set userEntities = new HashSet<>(); 64 | 65 | @OneToMany(mappedBy = "groupUsers", fetch = FetchType.EAGER) 66 | @JsonIgnore 67 | private Set groupUsers = new HashSet<>(); 68 | } 69 | -------------------------------------------------------------------------------- /frontend-web/src/components/partials/alert-component.tsx: -------------------------------------------------------------------------------- 1 | import {Alert, AlertTitle, Collapse, Snackbar} from "@mui/material" 2 | import React, {useContext} from "react" 3 | import {AlertAction, AlertContext, AlertSeverityType} from "../../context/AlertContext" 4 | 5 | export function AlertComponent(): React.JSX.Element { 6 | const {alerts, dispatch} = useContext(AlertContext)! 7 | 8 | function closeAlert(id: string) { 9 | dispatch({type: AlertAction.DELETE_ALERT, payload: id}) 10 | } 11 | 12 | function getAlertTitle(severity: AlertSeverityType): string { 13 | switch (severity) { 14 | case "error": 15 | return "Error" 16 | case "warning": 17 | return "Warning" 18 | case "info": 19 | return "Info" 20 | case "success": 21 | return "Success" 22 | default: 23 | return "Error" 24 | } 25 | } 26 | 27 | return ( 28 |
33 | { 34 | alerts.map((value) => ( 35 |
36 | 37 | closeAlert(value.id)}> 38 | closeAlert(value.id)} 39 | severity={value.alert} 40 | variant={"filled"}> 41 | {getAlertTitle(value.alert)} 42 | {value.text} 43 | 44 | 45 | 46 |
47 | )) 48 | } 49 |
50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /frontend-web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend-web", 3 | "version": "0.0.1", 4 | "private": true, 5 | "author": { 6 | "name": "Thibaut MOUTON", 7 | "email": "thibautmouton22@gmail.com" 8 | }, 9 | "engines": { 10 | "node": ">= 20", 11 | "npm": ">= 8.0.0" 12 | }, 13 | "scripts": { 14 | "start": "react-scripts start", 15 | "start:test": "tsc && set PORT=8080 && react-scripts start", 16 | "build": "react-scripts build", 17 | "test": "react-scripts test --watchAll=false", 18 | "lint": "eslint -c .eslintrc.json --ext .ts,.tsx .", 19 | "lint:fix": "npm run lint -- --fix", 20 | "eject": "react-scripts eject" 21 | }, 22 | "dependencies": { 23 | "@emotion/react": "11.11.4", 24 | "@emotion/styled": "11.11.5", 25 | "@mui/icons-material": "5.15.20", 26 | "@mui/lab": "5.0.0-alpha.170", 27 | "@mui/material": "5.15.21", 28 | "@stomp/stompjs": "7.0.0", 29 | "axios": "1.12.0", 30 | "history": "5.3.0", 31 | "jest": "29.7.0", 32 | "moment": "2.30.1", 33 | "react": "18.2.0", 34 | "react-dom": "18.2.0", 35 | "react-router-dom": "6.22.3", 36 | "react-scripts": "5.0.1", 37 | "react-toastify": "10.0.5", 38 | "sockjs-client": "1.6.1", 39 | "ts-jest": "29.1.5", 40 | "typescript": "4.7.4" 41 | }, 42 | "eslintConfig": { 43 | "extends": "react-app" 44 | }, 45 | "devDependencies": { 46 | "@testing-library/dom": "10.3.0", 47 | "@testing-library/jest-dom": "6.4.6", 48 | "@testing-library/react": "16.0.0", 49 | "@types/react": "18.3.3", 50 | "@types/react-dom": "18.3.0", 51 | "@types/jest": "29.5.12", 52 | "@typescript-eslint/eslint-plugin": "7.5.0", 53 | "@typescript-eslint/parser": "7.5.0", 54 | "eslint-plugin-react": "7.34.1" 55 | }, 56 | "browserslist": { 57 | "production": [ 58 | ">0.2%", 59 | "not dead", 60 | "not op_mini all" 61 | ], 62 | "development": [ 63 | "last 1 chrome version", 64 | "last 1 firefox version", 65 | "last 1 safari version" 66 | ] 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /frontend-web/src/components/welcome/WelcomeComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import {Box, Card, CardContent, Grid, Typography} from "@mui/material" 3 | import {FooterComponent} from "../partials/FooterComponent" 4 | import {generateColorMode} from "../utils/enable-dark-mode" 5 | import {useThemeContext} from "../../context/theme-context" 6 | 7 | interface WelcomeComponentProps { 8 | children: React.ReactNode 9 | } 10 | 11 | export function WelcomeComponent({children}: WelcomeComponentProps) { 12 | const {theme} = useThemeContext() 13 | 14 | return
15 | 16 | 17 | 18 | 19 | Welcome to FastLiteMessage 20 | 21 | 22 | 23 | Simple, fast and secure 24 | 25 | 26 | 27 | {"test 28 | 29 | FastLiteMessage allows to communicate with other people everywhere, create groups, 30 | make 31 | serverless video calls in an easy way. Log into your account or register to start 32 | using FastLiteMessage. 33 | 34 | {"test 35 | 36 | {children} 37 | 38 | 39 | 40 | 41 | 42 |
43 | } -------------------------------------------------------------------------------- /backend/core/src/main/resources/db/changelog/createFileBlobTable.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /frontend-web/src/components/partials/all-users-dialog.tsx: -------------------------------------------------------------------------------- 1 | import AccountCircleIcon from "@mui/icons-material/AccountCircle" 2 | import { Avatar, Dialog, DialogTitle, List, ListItemAvatar, ListItemButton, ListItemText } from "@mui/material" 3 | import React from "react" 4 | import { useThemeContext } from "../../context/theme-context" 5 | import { GroupUserModel } from "../../interface-contract/group-user-model" 6 | import { generateIconColorMode } from "../utils/enable-dark-mode" 7 | 8 | interface AllUsersDialogType { 9 | setOpen: (open: boolean) => void 10 | usersList: GroupUserModel[] 11 | open: boolean 12 | dialogTitle: string 13 | action: (userId: string | number) => void 14 | } 15 | 16 | export const AllUsersDialog: React.FunctionComponent = ({ 17 | usersList, 18 | open, 19 | setOpen, 20 | dialogTitle, 21 | action 22 | }) => { 23 | const { theme } = useThemeContext() 24 | const { user } = {} as any // TODO remove any 25 | 26 | return ( 27 | { 29 | if (reason === "backdropClick") setOpen(false) 30 | }} 31 | scroll={"paper"} 32 | aria-labelledby="simple-dialog-title" 33 | fullWidth 34 | open={open}> 35 | {dialogTitle} 36 | 37 | { 38 | usersList && usersList.map((users) => ( 39 | action(users.userId)}> 41 | 42 | 43 | 45 | 46 | 47 | 49 | 53 | {users.firstName + " " + users.lastName} 54 | { 55 | users.userId === user?.id && " (You)" 56 | } 57 | 58 | 59 | } 60 | /> 61 | 62 | )) 63 | } 64 | 65 | 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /frontend-web/src/index.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | overflow: hidden; 3 | height: 100%; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 6 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 7 | sans-serif; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | } 11 | 12 | #root { 13 | height: 100vh; 14 | } 15 | 16 | .msg { 17 | white-space: pre-wrap; 18 | padding: 2px 0 2px 5px; 19 | } 20 | 21 | .bold-unread-message-light { 22 | font-weight: bold !important; 23 | color: white !important; 24 | } 25 | 26 | .bold-unread-message-dark { 27 | font-weight: bold !important; 28 | color: black !important; 29 | } 30 | 31 | .hover-msg-light:hover { 32 | background-color: #dce9ff !important; 33 | } 34 | 35 | .hover-msg-dark:hover { 36 | background-color: #262626; 37 | } 38 | 39 | .selected-group-light { 40 | background-color: #dce9ff !important; 41 | } 42 | 43 | .selected-group-dark { 44 | background-color: #262626 !important; 45 | } 46 | 47 | .group-subtitle-color { 48 | color: #787878 49 | } 50 | 51 | .lnk { 52 | text-decoration: none; 53 | } 54 | 55 | .mnu { 56 | display: flex; 57 | } 58 | 59 | .clrcstm { 60 | color: inherit; 61 | font-weight: inherit; 62 | } 63 | 64 | code { 65 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 66 | monospace; 67 | } 68 | 69 | .dark { 70 | color: white; 71 | background-color: #393939; 72 | } 73 | 74 | .light { 75 | color: black; 76 | background-color: #ffffff; 77 | /*background-color: #A9A9A9*/ 78 | } 79 | 80 | .jsLink { 81 | color: white; 82 | } 83 | 84 | 85 | ::-webkit-scrollbar { 86 | width: 10px; 87 | } 88 | 89 | ::-webkit-scrollbar-track { 90 | background: #c4c4c4; 91 | } 92 | 93 | /* Handle */ 94 | ::-webkit-scrollbar-thumb { 95 | background: #656565; 96 | } 97 | 98 | /* Handle on hover */ 99 | ::-webkit-scrollbar-thumb:hover { 100 | background: #525252; 101 | } 102 | 103 | 104 | -------------------------------------------------------------------------------- /backend/core/src/main/resources/db/changelog/createJoinTableUserGroup.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 32 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /frontend-web/public/assets/icons/landing_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /backend/core/src/main/resources/db/changelog/createJoinTableMessageUserSeen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 32 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /backend/core/src/main/java/com/mercure/core/config/JwtWebConfig.java: -------------------------------------------------------------------------------- 1 | package com.mercure.core.config; 2 | 3 | import com.mercure.core.service.CustomUserDetailsService; 4 | import com.mercure.core.service.JwtUtil; 5 | import com.mercure.commons.utils.StaticVariable; 6 | import jakarta.servlet.FilterChain; 7 | import jakarta.servlet.ServletException; 8 | import jakarta.servlet.http.Cookie; 9 | import jakarta.servlet.http.HttpServletRequest; 10 | import jakarta.servlet.http.HttpServletResponse; 11 | import lombok.AllArgsConstructor; 12 | import lombok.NonNull; 13 | import org.springframework.context.annotation.Configuration; 14 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 15 | import org.springframework.security.core.context.SecurityContextHolder; 16 | import org.springframework.security.core.userdetails.UserDetails; 17 | import org.springframework.web.filter.OncePerRequestFilter; 18 | import org.springframework.web.util.WebUtils; 19 | 20 | import java.io.IOException; 21 | 22 | @Configuration 23 | @AllArgsConstructor 24 | public class JwtWebConfig extends OncePerRequestFilter { 25 | 26 | private CustomUserDetailsService userDetailsService; 27 | 28 | private JwtUtil jwtUtil; 29 | 30 | @Override 31 | protected void doFilterInternal(@NonNull HttpServletRequest request,@NonNull HttpServletResponse response,@NonNull FilterChain filterChain) throws IOException, ServletException { 32 | String jwtToken = null; 33 | String username; 34 | Cookie cookie = WebUtils.getCookie(request, StaticVariable.SECURE_COOKIE); 35 | 36 | if (cookie != null) { 37 | jwtToken = cookie.getValue(); 38 | } 39 | if (jwtToken != null) { 40 | username = jwtUtil.getUserNameFromJwtToken(jwtToken); 41 | UserDetails userDetails = userDetailsService.loadUserByUsername(username); 42 | if (jwtUtil.validateToken(jwtToken, userDetails)) { 43 | UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); 44 | SecurityContextHolder.getContext().setAuthentication(auth); 45 | } 46 | } 47 | filterChain.doFilter(request, response); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /backend/core/src/main/resources/db/changelog/createMessageWsTable.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 42 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /backend/core/src/main/java/com/mercure/core/service/DbInit.java: -------------------------------------------------------------------------------- 1 | package com.mercure.core.service; 2 | 3 | import com.mercure.commons.utils.ColorsUtils; 4 | import com.mercure.commons.entity.UserEntity; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.boot.CommandLineRunner; 8 | import org.springframework.security.crypto.password.PasswordEncoder; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.util.Arrays; 12 | import java.util.List; 13 | import java.util.UUID; 14 | 15 | /** 16 | * Class used to fill the database on startup if no data detected 17 | */ 18 | @Service 19 | public class DbInit implements CommandLineRunner { 20 | 21 | static Logger log = LoggerFactory.getLogger(DbInit.class); 22 | 23 | private final UserService userService; 24 | 25 | private final PasswordEncoder passwordEncoder; 26 | 27 | public DbInit(UserService userService, PasswordEncoder passwordEncoder) { 28 | this.userService = userService; 29 | this.passwordEncoder = passwordEncoder; 30 | } 31 | 32 | @Override 33 | public void run(String... args) { 34 | try { 35 | if (userService.findAll().isEmpty()) { 36 | List sourceList = Arrays.asList("Thibaut", "Mark", "John", "Luke", "Steve"); 37 | sourceList.forEach(val -> { 38 | UserEntity user = new UserEntity(); 39 | user.setFirstName(val); 40 | user.setLastName("Williams"); 41 | user.setPassword(passwordEncoder.encode("root")); 42 | user.setMail(val.toLowerCase() + "@fastlitemessage.com"); 43 | user.setEnabled(true); 44 | user.setColor(new ColorsUtils().getRandomColor()); 45 | user.setCredentialsNonExpired(true); 46 | user.setAccountNonLocked(true); 47 | user.setAccountNonExpired(true); 48 | user.setWsToken(UUID.randomUUID().toString()); 49 | user.setRole(1); 50 | userService.save(user); 51 | }); 52 | log.info("No entries detected in User table, data created"); 53 | } else { 54 | log.info("Data already set in User table, skipping init step"); 55 | } 56 | } catch (Exception e) { 57 | log.error("Cannot init DB : {}", e.getMessage()); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /backend/core/src/main/java/com/mercure/core/repository/MessageRepository.java: -------------------------------------------------------------------------------- 1 | package com.mercure.core.repository; 2 | 3 | import com.mercure.commons.dto.search.FullTextSearchDatabaseResponse; 4 | import com.mercure.commons.entity.MessageEntity; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.data.jpa.repository.Modifying; 7 | import org.springframework.data.jpa.repository.Query; 8 | import org.springframework.data.repository.query.Param; 9 | import org.springframework.stereotype.Repository; 10 | 11 | import java.util.List; 12 | 13 | @Repository 14 | public interface MessageRepository extends JpaRepository { 15 | 16 | @Query(value = "SELECT * FROM (SELECT * FROM message m WHERE m.msg_group_id=:id and ((:offset > 0 and m.id < :offset) or (:offset <= 0)) ORDER BY m.id DESC LIMIT 20)t order by id", nativeQuery = true) 17 | List findByGroupIdAndOffset(@Param(value = "id") int id, @Param(value = "offset") int offset); 18 | 19 | @Query(value = "SELECT * FROM (SELECT * FROM message m WHERE m.msg_group_id=:id ORDER BY m.id DESC LIMIT 20)t order by id", nativeQuery = true) 20 | List findLastMessagesByGroupId(@Param(value = "id") int id); 21 | 22 | @Query(value = "SELECT * FROM message m1 INNER JOIN (SELECT MAX(m.id) as mId FROM message m GROUP BY m.msg_group_id) temp ON temp.mId = m1.id WHERE msg_group_id = :idOfGroup", nativeQuery = true) 23 | MessageEntity findLastMessageByGroupId(@Param(value = "idOfGroup") int groupId); 24 | 25 | @Query(value = "SELECT m1.id FROM message m1 INNER JOIN (SELECT MAX(m.id) as id FROM message m GROUP BY m.msg_group_id) temp ON temp.id = m1.id WHERE msg_group_id = :idOfGroup", nativeQuery = true) 26 | int findLastMessageIdByGroupId(@Param(value = "idOfGroup") int groupId); 27 | 28 | @Query(value = "SELECT m.message as message, c.id, c.url as groupUrl, c.name as groupName FROM message m LEFT JOIN chat_group c ON c.id = m.msg_group_id WHERE m.msg_group_id IN :groupIds AND m.message LIKE %:searchQuery%", nativeQuery = true) 29 | List findMessagesBySearchQuery(@Param(value = "searchQuery") String searchQuery, @Param(value = "groupIds") List groupIds); 30 | 31 | @Modifying 32 | @Query(value = "DELETE m, mu FROM message m JOIN message_user mu ON m.id = mu.message_id WHERE m.msg_group_id = :groupId", nativeQuery = true) 33 | void deleteMessagesDataByGroupId(@Param(value = "groupId") int groupId); 34 | } 35 | -------------------------------------------------------------------------------- /backend/core/src/main/resources/db/changelog/createUserTable.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /backend/core/src/main/java/com/mercure/core/service/JwtUtil.java: -------------------------------------------------------------------------------- 1 | package com.mercure.core.service; 2 | 3 | import io.jsonwebtoken.Claims; 4 | import io.jsonwebtoken.Jwts; 5 | import io.jsonwebtoken.SignatureAlgorithm; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.security.core.userdetails.UserDetails; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.util.Date; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | import java.util.function.Function; 14 | 15 | @Component 16 | public class JwtUtil { 17 | 18 | public static final long JWT_TOKEN_VALIDITY = 1000 * 3600 * 365; 19 | 20 | @Value("${jwt.secret}") 21 | private String JWT_TOKEN; 22 | 23 | public String getUserNameFromJwtToken(String token) { 24 | return Jwts.parser().setSigningKey(JWT_TOKEN).parseClaimsJws(token).getBody().getSubject(); 25 | } 26 | 27 | public Date getExpirationDateFromToken(String token) { 28 | return getClaimFromToken(token, Claims::getExpiration); 29 | } 30 | 31 | public T getClaimFromToken(String token, Function claimsResolver) { 32 | final Claims claims = getAllClaimsFromToken(token); 33 | return claimsResolver.apply(claims); 34 | } 35 | 36 | private Claims getAllClaimsFromToken(String token) { 37 | return Jwts.parser().setSigningKey(JWT_TOKEN) 38 | .parseClaimsJws(token).getBody(); 39 | } 40 | 41 | private Boolean isTokenExpired(String token) { 42 | final Date expiration = getExpirationDateFromToken(token); 43 | return expiration.before(new Date()); 44 | } 45 | 46 | public String generateToken(UserDetails userDetails) { 47 | Map claims = new HashMap<>(); 48 | String username = userDetails.getUsername(); 49 | return doGenerateToken(claims, username); 50 | } 51 | 52 | private String doGenerateToken(Map claims, String subject) { 53 | return Jwts.builder().setClaims(claims).setSubject(subject) 54 | .setIssuedAt(new Date(System.currentTimeMillis())) 55 | .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY)) 56 | .signWith(SignatureAlgorithm.HS512, JWT_TOKEN).compact(); 57 | } 58 | 59 | public Boolean validateToken(String token, UserDetails userDetails) { 60 | final String username = getUserNameFromJwtToken(token); 61 | return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /frontend-web/src/components/search/SearchComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, {useContext, useState} from "react" 2 | import {InputAdornment, OutlinedInput} from "@mui/material" 3 | import {HttpGroupService} from "../../service/http-group-service" 4 | import {AlertAction, AlertContext} from "../../context/AlertContext" 5 | import {SearchContext} from "../../context/SearchContext" 6 | import IconButton from "@mui/material/IconButton" 7 | import CloseIcon from "@mui/icons-material/Close" 8 | 9 | export function SearchComponent() { 10 | const [search, setTextSearch] = useState("") 11 | const {dispatch} = useContext(AlertContext)! 12 | const {setSearchText, setSearchResponse} = useContext(SearchContext)! 13 | const [searchingLoading, setSearchingLoading] = useState(false) 14 | const http = new HttpGroupService() 15 | 16 | function handleChange(event: any) { 17 | event.preventDefault() 18 | setTextSearch(event.target.value) 19 | } 20 | 21 | function resetSearch() { 22 | setSearchText("") 23 | setTextSearch("") 24 | } 25 | 26 | async function startSearch(event: any) { 27 | if (event.key === undefined || event.key === "Enter") { 28 | if (search !== "") { 29 | setSearchingLoading(true) 30 | setSearchText(event.target.value) 31 | try { 32 | const {data} = await http.searchInConversations({text: search}) 33 | setSearchResponse(data) 34 | } catch (error) { 35 | dispatch({ 36 | type: AlertAction.ADD_ALERT, 37 | payload: { 38 | alert: "error", 39 | id: crypto.randomUUID(), 40 | isOpen: true, 41 | text: "Cannot perform search in conversations." 42 | } 43 | }) 44 | } finally { 45 | if (searchingLoading) { 46 | setSearchingLoading(false) 47 | } 48 | } 49 | } 50 | } 51 | } 52 | 53 | return 55 | 56 | 57 | 58 | } 59 | type={"text"} 60 | onChange={handleChange} 61 | value={search} 62 | onKeyDown={startSearch} style={{width: "50%"}} 63 | size={"small"} 64 | label={"Search in conversations"}/> 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/front.yml: -------------------------------------------------------------------------------- 1 | name: front 2 | 3 | on: 4 | push: 5 | branches: [develop] 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: setup node 20 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: 20.x 17 | - name: install dependencies 18 | run: npm --prefix ./frontend-web install 19 | - name: test front 20 | id: test 21 | run: npm --prefix ./frontend-web run test 22 | 23 | build: 24 | runs-on: ubuntu-latest 25 | steps: 26 | - uses: actions/checkout@v4 27 | - name: setup node 20 28 | uses: actions/setup-node@v4 29 | with: 30 | node-version: 20.x 31 | - name: install 32 | run: npm --prefix ./frontend-web install 33 | - name: build 34 | run: npm --prefix ./frontend-web run build 35 | 36 | login: 37 | needs: [build, test] 38 | runs-on: ubuntu-latest 39 | steps: 40 | - name: Login to GAR 41 | uses: docker/login-action@v3 42 | with: 43 | registry: ${{ secrets.DOCKER_HOST }} 44 | username: _json_key 45 | password: ${{ secrets.DOCKER_REGISTRY_SA }} 46 | 47 | push: 48 | needs: login 49 | runs-on: ubuntu-latest 50 | steps: 51 | - name: Build and push 52 | uses: docker/build-push-action@v6 53 | with: 54 | context: frontend-web 55 | push: true 56 | tags: ${{ secrets.SERVICE_NAME }}:latest 57 | 58 | deploy: 59 | needs: push 60 | runs-on: ubuntu-latest 61 | 62 | steps: 63 | #- name: Deploy dev 64 | # if: branch. 65 | # run: | 66 | # gcloud deploy DEV 67 | - uses: actions/checkout@v4 68 | - name: Deploy artifact 69 | #if: branch 70 | run: | 71 | APP_VERSION=`cat ./frontend-web/package.json | grep version | head -1 | awk -F= "{ print $2 }" | sed 's/[version:,\",]//g' | tr -d '[[:space:]]'` 72 | gcloud auth activate-service-account ${{ secrets.CLOUD_RUN_SA }} --key-file=${{ secrets.CLOUD_RUN_SA_KEY }} 73 | gcloud run deploy ${{ secrets.SERVICE_NAME }} --service-account=${{ secrets.CLOUD_RUN_SA }} --allow-unauthenticated --region=${{ secrets.REGION }} --project=${{ secrets.REGION }} --port=80 --image=${{ secrets.REGION }}-docker.pkg.dev/${{ secrets.PROJECT_ID }}/${{ secrets.SERVICE_NAME }}/${{ secrets.SERVICE_NAME }}:$APP_VERSION 74 | gcloud beta run services add-iam-policy-binding --region=${{ secrets.REGION }} --member=allUsers --role=roles/run.invoker ${{ secrets.SERVICE_NAME }} --project=${{ secrets.PROJECT_ID}} 75 | -------------------------------------------------------------------------------- /backend/commons/src/main/java/com/mercure/commons/entity/UserEntity.java: -------------------------------------------------------------------------------- 1 | package com.mercure.commons.entity; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.Setter; 8 | import org.springframework.security.core.GrantedAuthority; 9 | import org.springframework.security.core.userdetails.UserDetails; 10 | 11 | import java.util.*; 12 | 13 | @Entity 14 | @Table(name = "users") 15 | @Getter 16 | @Setter 17 | @AllArgsConstructor 18 | @NoArgsConstructor 19 | public class UserEntity implements UserDetails { 20 | 21 | @Id 22 | private int id; 23 | 24 | @Column(name = "firstname") 25 | private String firstName; 26 | 27 | @Column(name = "lastname") 28 | private String lastName; 29 | 30 | private String password; 31 | 32 | @Column(name = "wstoken") 33 | private String wsToken; 34 | 35 | private String jwt; 36 | 37 | private String color; 38 | 39 | @ManyToMany(fetch = FetchType.EAGER, mappedBy = "userEntities", cascade = CascadeType.ALL) 40 | private Set groupSet = new HashSet<>(); 41 | 42 | @Column(name = "expiration_date") 43 | @Temporal(TemporalType.TIMESTAMP) 44 | private Date expiration_date; 45 | 46 | @Column(name = "short_url") 47 | private String shortUrl; 48 | 49 | @Column(name = "email") 50 | private String mail; 51 | 52 | @Column(name = "account_non_expired") 53 | private boolean accountNonExpired; 54 | 55 | @Column(name = "account_non_locked") 56 | private boolean accountNonLocked; 57 | 58 | @Column(name = "credentials_non_expired") 59 | private boolean credentialsNonExpired; 60 | 61 | @Column(name = "enabled") 62 | private boolean enabled; 63 | 64 | @Column(name = "role_id") 65 | private int role; 66 | 67 | @Override 68 | public Collection getAuthorities() { 69 | return new ArrayList<>(); 70 | } 71 | 72 | @Override 73 | public String getPassword() { 74 | return password; 75 | } 76 | 77 | @Override 78 | public String getUsername() { 79 | return firstName; 80 | } 81 | 82 | @Override 83 | public boolean isAccountNonExpired() { 84 | return accountNonExpired; 85 | } 86 | 87 | @Override 88 | public boolean isAccountNonLocked() { 89 | return accountNonLocked; 90 | } 91 | 92 | @Override 93 | public boolean isCredentialsNonExpired() { 94 | return credentialsNonExpired; 95 | } 96 | 97 | @Override 98 | public boolean isEnabled() { 99 | return enabled; 100 | } 101 | } -------------------------------------------------------------------------------- /frontend-web/src/components/messages/SearchMessageComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, {useContext} from "react" 2 | import {SearchContext} from "../../context/SearchContext" 3 | import "./SearchMessageComponent.css" 4 | import {useNavigate} from "react-router-dom" 5 | import {Box, List, ListItem, ListItemButton, ListItemText, Typography} from "@mui/material" 6 | 7 | // export function DisplayMessagesComponent({messages, groupUrl, updateMessages}: DisplayMessagesProps) { 8 | 9 | interface HighlightSearchedTextProps { 10 | message: string 11 | searchText: string 12 | } 13 | 14 | function HighlightSearchedText({message, searchText}: HighlightSearchedTextProps): React.JSX.Element { 15 | const position = message.search(searchText) 16 | const extracted = message.substring(position, position + searchText.length) 17 | const text = message.split(searchText) 18 | return {text[0]} 19 | {extracted} 20 | {text[1]} 21 | 22 | } 23 | 24 | export function SearchMessageComponent() { 25 | const {searchResponse, setSearchText} = useContext(SearchContext)! 26 | const navigate = useNavigate() 27 | 28 | function redirectToGroup(groupUrl: string) { 29 | setSearchText("") 30 | navigate(`/t/messages/${groupUrl}`) 31 | } 32 | 33 | return 34 |
Search results
35 |
36 | { 37 | searchResponse && searchResponse.matchingMessages.map((searchResponseData, index) => ( 38 | 39 | {searchResponseData.groupName} 40 | }> 41 | { 42 | searchResponseData.messages.map((message, messageIndex) => ( 43 | redirectToGroup(searchResponseData.groupUrl)} key={messageIndex} 44 | disablePadding> 45 | 46 | }/> 49 | 50 | 51 | )) 52 | } 53 | 54 | )) 55 | } 56 |
57 |
58 | } 59 | -------------------------------------------------------------------------------- /backend/core/src/main/java/com/mercure/core/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package com.mercure.core.config; 2 | 3 | import com.mercure.core.service.CustomUserDetailsService; 4 | import lombok.AllArgsConstructor; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.security.authentication.AuthenticationProvider; 8 | import org.springframework.security.authentication.dao.DaoAuthenticationProvider; 9 | import org.springframework.security.config.Customizer; 10 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 11 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 12 | import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; 13 | import org.springframework.security.config.http.SessionCreationPolicy; 14 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 15 | import org.springframework.security.crypto.password.PasswordEncoder; 16 | import org.springframework.security.web.SecurityFilterChain; 17 | import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; 18 | 19 | @AllArgsConstructor 20 | @Configuration 21 | @EnableWebSecurity 22 | public class SecurityConfig { 23 | 24 | private JwtWebConfig jwtWebConfig; 25 | 26 | private CustomUserDetailsService customUserDetailsService; 27 | 28 | private static final String[] AUTHORIZED_URLS = {"/messenger", "/health-check", "/websocket", "/ws", "/csrf", "/auth", "/user/register"}; 29 | 30 | @Bean 31 | public PasswordEncoder passwordEncoder() { 32 | return new BCryptPasswordEncoder(); 33 | } 34 | 35 | @Bean 36 | public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { 37 | http 38 | .csrf(AbstractHttpConfigurer::disable) 39 | .cors(Customizer.withDefaults()) 40 | .authorizeHttpRequests((request) -> request 41 | .requestMatchers(AUTHORIZED_URLS).permitAll() 42 | .anyRequest().authenticated()) 43 | .logout(AbstractHttpConfigurer::disable) 44 | .sessionManagement(httpSecuritySessionManagementConfigurer -> httpSecuritySessionManagementConfigurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) 45 | .addFilterBefore(jwtWebConfig, UsernamePasswordAuthenticationFilter.class); 46 | return http.build(); 47 | } 48 | 49 | @Bean 50 | AuthenticationProvider authenticationProvider() { 51 | DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); 52 | provider.setUserDetailsService(customUserDetailsService); 53 | provider.setPasswordEncoder(passwordEncoder()); 54 | return provider; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /frontend-web/src/components/partials/list-items/MultimediaListComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, {useContext, useEffect, useState} from "react" 2 | import "./MultimediaListStyle.css" 3 | import {CircularProgress, Collapse, ListItemButton, ListItemIcon, ListItemText} from "@mui/material" 4 | import {ExpandLess, FolderCopy} from "@mui/icons-material" 5 | import ExpandMore from "@mui/icons-material/ExpandMore" 6 | import {HttpGroupService} from "../../../service/http-group-service" 7 | import {AlertAction, AlertContext} from "../../../context/AlertContext" 8 | 9 | interface MultimediaListProps { 10 | isDisabled: boolean 11 | groupUrl: string 12 | } 13 | 14 | export function MultimediaListComponent({groupUrl, isDisabled}: MultimediaListProps) { 15 | const {dispatch} = useContext(AlertContext)! 16 | const [isListOpened, setListStatus] = useState(false) 17 | const [multimediaContent, setMultimediaContent] = useState([]) 18 | const http = new HttpGroupService() 19 | const [mediaLoading, setMediaLoading] = useState(false) 20 | 21 | useEffect(() => { 22 | setMultimediaContent([]) 23 | }, [groupUrl]) 24 | 25 | async function changeState() { 26 | if (!isListOpened && multimediaContent.length === 0) { 27 | setMediaLoading(true) 28 | try { 29 | const {data} = await http.getMultimediaFiles(groupUrl) 30 | setMultimediaContent(data) 31 | } catch (error) { 32 | dispatch({ 33 | type: AlertAction.ADD_ALERT, 34 | payload: { 35 | id: crypto.randomUUID(), 36 | text: `Cannot fetch multimedia content : ${error}`, 37 | isOpen: true, 38 | alert: "error", 39 | } 40 | }) 41 | } finally { 42 | 43 | setMediaLoading(false) 44 | } 45 | } 46 | setListStatus(!isListOpened) 47 | } 48 | 49 | return <> 50 | 51 | 52 | 53 | 54 | 55 | {isListOpened ? : } 56 | 57 | 58 |
59 | { 60 | multimediaContent.map((file, index) => { 61 | return ( 62 |
63 | ) 64 | }) 65 | } 66 |
67 | 68 | {mediaLoading && } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /frontend-web/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import "./index.css" 3 | import * as serviceWorker from "./serviceWorker" 4 | import {createBrowserRouter, redirect, RouterProvider} from "react-router-dom" 5 | import {createRoot} from "react-dom/client" 6 | import {WebSocketMainComponent} from "./components/websocket/websocket-main-component" 7 | import {VideoComponent} from "./components/websocket/video-component" 8 | import {LoaderProvider} from "./context/loader-context" 9 | import {AlertComponent} from "./components/partials/alert-component" 10 | import {LoaderComponent} from "./components/partials/loader/LoaderComponent" 11 | import {HttpGroupService} from "./service/http-group-service" 12 | import {AlertContextProvider} from "./context/AlertContext" 13 | import {UserContextProvider} from "./context/UserContext" 14 | import {GroupContextProvider} from "./context/GroupContext" 15 | import {SearchProvider} from "./context/SearchContext" 16 | import {LoginWrapperComponent} from "./components/login/LoginWrapperComponent" 17 | import {RegisterUserWrapper} from "./components/register/RegisterUserWrapper" 18 | import {ResetPasswordComponent} from "./components/reset-password/ResetPasswordComponent" 19 | 20 | const router = createBrowserRouter([ 21 | { 22 | path: "/", 23 | loader: async () => { 24 | return new HttpGroupService().pingRoute() 25 | .then(() => redirect("/t/messages")) 26 | .catch(() => redirect("/login")) 27 | }, 28 | }, 29 | { 30 | path: "login", 31 | element: , 32 | }, 33 | { 34 | path: "register", 35 | element: 36 | }, 37 | { 38 | path: "t/messages", 39 | element: 40 | }, 41 | { 42 | path: "t/messages/:groupId", 43 | element: 44 | }, 45 | { 46 | path: "room/:callUrl/:groupUrl", 47 | element: 48 | }, 49 | { 50 | path: "reset/password", 51 | element: 52 | } 53 | ]) 54 | 55 | function RootComponent() { 56 | return ( 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | ) 71 | } 72 | 73 | createRoot(document.getElementById("root")!) 74 | .render( 75 | 76 | ) 77 | 78 | // If you want your app to work offline and load faster, you can change 79 | // unregister() to register() below. Note this comes with some pitfalls. 80 | // Learn more about service workers: https://bit.ly/CRA-PWA 81 | serviceWorker.unregister() 82 | -------------------------------------------------------------------------------- /backend/storage/src/main/java/com/mercure/storage/service/StorageService.java: -------------------------------------------------------------------------------- 1 | package com.mercure.storage.service; 2 | 3 | import com.mercure.commons.entity.FileEntity; 4 | import com.mercure.commons.service.FileService; 5 | import com.mercure.commons.utils.StaticVariable; 6 | import com.mercure.storage.config.StorageOptions; 7 | import jakarta.annotation.PostConstruct; 8 | import lombok.AllArgsConstructor; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import org.springframework.stereotype.Service; 12 | import org.springframework.util.StringUtils; 13 | import org.springframework.web.multipart.MultipartFile; 14 | import org.springframework.web.servlet.support.ServletUriComponentsBuilder; 15 | 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.nio.file.Files; 19 | import java.nio.file.Paths; 20 | import java.nio.file.StandardCopyOption; 21 | import java.util.Objects; 22 | import java.util.UUID; 23 | 24 | @Service 25 | @AllArgsConstructor 26 | public class StorageService { 27 | 28 | private static final Logger log = LoggerFactory.getLogger(StorageService.class); 29 | 30 | private FileService fileService; 31 | 32 | private StorageOptions storageOptions; 33 | 34 | @PostConstruct 35 | public void init() { 36 | try { 37 | Files.createDirectories(Paths.get(StaticVariable.FILE_STORAGE_PATH)); 38 | } catch (IOException e) { 39 | log.error("Cannot initialize directory : {}", e.getMessage()); 40 | } 41 | } 42 | 43 | public void store(MultipartFile file, int messageId) { 44 | String completeName = StringUtils.cleanPath(Objects.requireNonNull(file.getOriginalFilename())); 45 | String[] array = completeName.split("\\."); 46 | String fileExtension = array[array.length - 1]; 47 | String fileName = UUID.randomUUID().toString(); 48 | 49 | String newName = fileName + "." + fileExtension; 50 | String uri = ServletUriComponentsBuilder.fromCurrentContextPath() 51 | .path("/uploads/") 52 | .path(newName) 53 | .toUriString(); 54 | 55 | FileEntity fileEntity = new FileEntity(); 56 | fileEntity.setUrl(uri); 57 | fileEntity.setFilename(fileName); 58 | fileEntity.setMessageId(messageId); 59 | try { 60 | if (file.isEmpty()) { 61 | log.warn("Cannot save empty file with name : {}", newName); 62 | return; 63 | } 64 | if (fileName.contains("..")) { 65 | log.warn("Cannot store file with relative path outside current directory {}", newName); 66 | } 67 | try (InputStream inputStream = file.getInputStream()) { 68 | storageOptions.uploadFile(file.getResource().getFile()); 69 | Files.copy(inputStream, Paths.get(StaticVariable.FILE_STORAGE_PATH).resolve(newName), 70 | StandardCopyOption.REPLACE_EXISTING); 71 | fileService.save(fileEntity); 72 | } 73 | } catch (Exception e) { 74 | log.error(e.getMessage()); 75 | } 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /backend/storage/src/main/java/com/mercure/storage/controller/WsFileController.java: -------------------------------------------------------------------------------- 1 | package com.mercure.storage.controller; 2 | 3 | import com.mercure.commons.dto.MessageDTO; 4 | import com.mercure.commons.dto.OutputTransportDTO; 5 | import com.mercure.commons.entity.MessageEntity; 6 | import com.mercure.core.service.GroupService; 7 | import com.mercure.core.service.MessageService; 8 | import com.mercure.storage.service.StorageService; 9 | import com.mercure.core.service.UserSeenMessageService; 10 | import com.mercure.commons.utils.MessageTypeEnum; 11 | import com.mercure.commons.utils.TransportActionEnum; 12 | import lombok.AllArgsConstructor; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.springframework.http.MediaType; 15 | import org.springframework.http.ResponseEntity; 16 | import org.springframework.messaging.simp.SimpMessagingTemplate; 17 | import org.springframework.web.bind.annotation.*; 18 | import org.springframework.web.multipart.MultipartFile; 19 | 20 | import java.util.List; 21 | 22 | @RestController 23 | @AllArgsConstructor 24 | @Slf4j 25 | public class WsFileController { 26 | 27 | private MessageService messageService; 28 | 29 | private GroupService groupService; 30 | 31 | private SimpMessagingTemplate messagingTemplate; 32 | 33 | private StorageService storageService; 34 | 35 | private UserSeenMessageService seenMessageService; 36 | 37 | /** 38 | * Receive file to put in DB and send it back to the group conversation 39 | * 40 | * @param file The file to be uploaded 41 | * @param userId int value for user ID sender of the message 42 | * @param groupUrl string value for the group URL 43 | * @return a {@link ResponseEntity} with HTTP code 44 | */ 45 | @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) 46 | public ResponseEntity uploadFile(@RequestParam(name = "file") MultipartFile file, @RequestParam(name = "userId") int userId, @RequestParam(name = "groupUrl") String groupUrl) { 47 | int groupId = groupService.findGroupByUrl(groupUrl); 48 | try { 49 | MessageEntity messageEntity = messageService.createAndSaveMessage(userId, groupId, MessageTypeEnum.FILE.toString(), "have send a file"); 50 | storageService.store(file, messageEntity.getId()); 51 | OutputTransportDTO res = new OutputTransportDTO(); 52 | MessageDTO messageDTO = messageService.createNotificationMessageDTO(messageEntity, userId); 53 | res.setAction(TransportActionEnum.NOTIFICATION_MESSAGE); 54 | res.setObject(messageDTO); 55 | seenMessageService.saveMessageNotSeen(messageEntity, groupId); 56 | List toSend = messageService.createNotificationList(userId, groupUrl); 57 | toSend.forEach(toUserId -> messagingTemplate.convertAndSend("/topic/user/" + toUserId, res)); 58 | } catch (Exception e) { 59 | log.error("Cannot save file, caused by {}", e.getMessage()); 60 | return ResponseEntity.status(500).build(); 61 | } 62 | return ResponseEntity.ok().build(); 63 | } 64 | 65 | @GetMapping("files/groupUrl/{groupUrl}") 66 | public List getMultimediaContent(@PathVariable String groupUrl) { 67 | return messageService.getMultimediaContentByGroup(groupUrl); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /frontend-web/src/context/GroupContext.tsx: -------------------------------------------------------------------------------- 1 | import React, {createContext, Dispatch, useReducer} from "react" 2 | import {GroupModel} from "../interface-contract/group-model" 3 | 4 | enum GroupContextAction { 5 | ADD_GROUP = "ADD_GROUP", 6 | UPDATE_GROUPS = "UPDATE_GROUPS", 7 | UPDATE_LAST_MESSAGE_GROUP = "UPDATE_LAST_MESSAGE_GROUP", 8 | UPDATE_SEEN_MESSAGE = "UPDATE_SEEN_MESSAGE", 9 | SET_GROUPS = "SET_GROUPS", 10 | } 11 | 12 | export type GroupActionType = 13 | | { type: GroupContextAction.UPDATE_GROUPS; payload: { id: number; field: Partial } } 14 | | { type: GroupContextAction.ADD_GROUP; payload: GroupModel } 15 | | { type: GroupContextAction.UPDATE_SEEN_MESSAGE; payload: { groupUrl: string; isMessageSeen: boolean } } 16 | | { type: GroupContextAction.UPDATE_LAST_MESSAGE_GROUP; payload: { groupUrl: string; field: Partial } } 17 | | { type: GroupContextAction.SET_GROUPS; payload: GroupModel[] } 18 | 19 | const GroupContext = createContext<{ 20 | groups: GroupModel[] 21 | changeGroupState: Dispatch; 22 | } | undefined>(undefined) 23 | 24 | export const groupReducer = (state: GroupModel[], action: GroupActionType): GroupModel[] => { 25 | switch (action.type) { 26 | case GroupContextAction.UPDATE_GROUPS: { 27 | const index = state.findIndex((group) => group.id === action.payload.id) 28 | if (index >= 0) { 29 | return state 30 | } 31 | return state 32 | } 33 | case GroupContextAction.ADD_GROUP: { 34 | return [action.payload, ...state] // at first index because new conversation 35 | } 36 | case GroupContextAction.UPDATE_LAST_MESSAGE_GROUP: { 37 | const index = state.findIndex((group) => group.url === action.payload.groupUrl) 38 | if (index > -1) { 39 | state[index].lastMessageSender = action.payload.field.lastMessageSender 40 | state[index].lastMessageDate = action.payload.field.lastMessageDate || "" 41 | state[index].lastMessageSeen = action.payload.field.lastMessageSeen || false 42 | state[index].lastMessage = action.payload.field.lastMessage || "" 43 | } 44 | const groupToUpdatePosition = state[index] 45 | state.splice(index, 1) 46 | state.unshift(groupToUpdatePosition) 47 | return state 48 | } 49 | case GroupContextAction.UPDATE_SEEN_MESSAGE: { 50 | const newState = [...state] 51 | const index = newState.findIndex((group) => group.url === action.payload.groupUrl) 52 | if (index > -1) { 53 | newState[index].lastMessageSeen = action.payload.isMessageSeen 54 | } 55 | return newState 56 | } 57 | case GroupContextAction.SET_GROUPS: { 58 | return action.payload 59 | } 60 | default: 61 | return state 62 | } 63 | } 64 | 65 | const GroupContextProvider: React.FC<{ children: React.ReactNode }> = ({children}) => { 66 | const [groups, changeGroupState] = useReducer(groupReducer, []) 67 | return ( 68 | 69 | {children} 70 | 71 | ) 72 | } 73 | 74 | export {GroupContextProvider, GroupContext, GroupContextAction} 75 | 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![maven build status](https://github.com/Thibaut-Mouton/react-spring-messenger-project/workflows/build-back/badge.svg?branch=develop) 2 | ![npm build status](https://github.com/Thibaut-Mouton/react-spring-messenger-project/workflows/build-front/badge.svg?branch=develop) 3 | ![coverage back](https://github.com/Thibaut-Mouton/react-spring-messenger-project/workflows/test-back/badge.svg?branch=develop) 4 | ![coverage front](https://github.com/Thibaut-Mouton/react-spring-messenger-project/workflows/test-front/badge.svg?branch=develop) 5 | 6 | # FastLiteMessage 7 | 8 | Real time chat application group oriented built with React and Spring Boot. Talk with your friends, create and add users to conversation, send messages or images, set groups administrators and start video calls ! (coming soon) 9 | 10 | # Project Requirements 11 | 12 | * [JDK](https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html) 17 13 | * [NodeJS](https://nodejs.org/en/download/) v20.12.1 14 | * [ReactJS](https://reactjs.org/) v18 15 | * [Material UI](https://mui.com/) v5.7.0 16 | * [MySQL Server](https://www.mysql.com/) 17 | 18 | # What's next ? 19 | You want to know the roadmap of the project, next release or next fixes ? Check the "Projects" tab or visit the [Fast Lite Message's roadmap](https://github.com/users/Thibaut-Mouton/projects/4) 20 | 21 | # Project fast start up 22 | In a hurry ? Juste type ```docker-compose up``` in the root directory. 23 | This will start 3 containers : MySQL, backend and frontend together. Liquibase will take care of the database setup. For development purpose, the DB is filled with 5 accounts (password: ```root```) : 24 | * Thibaut 25 | * Mark 26 | * John 27 | * Luke 28 | * Steve 29 | ``` 30 | Warning : Be sure that no other app is running on port 3000, 9090 or 3306 31 | ``` 32 | 33 | # Project development set up 34 | 35 | * This project use [liquibase](https://www.liquibase.org/) as a version control for database. When you will start backend, all tables and structures will be generated automatically. 36 | * You can disable Liquibase by setting ```spring.liquibase.enabled=false``` in ```application.properties```. 37 | * To try the project on localhost, check that nothing runs on ports 9090 (Spring Boot) and 3000 (React app) 38 | * You can edit ````spring.datasource```` in ```backend/src/main/resources/application.properties``` and ```username``` and ```password``` in ```backend/src/main/resources/liquibase.properties``` with your own MySQL login / password 39 | * Create a database named "fastlitemessage_dev" or you can also modify the name in the properties files mentioned just above. 40 | 41 | ## Start backend 42 | * Go inside backend folder then type ```mvn spring-boot:run``` to launch backend. 43 | * Or you can type ```mvn clean package``` to generate a JAR file and then start server with ```java -jar path/to/jar/file``` (Normally in inside backend/target/) 44 | ## Start frontend 45 | * Go inside frontend-web folder and then type ```npm run start``` 46 | 47 | # Disclaimer 48 | * Please note there is no specific security over websockets. 49 | * Docker setup is not production ready 50 | 51 | # Project overview 52 | 53 | ![Project overview](assets/flm.PNG?raw=true "Project overview 1") 54 | 55 | ![Project overview](assets/flm2.PNG?raw=true "Project overview 2") 56 | 57 | * Simple chat group application 58 | * Send images 59 | * Start video calls 60 | * Secure user account 61 | * Room discussion 62 | * Chat group administrators 63 | * Add / remove users from conversation 64 | -------------------------------------------------------------------------------- /backend/core/src/main/java/com/mercure/core/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.mercure.core.service; 2 | 3 | import com.mercure.commons.dto.GroupMemberDTO; 4 | import com.mercure.commons.entity.GroupRoleKey; 5 | import com.mercure.commons.entity.GroupUser; 6 | import com.mercure.commons.entity.UserEntity; 7 | import com.mercure.core.repository.UserRepository; 8 | import jakarta.transaction.Transactional; 9 | import lombok.AllArgsConstructor; 10 | import lombok.Getter; 11 | import lombok.Setter; 12 | import org.springframework.stereotype.Service; 13 | 14 | import java.text.Normalizer; 15 | import java.util.*; 16 | 17 | @Service 18 | @Getter 19 | @Setter 20 | @AllArgsConstructor 21 | public class UserService { 22 | 23 | // private PasswordEncoder passwordEncoder; 24 | 25 | private UserRepository userRepository; 26 | 27 | private GroupUserJoinService groupUserJoinService; 28 | 29 | private static Map wsSessions = new HashMap<>(); 30 | 31 | public List findAll() { 32 | return userRepository.findAll(); 33 | } 34 | 35 | @Transactional 36 | public void save(UserEntity userEntity) { 37 | userRepository.save(userEntity); 38 | } 39 | 40 | public List fetchAllUsers(int[] ids) { 41 | List toSend = new ArrayList<>(); 42 | List list = userRepository.getAllUsersNotAlreadyInConversation(ids); 43 | list.forEach(user -> toSend.add(new GroupMemberDTO(user.getId(), user.getFirstName(), user.getLastName(), false))); 44 | return toSend; 45 | } 46 | 47 | public UserEntity findByNameOrEmail(String str0, String str1) { 48 | return userRepository.getUserByFirstNameOrMail(str0, str1); 49 | } 50 | 51 | public boolean checkIfUserIsAdmin(int userId, int groupIdToCheck) { 52 | GroupRoleKey id = new GroupRoleKey(groupIdToCheck, userId); 53 | Optional optional = groupUserJoinService.findById(id); 54 | if (optional.isPresent()) { 55 | GroupUser groupUser = optional.get(); 56 | return groupUser.getRole() == 1; 57 | } 58 | return false; 59 | } 60 | 61 | public String createShortUrl(String firstName, String lastName) { 62 | StringBuilder sb = new StringBuilder(); 63 | sb.append(firstName); 64 | sb.append("."); 65 | sb.append(Normalizer.normalize(lastName.toLowerCase(), Normalizer.Form.NFD)); 66 | boolean isShortUrlAvailable = true; 67 | int counter = 0; 68 | while (isShortUrlAvailable) { 69 | sb.append(counter); 70 | if (userRepository.countAllByShortUrl(sb.toString()) == 0) { 71 | isShortUrlAvailable = false; 72 | } 73 | counter++; 74 | } 75 | return sb.toString(); 76 | } 77 | 78 | public String findUsernameById(int id) { 79 | return userRepository.getUsernameByUserId(id); 80 | } 81 | 82 | public String findFirstNameById(int id) { 83 | return userRepository.getFirstNameByUserId(id); 84 | } 85 | 86 | public UserEntity findById(int id) { 87 | return userRepository.findById(id).orElse(null); 88 | } 89 | 90 | public String passwordEncoder(String str) { 91 | // return passwordEncoder.encode(str); 92 | return str; 93 | } 94 | 95 | public boolean checkIfUserNameOrMailAlreadyUsed(String mail) { 96 | return userRepository.countAllByMail(mail) > 0; 97 | } 98 | } 99 | --------------------------------------------------------------------------------