Compare commits

..

No commits in common. "main" and "v0.2.1" have entirely different histories.

9 changed files with 17868 additions and 17921 deletions

35443
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "epq-web-project", "name": "epq-web-project",
"version": "0.3.0", "version": "0.2.1",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@stomp/stompjs": "^7.0.0", "@stomp/stompjs": "^7.0.0",
@ -11,7 +11,6 @@
"@types/node": "^16.18.68", "@types/node": "^16.18.68",
"@types/react": "^18.2.45", "@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18", "@types/react-dom": "^18.2.18",
"ed25519": "^0.0.5",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
@ -42,8 +41,5 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari version" "last 1 safari version"
] ]
},
"devDependencies": {
"@types/ed25519": "^0.0.3"
} }
} }

View file

@ -52,7 +52,7 @@ const Wrapper = (): React.ReactElement => {
}; };
const setNameOnServer = async (name: string) => { const setNameOnServer = async (name: string) => {
const responseRaw = await fetch( const responseRaw = await fetch(
`https://${domain}:${port}${endpoints.user}`, `http://${domain}:${port}${endpoints.user}`,
{ {
method: "POST", method: "POST",
mode: "cors", mode: "cors",

View file

@ -1,158 +1,171 @@
import { useContext, useState } from "react"; import { useContext, useState } from "react";
import { contentTypes, domain, endpoints, port } from "../consts"; import { contentTypes, domain, endpoints, port } from "../consts";
import { LangContext, LoginType } from "../context"; import { LangContext, LoginType } from "../context";
import { AuthData, User } from "../type/userTypes"; import { User } from "../type/userTypes";
import "./Login.css"; import "./Login.css";
import strings from "../Intl/strings.json"; import strings from "../Intl/strings.json";
import { ECDH } from "crypto";
import { digestMessage } from "../crypto";
const encrypt = (rawPasswordString: string) => { const encrypt = (rawPasswordString: string) => {
// TODO Encryption method stub // TODO Encryption method stub
return rawPasswordString; return rawPasswordString;
}; };
export const Login = ({ export const Login = ({
setLogin, setLogin,
}: { }: {
setLogin: (newLogin: LoginType | undefined) => void; setLogin: (newLogin: LoginType | undefined) => void;
}): React.ReactElement => { }): React.ReactElement => {
const [valid, setValid] = useState<boolean | undefined>(undefined); const [valid, setValid] = useState<boolean | undefined>(undefined);
const [validText, setValidText] = useState<string | undefined>(); const [validText, setValidText] = useState<string | undefined>();
const lang = useContext(LangContext); const lang = useContext(LangContext);
const loginPage = strings[lang].login; const loginPage = strings[lang].login;
// TODO mk unit test const registrationHandler = () => {
const registrationHandler = () => { const uname = (
const uname = (document.getElementById("username") as HTMLInputElement) document.getElementById("username") as HTMLInputElement
.value; ).value;
digestMessage((document.getElementById("passwd") as HTMLInputElement).value).then((passwd) => { const passwd = encrypt(
fetch(`https://${domain}:${port}${endpoints.register}`, { (document.getElementById("passwd") as HTMLInputElement)
method: "POST", .value
mode: "cors", );
headers: contentTypes.json, fetch(`http://${domain}:${port}${endpoints.user}`, {
body: JSON.stringify({ method: "POST",
userName: uname, mode: "cors",
newUserPassword: passwd, headers: contentTypes.json,
}), body: JSON.stringify({
}) userName: uname,
.then((res) => res.json()).then((body) => { dateJoined: Date.now(),
const response = body as AuthData; passwordHash: passwd,
if (response.exists && !response.success) {throw new Error(loginPage.error.unameTakenOrInvalid)} }),
getProfile(uname).then((user) => { }).then((response) => {
setValid(true); if (response.status === 400) {
const futureDate = new Date(); // 400 Bad request
futureDate.setHours(futureDate.getHours() + 2); console.log("Username is taken or invalid!");
setLogin({ setValid(false);
username: user.userName, setValidText(
lastSeen: Date.now(), loginPage.error.unameTakenOrInvalid
validUntil: futureDate.getUTCMilliseconds(), );
}); } else if (response.status === 200) {
document.title = `IRC User ${user.userName}`; // 200 OK
}); const futureDate = new Date();
}) futureDate.setHours(futureDate.getHours() + 2);
.catch((error: Error) => { setLogin({
setValid(false); username: uname,
setValidText(error.message); lastSeen: Date.now(),
}) validUntil: futureDate.getUTCMilliseconds(),
}); });
}; document.title = `IRC User ${uname}`;
}
// TODO Make unit test });
const getProfile = async(userName: string): Promise<User> => { };
const res = await (await fetch(`https://${domain}:${port}${endpoints.user}?name=${userName}`, // login button press handler
{ const loginHandler = () => {
method: "GET", const uname = (
mode: "cors" document.getElementById("username") as HTMLInputElement
} ).value;
)).json() const passwd = encrypt(
return res; (document.getElementById("passwd") as HTMLInputElement)
} .value
// login button press handler );
// TODO make unit test // async invocation of Fetch API
const loginHandler = () => { fetch(
const uname = (document.getElementById("username") as HTMLInputElement) `http://${domain}:${port}${endpoints.user}?name=${uname}`,
.value; {
digestMessage( method: "GET",
(document.getElementById("passwd") as HTMLInputElement).value mode: "cors",
).then((passwd) => { }
// async invocation of Fetch API )
fetch(`https://${domain}:${port}${endpoints.auth}`, { .then((res) => {
method: "POST", if (res.status === 404) {
mode: "cors", console.log(
headers: {"Content-Type": "application/json"}, "404 not found encountered"
body: JSON.stringify({ );
userName: uname, throw new Error(
userPasswordHash: passwd, loginPage.error.unameNotExists
}) );
}) } else if (res.status === 200) {
.then(res => res.json()) console.log("200 OK");
.then((body) => { }
const response = body as AuthData; return res.json();
if (!response.exists) {throw new Error(loginPage.error.unameNotExists)} })
else if (!response.success) {throw new Error(loginPage.error.passwdInvalid)} .then((userObject) => {
else { if (!userObject) {
getProfile(uname).then((user) => { return;
// login valid }
setValid(true); const user = userObject as User;
const validUntilDate: Date = new Date(); const validLogin = passwd === user.passwordHash;
validUntilDate.setHours(validUntilDate.getHours() + 2); if (!validLogin) {
setLogin({ // login invalid
username: user.userName, throw new Error(
lastSeen: user.lastSeen, loginPage.error.passwdInvalid
validUntil: validUntilDate.getUTCMilliseconds(), );
}); } else {
document.title = `IRC User ${uname}`; // login valid
}); setValid(true);
} const validUntilDate: Date = new Date();
}) validUntilDate.setHours(
.catch((reason: Error) => { validUntilDate.getHours() + 2
setValid(false); );
setValidText(reason.message); setLogin({
}); username: user.userName,
}); lastSeen: user.lastSeen,
}; validUntil: validUntilDate.getUTCMilliseconds(),
return ( });
<div className="login"> document.title = `IRC User ${uname}`;
<fieldset> }
<legend>{loginPage.window.title}</legend> })
<p className="uname-error-text"> .catch((reason: Error) => {
{!valid && valid !== undefined ? validText : ""} setValid(false);
</p> setValidText(reason.message);
<label htmlFor="username">{loginPage.window.uname}</label> });
<br /> };
<input id="username" type="text"></input> return (
<br /> <div className="login">
<label htmlFor="passwd">{loginPage.window.passwd}</label> <fieldset>
<br /> <legend>{loginPage.window.title}</legend>
<input id="passwd" type="password"></input> <p className="uname-error-text">
<br /> {!valid && valid !== undefined
<button ? validText
disabled={valid} : ""}
type="submit" </p>
onClick={() => { <label htmlFor="username">
loginHandler(); {loginPage.window.uname}
}} </label>
> <br />
{loginPage.window.login} <input id="username" type="text"></input>
</button> <br />
<button <label htmlFor="passwd">
disabled={valid} {loginPage.window.passwd}
type="submit" </label>
onClick={() => { <br />
registrationHandler(); <input id="passwd" type="password"></input>
}} <br />
> <button
{loginPage.window.register} disabled={valid}
</button> type="submit"
<button onClick={() => {
disabled={!valid} loginHandler();
type="submit" }}
onClick={() => { >
setLogin(undefined); {loginPage.window.login}
setValid(undefined); </button>
}} <button
> disabled={valid}
{loginPage.window.logout} type="submit"
</button> onClick={() => {
</fieldset> registrationHandler();
</div> }}
); >
{loginPage.window.register}
</button>
<button
disabled={!valid}
type="submit"
onClick={() => {
setLogin(undefined);
setValid(undefined);
}}
>
{loginPage.window.logout}
</button>
</fieldset>
</div>
);
}; };

View file

@ -1,3 +1,4 @@
import { useState } from "react";
import "./Sidebar.css"; import "./Sidebar.css";
import { SidebarMenu } from "./Components/SidebarMenu"; import { SidebarMenu } from "./Components/SidebarMenu";
export const Sidebar = ({ export const Sidebar = ({

View file

@ -24,6 +24,9 @@ export const Topbar = ({
<h1 className="topbar-span children"> <h1 className="topbar-span children">
{strings[lang].homepage.title} {strings[lang].homepage.title}
</h1> </h1>
{/* <p className="topbar-span children">
{strings[lang].homepage.description}
</p> */}
</span> </span>
</div> </div>
); );

View file

@ -1,14 +1,11 @@
export const domain = window.location.hostname; export const domain = window.location.hostname;
export const port = "8080"; export const port = "8080";
export const connectionAddress = `wss://${domain}:${port}/ws`; export const connectionAddress = `ws://${domain}:${port}/ws`;
export const endpoints = { export const endpoints = {
destination: "/app/chat", destination: "/app/chat",
subscription: "/sub/chat", subscription: "/sub/chat",
history: "/api/v1/msg/", history: "/api/v1/msg/",
user: "/api/v1/user", user: "/api/v1/user",
auth: "/api/v1/auth",
register: "/api/v1/register",
oauth2: "",
}; };
export const contentTypes = { export const contentTypes = {
json: { json: {

View file

@ -1,15 +0,0 @@
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("");
}

View file

@ -11,10 +11,3 @@ export type User = {
passwordHash: string; passwordHash: string;
// avatar: UserAvatar; // avatar: UserAvatar;
}; };
export type AuthData = {
success: boolean;
hasProfile: boolean;
exists: boolean;
authMessage: string;
authResponseTimestampMillis: number;
}