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