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