Compare commits
6 commits
Author | SHA1 | Date | |
---|---|---|---|
a38c09df73 |
|||
3fb94984ec |
|||
3ceee1db4f |
|||
eb76e7203b |
|||
e205d6e149 |
|||
3957f49e49 |
9 changed files with 17921 additions and 17868 deletions
35443
package-lock.json
generated
35443
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "epq-web-project",
|
||||
"version": "0.2.1",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@stomp/stompjs": "^7.0.0",
|
||||
|
@ -11,6 +11,7 @@
|
|||
"@types/node": "^16.18.68",
|
||||
"@types/react": "^18.2.45",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"ed25519": "^0.0.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-scripts": "5.0.1",
|
||||
|
@ -41,5 +42,8 @@
|
|||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/ed25519": "^0.0.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ const Wrapper = (): React.ReactElement => {
|
|||
};
|
||||
const setNameOnServer = async (name: string) => {
|
||||
const responseRaw = await fetch(
|
||||
`http://${domain}:${port}${endpoints.user}`,
|
||||
`https://${domain}:${port}${endpoints.user}`,
|
||||
{
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
|
|
|
@ -1,171 +1,158 @@
|
|||
import { useContext, useState } from "react";
|
||||
import { contentTypes, domain, endpoints, port } from "../consts";
|
||||
import { LangContext, LoginType } from "../context";
|
||||
import { User } from "../type/userTypes";
|
||||
import { AuthData, User } from "../type/userTypes";
|
||||
import "./Login.css";
|
||||
import strings from "../Intl/strings.json";
|
||||
import { ECDH } from "crypto";
|
||||
import { digestMessage } from "../crypto";
|
||||
const encrypt = (rawPasswordString: string) => {
|
||||
// TODO Encryption method stub
|
||||
return rawPasswordString;
|
||||
// TODO Encryption method stub
|
||||
return rawPasswordString;
|
||||
};
|
||||
export const Login = ({
|
||||
setLogin,
|
||||
setLogin,
|
||||
}: {
|
||||
setLogin: (newLogin: LoginType | undefined) => void;
|
||||
setLogin: (newLogin: LoginType | undefined) => void;
|
||||
}): React.ReactElement => {
|
||||
const [valid, setValid] = useState<boolean | undefined>(undefined);
|
||||
const [validText, setValidText] = useState<string | undefined>();
|
||||
const lang = useContext(LangContext);
|
||||
const loginPage = strings[lang].login;
|
||||
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",
|
||||
mode: "cors",
|
||||
headers: contentTypes.json,
|
||||
body: JSON.stringify({
|
||||
userName: uname,
|
||||
dateJoined: Date.now(),
|
||||
passwordHash: passwd,
|
||||
}),
|
||||
}).then((response) => {
|
||||
if (response.status === 400) {
|
||||
// 400 Bad request
|
||||
console.log("Username is taken or invalid!");
|
||||
setValid(false);
|
||||
setValidText(
|
||||
loginPage.error.unameTakenOrInvalid
|
||||
);
|
||||
} else if (response.status === 200) {
|
||||
// 200 OK
|
||||
const futureDate = new Date();
|
||||
futureDate.setHours(futureDate.getHours() + 2);
|
||||
setLogin({
|
||||
username: uname,
|
||||
lastSeen: Date.now(),
|
||||
validUntil: futureDate.getUTCMilliseconds(),
|
||||
});
|
||||
document.title = `IRC User ${uname}`;
|
||||
}
|
||||
});
|
||||
};
|
||||
// login button press handler
|
||||
const loginHandler = () => {
|
||||
const uname = (
|
||||
document.getElementById("username") as HTMLInputElement
|
||||
).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",
|
||||
mode: "cors",
|
||||
}
|
||||
)
|
||||
.then((res) => {
|
||||
if (res.status === 404) {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
const user = userObject as User;
|
||||
const validLogin = passwd === user.passwordHash;
|
||||
if (!validLogin) {
|
||||
// login invalid
|
||||
throw new Error(
|
||||
loginPage.error.passwdInvalid
|
||||
);
|
||||
} else {
|
||||
// login valid
|
||||
setValid(true);
|
||||
const validUntilDate: Date = new Date();
|
||||
validUntilDate.setHours(
|
||||
validUntilDate.getHours() + 2
|
||||
);
|
||||
setLogin({
|
||||
username: user.userName,
|
||||
lastSeen: user.lastSeen,
|
||||
validUntil: validUntilDate.getUTCMilliseconds(),
|
||||
});
|
||||
document.title = `IRC User ${uname}`;
|
||||
}
|
||||
})
|
||||
.catch((reason: Error) => {
|
||||
setValid(false);
|
||||
setValidText(reason.message);
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div className="login">
|
||||
<fieldset>
|
||||
<legend>{loginPage.window.title}</legend>
|
||||
<p className="uname-error-text">
|
||||
{!valid && valid !== undefined
|
||||
? validText
|
||||
: ""}
|
||||
</p>
|
||||
<label htmlFor="username">
|
||||
{loginPage.window.uname}
|
||||
</label>
|
||||
<br />
|
||||
<input id="username" type="text"></input>
|
||||
<br />
|
||||
<label htmlFor="passwd">
|
||||
{loginPage.window.passwd}
|
||||
</label>
|
||||
<br />
|
||||
<input id="passwd" type="password"></input>
|
||||
<br />
|
||||
<button
|
||||
disabled={valid}
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
loginHandler();
|
||||
}}
|
||||
>
|
||||
{loginPage.window.login}
|
||||
</button>
|
||||
<button
|
||||
disabled={valid}
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
registrationHandler();
|
||||
}}
|
||||
>
|
||||
{loginPage.window.register}
|
||||
</button>
|
||||
<button
|
||||
disabled={!valid}
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
setLogin(undefined);
|
||||
setValid(undefined);
|
||||
}}
|
||||
>
|
||||
{loginPage.window.logout}
|
||||
</button>
|
||||
</fieldset>
|
||||
</div>
|
||||
);
|
||||
const [valid, setValid] = useState<boolean | undefined>(undefined);
|
||||
const [validText, setValidText] = useState<string | undefined>();
|
||||
const lang = useContext(LangContext);
|
||||
const loginPage = strings[lang].login;
|
||||
// TODO mk unit test
|
||||
const registrationHandler = () => {
|
||||
const uname = (document.getElementById("username") as HTMLInputElement)
|
||||
.value;
|
||||
digestMessage((document.getElementById("passwd") as HTMLInputElement).value).then((passwd) => {
|
||||
fetch(`https://${domain}:${port}${endpoints.register}`, {
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
headers: contentTypes.json,
|
||||
body: JSON.stringify({
|
||||
userName: uname,
|
||||
newUserPassword: passwd,
|
||||
}),
|
||||
})
|
||||
.then((res) => res.json()).then((body) => {
|
||||
const response = body as AuthData;
|
||||
if (response.exists && !response.success) {throw new Error(loginPage.error.unameTakenOrInvalid)}
|
||||
getProfile(uname).then((user) => {
|
||||
setValid(true);
|
||||
const futureDate = new Date();
|
||||
futureDate.setHours(futureDate.getHours() + 2);
|
||||
setLogin({
|
||||
username: user.userName,
|
||||
lastSeen: Date.now(),
|
||||
validUntil: futureDate.getUTCMilliseconds(),
|
||||
});
|
||||
document.title = `IRC User ${user.userName}`;
|
||||
});
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
setValid(false);
|
||||
setValidText(error.message);
|
||||
})
|
||||
});
|
||||
};
|
||||
|
||||
// TODO Make unit test
|
||||
const getProfile = async(userName: string): Promise<User> => {
|
||||
const res = await (await fetch(`https://${domain}:${port}${endpoints.user}?name=${userName}`,
|
||||
{
|
||||
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",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({
|
||||
userName: uname,
|
||||
userPasswordHash: passwd,
|
||||
})
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then((body) => {
|
||||
const response = body as AuthData;
|
||||
if (!response.exists) {throw new Error(loginPage.error.unameNotExists)}
|
||||
else if (!response.success) {throw new Error(loginPage.error.passwdInvalid)}
|
||||
else {
|
||||
getProfile(uname).then((user) => {
|
||||
// login valid
|
||||
setValid(true);
|
||||
const validUntilDate: Date = new Date();
|
||||
validUntilDate.setHours(validUntilDate.getHours() + 2);
|
||||
setLogin({
|
||||
username: user.userName,
|
||||
lastSeen: user.lastSeen,
|
||||
validUntil: validUntilDate.getUTCMilliseconds(),
|
||||
});
|
||||
document.title = `IRC User ${uname}`;
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((reason: Error) => {
|
||||
setValid(false);
|
||||
setValidText(reason.message);
|
||||
});
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div className="login">
|
||||
<fieldset>
|
||||
<legend>{loginPage.window.title}</legend>
|
||||
<p className="uname-error-text">
|
||||
{!valid && valid !== undefined ? validText : ""}
|
||||
</p>
|
||||
<label htmlFor="username">{loginPage.window.uname}</label>
|
||||
<br />
|
||||
<input id="username" type="text"></input>
|
||||
<br />
|
||||
<label htmlFor="passwd">{loginPage.window.passwd}</label>
|
||||
<br />
|
||||
<input id="passwd" type="password"></input>
|
||||
<br />
|
||||
<button
|
||||
disabled={valid}
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
loginHandler();
|
||||
}}
|
||||
>
|
||||
{loginPage.window.login}
|
||||
</button>
|
||||
<button
|
||||
disabled={valid}
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
registrationHandler();
|
||||
}}
|
||||
>
|
||||
{loginPage.window.register}
|
||||
</button>
|
||||
<button
|
||||
disabled={!valid}
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
setLogin(undefined);
|
||||
setValid(undefined);
|
||||
}}
|
||||
>
|
||||
{loginPage.window.logout}
|
||||
</button>
|
||||
</fieldset>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { useState } from "react";
|
||||
import "./Sidebar.css";
|
||||
import { SidebarMenu } from "./Components/SidebarMenu";
|
||||
export const Sidebar = ({
|
||||
|
|
|
@ -24,9 +24,6 @@ export const Topbar = ({
|
|||
<h1 className="topbar-span children">
|
||||
{strings[lang].homepage.title}
|
||||
</h1>
|
||||
{/* <p className="topbar-span children">
|
||||
{strings[lang].homepage.description}
|
||||
</p> */}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
export const domain = window.location.hostname;
|
||||
export const port = "8080";
|
||||
export const connectionAddress = `ws://${domain}:${port}/ws`;
|
||||
export const connectionAddress = `wss://${domain}:${port}/ws`;
|
||||
export const endpoints = {
|
||||
destination: "/app/chat",
|
||||
subscription: "/sub/chat",
|
||||
history: "/api/v1/msg/",
|
||||
user: "/api/v1/user",
|
||||
auth: "/api/v1/auth",
|
||||
register: "/api/v1/register",
|
||||
oauth2: "",
|
||||
};
|
||||
export const contentTypes = {
|
||||
json: {
|
||||
|
|
15
src/crypto.ts
Normal file
15
src/crypto.ts
Normal 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("");
|
||||
}
|
|
@ -11,3 +11,10 @@ export type User = {
|
|||
passwordHash: string;
|
||||
// avatar: UserAvatar;
|
||||
};
|
||||
export type AuthData = {
|
||||
success: boolean;
|
||||
hasProfile: boolean;
|
||||
exists: boolean;
|
||||
authMessage: string;
|
||||
authResponseTimestampMillis: number;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue