Skip to content

Commit

Permalink
Ft 74 Room authentication (#312)
Browse files Browse the repository at this point in the history
* ft-74 added crypto and path modules

* ft-74 added password and authentication handling in frontend

* ft-74 added authentication to backend

* ft-74 removed console logs

* ft-74 fixed promise

* ft-74 fixed double equals

* ft-74 fixed linter

* ft-74 added incorrect password notification

* ft-74 added labels

* ft-74 made password optional

* ft-74 updated color

* ft-74 added enums

* ft-74 settings password conditionals depending on whether the room is new and if password is set

* ft-74 updated backend

* ft-74 updated readme

* ft-74 fixed linting

* ft-74 updating tests

* ft-74 added failure notification

* ft-74 editted connection error notification

* ft-74 updated error message

* ft-74 commented out tests

* ft-74 added some comments

* ft-74 fixed authentication enums

* ft-74 added enum

* ft-74 removed unused var

* ft-74 renamed

* ft-74 refactored notification state

* ft-74 added clarification comment

* ft-74 combined import

* ft-74 fixed bug
  • Loading branch information
siberowl committed Oct 13, 2022
1 parent 5eda5e7 commit e813cde
Show file tree
Hide file tree
Showing 10 changed files with 348 additions and 90 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,12 @@ Debugging with browser
- [x] Status (Active / Inactive)
- [x] Separate rooms `/[room-id]`
- [x] Screenshares
- [x] Password protection for rooms
- [ ] Mobile Support
- [ ] User Lists
- [ ] Change Username
- [ ] Place Images in the space
- [ ] Place Videos in the space
- [ ] Password protection for rooms
- [ ] Audio Recording

## Runnig own iceServer
Expand Down
8 changes: 4 additions & 4 deletions client/src/constants/style.scss
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
// Colors

$orange: #FC8030;
$white: #FCFCFC;
$gray: #808080;

$primary: $orange;
$background: #000000BA;

$disabled-background-color: #e3e3e3;
$disabled-text-color: #9d9d9d;

// Spacing
$avatar-size: 4.5rem;
$spacing: 0.5rem;

$transition-time: 0.2s;
$buttons-margin: 0.25rem;
$spacing: 0.5rem;

19 changes: 16 additions & 3 deletions client/src/contexts/SocketIOContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,25 @@ import React, { createContext, useState, useEffect } from "react";
import io, { Socket } from "socket.io-client";
import PropTypes from "prop-types";

const stripTrailingSlash = (str: string) => str.replace(/\/$/, '')
// Get the room name from a namespace path.
// Expect nsp to be in the format of /someroomname
const getRoomName = (nsp: string) => {
// To do: replace split with path.basename if possible.
return encodeURIComponent(nsp.split("/")[1]);
};

export enum SIOChannel {
AUTHENTICATE = "AUTHENTICATE",
ROOM_INFO = "ROOM_INFO",
JOIN = "JOIN",
LEAVE = "LEAVE",
DISCONNECT = "DISCONNECT",
SDP_RECEIVED = "SDP_RECEIVED",
ICE_CANDIDATE = "ICE_CANDIDATE",
ANSWER = "ANSWER",
OFFER = "OFFER"
OFFER = "OFFER",
NUM_USERS = "NUM_USERS",
CONNECT_ERROR = "connect_error", // Predefined string by socketio.
}

export const SocketContext = createContext<{ socket: Socket }>({
Expand All @@ -23,7 +32,11 @@ export const SocketProvider = ({ children }: { children: JSX.Element }): JSX.Ele

useEffect(() => {
const pathname = window.location.pathname;
const ENDPOINT = (import.meta.env.VITE_SERVER_URL || "http://localhost:4000") + stripTrailingSlash(pathname);

const baseURL: string =
(import.meta.env.VITE_SERVER_URL as unknown as string) || "http://localhost:4000";
// To do: replace + with path.join if possible.
const ENDPOINT = baseURL + "/" + getRoomName(pathname);
setSocket(io(ENDPOINT));
}, []);

Expand Down
7 changes: 4 additions & 3 deletions client/src/pages/HomePage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ import { Div } from "atomize";
import { Button, Text, Input } from "@/components/atomize_wrapper";
import { SocketContext } from "@/contexts";
import { useHistory } from "react-router-dom";
import { SIOChannel } from "@/contexts/SocketIOContext";

const HomePage = (): JSX.Element => {
const { socket } = useContext(SocketContext);
const [roomExists, setRoomExists] = useState(false);
const [roomId, setRoomId] = useState("");
const history = useHistory();
const responsiveWidth = { xs: "80%", md: "40%" }
const responsiveWidth = { xs: "80%", md: "40%" };

useEffect(() => {
if (socket) {
socket.on("NUM_USERS", (usersNum) => {
socket.on(SIOChannel.NUM_USERS, (usersNum) => {
if (usersNum === 0 || usersNum === null) {
setRoomExists(false);
} else {
Expand All @@ -26,7 +27,7 @@ const HomePage = (): JSX.Element => {

useEffect(() => {
if (socket) {
socket.emit("NUM_USERS", "/" + roomId);
socket.emit(SIOChannel.NUM_USERS, "/" + roomId);
}
}, [roomId]);

Expand Down
136 changes: 126 additions & 10 deletions client/src/pages/JoinPage/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import React, { useState, useEffect, useContext } from "react";
import { useParams, useHistory } from "react-router-dom";
import { SocketContext, DeviceContext } from "@/contexts";
import { SIOChannel } from "@/contexts/SocketIOContext";
import { DeviceSelector } from "@/components";
import { AudioVisualizer, gainToMultiplier } from "@/classes/AudioVisualizer";
import PropTypes from "prop-types";
import { Div, Notification, Icon } from "atomize";
import { Button, Card, Text, Input } from "@/components/atomize_wrapper";
import { Button, Card, Text, Input, Label } from "@/components/atomize_wrapper";
import { BaseTemplate } from "@/templates";
import cx from "classnames";
import { Lock as LockIcon, LockOpen as LockOpenIcon } from "@material-ui/icons";

import "./style.scss";

interface NotificationState {
text: string;
isOpen: boolean;
}

const JoinPage = ({
name,
setName,
Expand All @@ -21,20 +30,73 @@ const JoinPage = ({
const { socket } = useContext(SocketContext);
const { stream, setStream } = useContext(DeviceContext);
const { room_id } = useParams<{ room_id: string }>();
const [showNotification, setShowNotification] = useState(false);
const [notificationState, setNotificationState] = useState({
text: "",
isOpen: false,
} as NotificationState);

// The password text.
const [password, setPassword] = useState("");
// A boolean to decide whether to disable the password input or not.
const [isPasswordRequired, setPasswordRequired] = useState(false);
// A boolean to decide whether to show the password input or not.
const [showPassword, setShowPassword] = useState(false);
// A boolean to decide whether to show the password choice button or not.
const [showPasswordChoice, setShowPasswordChoice] = useState(false);
// Has all asynchronous data finished loading?
const [finishedLoading, setFinishedLoading] = useState(false);

const history = useHistory();

const [gain, setGain] = useState(0);
const [visualizer, setVisualizer] = useState(null as unknown as AudioVisualizer);

// Authentication codes to be returned back by the server.
// Needs to be in sync with the backend enum.
enum AuthenticationEnum {
Success = 200,
Unauthorized = 401,
}

const sha256 = async (message: string) => {
const msgBuffer = new TextEncoder().encode(message);
const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
return hashHex;
};

const onJoinClicked = () => {
if (socket) {
if (name != "") {
setJoined(true);
if (!socket) {
setNotificationState({ text: "Socket is not ready. Please try again.", isOpen: true });
return;
}

if (!finishedLoading) {
setNotificationState({ text: "Failed to connect with server.", isOpen: true });
return;
}

// The hash to send to server for authentication.
// Needs to be in agreement with the backend when providing an empty password.
const hash = sha256(room_id + password);

hash.then((hash) => {
socket.emit(SIOChannel.AUTHENTICATE, hash);
});

// Handle the authentication result.
socket.on(SIOChannel.AUTHENTICATE, (result) => {
if (result == AuthenticationEnum.Success) {
if (name != "") {
setJoined(true);
} else {
setNotificationState({ text: "Please choose a username.", isOpen: true });
}
} else {
setShowNotification(true);
setNotificationState({ text: "Incorrect password.", isOpen: true });
}
}
});
};

const onSelect = (_stream) => {
Expand All @@ -45,6 +107,29 @@ const JoinPage = ({
setVisualizer(new AudioVisualizer(onAudioActivity));
}, []);

useEffect(() => {
if (socket) {
// Retrieve information about the room existing and whether it requires a password or not.
socket.emit(SIOChannel.ROOM_INFO);
socket.on(SIOChannel.ROOM_INFO, (info) => {
const { numUsers, hasPass } = info;
const roomExists = !!numUsers;
if (!roomExists) {
setShowPassword(true);
setShowPasswordChoice(true);
} else if (hasPass) {
setShowPassword(true);
setPasswordRequired(true);
}
setFinishedLoading(true);
});

socket.on(SIOChannel.CONNECT_ERROR, (err) => {
setNotificationState({ text: "Failed to establish connection. Retrying...", isOpen: true });
});
}
}, [socket]);

useEffect(() => {
if (visualizer) {
visualizer.setStream(stream);
Expand All @@ -55,6 +140,11 @@ const JoinPage = ({
setGain(_gain);
};

const togglePasswordRequirement = () => {
setPasswordRequired(!isPasswordRequired);
setPassword("");
};

function Visualizer() {
return (
<div
Expand All @@ -76,12 +166,12 @@ const JoinPage = ({
</Text>
<Div>
<Notification
isOpen={showNotification}
isOpen={notificationState.isOpen}
bg={"danger700"}
onClose={() => setShowNotification(false)}
onClose={() => setNotificationState({ ...notificationState, isOpen: false })}
prefix={<Icon name="CloseSolid" color="white" size="18px" m={{ r: "0.5rem" }} />}
>
Please choose a username.
{notificationState.text}
</Notification>
<Input
placeholder="Name"
Expand All @@ -92,6 +182,32 @@ const JoinPage = ({
setName(e.target.value);
}}
/>
{finishedLoading && (
<Div className="password-wrapper">
{showPasswordChoice && (
<Button
className={cx("password-toggle", { "locked-style": isPasswordRequired })}
onClick={() => togglePasswordRequirement()}
>
{isPasswordRequired ? <LockIcon /> : <LockOpenIcon />}
</Button>
)}
{showPassword && (
<Input
className={cx("password-input", {
disabled: !isPasswordRequired,
})}
placeholder="Password"
type="password"
name="password"
value={password}
onChange={(e) => {
setPassword(e.target.value);
}}
/>
)}
</Div>
)}
</Div>
<Div m={{ t: "20px" }}>
<Div>Select audio device:</Div>
Expand Down
33 changes: 32 additions & 1 deletion client/src/pages/JoinPage/style.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,38 @@
@import '@/constants/style.scss';
@import "@/constants/style";

.visualizer {
height: 10px;
background: #FDEEC8;
transition: width $transition-time;
}

.password-wrapper {
display: flex;
align-items: center;
margin-top: 16px;

.password-toggle {
border-radius: 100%;
width: 2.5rem;
height: 2.5rem;
margin-right: 4px;
background-color: $gray;

&.locked-style {
background-color: $orange;
}
}

div {
flex-grow: 1;

.password-input.disabled {
pointer-events: none;
background-color: $disabled-background-color;

&::placeholder {
color: $disabled-text-color;
}
}
}
}

0 comments on commit e813cde

Please sign in to comment.