UNTESTED CHANGES to login system
Extracted login logic to independent component Login.tsx Implemented dummy password encryption logic Created LoginContext, login+registration handlers relevant type annots added untested change username function in main App button updated PAPERWORK and README
This commit is contained in:
parent
e85f778774
commit
5ce5f9e4eb
11 changed files with 303 additions and 79 deletions
26
PAPERWORK.md
26
PAPERWORK.md
|
@ -1,25 +1,39 @@
|
|||
# Paperwork related to EPQ
|
||||
|
||||
## P304 - Project Proposal Form
|
||||
|
||||
### Section 1
|
||||
|
||||
#### Metadata
|
||||
|
||||
Learner Name: Zhongheng Liu
|
||||
Centre Name: Byron College?
|
||||
Teacher Assessor: Mr Charis Dedousis
|
||||
Proposed project title: Designing and implementing an open-source, privacy-oriented Internet Relay Chat (IRC) application with modern programming paradigms.
|
||||
Title or working title of project: ibid.
|
||||
|
||||
#### Project objectives
|
||||
|
||||
I would like to find out how enterprise-level text chatting applications such as WhatsApp and Telegam operate on the code infrastructure level by designing a similar application myself using open-source technologies.
|
||||
I would also like to learn, in the creation of this artifact, how several programming paradigms like object-oriented programming, functional programming, and annotation-based programming interact with each other to achieve best results.
|
||||
I would also like to find out to what extent can unit testing and integration testing coverage contribute to test-driven development (TDD) as a practice in enterprise-level software design.
|
||||
|
||||
#### Role or responsibility in a group project
|
||||
|
||||
Not a group project.
|
||||
|
||||
### Section 2
|
||||
|
||||
#### Reasons for choosing the project
|
||||
|
||||
The development of this IRC chat application directly links to the field of computer science and similarly software engineering, both fields which I have a large interest in and would like to pursue in the future.
|
||||
In the past, I have taken less care than desirable into the security of my software and the usage of test-driven development through unit, integration, and penetration testing. Additionally, I would like to make use of, in the construction of my Java backend API, the principles defined in Erich Gamma et. al.'s book Design Patterns to create clean and maintainable software that can be refactored easily in the future through the construction of sufficient and robust data structures and paradigms.
|
||||
### Section 3
|
||||
In the past, I have taken less care than desirable into the security of my software and the usage of test-driven development through unit, integration, and penetration testing. Additionally, I would like to make use of, in the construction of my Java backend API, the principles defined in Erich Gamma et. al.'s book Design Patterns to create clean and maintainable software that can be refactored easily in the future through the construction of sufficient and robust data structures and paradigms.
|
||||
|
||||
### Section 3
|
||||
|
||||
#### Activities to be carried out during the project
|
||||
|Activities|Duration|
|
||||
|-|-|
|
||||
|Research into the use of the technologies employed in the project| idk|
|
||||
|||
|
||||
|
||||
| Activities | Duration |
|
||||
| ----------------------------------------------------------------- | -------- |
|
||||
| Research into the use of the technologies employed in the project | idk |
|
||||
| | |
|
||||
|
|
50
README.md
50
README.md
|
@ -1,14 +1,20 @@
|
|||
# EPQ IRC Chat App Front-end
|
||||
|
||||
> This project is the client-side companion to the Java 17 WebSockets and MySQL backend.
|
||||
> Copyright 2024-2025 Zhongheng Liu
|
||||
|
||||
## Purpose
|
||||
|
||||
This repository stores code that partially comprises the IRC chat project. This is an A-Level Edexcel Level 3 Extended Project Qualification in the form of an Artifact. Its purpose is to create a convenient self-hosted IRC chat solution, with HTTPS encrypted text transfer and end-to-end SSH asymmetric key-exchange, encrypt-decrypt functions to ensure that the messages uploaded and downloaded are anomynous and untrackable.
|
||||
It hopes to produce an aesthetically-pleasing and modern-looking interface through the use of a combination of HTML, JavaScript/TypeScript logic, and CSS formatting to create an interactible UI/UX for users of the application to communicate with each other through a server that acts as the message broker between 2 or more parties through the use of the STOMP library and the WebSockets protocol.
|
||||
It hopes to produce an aesthetically-pleasing and modern-looking interface through the use of a combination of HTML, JavaScript/TypeScript logic, and CSS formatting to create an interactible UI/UX for users of the application to communicate with each other through a server that acts as the message broker between 2 or more parties through the use of the STOMP library and the WebSockets protocol.
|
||||
|
||||
## Explanation
|
||||
|
||||
### Project initialisation
|
||||
|
||||
The skeleton code of the epq-web project is first generated by the command `npx create-react-app <directory> --template typescript`, by incorporating TypeScript support into the project, easy access to static type-checking is granted, providing for a more efficient debugging process, since common errors such as type mismatchs will be detected first instead of during the build-and-production cycle.
|
||||
which produced a directory tree structure similar to the following:
|
||||
|
||||
```
|
||||
<directory root>
|
||||
├── node_modules
|
||||
|
@ -35,19 +41,53 @@ which produced a directory tree structure similar to the following:
|
|||
|
||||
3 directories, 17 files (ignored node_modules/*)
|
||||
```
|
||||
|
||||
This part of the EPQ artifact creation process utilized predominantly 3 libraries. It involved ReactJS, TypeScript, `@types/*` type notation libraries, and most crucially the underlying Node.js application structure.
|
||||
Of this newly created tree structure, most of the code encapsulated within the generated files are redundant and are immediately purged from the repository. The file `/package.json` auto-generated by `npm` documents the libraries that are imported throughout the project, tracking its dependencies. The actual development of the software required 1 more library called StompJS.
|
||||
|
||||
### Project dependencies
|
||||
|
||||
#### StompJS and WebSockets
|
||||
STOMP is the name of a protocol that allowed clients to talk to each other using JSON and WebSockets technology. It also provides an interface for WebSocket (aka. an abstraction from the functions provided by the SockJS library).
|
||||
|
||||
STOMP is the name of a protocol that allowed clients to talk to each other using plaintext JSON and WebSockets technology. It also provides an interface for WebSocket (aka. an abstraction from the functions provided by the SockJS library).
|
||||
|
||||
#### ReactJS
|
||||
Lorem ipsum
|
||||
|
||||
React JS is a JavaScript library, through the use of which a dynamically updated, efficient, and most crucially state-controlled application may be developed. It is most commonly utilised for the development of single-page applications. For the purposes of this application, it is used to encapsulate and instantiate dynamic React elements for re-use in code. JSX syntax is used which can make incorporating JavaScript into HTML code easier to handle for the React library. In the newest standard, functional components are recommended for a more concise and hook-centric way of writing applications.
|
||||
A React hook in use for a functional component can be defined as similar to this syntax:
|
||||
|
||||
```typescript
|
||||
export function Component({ ...props }: PropType) {
|
||||
const [state, setState] = useState<StateType>(undefined);
|
||||
return (
|
||||
<div id="App">
|
||||
<h1>Title</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
The example above is a React functional component with TypeScript type notations. State variables such as the one defined in the Component invoke a page re-render each time the state is updated. This is done though the `setState()` function which acts as a state-setter returned by the `useState` hook, as provided for by the React library.
|
||||
|
||||
Another function of React used in this project is `useEffect` and `useContext`. Due to components being dependent on prop inputs and state variables, it may be considered inefficient to provide additional parameters for global variables such as language information to the child components through their specific inclusion into the child's props. It is for this reason that the `useContext` hook is used to provide context for certain dependencies that are known to be required for most, if not all, of the components in the page.
|
||||
For this project specifically, `useContext` is used as a global user and language context provider, such that the page refreshes each time such settings are changed. For example, early on it is found that the STOMP connection is disconnected each time a new message is collected, because as the page refreshes, the code that establishes a STOMP connection to the server is terminated each time, causing the connection to fail.
|
||||
As it has been found out later in the development process, the `useEffect` hook could be used to change the procedure such that, by defining an empty array of dependencies for which the code is cleaned up and re-run, the code that instantiates the STOMP connection is not terminated and re-run following each page update.
|
||||
|
||||
#### TypeScript
|
||||
Lorem ipsum
|
||||
|
||||
JavaScript is considered as a weakly-typed language. By being weakly-typed largely scripting language, there is a significant chance that, due to the potential for malicious or malformed requests and operations, the entire webpage may suddenly stop working, while there was no warning during development, and a problem is only detected during runtime, a time by which a problem may turn out to be significantly harder to resolve, compared to if it was detected as the code was being written.
|
||||
By explicitly defining and performing operations on datatypes in TypeScript, type annotations act as a sort of safety net for your code such that invalid data structures are immediately detected and analysed, ensuring that your TS code cannot pass the TSC compiler if it has type annotation-related issues. This makes issues surface earlier in the development process and can make for a large increase in productivity when developing an application, also ensuring more secure and well-written applications could be made.
|
||||
|
||||
#### Node.js
|
||||
|
||||
Lorem ipsum
|
||||
|
||||
### Project development
|
||||
|
||||
#### Initial project infrastructure
|
||||
|
||||
Lorem ipsum
|
||||
|
||||
### License
|
||||
This project is licensed under the MIT License, found under `/LICENSE`.
|
||||
|
||||
This project is licensed under the MIT License, found under `/LICENSE`.
|
||||
|
|
83
src/App.tsx
83
src/App.tsx
|
@ -1,15 +1,16 @@
|
|||
import React, { createContext, useContext, useState } from "react";
|
||||
import Chat from "./Chat/Chat";
|
||||
import "./App.css";
|
||||
import { LangType, Message } from "./Chat/types";
|
||||
import { LangType, Message } from "./Chat/messageTypes";
|
||||
import { MessageContainer } from "./Chat/MessageContainer";
|
||||
import strings from "./Intl/strings.json";
|
||||
import { LangContext } from "./context";
|
||||
import { LangContext, LoginType } from "./context";
|
||||
import { contentTypes, domain, endpoints, port } from "./consts";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
// what we call "in the business" type gymnastics
|
||||
// what we "in the business" call type gymnastics
|
||||
const Wrapper = (): React.ReactElement => {
|
||||
const [lang, setLang] = useState<LangType>("en_US");
|
||||
|
||||
return (
|
||||
<LangContext.Provider value={lang}>
|
||||
<App
|
||||
|
@ -20,24 +21,69 @@ const Wrapper = (): React.ReactElement => {
|
|||
</LangContext.Provider>
|
||||
);
|
||||
};
|
||||
const setNameOnServer = async (name: string) => {
|
||||
const responseRaw = await fetch(
|
||||
`http://${domain}:${port}${endpoints.user}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: contentTypes.json,
|
||||
body: JSON.stringify({
|
||||
userName: name,
|
||||
dateJoined: Date.now(),
|
||||
}),
|
||||
}
|
||||
);
|
||||
if (responseRaw.status === 400) {
|
||||
return { success: false, reason: "Username taken or invalid!" };
|
||||
} else return { success: true, reason: "" };
|
||||
};
|
||||
const validateName = (name: string): boolean => {
|
||||
// TODO Name validation
|
||||
return !(name === null || name === undefined || name === "");
|
||||
};
|
||||
|
||||
const App = ({
|
||||
changeLang,
|
||||
}: {
|
||||
changeLang: (value: string) => void;
|
||||
}): React.ReactElement => {
|
||||
const [login, setLogin] = useState<LoginType | undefined>(undefined);
|
||||
const [username, setUsername] = useState<string>();
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
// const [lang, setLang] = useState<LangType>("en_US");
|
||||
const lang = useContext(LangContext);
|
||||
const home = strings[lang].homepage;
|
||||
// TODO refine setName logic -- move to Login handler
|
||||
const setNamePrompt = () => {
|
||||
var newName = prompt(home.userNamePrompt) as string;
|
||||
while (!validateName(newName)) {
|
||||
console.log(newName);
|
||||
|
||||
prompt("Username invalid! Please enter again.") as string;
|
||||
}
|
||||
setNameOnServer(newName).then((value) => {
|
||||
if (!value.success) {
|
||||
alert(value.reason);
|
||||
return true;
|
||||
} else {
|
||||
setUsername(newName);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
if (!username) {
|
||||
const newName = prompt(home.userNamePrompt) as string;
|
||||
var newName = prompt(home.userNamePrompt) as string;
|
||||
while (!validateName(newName)) {
|
||||
console.log(newName);
|
||||
|
||||
prompt("Username invalid! Please enter again.") as string;
|
||||
}
|
||||
setUsername(newName);
|
||||
}
|
||||
return (
|
||||
<div className="App">
|
||||
<h1>{home.title}</h1>
|
||||
<pre>{home.description}</pre>
|
||||
<h3>Your name is: {username}</h3>
|
||||
<button
|
||||
onClick={(ev) => {
|
||||
const selection = prompt(home.newLangPrompt);
|
||||
|
@ -46,6 +92,31 @@ const App = ({
|
|||
>
|
||||
{home.switchLang}
|
||||
</button>
|
||||
<button
|
||||
onClick={(ev) => {
|
||||
// For passing new username to the backend
|
||||
// In the future, this could be done with the async/await JS/TS syntax
|
||||
const newUsername = prompt("New username: ");
|
||||
fetch(`${endpoints.user}?name=${newUsername}`, {
|
||||
method: "POST",
|
||||
})
|
||||
.then((response) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((responseBody: { success: boolean }) => {
|
||||
if (responseBody.success) {
|
||||
setUsername(newUsername as string);
|
||||
} else {
|
||||
console.error("Server POST message failed.");
|
||||
alert(
|
||||
"The server encountered an internal error."
|
||||
);
|
||||
}
|
||||
});
|
||||
}}
|
||||
>
|
||||
Change Username
|
||||
</button>
|
||||
{messages.map((message) => {
|
||||
return <MessageContainer {...message} />;
|
||||
})}
|
||||
|
|
|
@ -6,23 +6,12 @@ import React, {
|
|||
useState,
|
||||
} from "react";
|
||||
import { MessageContainer } from "./MessageContainer";
|
||||
import { Client, Stomp, StompHeaders } from "@stomp/stompjs";
|
||||
import { LangType, Message, MessageType } from "./types";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { Client } from "@stomp/stompjs";
|
||||
import { Message, MessageType } from "./messageTypes";
|
||||
import "./Chat.css";
|
||||
import strings from "../Intl/strings.json";
|
||||
import { LangContext } from "../context";
|
||||
// The last bit of magic sauce to make this work
|
||||
// EXPLANATION
|
||||
//
|
||||
const domain = window.location.hostname;
|
||||
const port = "8080";
|
||||
const connectionAddress = `ws://${domain}:${port}/ws`;
|
||||
const endpoints = {
|
||||
destination: "/app/chat",
|
||||
subscription: "/sub/chat",
|
||||
history: "/api/v1/msg/",
|
||||
};
|
||||
import { connectionAddress, endpoints } from "../consts";
|
||||
const Chat = ({ user }: { user: string }): React.ReactElement => {
|
||||
const lang = useContext(LangContext);
|
||||
const chatPage = strings[lang].chat;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useContext } from "react";
|
||||
import { Message, MessageType } from "./types";
|
||||
import { Message, MessageType } from "./messageTypes";
|
||||
import { LangContext } from "../context";
|
||||
import strings from "../Intl/strings.json";
|
||||
export const MessageContainer = ({
|
||||
|
@ -41,7 +41,7 @@ export const MessageContainer = ({
|
|||
);
|
||||
case MessageType.DATA as MessageType:
|
||||
return <></>;
|
||||
case MessageType.SYSTEM as MessageType:
|
||||
case MessageType.CHNAME as MessageType:
|
||||
return <></>;
|
||||
default:
|
||||
console.error("Illegal MessageType reported!");
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export const enum MessageType {
|
||||
MESSAGE = "MESSAGE",
|
||||
SYSTEM = "SYSTEM",
|
||||
CHNAME = "CHNAME",
|
||||
HELLO = "HELLO",
|
||||
DATA = "DATA",
|
||||
}
|
||||
|
@ -42,5 +42,10 @@ export type Message = {
|
|||
content: string;
|
||||
timeMillis: number;
|
||||
};
|
||||
export const acceptedLangs = ["en_US", "zh_TW", "el_GR"] as const;
|
||||
export const acceptedLangs = [
|
||||
"en_US",
|
||||
"zh_TW",
|
||||
"el_GR",
|
||||
"ar_SA"
|
||||
] as const;
|
||||
export type LangType = (typeof acceptedLangs)[number];
|
7
src/Chat/userTypes.tsx
Normal file
7
src/Chat/userTypes.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
export type User = {
|
||||
id: number;
|
||||
userName: string;
|
||||
dateJoined: number;
|
||||
lastSeen: number;
|
||||
passwordHash: string;
|
||||
};
|
76
src/Login/Login.tsx
Normal file
76
src/Login/Login.tsx
Normal file
|
@ -0,0 +1,76 @@
|
|||
import { useState } from "react";
|
||||
import { contentTypes, domain, endpoints, port } from "../consts";
|
||||
import { LoginType } from "../context";
|
||||
import { User } from "../Chat/userTypes";
|
||||
const encrypt = (rawPasswordString: string) => {
|
||||
// TODO Encryption method stub
|
||||
return rawPasswordString;
|
||||
};
|
||||
const Login = ({
|
||||
setLogin,
|
||||
}: {
|
||||
setLogin: (newLogin: LoginType) => void;
|
||||
}): React.ReactElement => {
|
||||
const [valid, setValid] = useState(true);
|
||||
const registrationHandler = () => {
|
||||
const uname = (document.getElementById("username") as HTMLInputElement)
|
||||
.value;
|
||||
const passwd = encrypt(
|
||||
(document.getElementById("passwd") as HTMLInputElement).value
|
||||
);
|
||||
fetch(`http://${domain}:${port}${endpoints.user}`, {
|
||||
method: "POST",
|
||||
headers: contentTypes.json,
|
||||
body: JSON.stringify({
|
||||
userName: uname,
|
||||
dateJoined: Date.now(),
|
||||
passwordHash: passwd,
|
||||
}),
|
||||
}).then((response) => {
|
||||
if (response.status === 400) {
|
||||
console.log("Username is taken or invalid!");
|
||||
} else {
|
||||
const futureDate = new Date();
|
||||
futureDate.setHours(futureDate.getHours() + 2);
|
||||
setLogin({
|
||||
username: uname,
|
||||
lastSeen: Date.now(),
|
||||
validUntil: futureDate.getUTCMilliseconds(),
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
const loginHandler = () => {
|
||||
const uname = (document.getElementById("username") as HTMLInputElement)
|
||||
.value;
|
||||
const passwd = encrypt(
|
||||
(document.getElementById("passwd") as HTMLInputElement).value
|
||||
);
|
||||
fetch(`http://${domain}:${port}${endpoints.user}?user=${uname}`, {
|
||||
method: "GET",
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((userObject) => {
|
||||
const user = userObject as User;
|
||||
const validLogin = passwd === user.passwordHash;
|
||||
if (!validLogin) {
|
||||
}
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<fieldset>
|
||||
<legend>Login window</legend>
|
||||
<p className="uname-error-text">
|
||||
{valid ? "Error in your username or password" : ""}
|
||||
</p>
|
||||
<label htmlFor="username">Username: </label>
|
||||
<input id="username" type="text"></input>
|
||||
<label htmlFor="passwd">Password: </label>
|
||||
<input id="passwd" type="password"></input>
|
||||
<button type="submit">Login</button>
|
||||
<button type="submit">Register</button>
|
||||
</fieldset>
|
||||
</div>
|
||||
);
|
||||
};
|
14
src/consts.ts
Normal file
14
src/consts.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
export const domain = window.location.hostname;
|
||||
export const port = "8080";
|
||||
export const connectionAddress = `ws://${domain}:${port}/ws`;
|
||||
export const endpoints = {
|
||||
destination: "/app/chat",
|
||||
subscription: "/sub/chat",
|
||||
history: "/api/v1/msg/",
|
||||
user: "/api/v1/user",
|
||||
};
|
||||
export const contentTypes = {
|
||||
json: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
};
|
|
@ -1,4 +1,9 @@
|
|||
import { createContext } from "react";
|
||||
import { LangType } from "./Chat/types";
|
||||
|
||||
export const LangContext = createContext<LangType>("en_US");
|
||||
import { LangType } from "./Chat/messageTypes";
|
||||
export type LoginType = {
|
||||
username: string;
|
||||
lastSeen: number;
|
||||
validUntil: number;
|
||||
};
|
||||
export const LangContext = createContext<LangType>("en_US");
|
||||
export const LoginContext = createContext<LoginType | undefined>(undefined);
|
||||
|
|
|
@ -1,44 +1,47 @@
|
|||
import { Client } from "@stomp/stompjs";
|
||||
import { Message, MessageType } from "../Chat/types";
|
||||
const domain = window.location.hostname
|
||||
const port = "8080"
|
||||
const connectionAddress = `ws://${domain}:${port}/ws`
|
||||
import { Message, MessageType } from "../Chat/messageTypes";
|
||||
const domain = window.location.hostname;
|
||||
const port = "8080";
|
||||
const connectionAddress = `ws://${domain}:${port}/ws`;
|
||||
const endpoints = {
|
||||
destination: "/app/chat",
|
||||
subscription: "/sub/chat",
|
||||
history: "/api/v1/msg/"
|
||||
}
|
||||
export const createStompConnection = (user: string, subUpdateHandler: (message: Message) => void) => {
|
||||
const stompClient = new Client({
|
||||
brokerURL: connectionAddress
|
||||
})
|
||||
stompClient.onConnect = (frame) => {
|
||||
stompClient.subscribe(endpoints.subscription, (message) => {
|
||||
console.log(`Collected new message: ${message.body}`);
|
||||
const messageBody = JSON.parse(message.body) as Message
|
||||
console.log(messageBody);
|
||||
subUpdateHandler(messageBody);
|
||||
});
|
||||
stompClient.publish({
|
||||
body: JSON.stringify({
|
||||
type: MessageType.HELLO,
|
||||
fromUserId: user,
|
||||
toUserId: "everyone",
|
||||
content: `${user} has joined the server!`,
|
||||
timeMillis: Date.now()
|
||||
}),
|
||||
destination: endpoints.destination
|
||||
})
|
||||
}
|
||||
destination: "/app/chat",
|
||||
subscription: "/sub/chat",
|
||||
history: "/api/v1/msg/",
|
||||
};
|
||||
export const createStompConnection = (
|
||||
user: string,
|
||||
subUpdateHandler: (message: Message) => void
|
||||
) => {
|
||||
const stompClient = new Client({
|
||||
brokerURL: connectionAddress,
|
||||
});
|
||||
stompClient.onConnect = (frame) => {
|
||||
stompClient.subscribe(endpoints.subscription, (message) => {
|
||||
console.log(`Collected new message: ${message.body}`);
|
||||
const messageBody = JSON.parse(message.body) as Message;
|
||||
console.log(messageBody);
|
||||
subUpdateHandler(messageBody);
|
||||
});
|
||||
stompClient.publish({
|
||||
body: JSON.stringify({
|
||||
type: MessageType.HELLO,
|
||||
fromUserId: user,
|
||||
toUserId: "everyone",
|
||||
content: `${user} has joined the server!`,
|
||||
timeMillis: Date.now(),
|
||||
}),
|
||||
destination: endpoints.destination,
|
||||
});
|
||||
};
|
||||
|
||||
// Generic error handlers
|
||||
stompClient.onWebSocketError = (error) => {
|
||||
console.error('Error with websocket', error);
|
||||
};
|
||||
|
||||
stompClient.onStompError = (frame) => {
|
||||
console.error('Broker reported error: ' + frame.headers['message']);
|
||||
console.error('Additional details: ' + frame.body);
|
||||
};
|
||||
return stompClient;
|
||||
}
|
||||
// Generic error handlers
|
||||
stompClient.onWebSocketError = (error) => {
|
||||
console.error("Error with websocket", error);
|
||||
};
|
||||
|
||||
stompClient.onStompError = (frame) => {
|
||||
console.error("Broker reported error: " + frame.headers["message"]);
|
||||
console.error("Additional details: " + frame.body);
|
||||
};
|
||||
return stompClient;
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue