Compare commits
13 commits
Author | SHA1 | Date | |
---|---|---|---|
a38c09df73 |
|||
3fb94984ec |
|||
3ceee1db4f |
|||
eb76e7203b |
|||
e205d6e149 |
|||
3957f49e49 |
|||
513e8d6841 |
|||
4117c11c35 |
|||
7e5f5bdf77 |
|||
47b0925db9 |
|||
4df2355001 |
|||
56feab2ea1 |
|||
7d4953fea6 |
33 changed files with 18445 additions and 18133 deletions
10
TODO.md
Normal file
10
TODO.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
# TODOs for the project
|
||||
## UI/UX
|
||||
1. Beautify interface
|
||||
2. Implement avatar support
|
||||
## Cybersecurity
|
||||
1. Implement SSH-based encryption/decryption in transmission
|
||||
Client 1 => encrypt with pubcert_s => Server => decrypt with privcert_s => encrypt with pubcert_2 => Client 2 => decrypt with privcert2
|
||||
plaintext => ciphertext => ciphertext => plaintext => ciphertext => ciphertext => plaintext
|
||||
Clients need to have server's pubcert,
|
||||
Server needs to contain pubcerts of each client,
|
35443
package-lock.json
generated
35443
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "epq-web-project",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@stomp/stompjs": "^7.0.0",
|
||||
|
@ -11,6 +11,7 @@
|
|||
"@types/node": "^16.18.68",
|
||||
"@types/react": "^18.2.45",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"ed25519": "^0.0.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "5.0.1",
|
||||
|
@ -41,5 +42,8 @@
|
|||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ed25519": "^0.0.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,9 +29,6 @@
|
|||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<div id="copyright">
|
||||
<pre>Copyright 2024-2025 Zhongheng Liu @ Byron College</pre>
|
||||
</div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
|
10
src/App.css
10
src/App.css
|
@ -1,6 +1,6 @@
|
|||
body {
|
||||
background-color: black;
|
||||
color: #00FF33;
|
||||
margin: 1%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
/* background-color: black;
|
||||
color: #00FF33; */
|
||||
margin: 1%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
|
50
src/App.tsx
50
src/App.tsx
|
@ -1,12 +1,14 @@
|
|||
import React, { useContext, useEffect, useState } from "react";
|
||||
import Chat from "./Chat/Chat";
|
||||
import "./App.css";
|
||||
import { LangType, Message } from "./Chat/messageTypes";
|
||||
import { MessageDisplay } from "./Chat/MessageDisplay";
|
||||
import { LangType, Message } from "./type/messageTypes";
|
||||
import { MessageDisplay } from "./MessageDisplay/MessageDisplay";
|
||||
import strings from "./Intl/strings.json";
|
||||
import { LangContext, LoginContext, LoginType } from "./context";
|
||||
import { contentTypes, domain, endpoints, port } from "./consts";
|
||||
import { Login } from "./Login/Login";
|
||||
import { Sidebar } from "./Sidebar/Sidebar";
|
||||
import { Topbar } from "./Topbar/Topbar";
|
||||
// what we "in the business" call type gymnastics
|
||||
const Wrapper = (): React.ReactElement => {
|
||||
const [lang, setLang] = useState<LangType>("en_US");
|
||||
|
@ -16,11 +18,19 @@ const Wrapper = (): React.ReactElement => {
|
|||
? `IRC logged in as ${login.username}`
|
||||
: "IRC Chat";
|
||||
}, [login]);
|
||||
const [sidebarEnabled, setSidebarEnabled] = useState(false);
|
||||
return (
|
||||
<LangContext.Provider value={lang}>
|
||||
<h1>{strings[lang].homepage.title}</h1>
|
||||
<p>{strings[lang].homepage.description}</p>
|
||||
<LoginContext.Provider value={login}>
|
||||
<Topbar
|
||||
setSidebarEnable={(enabled: boolean) =>
|
||||
setSidebarEnabled(enabled)
|
||||
}
|
||||
></Topbar>
|
||||
<Sidebar
|
||||
isEnabled={sidebarEnabled}
|
||||
setEnable={(enabled: boolean) => setSidebarEnabled(enabled)}
|
||||
></Sidebar>
|
||||
{/* callbacks for altering the Lang/Login contexts */}
|
||||
<Login
|
||||
setLogin={(value) => {
|
||||
|
@ -42,7 +52,7 @@ const Wrapper = (): React.ReactElement => {
|
|||
};
|
||||
const setNameOnServer = async (name: string) => {
|
||||
const responseRaw = await fetch(
|
||||
`http://${domain}:${port}${endpoints.user}`,
|
||||
`https://${domain}:${port}${endpoints.user}`,
|
||||
{
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
|
@ -67,38 +77,10 @@ const App = ({
|
|||
}: {
|
||||
changeLang: (value: string) => void;
|
||||
}): React.ReactElement => {
|
||||
const [username, setUsername] = useState<string>();
|
||||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const login = useContext(LoginContext);
|
||||
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) {
|
||||
// var newName = prompt(home.userNamePrompt) as string;
|
||||
// while (!validateName(newName)) {
|
||||
// console.log(newName);
|
||||
|
||||
// prompt("Username invalid! Please enter again.") as string;
|
||||
// }
|
||||
// setUsername(newName);
|
||||
// }
|
||||
if (!login) {
|
||||
return <></>;
|
||||
} else
|
||||
|
@ -125,7 +107,7 @@ const App = ({
|
|||
})
|
||||
.then((responseBody: { success: boolean }) => {
|
||||
if (responseBody.success) {
|
||||
setUsername(newUsername as string);
|
||||
// TODO Put new username response true handler method stub
|
||||
} else {
|
||||
console.error(
|
||||
"Server POST message failed."
|
||||
|
|
|
@ -1,15 +1,25 @@
|
|||
.chat-inner-wrapper {
|
||||
height: 50vh;
|
||||
overflow-y:scroll;
|
||||
/* overflow-wrap: normal; */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 50vh;
|
||||
overflow-y: scroll;
|
||||
/* overflow-wrap: normal; */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
}
|
||||
|
||||
.entry-box {
|
||||
margin-top: 5px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.chat {
|
||||
/* min-height: 80vh; */
|
||||
position: relative;
|
||||
}
|
||||
/* float: left; */
|
||||
/* min-height: 80vh; */
|
||||
position: relative;
|
||||
box-shadow:
|
||||
0 2.8px 2.2px rgba(0, 0, 0, 0.034),
|
||||
0 6.7px 5.3px rgba(0, 0, 0, 0.048),
|
||||
0 12.5px 10px rgba(0, 0, 0, 0.06),
|
||||
0 22.3px 17.9px rgba(0, 0, 0, 0.072),
|
||||
0 41.8px 33.4px rgba(0, 0, 0, 0.086),
|
||||
0 100px 80px rgba(0, 0, 0, 0.12)
|
||||
}
|
||||
|
|
|
@ -1,129 +1,143 @@
|
|||
import React, {
|
||||
ReactElement,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
ReactElement,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { MessageDisplay } from "./MessageDisplay";
|
||||
import { MessageDisplay } from "../MessageDisplay/MessageDisplay";
|
||||
import { Client } from "@stomp/stompjs";
|
||||
import { Message, MessageType } from "./messageTypes";
|
||||
import { Message, MessageType } from "../type/messageTypes";
|
||||
import "./Chat.css";
|
||||
import strings from "../Intl/strings.json";
|
||||
import { LangContext } from "../context";
|
||||
import { connectionAddress, endpoints } from "../consts";
|
||||
const Chat = ({ user }: { user: string }): React.ReactElement => {
|
||||
const lang = useContext(LangContext);
|
||||
const chatPage = strings[lang].chat;
|
||||
const [messages, setMessages] = useState<ReactElement[]>([]);
|
||||
let stompClientRef = useRef(
|
||||
new Client({
|
||||
brokerURL: connectionAddress,
|
||||
})
|
||||
);
|
||||
// TODO solve issue with non-static markup
|
||||
stompClientRef.current.onConnect = (frame) => {
|
||||
stompClientRef.current.subscribe(endpoints.subscription, (message) => {
|
||||
console.log(`Collected new message: ${message.body}`);
|
||||
const messageBody = JSON.parse(message.body) as Message;
|
||||
console.log(messageBody);
|
||||
setMessages((message) => {
|
||||
return message.concat([
|
||||
<MessageDisplay
|
||||
key={`${messageBody.type}@${messageBody.timeMillis}`}
|
||||
{...messageBody}
|
||||
/>,
|
||||
]);
|
||||
});
|
||||
});
|
||||
stompClientRef.current.publish({
|
||||
body: JSON.stringify({
|
||||
type: MessageType.HELLO,
|
||||
fromUserId: user,
|
||||
toUserId: "everyone",
|
||||
content: `${user} has joined the server!`,
|
||||
timeMillis: Date.now(),
|
||||
}),
|
||||
destination: endpoints.destination,
|
||||
});
|
||||
};
|
||||
const lang = useContext(LangContext);
|
||||
const chatPage = strings[lang].chat;
|
||||
const [messages, setMessages] = useState<ReactElement[]>([]);
|
||||
let stompClientRef = useRef(
|
||||
new Client({
|
||||
brokerURL: connectionAddress,
|
||||
})
|
||||
);
|
||||
// TODO solve issue with non-static markup
|
||||
stompClientRef.current.onConnect = (frame) => {
|
||||
stompClientRef.current.subscribe(
|
||||
endpoints.subscription,
|
||||
(message) => {
|
||||
console.log(
|
||||
`Collected new message: ${message.body}`
|
||||
);
|
||||
const messageBody = JSON.parse(
|
||||
message.body
|
||||
) as Message;
|
||||
console.log(messageBody);
|
||||
setMessages((message) => {
|
||||
return message.concat([
|
||||
<MessageDisplay
|
||||
key={`${messageBody.type}@${messageBody.timeMillis}`}
|
||||
{...messageBody}
|
||||
/>,
|
||||
]);
|
||||
});
|
||||
}
|
||||
);
|
||||
stompClientRef.current.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
|
||||
stompClientRef.current.onWebSocketError = (error) => {
|
||||
console.error("Error with websocket", error);
|
||||
};
|
||||
|
||||
// Generic error handlers
|
||||
stompClientRef.current.onWebSocketError = (error) => {
|
||||
console.error("Error with websocket", error);
|
||||
};
|
||||
stompClientRef.current.onStompError = (frame) => {
|
||||
console.error(
|
||||
"Broker reported error: " + frame.headers["message"]
|
||||
);
|
||||
console.error("Additional details: " + frame.body);
|
||||
};
|
||||
|
||||
stompClientRef.current.onStompError = (frame) => {
|
||||
console.error("Broker reported error: " + frame.headers["message"]);
|
||||
console.error("Additional details: " + frame.body);
|
||||
};
|
||||
|
||||
// Button press event handler.
|
||||
const sendData = () => {
|
||||
const entryElement: HTMLInputElement = document.getElementById(
|
||||
"data-entry"
|
||||
) as HTMLInputElement;
|
||||
if (entryElement.value === "") {
|
||||
return;
|
||||
}
|
||||
const messageData: Message = {
|
||||
type: MessageType.MESSAGE,
|
||||
fromUserId: user,
|
||||
toUserId: "everyone",
|
||||
content: entryElement.value,
|
||||
timeMillis: Date.now(),
|
||||
};
|
||||
console.log(
|
||||
`STOMP connection status: ${stompClientRef.current.connected}`
|
||||
);
|
||||
stompClientRef.current.publish({
|
||||
body: JSON.stringify(messageData),
|
||||
destination: endpoints.destination,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
entryElement.value = "";
|
||||
};
|
||||
useEffect(() => {
|
||||
// Stomp client is disconnected after each re-render
|
||||
// This should be actively avoided
|
||||
stompClientRef.current.activate();
|
||||
return () => {
|
||||
stompClientRef.current.deactivate();
|
||||
};
|
||||
}, []);
|
||||
// https://www.w3schools.com/jsref/obj_keyboardevent.asp
|
||||
document.addEventListener("keydown", (ev: KeyboardEvent) => {
|
||||
if (ev.key === "Enter") {
|
||||
sendData();
|
||||
}
|
||||
});
|
||||
useEffect(() => {
|
||||
try {
|
||||
const elem = document.querySelector(".chat-inner-wrapper");
|
||||
if (elem) {
|
||||
elem.scrollTop = elem.scrollHeight;
|
||||
} else {
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("error encountered");
|
||||
}
|
||||
return () => {};
|
||||
}, [messages]);
|
||||
return (
|
||||
<fieldset className="chat">
|
||||
<legend>
|
||||
Logged in as <b>{user}</b>
|
||||
</legend>
|
||||
<div className="chat-inner-wrapper">{messages}</div>
|
||||
<span className="entry-box">
|
||||
<input id="data-entry"></input>
|
||||
<button onClick={() => sendData()}>
|
||||
{chatPage.sendButtonPrompt}
|
||||
</button>
|
||||
</span>
|
||||
</fieldset>
|
||||
);
|
||||
// Button press event handler.
|
||||
const sendData = () => {
|
||||
const entryElement: HTMLInputElement = document.getElementById(
|
||||
"data-entry"
|
||||
) as HTMLInputElement;
|
||||
if (entryElement.value === "") {
|
||||
return;
|
||||
}
|
||||
const messageData: Message = {
|
||||
type: MessageType.MESSAGE,
|
||||
fromUserId: user,
|
||||
toUserId: "everyone",
|
||||
content: entryElement.value,
|
||||
timeMillis: Date.now(),
|
||||
};
|
||||
console.log(
|
||||
`STOMP connection status: ${stompClientRef.current.connected}`
|
||||
);
|
||||
stompClientRef.current.publish({
|
||||
body: JSON.stringify(messageData),
|
||||
destination: endpoints.destination,
|
||||
headers: {
|
||||
"Content-Type":
|
||||
"application/json; charset=utf-8",
|
||||
},
|
||||
});
|
||||
entryElement.value = "";
|
||||
};
|
||||
useEffect(() => {
|
||||
// Stomp client is disconnected after each re-render
|
||||
// This should be actively avoided
|
||||
stompClientRef.current.activate();
|
||||
return () => {
|
||||
stompClientRef.current.deactivate();
|
||||
};
|
||||
}, []);
|
||||
// https://www.w3schools.com/jsref/obj_keyboardevent.asp
|
||||
document.addEventListener("keydown", (ev: KeyboardEvent) => {
|
||||
if (ev.key === "Enter") {
|
||||
sendData();
|
||||
}
|
||||
});
|
||||
useEffect(() => {
|
||||
try {
|
||||
const elem = document.querySelector(
|
||||
".chat-inner-wrapper"
|
||||
);
|
||||
if (elem) {
|
||||
elem.scrollTop = elem.scrollHeight;
|
||||
} else {
|
||||
}
|
||||
} catch (err) {
|
||||
console.log("error encountered");
|
||||
}
|
||||
return () => {};
|
||||
}, [messages]);
|
||||
return (
|
||||
<fieldset className="chat">
|
||||
<legend>
|
||||
{chatPage.window.title.replaceAll(
|
||||
"$userName",
|
||||
user
|
||||
)}
|
||||
</legend>
|
||||
<div className="chat-inner-wrapper">{messages}</div>
|
||||
<span className="entry-box">
|
||||
<input id="data-entry"></input>
|
||||
<button onClick={() => sendData()}>
|
||||
{chatPage.sendButtonPrompt}
|
||||
</button>
|
||||
</span>
|
||||
</fieldset>
|
||||
);
|
||||
};
|
||||
export default Chat;
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
.msg .msg-err {
|
||||
overflow-wrap: break-word;
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
export const enum MessageType {
|
||||
MESSAGE = "MESSAGE",
|
||||
CHNAME = "CHNAME",
|
||||
HELLO = "HELLO",
|
||||
DATA = "DATA",
|
||||
}
|
||||
export enum SystemMessageCode {
|
||||
REQ,
|
||||
RES,
|
||||
ERR,
|
||||
}
|
||||
export type HistoryFetchResult = {
|
||||
count: number;
|
||||
items: Array<ChatMessage>;
|
||||
};
|
||||
export type ErrorResult = {
|
||||
text: string;
|
||||
};
|
||||
export type TimestampSendRequest = {
|
||||
ts: number;
|
||||
};
|
||||
export type SystemMessage = {
|
||||
code: SystemMessageCode;
|
||||
data: HistoryFetchResult | ErrorResult | TimestampSendRequest;
|
||||
};
|
||||
export type ChatMessage = {
|
||||
fromUserId: string;
|
||||
toUserId: string;
|
||||
content: string;
|
||||
timeMillis: number;
|
||||
};
|
||||
export type HelloMessage = {
|
||||
fromUserId: string;
|
||||
timeMillis: number;
|
||||
};
|
||||
export type DataMessage = {};
|
||||
export type Message = {
|
||||
type: MessageType;
|
||||
// data: SystemMessage | ChatMessage | HelloMessage
|
||||
fromUserId: string;
|
||||
toUserId: string;
|
||||
content: string;
|
||||
timeMillis: number;
|
||||
};
|
||||
export const acceptedLangs = [
|
||||
"en_US",
|
||||
"zh_TW",
|
||||
"el_GR",
|
||||
"ar_SA"
|
||||
] as const;
|
||||
export type LangType = (typeof acceptedLangs)[number];
|
|
@ -1,7 +0,0 @@
|
|||
export type User = {
|
||||
id: number;
|
||||
userName: string;
|
||||
dateJoined: number;
|
||||
lastSeen: number;
|
||||
passwordHash: string;
|
||||
};
|
|
@ -1,64 +1,136 @@
|
|||
{
|
||||
"variableNames": ["userName", "content"],
|
||||
"acceptedLangs": ["en_US", "zh_TW", "el_GR"],
|
||||
"en_US": {
|
||||
"homepage": {
|
||||
"userNamePrompt": "Your username: ",
|
||||
"title": "LAN Chat Server",
|
||||
"description": "This web application was built for the purposes of an EPQ project.",
|
||||
"copyrightText": "Copyright 2024 - 2025 Zhongheng Liu @ Byron College",
|
||||
"switchLang": "Switch Language",
|
||||
"newLangPrompt": "Input your ISO-639_ISO-3166 language-contry code below - for example: \"en_US\": "
|
||||
},
|
||||
"chat": {
|
||||
"sendButtonPrompt": "Send",
|
||||
"serverMessage": "Message from $userName: $content",
|
||||
"joinMessage": "$userName joined the server"
|
||||
}
|
||||
},
|
||||
"zh_TW": {
|
||||
"homepage": {
|
||||
"userNamePrompt": "您的用戶名: ",
|
||||
"title": "本地聊天伺服器",
|
||||
"description": "該網絡伺服器應用程式專爲Edexcel Level 3 EPQ而製作",
|
||||
"copyrightText": "版權所有 2024 - 2025 Zhongheng Liu @ Byron College",
|
||||
"switchLang": "切換語言",
|
||||
"newLangPrompt": "在下方輸入您想使用的語言的ISO-639_ISO-3166組合語言代碼 - 例如:\"en_US\":"
|
||||
},
|
||||
"chat": {
|
||||
"sendButtonPrompt": "發送",
|
||||
"serverMessage": "來自 $userName 的訊息:$content",
|
||||
"joinMessage": "$userName 加入了伺服器!"
|
||||
}
|
||||
},
|
||||
"el_GR": {
|
||||
"homepage": {
|
||||
"userNamePrompt": "το όνομα χρήστη σας: ",
|
||||
"title": "Διακομιστής τοπικού δικτύου συνομιλίας",
|
||||
"description": "Αυτή η διαδικτυακή εφαρμογή δημιουργήθηκε για τους σκοπούς ενός έργου EPQ.",
|
||||
"copyrightText": "Πνευματικά δικαιώματα 2024 - 2025 Zhongheng Liu @ Byron College",
|
||||
"switchLang": "Αλλαγή γλώσσας",
|
||||
"newLangPrompt": "Εισαγάγετε τον κωδικό γλώσσας-χώρας ISO-639 ISO-3166 παρακάτω - για παράδειγμα: \"en_US\":"
|
||||
},
|
||||
"chat": {
|
||||
"sendButtonPrompt": "Στείλετε",
|
||||
"joinMessage": "$userName έγινε μέλος του διακομιστή",
|
||||
"serverMessage": "μήνυμα από $userName: $content"
|
||||
}
|
||||
},
|
||||
"ar_SA": {
|
||||
"homepage": {
|
||||
"userNamePrompt": "اسم المستخدم الخاص بك:",
|
||||
"title": "خادم الدردشة LAN",
|
||||
"description": "تم إنشاء تطبيق الويب هذا لأغراض مشروع EPQ.",
|
||||
"copyrightText": "حقوق الطبع والنشر 2024 - 2025 Zhongheng Liu @ كلية بايرون",
|
||||
"switchLang": "تبديل اللغة",
|
||||
"newLangPrompt": "أدخل رمز اللغة ISO-639_ISO-3166 أدناه - على سبيل المثال: \"en_US\":"
|
||||
},
|
||||
"chat": {
|
||||
"sendButtonPrompt": "يرسل",
|
||||
"joinMessage": "$userName انضم إلى الخادم",
|
||||
"serverMessage": "رسالة من $userName: $content"
|
||||
}
|
||||
}
|
||||
"variableNames": ["userName", "content"],
|
||||
"acceptedLangs": ["en_US", "zh_TW", "el_GR", "ar_SA"],
|
||||
"en_US": {
|
||||
"homepage": {
|
||||
"userNamePrompt": "Your username: ",
|
||||
"title": "LAN Chat Server",
|
||||
"description": "This web application was built for the purposes of an EPQ project.",
|
||||
"copyrightText": "Copyright 2024 - 2025 Zhongheng Liu @ Byron College",
|
||||
"switchLang": "Switch Language",
|
||||
"newLangPrompt": "Input your ISO-639_ISO-3166 language-contry code below - for example: \"en_US\": "
|
||||
},
|
||||
"chat": {
|
||||
"window": {
|
||||
"title": "Logged in as $userName"
|
||||
},
|
||||
"sendButtonPrompt": "Send",
|
||||
"serverMessage": "Message from $userName: $content",
|
||||
"joinMessage": "$userName joined the server"
|
||||
},
|
||||
"login": {
|
||||
"error": {
|
||||
"unameTakenOrInvalid": "Username is taken or invalid!",
|
||||
"unameNotExists": "Username does not exist!",
|
||||
"passwdInvalid": "Password incorrect!"
|
||||
},
|
||||
"window": {
|
||||
"title": "Login window",
|
||||
"uname": "Username: ",
|
||||
"login": "Login",
|
||||
"passwd": "Password: ",
|
||||
"register": "Register",
|
||||
"logout": "Logout"
|
||||
}
|
||||
}
|
||||
},
|
||||
"zh_TW": {
|
||||
"homepage": {
|
||||
"userNamePrompt": "您的用戶名: ",
|
||||
"title": "本地聊天伺服器",
|
||||
"description": "該網絡伺服器應用程式專爲Edexcel Level 3 EPQ而製作",
|
||||
"copyrightText": "版權所有 2024 - 2025 Zhongheng Liu @ Byron College",
|
||||
"switchLang": "切換語言",
|
||||
"newLangPrompt": "在下方輸入您想使用的語言的ISO-639_ISO-3166組合語言代碼 - 例如:\"en_US\":"
|
||||
},
|
||||
"chat": {
|
||||
"window": {
|
||||
"title": "當前以 $userName 登入"
|
||||
},
|
||||
"sendButtonPrompt": "發送",
|
||||
"serverMessage": "來自 $userName 的訊息:$content",
|
||||
"joinMessage": "$userName 加入了伺服器!"
|
||||
},
|
||||
"login": {
|
||||
"error": {
|
||||
"unameTakenOrInvalid": "用戶名已存在或不合規!",
|
||||
"unameNotExists": "該用戶不存在!",
|
||||
"passwdInvalid": "密碼不正確!"
|
||||
},
|
||||
"window": {
|
||||
"title": "登入窗口",
|
||||
"uname": "用戶名:",
|
||||
"login": "登入",
|
||||
"passwd": "密碼:",
|
||||
"register": "註冊",
|
||||
"logout": "登出"
|
||||
}
|
||||
}
|
||||
},
|
||||
"el_GR": {
|
||||
"homepage": {
|
||||
"userNamePrompt": "το όνομα χρήστη σας: ",
|
||||
"title": "Διακομιστής τοπικού δικτύου συνομιλίας",
|
||||
"description": "Αυτή η διαδικτυακή εφαρμογή δημιουργήθηκε για τους σκοπούς ενός έργου EPQ.",
|
||||
"copyrightText": "Πνευματικά δικαιώματα 2024 - 2025 Zhongheng Liu @ Byron College",
|
||||
"switchLang": "Αλλαγή γλώσσας",
|
||||
"newLangPrompt": "Εισαγάγετε τον κωδικό γλώσσας-χώρας ISO-639 ISO-3166 παρακάτω - για παράδειγμα: \"en_US\":"
|
||||
},
|
||||
"chat": {
|
||||
"window": {
|
||||
"title": "Συνδεδεμένος ως $userName"
|
||||
},
|
||||
"sendButtonPrompt": "Στείλετε",
|
||||
"joinMessage": "$userName έγινε μέλος του διακομιστή",
|
||||
"serverMessage": "μήνυμα από $userName: $content"
|
||||
},
|
||||
"login": {
|
||||
"error": {
|
||||
"unameTakenOrInvalid": "Το όνομα χρήστη έχει ληφθεί ή δεν είναι έγκυρο!",
|
||||
"unameNotExists": "Το όνομα χρήστη δεν υπάρχει!",
|
||||
"passwdInvalid": "Λάθος κωδικός!"
|
||||
},
|
||||
"window": {
|
||||
"title": "Παράθυρο σύνδεσης",
|
||||
"uname": "Όνομα χρήστη: ",
|
||||
"login": "Σύνδεση",
|
||||
"passwd": "Κωδικός πρόσβασης: ",
|
||||
"register": "Εγγραφή",
|
||||
"logout": "Αποσύνδεση"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ar_SA": {
|
||||
"homepage": {
|
||||
"userNamePrompt": "اسم المستخدم الخاص بك:",
|
||||
"title": "خادم الدردشة LAN",
|
||||
"description": "تم إنشاء تطبيق الويب هذا لأغراض مشروع EPQ.",
|
||||
"copyrightText": "حقوق الطبع والنشر 2024 - 2025 Zhongheng Liu @ كلية بايرون",
|
||||
"switchLang": "تبديل اللغة",
|
||||
"newLangPrompt": "أدخل رمز اللغة ISO-639_ISO-3166 أدناه - على سبيل المثال: \"en_US\":"
|
||||
},
|
||||
"chat": {
|
||||
"window": {
|
||||
"title": "Logged in as $userName"
|
||||
},
|
||||
"sendButtonPrompt": "يرسل",
|
||||
"joinMessage": "$userName انضم إلى الخادم",
|
||||
"serverMessage": "رسالة من $userName: $content"
|
||||
},
|
||||
"login": {
|
||||
"error": {
|
||||
"unameTakenOrInvalid": "Username is taken or invalid!",
|
||||
"unameNotExists": "Username does not exist!",
|
||||
"passwdInvalid": "Password incorrect!"
|
||||
},
|
||||
"window": {
|
||||
"title": "Login window",
|
||||
"uname": "Username: ",
|
||||
"login": "Login",
|
||||
"passwd": "Password: ",
|
||||
"register": "Register",
|
||||
"logout": "Logout"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
.uname-error-text {
|
||||
color: red;
|
||||
}
|
||||
color: red;
|
||||
}
|
||||
.login {}
|
|
@ -1,8 +1,11 @@
|
|||
import { useState } from "react";
|
||||
import { useContext, useState } from "react";
|
||||
import { contentTypes, domain, endpoints, port } from "../consts";
|
||||
import { LoginType } from "../context";
|
||||
import { User } from "../Chat/userTypes";
|
||||
import { LangContext, LoginType } from "../context";
|
||||
import { AuthData, User } from "../type/userTypes";
|
||||
import "./Login.css";
|
||||
import strings from "../Intl/strings.json";
|
||||
import { ECDH } from "crypto";
|
||||
import { digestMessage } from "../crypto";
|
||||
const encrypt = (rawPasswordString: string) => {
|
||||
// TODO Encryption method stub
|
||||
return rawPasswordString;
|
||||
|
@ -12,128 +15,142 @@ export const Login = ({
|
|||
}: {
|
||||
setLogin: (newLogin: LoginType | undefined) => void;
|
||||
}): React.ReactElement => {
|
||||
const [valid, setValid] = useState<boolean | undefined>(true);
|
||||
const [valid, setValid] = useState<boolean | undefined>(undefined);
|
||||
const [validText, setValidText] = useState<string | undefined>();
|
||||
const lang = useContext(LangContext);
|
||||
const loginPage = strings[lang].login;
|
||||
// TODO mk unit test
|
||||
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",
|
||||
mode: "cors",
|
||||
headers: contentTypes.json,
|
||||
body: JSON.stringify({
|
||||
userName: uname,
|
||||
dateJoined: Date.now(),
|
||||
passwordHash: passwd,
|
||||
}),
|
||||
}).then((response) => {
|
||||
if (response.status === 400) {
|
||||
// 400 Bad request
|
||||
console.log("Username is taken or invalid!");
|
||||
setValid(false);
|
||||
setValidText("Username is taken or invalid!");
|
||||
} else if (response.status === 200) {
|
||||
// 200 OK
|
||||
const futureDate = new Date();
|
||||
futureDate.setHours(futureDate.getHours() + 2);
|
||||
setLogin({
|
||||
username: uname,
|
||||
lastSeen: Date.now(),
|
||||
validUntil: futureDate.getUTCMilliseconds(),
|
||||
digestMessage((document.getElementById("passwd") as HTMLInputElement).value).then((passwd) => {
|
||||
fetch(`https://${domain}:${port}${endpoints.register}`, {
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
headers: contentTypes.json,
|
||||
body: JSON.stringify({
|
||||
userName: uname,
|
||||
newUserPassword: passwd,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json()).then((body) => {
|
||||
const response = body as AuthData;
|
||||
if (response.exists && !response.success) {throw new Error(loginPage.error.unameTakenOrInvalid)}
|
||||
getProfile(uname).then((user) => {
|
||||
setValid(true);
|
||||
const futureDate = new Date();
|
||||
futureDate.setHours(futureDate.getHours() + 2);
|
||||
setLogin({
|
||||
username: user.userName,
|
||||
lastSeen: Date.now(),
|
||||
validUntil: futureDate.getUTCMilliseconds(),
|
||||
});
|
||||
document.title = `IRC User ${user.userName}`;
|
||||
});
|
||||
document.title = `IRC User ${uname}`;
|
||||
}
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
setValid(false);
|
||||
setValidText(error.message);
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
// TODO Make unit test
|
||||
const getProfile = async(userName: string): Promise<User> => {
|
||||
const res = await (await fetch(`https://${domain}:${port}${endpoints.user}?name=${userName}`,
|
||||
{
|
||||
method: "GET",
|
||||
mode: "cors"
|
||||
}
|
||||
)).json()
|
||||
return res;
|
||||
}
|
||||
// login button press handler
|
||||
// TODO make unit test
|
||||
const loginHandler = () => {
|
||||
const uname = (document.getElementById("username") as HTMLInputElement)
|
||||
.value;
|
||||
const passwd = encrypt(
|
||||
digestMessage(
|
||||
(document.getElementById("passwd") as HTMLInputElement).value
|
||||
);
|
||||
// async invocation of Fetch API
|
||||
fetch(`http://${domain}:${port}${endpoints.user}?name=${uname}`, {
|
||||
method: "GET",
|
||||
mode: "cors",
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.status === 404) {
|
||||
console.log("404 not found encountered");
|
||||
throw new Error("Username does not exist");
|
||||
} else if (res.status === 200) {
|
||||
console.log("200 OK");
|
||||
}
|
||||
return res.json();
|
||||
).then((passwd) => {
|
||||
// async invocation of Fetch API
|
||||
fetch(`https://${domain}:${port}${endpoints.auth}`, {
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({
|
||||
userName: uname,
|
||||
userPasswordHash: passwd,
|
||||
})
|
||||
})
|
||||
.then((userObject) => {
|
||||
if (!userObject) {
|
||||
return;
|
||||
}
|
||||
const user = userObject as User;
|
||||
const validLogin = passwd === user.passwordHash;
|
||||
if (!validLogin) {
|
||||
// login invalid
|
||||
throw new Error("Password incorrect!");
|
||||
} else {
|
||||
// login valid
|
||||
const validUntilDate: Date = new Date();
|
||||
validUntilDate.setHours(validUntilDate.getHours() + 2);
|
||||
setLogin({
|
||||
username: user.userName,
|
||||
lastSeen: user.lastSeen,
|
||||
validUntil: validUntilDate.getUTCMilliseconds(),
|
||||
});
|
||||
document.title = `IRC User ${uname}`;
|
||||
}
|
||||
})
|
||||
.catch((reason: Error) => {
|
||||
setValid(false);
|
||||
setValidText(reason.message);
|
||||
.then(res => res.json())
|
||||
.then((body) => {
|
||||
const response = body as AuthData;
|
||||
if (!response.exists) {throw new Error(loginPage.error.unameNotExists)}
|
||||
else if (!response.success) {throw new Error(loginPage.error.passwdInvalid)}
|
||||
else {
|
||||
getProfile(uname).then((user) => {
|
||||
// login valid
|
||||
setValid(true);
|
||||
const validUntilDate: Date = new Date();
|
||||
validUntilDate.setHours(validUntilDate.getHours() + 2);
|
||||
setLogin({
|
||||
username: user.userName,
|
||||
lastSeen: user.lastSeen,
|
||||
validUntil: validUntilDate.getUTCMilliseconds(),
|
||||
});
|
||||
document.title = `IRC User ${uname}`;
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((reason: Error) => {
|
||||
setValid(false);
|
||||
setValidText(reason.message);
|
||||
});
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div className="login">
|
||||
<fieldset>
|
||||
<legend>Login window</legend>
|
||||
<legend>{loginPage.window.title}</legend>
|
||||
<p className="uname-error-text">
|
||||
{valid && valid !== undefined ? "" : validText}
|
||||
{!valid && valid !== undefined ? validText : ""}
|
||||
</p>
|
||||
<label htmlFor="username">Username: </label>
|
||||
<label htmlFor="username">{loginPage.window.uname}</label>
|
||||
<br />
|
||||
<input id="username" type="text"></input>
|
||||
<br />
|
||||
<label htmlFor="passwd">Password: </label>
|
||||
<label htmlFor="passwd">{loginPage.window.passwd}</label>
|
||||
<br />
|
||||
<input id="passwd" type="password"></input>
|
||||
<br />
|
||||
<button
|
||||
disabled={valid}
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
loginHandler();
|
||||
}}
|
||||
>
|
||||
Login
|
||||
{loginPage.window.login}
|
||||
</button>
|
||||
<button
|
||||
disabled={valid}
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
registrationHandler();
|
||||
}}
|
||||
>
|
||||
Register
|
||||
{loginPage.window.register}
|
||||
</button>
|
||||
<button
|
||||
disabled={!valid}
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
setLogin(undefined);
|
||||
setValid(false);
|
||||
setValid(undefined);
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
{loginPage.window.logout}
|
||||
</button>
|
||||
</fieldset>
|
||||
</div>
|
||||
|
|
3
src/MessageDisplay/MessageDisplay.css
Normal file
3
src/MessageDisplay/MessageDisplay.css
Normal file
|
@ -0,0 +1,3 @@
|
|||
.msg .msg-err {
|
||||
overflow-wrap: break-word;
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
import React, { useContext } from "react";
|
||||
import { Message, MessageType } from "./messageTypes";
|
||||
import { Message, MessageType } from "../type/messageTypes";
|
||||
import { LangContext } from "../context";
|
||||
import strings from "../Intl/strings.json";
|
||||
import "./MessageDisplay.css";
|
||||
import { queryByRole } from "@testing-library/react";
|
||||
export const MessageDisplay = ({
|
||||
type,
|
||||
fromUserId,
|
||||
|
@ -26,7 +27,11 @@ export const MessageDisplay = ({
|
|||
dateTime.getHours() > 12
|
||||
? dateTime.getHours() - 12
|
||||
: dateTime.getHours()
|
||||
}:${dateTime.getMinutes()} ${dateTime.getHours() > 12 ? "PM" : "AM"}`;
|
||||
}:${
|
||||
dateTime.getMinutes() >= 10
|
||||
? dateTime.getMinutes()
|
||||
: `0${dateTime.getMinutes().toString()}`
|
||||
} ${dateTime.getHours() > 12 ? "PM" : "AM"}`;
|
||||
switch (type) {
|
||||
case MessageType.HELLO as MessageType:
|
||||
return (
|
5
src/MessageDisplay/messageHandler.tsx
Normal file
5
src/MessageDisplay/messageHandler.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import {Message, MessageType } from "../type/messageTypes"
|
||||
export const dataMsgHandler = (msg: Message) => {
|
||||
if (msg.type !== MessageType.DATA) {return <></>}
|
||||
}
|
||||
export const chnameMsgHandler = () => {}
|
5
src/Sidebar/Components/Avatar.css
Normal file
5
src/Sidebar/Components/Avatar.css
Normal file
|
@ -0,0 +1,5 @@
|
|||
.avatar {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content:center;
|
||||
}
|
9
src/Sidebar/Components/Avatar.tsx
Normal file
9
src/Sidebar/Components/Avatar.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import "./Avatar.css";
|
||||
import placeholderImage from "./placeholder.jpg";
|
||||
export const Avatar = () => {
|
||||
return (
|
||||
<div className="avatar">
|
||||
<img src={placeholderImage} width="75px"></img>
|
||||
</div>
|
||||
);
|
||||
};
|
50
src/Sidebar/Components/SidebarMenu.tsx
Normal file
50
src/Sidebar/Components/SidebarMenu.tsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
import "../Sidebar.css";
|
||||
import { Avatar } from "./Avatar";
|
||||
export const SidebarMenuItem = ({
|
||||
text,
|
||||
href,
|
||||
handler,
|
||||
}: {
|
||||
text: string;
|
||||
href?: string;
|
||||
handler?: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className="sidebar-menu-item"
|
||||
onClick={
|
||||
handler
|
||||
? () => {
|
||||
handler();
|
||||
}
|
||||
: () => {}
|
||||
}
|
||||
>
|
||||
<li>
|
||||
<span>
|
||||
<i>{href ? <a href={href}>{text}</a> : text}</i>
|
||||
</span>
|
||||
</li>
|
||||
<hr></hr>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export const SidebarMenu = ({
|
||||
exitHandler,
|
||||
}: {
|
||||
exitHandler: (enabled: boolean) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className="sidebar-menu">
|
||||
<Avatar></Avatar>
|
||||
<SidebarMenuItem text="My Account"></SidebarMenuItem>
|
||||
<SidebarMenuItem text="Personalisation"></SidebarMenuItem>
|
||||
<SidebarMenuItem text="Language"></SidebarMenuItem>
|
||||
<SidebarMenuItem text="Server Configuration"></SidebarMenuItem>
|
||||
<SidebarMenuItem
|
||||
text="Return to homepage"
|
||||
handler={() => exitHandler(false)}
|
||||
></SidebarMenuItem>
|
||||
</div>
|
||||
);
|
||||
};
|
BIN
src/Sidebar/Components/placeholder.jpg
Normal file
BIN
src/Sidebar/Components/placeholder.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
47
src/Sidebar/Sidebar.css
Normal file
47
src/Sidebar/Sidebar.css
Normal file
|
@ -0,0 +1,47 @@
|
|||
|
||||
.sidebar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
animation: fadeIn, 5s;
|
||||
z-index: 10;
|
||||
}
|
||||
.sidebar-menu-item {
|
||||
margin: 5px;
|
||||
transition: all 0.5s;
|
||||
}
|
||||
|
||||
.sidebar-menu-item:hover {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 5px 5px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.sidebar-content {
|
||||
background-color: white;
|
||||
width: max(15%, 200px);
|
||||
height: 100%;
|
||||
box-shadow: 10px 10px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
0% {background-color: transparent;}
|
||||
100% {background-color: rgba(0, 0, 0, 0.5);;}
|
||||
}
|
||||
@-moz-keyframes fadeIn {
|
||||
0% {background-color: transparent;}
|
||||
100% {background-color: rgba(0, 0, 0, 0.5);;}
|
||||
}
|
||||
@-webkit-keyframes fadeIn {
|
||||
0% {background-color: transparent;}
|
||||
100% {background-color: rgba(0, 0, 0, 0.5);;}
|
||||
}
|
||||
|
||||
@-o-keyframes fadeIn {
|
||||
0% {background-color: transparent;}
|
||||
100% {background-color: rgba(0, 0, 0, 0.5);;}
|
||||
}
|
||||
@-ms-keyframes fadeIn {
|
||||
0% {background-color: transparent;}
|
||||
100% {background-color: rgba(0, 0, 0, 0.5);;}
|
||||
}
|
23
src/Sidebar/Sidebar.tsx
Normal file
23
src/Sidebar/Sidebar.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import "./Sidebar.css";
|
||||
import { SidebarMenu } from "./Components/SidebarMenu";
|
||||
export const Sidebar = ({
|
||||
isEnabled,
|
||||
setEnable,
|
||||
}: {
|
||||
isEnabled: boolean;
|
||||
setEnable: (enabled: boolean) => void;
|
||||
}) => {
|
||||
return isEnabled ? (
|
||||
<div className="sidebar">
|
||||
<div className="sidebar-content">
|
||||
<SidebarMenu
|
||||
exitHandler={(value) => {
|
||||
setEnable(value);
|
||||
}}
|
||||
></SidebarMenu>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
};
|
10
src/Topbar/Topbar.css
Normal file
10
src/Topbar/Topbar.css
Normal file
|
@ -0,0 +1,10 @@
|
|||
.topbar {
|
||||
width: 100vw;
|
||||
height: 10%;
|
||||
z-index: 5;
|
||||
display: flex;
|
||||
justify-content: baseline;
|
||||
}
|
||||
.topbar-span .children {
|
||||
margin-left: 10px;
|
||||
}
|
30
src/Topbar/Topbar.tsx
Normal file
30
src/Topbar/Topbar.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import "./Topbar.css";
|
||||
import strings from "../Intl/strings.json";
|
||||
import { useContext } from "react";
|
||||
import { LangContext } from "../context";
|
||||
import menu from "./menu.png";
|
||||
export const Topbar = ({
|
||||
setSidebarEnable,
|
||||
}: {
|
||||
setSidebarEnable: (enabled: boolean) => void;
|
||||
}) => {
|
||||
const lang = useContext(LangContext);
|
||||
return (
|
||||
<div className="topbar">
|
||||
<img
|
||||
onClick={() => {
|
||||
setSidebarEnable(true);
|
||||
}}
|
||||
src={menu}
|
||||
width="100px"
|
||||
height="100px"
|
||||
alt="Open Selection Menu"
|
||||
></img>
|
||||
<span className="topbar-span">
|
||||
<h1 className="topbar-span children">
|
||||
{strings[lang].homepage.title}
|
||||
</h1>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
BIN
src/Topbar/menu.png
Normal file
BIN
src/Topbar/menu.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
|
@ -1,11 +1,14 @@
|
|||
export const domain = window.location.hostname;
|
||||
export const port = "8080";
|
||||
export const connectionAddress = `ws://${domain}:${port}/ws`;
|
||||
export const connectionAddress = `wss://${domain}:${port}/ws`;
|
||||
export const endpoints = {
|
||||
destination: "/app/chat",
|
||||
subscription: "/sub/chat",
|
||||
history: "/api/v1/msg/",
|
||||
user: "/api/v1/user",
|
||||
auth: "/api/v1/auth",
|
||||
register: "/api/v1/register",
|
||||
oauth2: "",
|
||||
};
|
||||
export const contentTypes = {
|
||||
json: {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { createContext } from "react";
|
||||
import { LangType } from "./Chat/messageTypes";
|
||||
import { LangType } from "./type/messageTypes";
|
||||
export type LoginType = {
|
||||
username: string;
|
||||
lastSeen: number;
|
||||
|
|
15
src/crypto.ts
Normal file
15
src/crypto.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
export const ENCRYPTION_TYPE = "SHA-256";
|
||||
|
||||
// Implemented according to https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
|
||||
export const digestMessage = async (plaintext: string) => {
|
||||
const textEncoder = new TextEncoder();
|
||||
const digestArray = Array.from(
|
||||
new Uint8Array(
|
||||
await window.crypto.subtle.digest(
|
||||
ENCRYPTION_TYPE,
|
||||
textEncoder.encode(plaintext)
|
||||
)
|
||||
)
|
||||
)
|
||||
return digestArray.map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
import { Client } from "@stomp/stompjs";
|
||||
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,
|
||||
});
|
||||
};
|
||||
|
||||
// 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;
|
||||
};
|
|
@ -1,13 +1,13 @@
|
|||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
|
47
src/type/messageTypes.ts
Normal file
47
src/type/messageTypes.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
export const enum MessageType {
|
||||
MESSAGE = "MESSAGE",
|
||||
CHNAME = "CHNAME",
|
||||
HELLO = "HELLO",
|
||||
DATA = "DATA",
|
||||
}
|
||||
export enum SystemMessageCode {
|
||||
REQ,
|
||||
RES,
|
||||
ERR,
|
||||
}
|
||||
export type HistoryFetchResult = {
|
||||
count: number;
|
||||
items: Array<ChatMessage>;
|
||||
};
|
||||
export type ErrorResult = {
|
||||
text: string;
|
||||
};
|
||||
export type TimestampSendRequest = {
|
||||
ts: number;
|
||||
};
|
||||
export type SystemMessage = {
|
||||
code: SystemMessageCode;
|
||||
data: HistoryFetchResult | ErrorResult | TimestampSendRequest;
|
||||
};
|
||||
export type ChatMessage = {
|
||||
fromUserId: string;
|
||||
toUserId: string;
|
||||
content: string;
|
||||
timeMillis: number;
|
||||
};
|
||||
export type HelloMessage = {
|
||||
fromUserId: string;
|
||||
timeMillis: number;
|
||||
};
|
||||
export type DataMessage = {};
|
||||
export type Message = {
|
||||
type: MessageType;
|
||||
// data: SystemMessage | ChatMessage | HelloMessage
|
||||
fromUserId: string;
|
||||
toUserId: string;
|
||||
content: string;
|
||||
timeMillis: number;
|
||||
};
|
||||
// Type gymnastics to provide dynamic ESLint support
|
||||
export const acceptedLangs = ["en_US", "zh_TW", "el_GR", "ar_SA"] as const;
|
||||
export type LangType = (typeof acceptedLangs)[number];
|
20
src/type/userTypes.ts
Normal file
20
src/type/userTypes.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { URL } from "url";
|
||||
|
||||
export type UserAvatar = {
|
||||
iconUrls: URL[];
|
||||
};
|
||||
export type User = {
|
||||
id: number;
|
||||
userName: string;
|
||||
dateJoined: number;
|
||||
lastSeen: number;
|
||||
passwordHash: string;
|
||||
// avatar: UserAvatar;
|
||||
};
|
||||
export type AuthData = {
|
||||
success: boolean;
|
||||
hasProfile: boolean;
|
||||
exists: boolean;
|
||||
authMessage: string;
|
||||
authResponseTimestampMillis: number;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue