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:
Zhongheng Liu 2024-01-17 23:05:19 +02:00
commit 5ce5f9e4eb
No known key found for this signature in database
11 changed files with 303 additions and 79 deletions

View file

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

View file

@ -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`.

View file

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

View file

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

View file

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

View file

@ -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
View 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
View 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
View 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",
},
};

View file

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

View file

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