Added support for CORS and integrated login handler

This commit is contained in:
Zhongheng Liu 2024-01-19 20:59:30 +02:00
commit fdcd08dc50
No known key found for this signature in database
4 changed files with 125 additions and 43 deletions

View file

@ -1,8 +1,8 @@
import React, { createContext, useContext, useState } from "react"; import React, { useContext, useEffect, useState } from "react";
import Chat from "./Chat/Chat"; import Chat from "./Chat/Chat";
import "./App.css"; import "./App.css";
import { LangType, Message } from "./Chat/messageTypes"; import { LangType, Message } from "./Chat/messageTypes";
import { MessageContainer } from "./Chat/MessageContainer"; import { MessageDisplay } from "./Chat/MessageDisplay";
import strings from "./Intl/strings.json"; import strings from "./Intl/strings.json";
import { LangContext, LoginContext, LoginType } from "./context"; import { LangContext, LoginContext, LoginType } from "./context";
import { contentTypes, domain, endpoints, port } from "./consts"; import { contentTypes, domain, endpoints, port } from "./consts";
@ -11,19 +11,31 @@ import { Login } from "./Login/Login";
const Wrapper = (): React.ReactElement => { const Wrapper = (): React.ReactElement => {
const [lang, setLang] = useState<LangType>("en_US"); const [lang, setLang] = useState<LangType>("en_US");
const [login, setLogin] = useState<LoginType | undefined>(undefined); const [login, setLogin] = useState<LoginType | undefined>(undefined);
useEffect(() => {
document.title = login
? `IRC logged in as ${login.username}`
: "IRC Chat";
}, [login]);
return ( return (
<LangContext.Provider value={lang}> <LangContext.Provider value={lang}>
<h1>{strings[lang].homepage.title}</h1>
<p>{strings[lang].homepage.description}</p>
<LoginContext.Provider value={login}> <LoginContext.Provider value={login}>
{/* callbacks for altering the Lang/Login contexts */}
<Login <Login
setLogin={(value) => { setLogin={(value) => {
setLogin(value); setLogin(value);
}} }}
></Login> ></Login>
<App {login ? (
changeLang={(value: string) => { <App
setLang(value as LangType); changeLang={(value: string) => {
}} setLang(value as LangType);
/> }}
/>
) : (
<></>
)}
</LoginContext.Provider> </LoginContext.Provider>
</LangContext.Provider> </LangContext.Provider>
); );
@ -33,6 +45,7 @@ const setNameOnServer = async (name: string) => {
`http://${domain}:${port}${endpoints.user}`, `http://${domain}:${port}${endpoints.user}`,
{ {
method: "POST", method: "POST",
mode: "cors",
headers: contentTypes.json, headers: contentTypes.json,
body: JSON.stringify({ body: JSON.stringify({
userName: name, userName: name,
@ -77,23 +90,20 @@ const App = ({
} }
}); });
}; };
if (!username) { // if (!username) {
var newName = prompt(home.userNamePrompt) as string; // var newName = prompt(home.userNamePrompt) as string;
while (!validateName(newName)) { // while (!validateName(newName)) {
console.log(newName); // console.log(newName);
prompt("Username invalid! Please enter again.") as string; // prompt("Username invalid! Please enter again.") as string;
} // }
setUsername(newName); // setUsername(newName);
} // }
if (!login) { if (!login) {
return <></>; return <></>;
} else } else
return ( return (
<div className="App"> <div className="App">
<h1>{home.title}</h1>
<pre>{home.description}</pre>
<h3>Your name is: {username}</h3>
<button <button
onClick={(ev) => { onClick={(ev) => {
const selection = prompt(home.newLangPrompt); const selection = prompt(home.newLangPrompt);
@ -130,9 +140,9 @@ const App = ({
Change Username Change Username
</button> </button>
{messages.map((message) => { {messages.map((message) => {
return <MessageContainer {...message} />; return <MessageDisplay {...message} />;
})} })}
{<Chat user={username as string} />} {<Chat user={login.username as string} />}
</div> </div>
); );
}; };

View file

@ -5,7 +5,7 @@ import React, {
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import { MessageContainer } from "./MessageContainer"; import { MessageDisplay } from "./MessageDisplay";
import { Client } from "@stomp/stompjs"; import { Client } from "@stomp/stompjs";
import { Message, MessageType } from "./messageTypes"; import { Message, MessageType } from "./messageTypes";
import "./Chat.css"; import "./Chat.css";
@ -29,7 +29,7 @@ const Chat = ({ user }: { user: string }): React.ReactElement => {
console.log(messageBody); console.log(messageBody);
setMessages((message) => { setMessages((message) => {
return message.concat([ return message.concat([
<MessageContainer <MessageDisplay
key={`${messageBody.type}@${messageBody.timeMillis}`} key={`${messageBody.type}@${messageBody.timeMillis}`}
{...messageBody} {...messageBody}
/>, />,
@ -99,8 +99,23 @@ const Chat = ({ user }: { user: string }): React.ReactElement => {
sendData(); 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 ( return (
<div className="chat"> <fieldset className="chat">
<legend>
Logged in as <b>{user}</b>
</legend>
<div className="chat-inner-wrapper">{messages}</div> <div className="chat-inner-wrapper">{messages}</div>
<span className="entry-box"> <span className="entry-box">
<input id="data-entry"></input> <input id="data-entry"></input>
@ -108,7 +123,7 @@ const Chat = ({ user }: { user: string }): React.ReactElement => {
{chatPage.sendButtonPrompt} {chatPage.sendButtonPrompt}
</button> </button>
</span> </span>
</div> </fieldset>
); );
}; };
export default Chat; export default Chat;

View file

@ -2,7 +2,8 @@ import React, { useContext } from "react";
import { Message, MessageType } from "./messageTypes"; import { Message, MessageType } from "./messageTypes";
import { LangContext } from "../context"; import { LangContext } from "../context";
import strings from "../Intl/strings.json"; import strings from "../Intl/strings.json";
export const MessageContainer = ({ import "./MessageDisplay.css";
export const MessageDisplay = ({
type, type,
fromUserId, fromUserId,
toUserId, toUserId,
@ -21,19 +22,23 @@ export const MessageContainer = ({
* This caused the return statement to fail, and the message fails to render, despite it being correctly committed to the db. * This caused the return statement to fail, and the message fails to render, despite it being correctly committed to the db.
* Funny clown moment 🤡 * Funny clown moment 🤡
*/ */
const timeString = `${
dateTime.getHours() > 12
? dateTime.getHours() - 12
: dateTime.getHours()
}:${dateTime.getMinutes()} ${dateTime.getHours() > 12 ? "PM" : "AM"}`;
switch (type) { switch (type) {
case MessageType.HELLO as MessageType: case MessageType.HELLO as MessageType:
return ( return (
<p> <p className="msg">
[{dateTime.toLocaleString()}]{" "} [{timeString}]{" "}
{msgPage.joinMessage.replace("$userName", fromUserId)} {msgPage.joinMessage.replace("$userName", fromUserId)}
</p> </p>
); );
case MessageType.MESSAGE as MessageType: case MessageType.MESSAGE as MessageType:
return ( return (
<p> <p className="msg">
[{dateTime.toLocaleString()}]{" "} [{timeString}]{" "}
{msgPage.serverMessage {msgPage.serverMessage
.replace("$userName", fromUserId) .replace("$userName", fromUserId)
.replace("$content", content)} .replace("$content", content)}
@ -46,9 +51,9 @@ export const MessageContainer = ({
default: default:
console.error("Illegal MessageType reported!"); console.error("Illegal MessageType reported!");
return ( return (
<p> <p className="msg-err">
[{dateTime.toLocaleString()}] **THIS MESSAGE CANNOT BE [{timeString}] **THIS MESSAGE CANNOT BE CORRECTLY SHOWN
CORRECTLY SHOWN BECAUSE THE CLIENT ENCOUNTERED AN ERROR** BECAUSE THE CLIENT ENCOUNTERED AN ERROR**
</p> </p>
); );
} }

View file

@ -2,6 +2,7 @@ import { useState } from "react";
import { contentTypes, domain, endpoints, port } from "../consts"; import { contentTypes, domain, endpoints, port } from "../consts";
import { LoginType } from "../context"; import { LoginType } from "../context";
import { User } from "../Chat/userTypes"; import { User } from "../Chat/userTypes";
import "./Login.css";
const encrypt = (rawPasswordString: string) => { const encrypt = (rawPasswordString: string) => {
// TODO Encryption method stub // TODO Encryption method stub
return rawPasswordString; return rawPasswordString;
@ -9,9 +10,10 @@ const encrypt = (rawPasswordString: string) => {
export const Login = ({ export const Login = ({
setLogin, setLogin,
}: { }: {
setLogin: (newLogin: LoginType) => void; setLogin: (newLogin: LoginType | undefined) => void;
}): React.ReactElement => { }): React.ReactElement => {
const [valid, setValid] = useState(true); const [valid, setValid] = useState<boolean | undefined>(true);
const [validText, setValidText] = useState<string | undefined>();
const registrationHandler = () => { const registrationHandler = () => {
const uname = (document.getElementById("username") as HTMLInputElement) const uname = (document.getElementById("username") as HTMLInputElement)
.value; .value;
@ -20,6 +22,7 @@ export const Login = ({
); );
fetch(`http://${domain}:${port}${endpoints.user}`, { fetch(`http://${domain}:${port}${endpoints.user}`, {
method: "POST", method: "POST",
mode: "cors",
headers: contentTypes.json, headers: contentTypes.json,
body: JSON.stringify({ body: JSON.stringify({
userName: uname, userName: uname,
@ -28,8 +31,12 @@ export const Login = ({
}), }),
}).then((response) => { }).then((response) => {
if (response.status === 400) { if (response.status === 400) {
// 400 Bad request
console.log("Username is taken or invalid!"); console.log("Username is taken or invalid!");
} else { setValid(false);
setValidText("Username is taken or invalid!");
} else if (response.status === 200) {
// 200 OK
const futureDate = new Date(); const futureDate = new Date();
futureDate.setHours(futureDate.getHours() + 2); futureDate.setHours(futureDate.getHours() + 2);
setLogin({ setLogin({
@ -37,6 +44,7 @@ export const Login = ({
lastSeen: Date.now(), lastSeen: Date.now(),
validUntil: futureDate.getUTCMilliseconds(), validUntil: futureDate.getUTCMilliseconds(),
}); });
document.title = `IRC User ${uname}`;
} }
}); });
}; };
@ -48,16 +56,28 @@ export const Login = ({
(document.getElementById("passwd") as HTMLInputElement).value (document.getElementById("passwd") as HTMLInputElement).value
); );
// async invocation of Fetch API // async invocation of Fetch API
fetch(`http://${domain}:${port}${endpoints.user}?user=${uname}`, { fetch(`http://${domain}:${port}${endpoints.user}?name=${uname}`, {
method: "GET", method: "GET",
mode: "cors",
}) })
.then((res) => res.json()) .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((userObject) => { .then((userObject) => {
if (!userObject) {
return;
}
const user = userObject as User; const user = userObject as User;
const validLogin = passwd === user.passwordHash; const validLogin = passwd === user.passwordHash;
if (!validLogin) { if (!validLogin) {
// login invalid // login invalid
setValid(false); // triggers page re-render -- should refresh the page throw new Error("Password incorrect!");
} else { } else {
// login valid // login valid
const validUntilDate: Date = new Date(); const validUntilDate: Date = new Date();
@ -67,22 +87,54 @@ export const Login = ({
lastSeen: user.lastSeen, lastSeen: user.lastSeen,
validUntil: validUntilDate.getUTCMilliseconds(), validUntil: validUntilDate.getUTCMilliseconds(),
}); });
document.title = `IRC User ${uname}`;
} }
})
.catch((reason: Error) => {
setValid(false);
setValidText(reason.message);
}); });
}; };
return ( return (
<div> <div className="login">
<fieldset> <fieldset>
<legend>Login window</legend> <legend>Login window</legend>
<p className="uname-error-text"> <p className="uname-error-text">
{valid ? "Error in your username or password" : ""} {valid && valid !== undefined ? "" : validText}
</p> </p>
<label htmlFor="username">Username: </label> <label htmlFor="username">Username: </label>
<br />
<input id="username" type="text"></input> <input id="username" type="text"></input>
<br />
<label htmlFor="passwd">Password: </label> <label htmlFor="passwd">Password: </label>
<br />
<input id="passwd" type="password"></input> <input id="passwd" type="password"></input>
<button type="submit">Login</button> <br />
<button type="submit">Register</button> <button
type="submit"
onClick={() => {
loginHandler();
}}
>
Login
</button>
<button
type="submit"
onClick={() => {
registrationHandler();
}}
>
Register
</button>
<button
type="submit"
onClick={() => {
setLogin(undefined);
setValid(false);
}}
>
Logout
</button>
</fieldset> </fieldset>
</div> </div>
); );