Compare commits

..

6 commits

9 changed files with 17921 additions and 17868 deletions

45
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "epq-web-project", "name": "epq-web-project",
"version": "0.1.0", "version": "0.2.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "epq-web-project", "name": "epq-web-project",
"version": "0.1.0", "version": "0.2.1",
"dependencies": { "dependencies": {
"@stomp/stompjs": "^7.0.0", "@stomp/stompjs": "^7.0.0",
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
@ -16,12 +16,16 @@
"@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",
"react-use-websocket": "^4.5.0", "react-use-websocket": "^4.5.0",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
},
"devDependencies": {
"@types/ed25519": "^0.0.3"
} }
}, },
"node_modules/@aashutoshrathi/word-wrap": { "node_modules/@aashutoshrathi/word-wrap": {
@ -3968,6 +3972,15 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/ed25519": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/@types/ed25519/-/ed25519-0.0.3.tgz",
"integrity": "sha512-IaKIKabzDCteZ0e6y884BpGMn0ZNDHRT90Vob+Kq8o2sKYFaxkEfth8FCM9iPtk/cRr6HKq/+UectHKgbUP6rw==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/eslint": { "node_modules/@types/eslint": {
"version": "8.44.9", "version": "8.44.9",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.9.tgz", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.9.tgz",
@ -5416,6 +5429,14 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"dependencies": {
"file-uri-to-path": "1.0.0"
}
},
"node_modules/bluebird": { "node_modules/bluebird": {
"version": "3.7.2", "version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@ -6820,6 +6841,16 @@
"resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==" "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
}, },
"node_modules/ed25519": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/ed25519/-/ed25519-0.0.5.tgz",
"integrity": "sha512-bg8uNetUVrkTyAVsnCUUmC/we1CFB7DYa59rwEUHut26SISXN2o319HzIt0r7ncqbtjVi0hn9r/A0epUmMNulQ==",
"hasInstallScript": true,
"dependencies": {
"bindings": "^1.5.0",
"nan": "^2.14.1"
}
},
"node_modules/ee-first": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -7982,6 +8013,11 @@
"webpack": "^4.0.0 || ^5.0.0" "webpack": "^4.0.0 || ^5.0.0"
} }
}, },
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
},
"node_modules/filelist": { "node_modules/filelist": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz",
@ -12253,6 +12289,11 @@
"thenify-all": "^1.0.0" "thenify-all": "^1.0.0"
} }
}, },
"node_modules/nan": {
"version": "2.18.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz",
"integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w=="
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.7", "version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",

View file

@ -1,6 +1,6 @@
{ {
"name": "epq-web-project", "name": "epq-web-project",
"version": "0.2.1", "version": "0.3.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@stomp/stompjs": "^7.0.0", "@stomp/stompjs": "^7.0.0",
@ -11,6 +11,7 @@
"@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",
@ -41,5 +42,8 @@
"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(
`http://${domain}:${port}${endpoints.user}`, `https://${domain}:${port}${endpoints.user}`,
{ {
method: "POST", method: "POST",
mode: "cors", mode: "cors",

View file

@ -1,9 +1,11 @@
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 { User } from "../type/userTypes"; import { AuthData, 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;
@ -17,123 +19,108 @@ export const Login = ({
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)
.value
);
fetch(`http://${domain}:${port}${endpoints.user}`, {
method: "POST", method: "POST",
mode: "cors", mode: "cors",
headers: contentTypes.json, headers: contentTypes.json,
body: JSON.stringify({ body: JSON.stringify({
userName: uname, userName: uname,
dateJoined: Date.now(), newUserPassword: passwd,
passwordHash: passwd,
}), }),
}).then((response) => { })
if (response.status === 400) { .then((res) => res.json()).then((body) => {
// 400 Bad request const response = body as AuthData;
console.log("Username is taken or invalid!"); if (response.exists && !response.success) {throw new Error(loginPage.error.unameTakenOrInvalid)}
setValid(false); getProfile(uname).then((user) => {
setValidText( setValid(true);
loginPage.error.unameTakenOrInvalid
);
} 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({
username: uname, username: user.userName,
lastSeen: Date.now(), lastSeen: Date.now(),
validUntil: futureDate.getUTCMilliseconds(), validUntil: futureDate.getUTCMilliseconds(),
}); });
document.title = `IRC User ${uname}`; document.title = `IRC User ${user.userName}`;
} });
})
.catch((error: Error) => {
setValid(false);
setValidText(error.message);
})
}); });
}; };
// login button press handler
const loginHandler = () => { // TODO Make unit test
const uname = ( const getProfile = async(userName: string): Promise<User> => {
document.getElementById("username") as HTMLInputElement const res = await (await fetch(`https://${domain}:${port}${endpoints.user}?name=${userName}`,
).value;
const passwd = encrypt(
(document.getElementById("passwd") as HTMLInputElement)
.value
);
// async invocation of Fetch API
fetch(
`http://${domain}:${port}${endpoints.user}?name=${uname}`,
{ {
method: "GET", 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;
digestMessage(
(document.getElementById("passwd") as HTMLInputElement).value
).then((passwd) => {
// async invocation of Fetch API
fetch(`https://${domain}:${port}${endpoints.auth}`, {
method: "POST",
mode: "cors", mode: "cors",
} headers: {"Content-Type": "application/json"},
) body: JSON.stringify({
.then((res) => { userName: uname,
if (res.status === 404) { userPasswordHash: passwd,
console.log(
"404 not found encountered"
);
throw new Error(
loginPage.error.unameNotExists
);
} else if (res.status === 200) {
console.log("200 OK");
}
return res.json();
}) })
.then((userObject) => { })
if (!userObject) { .then(res => res.json())
return; .then((body) => {
} const response = body as AuthData;
const user = userObject as User; if (!response.exists) {throw new Error(loginPage.error.unameNotExists)}
const validLogin = passwd === user.passwordHash; else if (!response.success) {throw new Error(loginPage.error.passwdInvalid)}
if (!validLogin) { else {
// login invalid getProfile(uname).then((user) => {
throw new Error(
loginPage.error.passwdInvalid
);
} else {
// login valid // login valid
setValid(true); setValid(true);
const validUntilDate: Date = new Date(); const validUntilDate: Date = new Date();
validUntilDate.setHours( validUntilDate.setHours(validUntilDate.getHours() + 2);
validUntilDate.getHours() + 2
);
setLogin({ setLogin({
username: user.userName, username: user.userName,
lastSeen: user.lastSeen, lastSeen: user.lastSeen,
validUntil: validUntilDate.getUTCMilliseconds(), validUntil: validUntilDate.getUTCMilliseconds(),
}); });
document.title = `IRC User ${uname}`; document.title = `IRC User ${uname}`;
});
} }
}) })
.catch((reason: Error) => { .catch((reason: Error) => {
setValid(false); setValid(false);
setValidText(reason.message); setValidText(reason.message);
}); });
});
}; };
return ( return (
<div className="login"> <div className="login">
<fieldset> <fieldset>
<legend>{loginPage.window.title}</legend> <legend>{loginPage.window.title}</legend>
<p className="uname-error-text"> <p className="uname-error-text">
{!valid && valid !== undefined {!valid && valid !== undefined ? validText : ""}
? validText
: ""}
</p> </p>
<label htmlFor="username"> <label htmlFor="username">{loginPage.window.uname}</label>
{loginPage.window.uname}
</label>
<br /> <br />
<input id="username" type="text"></input> <input id="username" type="text"></input>
<br /> <br />
<label htmlFor="passwd"> <label htmlFor="passwd">{loginPage.window.passwd}</label>
{loginPage.window.passwd}
</label>
<br /> <br />
<input id="passwd" type="password"></input> <input id="passwd" type="password"></input>
<br /> <br />

View file

@ -1,4 +1,3 @@
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,9 +24,6 @@ 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,11 +1,14 @@
export const domain = window.location.hostname; export const domain = window.location.hostname;
export const port = "8080"; export const port = "8080";
export const connectionAddress = `ws://${domain}:${port}/ws`; export const connectionAddress = `wss://${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: {

15
src/crypto.ts Normal file
View 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("");
}

View file

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