From fa8ac2cfa8a8879559f15cd7776c5dfb04a2214c Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Tue, 15 Mar 2022 01:05:44 -0700 Subject: [PATCH] added websocket to context --- net/web/src/App.js | 13 +-- net/web/src/components/Access.js | 106 ++++++++++++++++++++++ net/web/src/components/Admin.js | 4 + net/web/src/components/Root.js | 45 ++------- net/web/src/components/User.js | 29 ++++++ net/web/src/context/AppContext.js | 4 +- net/web/src/context/fetchUtil.js | 45 +++++++++ net/web/src/context/useAppContext.hook.js | 102 +++++++++++++++++---- 8 files changed, 289 insertions(+), 59 deletions(-) create mode 100644 net/web/src/components/Access.js create mode 100644 net/web/src/components/Admin.js create mode 100644 net/web/src/components/User.js create mode 100644 net/web/src/context/fetchUtil.js diff --git a/net/web/src/App.js b/net/web/src/App.js index a00974c7..659906af 100644 --- a/net/web/src/App.js +++ b/net/web/src/App.js @@ -1,17 +1,18 @@ -import React, { useContext, useState, useEffect, useRef } from 'react' import login from './login.png'; -import { Input, Button } from 'antd'; -import { UserOutlined, LockOutlined } from '@ant-design/icons'; -import 'antd/dist/antd.css'; -import { BrowserRouter as Router, Routes, Route, useHistory } from "react-router-dom"; import { AppContextProvider } from './context/AppContext'; import { Root } from './components/Root'; +import 'antd/dist/antd.css'; function App() { return ( - +
+ +
+
+ +
); } diff --git a/net/web/src/components/Access.js b/net/web/src/components/Access.js new file mode 100644 index 00000000..79c8b404 --- /dev/null +++ b/net/web/src/components/Access.js @@ -0,0 +1,106 @@ +import React, { useContext, useState, useEffect, useRef } from 'react' +import { useNavigate } from "react-router-dom"; +import { AppContext } from '../context/AppContext'; +import { Input, Button } from 'antd'; +import { UserOutlined, LockOutlined } from '@ant-design/icons'; + +export function Access() { + const [available, setAvailable] = useState(false) + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const [confirmed, setConfirmed] = useState('') + const [creatable, setCreatable] = useState(false) + const [conflict, setConflict] = useState('') + const [loginMode, setLoginMode] = useState(true); + const [context, setContext] = useState(false); + + const appContext = useContext(AppContext); + const navigate = useNavigate(); + const debounce = useRef(null) + + useEffect(() => { + console.log(appContext) + if (appContext) { + if (appContext.access === 'admin') { + navigate("/admin") + } else if (appContext.access === 'user') { + navigate("/user") + } else { + setContext(true) + appContext.actions.available().then(a => { + setAvailable(a > 0) + }).catch(err => { + console.log(err) + }) + } + } + }, [appContext, navigate]) + + const usernameSet = (name) => { + setCreatable(false) + setUsername(name) + clearTimeout(debounce.current) + debounce.current = setTimeout(async () => { + let valid = await appContext.actions.username(name) + setCreatable(valid) + if (!valid) { + setConflict('not available') + } else { + setConflict('') + } + }, 500) + } + + const onLogin = async () => { + try { + await appContext.actions.login(username, password) + } catch(err) { + window.alert(err) + } + } + + const onCreate = async () => { + try { + await appContext.actions.create(username, password) + } + catch(err) { + window.alert(err) + } + } + + if (context && loginMode) { + return ( +
+
+
indicom
+
+ Communication for the Decentralized Web +
+ usernameSet(e.target.value)} value={username} placeholder="username" prefix={} style={{ marginTop: '16px' }} /> + setPassword(e.target.value)} value={password} placeholder="password" prefix={} style={{ marginTop: '16px' }} /> + +
+ +
+ ) + } + if (context && !loginMode) { + return ( +
+
+
indicom
+
+ Communication for the Decentralized Web +
+ usernameSet(e.target.value)} value={username} placeholder="username" prefix={} style={{ marginTop: '16px' }} /> + setPassword(e.target.value)} value={password} placeholder="password" prefix={} style={{ marginTop: '16px' }} /> + setConfirmed(e.target.value)} value={confirmed} placeholder="confirm password" prefix={} style={{ marginTop: '16px' }} /> + +
+ +
+ ) + } + return <> +} + diff --git a/net/web/src/components/Admin.js b/net/web/src/components/Admin.js new file mode 100644 index 00000000..f92fcac2 --- /dev/null +++ b/net/web/src/components/Admin.js @@ -0,0 +1,4 @@ +export function Admin() { + return
ADMIN
+} + diff --git a/net/web/src/components/Root.js b/net/web/src/components/Root.js index 6fb84487..d6e95347 100644 --- a/net/web/src/components/Root.js +++ b/net/web/src/components/Root.js @@ -1,44 +1,19 @@ -import React, { useContext, useState, useEffect, useRef } from 'react' -import { BrowserRouter as Router, Routes, Route, useNavigate } from "react-router-dom"; -import { AppContext } from '../context/AppContext'; +import { HashRouter as Router, Routes, Route } from "react-router-dom"; +import { Access } from './Access'; +import { Admin } from './Admin'; +import { User } from './User'; +import 'antd/dist/antd.css'; export function Root() { return ( - - } /> - } /> - } /> - + + } /> + } /> + } /> + ) } -function About() { - return (
ABOUT
) -} - -function Topic() { - return (
TOPIC
) -} - -function Empty() { - - const appContext = useContext(AppContext); - const navigate = useNavigate(); - - useEffect(() => { - console.log(appContext) - if (appContext.state) { - if (appContext.state.appToken) { - navigate("/topic") - } else { - navigate("/about") - } - } - }) - - return (
EMPTY
) -} - diff --git a/net/web/src/components/User.js b/net/web/src/components/User.js new file mode 100644 index 00000000..6c397cfe --- /dev/null +++ b/net/web/src/components/User.js @@ -0,0 +1,29 @@ +import React, { useContext, useState, useEffect } from 'react' +import { useNavigate } from "react-router-dom"; +import { AppContext } from '../context/AppContext'; +import { Button } from 'antd'; + +export function User() { + const [context, setContext] = useState(false) + const appContext = useContext(AppContext); + const navigate = useNavigate(); + + useEffect(() => { + if (appContext) { + if (appContext.access !== 'user') { + navigate("/") + } + setContext(true) + } + }, [appContext, navigate]) + + const onLogout = () => { + appContext.actions.logout() + } + + if (context) { + return + } + return <> +} + diff --git a/net/web/src/context/AppContext.js b/net/web/src/context/AppContext.js index 3ce23469..6f136a2d 100644 --- a/net/web/src/context/AppContext.js +++ b/net/web/src/context/AppContext.js @@ -4,9 +4,9 @@ import useAppContext from './useAppContext.hook'; export const AppContext = createContext({}); export function AppContextProvider({ children }) { - const { state, actions } = useAppContext(); + const state = useAppContext(); return ( - + {children} ); diff --git a/net/web/src/context/fetchUtil.js b/net/web/src/context/fetchUtil.js new file mode 100644 index 00000000..4058850f --- /dev/null +++ b/net/web/src/context/fetchUtil.js @@ -0,0 +1,45 @@ +var base64 = require('base-64'); + +const FETCH_TIMEOUT = 15000; + +function checkResponse(response) { + if(response.status >= 400 && response.status < 600) { + throw new Error(response.url + " failed"); + } +} + +async function fetchWithTimeout(url, options) { + return Promise.race([ + fetch(url, options).catch(err => { throw new Error(url + ' failed'); }), + new Promise((_, reject) => setTimeout(() => reject(new Error(url + ' timeout')), FETCH_TIMEOUT)) + ]); +} + +export async function getAvailable() { + let available = await fetchWithTimeout("/account/available", { method: 'GET', timeout: FETCH_TIMEOUT }) + checkResponse(available) + return await available.json() +} + +export async function getUsername(name: string) { + let available = await fetchWithTimeout('/account/username?name=' + encodeURIComponent(name), { method: 'GET', timeout: FETCH_TIMEOUT }) + checkResponse(available) + return await available.json() +} + +export async function setLogin(username: string, password: string) { + let headers = new Headers() + headers.append('Authorization', 'Basic ' + base64.encode(username + ":" + password)); + let app = { Name: "indicom", Description: "decentralized communication" } + let login = await fetchWithTimeout('/account/apps', { method: 'POST', timeout: FETCH_TIMEOUT, body: JSON.stringify(app), headers: headers }) + checkResponse(login) + return await login.json() +} + +export async function createAccount(username: string, password: string) { + let headers = new Headers() + headers.append('Credentials', 'Basic ' + base64.encode(username + ":" + password)); + let profile = await fetchWithTimeout("/account/profile", { method: 'POST', timeout: FETCH_TIMEOUT, headers: headers }) + checkResponse(profile); + return await profile.json() +} diff --git a/net/web/src/context/useAppContext.hook.js b/net/web/src/context/useAppContext.hook.js index ff86d15d..c0560c9f 100644 --- a/net/web/src/context/useAppContext.hook.js +++ b/net/web/src/context/useAppContext.hook.js @@ -1,24 +1,101 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useRef } from 'react'; +import { getAvailable, getUsername, setLogin, createAccount } from './fetchUtil'; export default function useAppContext() { const [state, setState] = useState(null); + const ws = useRef(null); - const actions = { + const login = async (username: string, password: string) => { + let access = await setLogin(username, password) + setState({ appToken: access, access: 'user', actions: userActions }); + localStorage.setItem("session", JSON.stringify({ token: access, access: 'user' })); + connectStatus(access); + } + + const create = async (username: string, password: string) => { + await createAccount(username, password); + try { + await login(username, password) + } catch(err) { + throw new Error("login failed after account createion") + } + } + + const logout = () => { + ws.current.onclose = () => {} + ws.current.close(1000, "bye") + ws.current = null + setState({ actions: accessActions }) + localStorage.removeItem("session"); + } + + const userActions = { setListener: setListener, clearListener: clearListener, - login: login, logout: logout, } - + + const adminActions = { + logout: logout, + } + + const accessActions = { + login: login, + create: create, + username: getUsername, + available: getAvailable, + } + + const connectStatus = (token: string) => { + ws.current = new WebSocket("wss://" + window.location.host + "/status"); + ws.current.onmessage = (ev) => { + console.log(ev) + } + ws.current.onclose = () => { + console.log('ws close') + setTimeout(() => { + if (ws.current != null) { + ws.current.onmessage = () => {} + ws.current.onclose = () => {} + ws.current.onopen = () => {} + ws.current.onerror = () => {} + connectStatus(token) + } + }, 2000) + } + ws.current.onopen = () => { + ws.current.send(JSON.stringify({ AppToken: token })) + } + ws.current.error = () => { + console.log('ws error') + } + } + useEffect(() => { - const token = localStorage.getItem('app_token'); - if (token) { - setState({ appToken: token }) + const storage = localStorage.getItem('session'); + if (storage != null) { + try { + const session = JSON.parse(storage) + if (session?.access === 'admin') { + setState({ appToken: session.token, access: session.access, actions: adminActions }) + connectStatus(session.token); + } else if (session?.access === 'user') { + setState({ appToken: session.token, access: session.access, actions: userActions }) + connectStatus(session.token); + } else { + setState({ actions: accessActions }) + } + } + catch(err) { + console.log(err) + setState({ actions: accessActions }) + } } else { - setState({ appToken: null }) + setState({ actions: accessActions }) } }, []); - return { state, actions }; + + return state; } function setListener(name: string, callback: (objectId: string) => void) { @@ -29,11 +106,4 @@ function clearListener(callback: (objectId: string) => void) { return } -async function login(username: string, password: string) { - return -} - -async function logout() { - return -}