integrate the new gateway, more or less

This commit is contained in:
hippoz 2021-03-27 07:27:57 +02:00
parent e3bb4ce125
commit 6111d1dfa3
Signed by: hippoz
GPG key ID: 7C52899193467641
13 changed files with 370 additions and 679 deletions

File diff suppressed because it is too large Load diff

View file

@ -3,21 +3,20 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@testing-library/jest-dom": "^5.11.6", "@testing-library/jest-dom": "^5.11.10",
"@testing-library/react": "^11.2.2", "@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^12.6.0", "@testing-library/user-event": "^13.0.16",
"nord": "^0.2.1", "nord": "^0.2.1",
"react": "^17.0.1", "react": "^17.0.2",
"react-content-loader": "^5.1.4", "react-content-loader": "^6.0.2",
"react-dom": "^17.0.1", "react-dom": "^17.0.2",
"react-media-hook": "^0.4.9", "react-media-hook": "^0.4.9",
"react-redux": "^7.2.2", "react-redux": "^7.2.3",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "4.0.1", "react-scripts": "4.0.3",
"redux": "^4.0.5", "redux": "^4.0.5",
"sass": "^1.32.0", "sass": "^1.32.8",
"socket.io-client": "^3.0.5", "web-vitals": "^1.1.1"
"web-vitals": "^0.2.4"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",

View file

@ -1,4 +1,5 @@
import config from '../Config'; import config from '../Config';
import token from "./TokenManager";
async function APIRequest(endpoint, options) { async function APIRequest(endpoint, options) {
let res; let res;
@ -9,11 +10,6 @@ async function APIRequest(endpoint, options) {
if (!options) options = {}; if (!options) options = {};
if (!options.headers) options.headers = {}; if (!options.headers) options.headers = {};
options = {
credentials: 'include',
...options
};
try { try {
res = await fetch(`${config.apiUrl}${endpoint}`, options); res = await fetch(`${config.apiUrl}${endpoint}`, options);
json = await res.json(); json = await res.json();
@ -41,12 +37,15 @@ APIRequest.authenticated = async function(endpoint, options) {
if (!options) options = {}; if (!options) options = {};
if (!options.headers) options.headers = {}; if (!options.headers) options.headers = {};
options = { console.log(options);
credentials: 'include',
...options options.headers = {
"Authorization": token.getToken(),
...options.headers
}; };
try { try {
console.log(options);
res = await fetch(`${config.apiUrl}${endpoint}`, options); res = await fetch(`${config.apiUrl}${endpoint}`, options);
json = await res.json(); json = await res.json();
isOK = true; isOK = true;

View file

@ -1,34 +1,15 @@
import APIRequest from './APIRequest';
import gatewayConnection from '../API/Gateway/globalGatewayConnection';
import Logger from '../Util/Logger'; import Logger from '../Util/Logger';
import gateway from "./Gateway/globalGatewayConnection";
const { log: authLog, error: authError } = Logger([ 'Authenticator' ]); const { log: authLog } = Logger([ 'Authenticator' ]);
const Authenticator = { const Authenticator = {
getToken: function() { getToken: function() {
return localStorage.getItem("token"); return localStorage.getItem("token");
}, },
getLoggedInUserFromCookie: async function() { login: async function() {
authLog('Fetching current logged in user status...'); authLog('Logging in through gateway...');
const { json, isOK, err } = await APIRequest.authenticated('/api/v1/users/current/info'); return gateway.connect();
if (!isOK && err) {
authLog('Exception while fetching current logged in user status', err);
throw new Error(err);
}
if (!isOK && !err) {
authError('User is not authenticated');
return undefined;
}
if (!json || !json.user) {
authError('User is not authenticated');
return undefined;
}
authLog(`Logged in as "${json.user.username || '[undefined username]'}"`);
// NOTE(hippoz): this function has a stupid side-effect but this will have to do for now...
gatewayConnection.connect(json.user.token);
return json.user;
} }
}; };

View file

@ -1,6 +1,6 @@
import logger from "../../Util/Logger"; import logger from "../../Util/Logger";
const logGateway = logger([ "Gateway" ]); const { log: logGateway } = logger([ "Gateway" ]);
const opcodes = { const opcodes = {
0: { name: "HELLO", data: "JSON" }, 0: { name: "HELLO", data: "JSON" },
@ -46,57 +46,62 @@ const getOpcodeByName = (name) => {
class GatewayConnection { class GatewayConnection {
constructor(token, gatewayUrl) { constructor(token, gatewayUrl) {
this.ws = new WebSocket(gatewayUrl); this.token = token;
this.gatewayUrl = gatewayUrl;
this.handshakeCompleted = false;
this.sessionInformation = null;
this.ws.onopen = () => logGateway("Open");
this.ws.onclose = (e) => {
this.handshakeCompleted = false;
logGateway(`Close: ${e.code}:${e.reason}`);
this.fire("onclose", e);
};
this.ws.onmessage = (message) => {
try {
const packet = parseMessage(message.data);
if (!packet) return console.error("gateway: invalid packet from server");
switch (packet.opcodeType) {
case "HELLO": {
// Got HELLO from server, send YOO as soon as possible
logGateway("Got HELLO", packet.data);
logGateway("Sending YOO");
this.ws.send(this.packet("YOO", { token }));
break;
}
case "YOO_ACK": {
// Server accepted connection
logGateway("Got YOO_ACK", packet.data);
this.handshakeCompleted = true;
this.sessionInformation = packet.data;
logGateway("Handshake complete");
this.fire("onopen", packet.data);
break;
}
case "EVENT_CREATE_MESSAGE": {
// New message
// console.log("gateway: got new message", packet.data);
this.fire("onmessage", packet.data);
break;
}
default: {
logGateway("Got unknown packet", message.data);
break;
}
}
} catch(e) {
return console.error("err: gateway:", e);
}
};
} }
} }
GatewayConnection.prototype.connect = function() {
this.ws = new WebSocket(this.gatewayUrl);
this.handshakeCompleted = false;
this.sessionInformation = null;
this.ws.onopen = () => logGateway("Open");
this.ws.onclose = (e) => {
this.handshakeCompleted = false;
logGateway(`Close: ${e.code}:${e.reason}`);
this.fire("onclose", e);
};
this.ws.onmessage = (message) => {
try {
const packet = parseMessage(message.data);
if (!packet) return console.error("gateway: invalid packet from server");
switch (packet.opcodeType) {
case "HELLO": {
// Got HELLO from server, send YOO as soon as possible
logGateway("Got HELLO", packet.data);
logGateway("Sending YOO");
this.ws.send(this.packet("YOO", { token: this.token }));
break;
}
case "YOO_ACK": {
// Server accepted connection
logGateway("Got YOO_ACK", packet.data);
this.handshakeCompleted = true;
this.sessionInformation = packet.data;
logGateway("Handshake complete");
this.fire("onopen", packet.data);
break;
}
case "EVENT_CREATE_MESSAGE": {
// New message
// console.log("gateway: got new message", packet.data);
this.fire("onmessage", packet.data);
break;
}
default: {
logGateway("Got unknown packet", message.data);
break;
}
}
} catch(e) {
return console.error("err: gateway:", e);
}
};
};
GatewayConnection.prototype.sendMessage = function(content, channelId) { GatewayConnection.prototype.sendMessage = function(content, channelId) {
if (!this.sessionInformation) throw new Error("gateway: tried to send message before handshake completion"); if (!this.sessionInformation) throw new Error("gateway: tried to send message before handshake completion");

View file

@ -1,15 +1,14 @@
import GatewayConnection from './GatewayConnection'; import GatewayConnection from './GatewayConnection';
import config from '../../Config'; import config from '../../Config';
import Authenticator from '../Authenticator'; import token from '../TokenManager';
import store from '../../Global/store'; import store from '../../Global/store';
console.log(Authenticator);
const globalGatewayConnection = new GatewayConnection(Authenticator.getToken(), config.gatewayUrl); const globalGatewayConnection = new GatewayConnection(token.getToken(), config.gatewayUrl);
globalGatewayConnection.onopen = (sessionData) => { globalGatewayConnection.onopen = (sessionData) => {
store.dispatch({ type: 'gateway/connectionstatus', gateway: { isConnected: true } }); store.dispatch({ type: 'gateway/connectionstatus', gateway: { isConnected: true } });
store.dispatch({ type: 'authenticator/updatelocaluserobject', user: sessionData.user }); store.dispatch({ type: 'authenticator/updatelocaluserobject', user: sessionData.user });
store.dispatch({ type: 'channels/updatechannellist', user: sessionData.channels }) store.dispatch({ type: 'channels/updatechannellist', channels: sessionData.channels })
}; };
globalGatewayConnection.onmessage = (message) => { globalGatewayConnection.onmessage = (message) => {
@ -17,6 +16,7 @@ globalGatewayConnection.onmessage = (message) => {
}; };
globalGatewayConnection.onclose = function() { globalGatewayConnection.onclose = function() {
store.dispatch({ type: 'authenticator/updatelocaluserobject', user: undefined });
store.dispatch({ type: 'gateway/connectionstatus', gateway: { isConnected: false } }); store.dispatch({ type: 'gateway/connectionstatus', gateway: { isConnected: false } });
}; };

View file

@ -0,0 +1,6 @@
const Auth = {
getToken: () => localStorage.getItem("token"),
setToken: (token) => localStorage.setItem("token", token)
};
export default Auth;

View file

@ -1,15 +1,14 @@
import { useState } from 'react'; import { useState } from 'react';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
import { useDispatch } from 'react-redux';
import Notification from '../UI/Notification'; import Notification from '../UI/Notification';
import APIRequest from '../../API/APIRequest'; import APIRequest from '../../API/APIRequest';
import Authenticator from '../../API/Authenticator'; import Authenticator from '../../API/Authenticator';
import token from "../../API/TokenManager";
import { getLoginMessageFromError } from '../../Util/Errors' import { getLoginMessageFromError } from '../../Util/Errors'
export default function Login() { export default function Login() {
const history = useHistory(); const history = useHistory();
const dispatch = useDispatch();
const [ usernameInput, setUsernameInput ] = useState(); const [ usernameInput, setUsernameInput ] = useState();
const [ passwordInput, setPasswordInput ] = useState(); const [ passwordInput, setPasswordInput ] = useState();
@ -30,8 +29,7 @@ export default function Login() {
}, },
body: JSON.stringify({ body: JSON.stringify({
username: usernameInput, username: usernameInput,
password: passwordInput, password: passwordInput
alsoSetCookie: true
}) })
}); });
@ -43,9 +41,9 @@ export default function Login() {
setInfo('Something went wrong'); setInfo('Something went wrong');
return; return;
} }
const res = await Authenticator.getLoggedInUserFromCookie(); token.setToken(json.token);
dispatch({ type: 'authenticator/updatelocaluserobject', user: res }); await Authenticator.login();
history.push('/'); history.push('/');
} }

View file

@ -10,7 +10,7 @@ export default function ChannelButton({ channel, selected }) {
if (selected) buttonClasses += ' pressed'; if (selected) buttonClasses += ' pressed';
const handleClick = () => { const handleClick = () => {
if (gatewayConnection.isConnected) { if (gatewayConnection.handshakeCompleted) {
history.push(`/channels/${channel._id}`); history.push(`/channels/${channel._id}`);
} }
}; };

View file

@ -1,65 +1,19 @@
import ChannelListLoader from './ChannelListLoader'; import ChannelListLoader from './ChannelListLoader';
import ChannelButton from './ChannelButton'; import ChannelButton from './ChannelButton';
import APIRequest from '../../API/APIRequest';
import { couldNotReach } from '../../Util/Errors';
import { connect, useDispatch } from 'react-redux' import { connect } from 'react-redux'
import { useState, useEffect } from 'react';
import Logger from '../../Util/Logger';
import gatewayConnection from '../../API/Gateway/globalGatewayConnection';
const { log: loaderLog } = Logger([ 'ChannelList', 'Loader' ]); function ChannelList({ selectedChannelId, channels }) {
if (!channels) {
function ChannelList({ selectedChannelId, gatewayStatus }) { return (
const [ channelList, setChannelList ] = useState(); <div>
const [ error, setError ] = useState(); <ChannelListLoader />
</div>
const dispatch = useDispatch(); );
useEffect(() => {
loaderLog('Loading ChannelList...');
APIRequest.authenticated('/api/v1/content/channel/list?count=50')
.then(({ isOK, json }) => {
if (!isOK) return setError(true);
loaderLog('Got channel list from server, dispatching...');
setChannelList(json.channels || []);
dispatch({ type: 'channels/updatechannellist', channels: json.channels });
loaderLog('Subscribing to all channels...');
})
.catch(() => {
setError(true);
});
}, [dispatch]);
useEffect(() => {
if (!channelList) return;
if (!gatewayStatus.isConnected) return;
// TODO: IMPORTANT: Subscribing to a lot of channels puts strain on the server
gatewayConnection.subscribeToChannelChats(channelList.map(channel => channel._id));
}, [gatewayStatus, channelList]);
if (!channelList) {
if (error) {
return (
<div>
<p>
{ couldNotReach }
</p>
</div>
);
} else {
return (
<div>
<ChannelListLoader />
</div>
);
}
} else { } else {
return ( return (
<div className="channel-list"> <div className="channel-list">
{ channelList.map((channel) => ( <ChannelButton key={ channel._id } channel={ channel } selected={ (channel._id === selectedChannelId) } /> )) } { channels.map((channel) => ( <ChannelButton key={ channel._id } channel={ channel } selected={ (channel._id === selectedChannelId) } /> )) }
</div> </div>
); );
} }
@ -68,7 +22,7 @@ function ChannelList({ selectedChannelId, gatewayStatus }) {
const stateToProps = (state) => { const stateToProps = (state) => {
return { return {
selectedChannelId: state?.selectedChannelId, selectedChannelId: state?.selectedChannelId,
gatewayStatus: state?.gateway, channels: state?.channels
}; };
}; };

View file

@ -26,7 +26,7 @@ function ChannelView({ channels, messages }) {
const handleTextboxKeydown = (e) => { const handleTextboxKeydown = (e) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
gatewayConnection.sendMessage(id, textInput); gatewayConnection.sendMessage(textInput, id);
textInputRef.current.value = ''; textInputRef.current.value = '';
setTextInput(''); setTextInput('');
} }
@ -36,13 +36,13 @@ function ChannelView({ channels, messages }) {
if (!channels) return; if (!channels) return;
setChannel(channels.find(x => x._id === id)); setChannel(channels.find(x => x._id === id));
}, [ channels, id ]); }, [channels, id]);
useEffect(() => { useEffect(() => {
if (!channel) return; if (!channel) return;
dispatch({ type: 'channels/selectchannel', channelId: channel._id }); dispatch({ type: 'channels/selectchannel', channelId: channel._id });
}, [ channel, dispatch ]); }, [channel, dispatch]);
if (channel) { if (channel) {
let messagesView = messages.map(m => <Message key={ m._id } message={ m } />); let messagesView = messages.map(m => <Message key={ m._id } message={ m } />);

View file

@ -3,7 +3,6 @@ import Root from '../Home/Root';
import Authenticator from '../../API/Authenticator'; import Authenticator from '../../API/Authenticator';
import Notification from '../UI/Notification'; import Notification from '../UI/Notification';
import './../../Styles/App.scss'; import './../../Styles/App.scss';
import { couldNotReach } from '../../Util/Errors';
import ChannelView from '../Channels/ChannelView'; import ChannelView from '../Channels/ChannelView';
import Sidebar from '../UI/Sidebar'; import Sidebar from '../UI/Sidebar';
@ -12,20 +11,13 @@ import { useDispatch, connect } from 'react-redux'
import { BrowserRouter, Switch, Route } from 'react-router-dom'; import { BrowserRouter, Switch, Route } from 'react-router-dom';
function App({ user }) { function App({ user }) {
const [ notificationText, setNotificationText ] = useState(''); const [ notificationText ] = useState('');
const [ hasError, setHasError ] = useState(false); const [ hasError ] = useState(false);
const dispatch = useDispatch(); const dispatch = useDispatch();
useEffect(() => { useEffect(() => {
Authenticator.getLoggedInUserFromCookie() Authenticator.login();
.then((res) => {
dispatch({ type: 'authenticator/updatelocaluserobject', user: res });
})
.catch(() => {
setNotificationText(couldNotReach);
setHasError(true);
});
}, [dispatch]); }, [dispatch]);
if (user === null && !hasError) { if (user === null && !hasError) {

View file

@ -1,6 +1,6 @@
const config = { const config = {
apiUrl: 'http://localhost:3000', apiUrl: 'http://localhost:3000',
gatewayUrl: '', // Leave blank for it to look for the gateway on the current page, if that makes sense gatewayUrl: 'ws://localhost:3005/gateway'
}; };
export default config; export default config;