session tokens!
parent
30ebc0fcf1
commit
93ad0dfb1a
|
|
@ -6,6 +6,8 @@ export {
|
||||||
getClientId,
|
getClientId,
|
||||||
setClientId,
|
setClientId,
|
||||||
getHomeServer,
|
getHomeServer,
|
||||||
setHomeServer
|
setHomeServer,
|
||||||
|
getSessionToken,
|
||||||
|
setSessionToken
|
||||||
} from './settings';
|
} from './settings';
|
||||||
export {versions} from './versions';
|
export {versions} from './versions';
|
||||||
|
|
@ -15,6 +15,7 @@ const appdataPath = process.env.APPDATA || // windows
|
||||||
const cornerDataPath = resolve(appdataPath, 'corner');
|
const cornerDataPath = resolve(appdataPath, 'corner');
|
||||||
const clientIdPath = resolve(cornerDataPath, 'clientId');
|
const clientIdPath = resolve(cornerDataPath, 'clientId');
|
||||||
const homeServerPath = resolve(cornerDataPath, 'homeServer');
|
const homeServerPath = resolve(cornerDataPath, 'homeServer');
|
||||||
|
const sessionTokenPath = resolve(cornerDataPath, 'sessionToken');
|
||||||
|
|
||||||
// --- setup ---
|
// --- setup ---
|
||||||
|
|
||||||
|
|
@ -22,6 +23,10 @@ if(!existsSync(cornerDataPath))
|
||||||
mkdirSync(cornerDataPath);
|
mkdirSync(cornerDataPath);
|
||||||
if(!existsSync(clientIdPath))
|
if(!existsSync(clientIdPath))
|
||||||
writeFileSync(clientIdPath, '');
|
writeFileSync(clientIdPath, '');
|
||||||
|
if(!existsSync(homeServerPath))
|
||||||
|
writeFileSync(homeServerPath, '');
|
||||||
|
if(!existsSync(sessionTokenPath))
|
||||||
|
writeFileSync(sessionTokenPath, '');
|
||||||
|
|
||||||
// --- helpers ---
|
// --- helpers ---
|
||||||
|
|
||||||
|
|
@ -60,3 +65,13 @@ export function setHomeServer(url: string) {
|
||||||
}
|
}
|
||||||
writeFileSync(homeServerPath, url);
|
writeFileSync(homeServerPath, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getSessionToken() {
|
||||||
|
const token = readFileSync(sessionTokenPath).toString();
|
||||||
|
if(token.length !== 512) return null;
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setSessionToken(token: string) {
|
||||||
|
writeFileSync(sessionTokenPath, token);
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,12 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<!-- <meta http-equiv="Content-Security-Policy" content="script-src 'self' blob:"> -->
|
<!-- <meta http-equiv="Content-Security-Policy" content="script-src 'self' blob:"> -->
|
||||||
|
<meta http-equiv="Content-Security-Policy" content="
|
||||||
|
default-src 'self' data: https://ssl.gstatic.com https://fonts.gstatic.com;
|
||||||
|
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
|
||||||
|
media-src *;
|
||||||
|
img-src 'self' data: content:;
|
||||||
|
connect-src *;">
|
||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
<meta content="width=device-width, initial-scale=1.0" name="viewport">
|
||||||
<title>Vite App</title>
|
<title>Vite App</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { createContext, PropsWithChildren, ReactNode, useEffect, useMemo } from "react";
|
||||||
|
import { connectApi } from "../lib/api";
|
||||||
|
import { usePrevious } from "./usePrevious";
|
||||||
|
|
||||||
|
interface ServerConnectionProps {
|
||||||
|
children: ReactNode,
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ServerConnectionContext = createContext<ReturnType<typeof connectApi>>({
|
||||||
|
async send() {
|
||||||
|
throw new Error('attempted to send an api call with no connection context');
|
||||||
|
},
|
||||||
|
registerRouter() {
|
||||||
|
throw new Error('attempted to register an api listener with no connection context');
|
||||||
|
},
|
||||||
|
unregisterRouter() {},
|
||||||
|
destroy() {}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default function ServerConnection(props: ServerConnectionProps) {
|
||||||
|
|
||||||
|
const serverConnection = useMemo(() => {
|
||||||
|
return connectApi(props.url);
|
||||||
|
}, [props.url]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if(!serverConnection) return;
|
||||||
|
serverConnection.destroy();
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return <ServerConnectionContext.Provider value={serverConnection}>
|
||||||
|
{props.children}
|
||||||
|
</ServerConnectionContext.Provider>
|
||||||
|
}
|
||||||
|
|
@ -3,18 +3,7 @@ import 'reactjs-popup/dist/index.css';
|
||||||
import { useApi } from '../lib/useApi';
|
import { useApi } from '../lib/useApi';
|
||||||
import { ClientIdContext } from '../pages/App';
|
import { ClientIdContext } from '../pages/App';
|
||||||
import QR from 'qrcode';
|
import QR from 'qrcode';
|
||||||
|
import { usePrevious } from './usePrevious';
|
||||||
function usePrevious(value: any) {
|
|
||||||
// The ref object is a generic container whose current property is mutable ...
|
|
||||||
// ... and can hold any value, similar to an instance property on a class
|
|
||||||
const ref = useRef<any>();
|
|
||||||
// Store current value in ref
|
|
||||||
useEffect(() => {
|
|
||||||
ref.current = value;
|
|
||||||
}, [value]); // Only re-run if value changes
|
|
||||||
// Return previous value (happens before update in useEffect above)
|
|
||||||
return ref.current;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Totp () {
|
export default function Totp () {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
export function usePrevious(value: any) {
|
||||||
|
// The ref object is a generic container whose current property is mutable ...
|
||||||
|
// ... and can hold any value, similar to an instance property on a class
|
||||||
|
const ref = useRef<any>();
|
||||||
|
// Store current value in ref
|
||||||
|
useEffect(() => {
|
||||||
|
ref.current = value;
|
||||||
|
}, [value]); // Only re-run if value changes
|
||||||
|
|
||||||
|
// Return previous value (happens before update in useEffect above)
|
||||||
|
return ref.current;
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom/client';
|
||||||
import Sidebar from './components/Sidebar';
|
import Sidebar from './components/Sidebar';
|
||||||
import App from './pages/App';
|
import App from './pages/App';
|
||||||
|
|
||||||
ReactDOM.render(
|
const container = document.getElementById('app');
|
||||||
<App></App>,
|
if(container !== null) {
|
||||||
document.getElementById('app'),
|
const root = ReactDOM.createRoot(container)
|
||||||
);
|
root.render(<App></App>);
|
||||||
|
} else {
|
||||||
|
throw new Error('Failed to initialize app, container not found!');
|
||||||
|
}
|
||||||
|
|
@ -1,73 +1,102 @@
|
||||||
|
|
||||||
|
export function connectApi(url: string) {
|
||||||
|
let socket: WebSocket | null = null;
|
||||||
|
let connectionAttempts = 0;
|
||||||
|
let destroy = false;
|
||||||
|
let routers: any[] = [];
|
||||||
|
|
||||||
|
const connect = async () => {
|
||||||
let socket: WebSocket | null = null;
|
|
||||||
let connectionAttempts = 0;
|
|
||||||
const url = 'wss://dev.valnet.xyz';
|
|
||||||
|
|
||||||
let routers: any[] = [];
|
|
||||||
|
|
||||||
const connect = async () => {
|
|
||||||
try {
|
|
||||||
connectionAttempts ++;
|
|
||||||
console.log('attempting api connection...');
|
|
||||||
socket = new WebSocket(url);
|
|
||||||
} catch (e) {
|
|
||||||
if(connectionAttempts === 1)
|
|
||||||
connect();
|
|
||||||
else {
|
|
||||||
const seconds = 2 ** connectionAttempts;
|
|
||||||
console.log(`waiting ${seconds} seconds before reconnecting`);
|
|
||||||
setTimeout(connect, 1000 * seconds);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
socket.addEventListener('open', () => {
|
|
||||||
if(socket === null) return;
|
|
||||||
connectionAttempts = 0;
|
|
||||||
// socket.send('Hello Server!');
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.addEventListener('message', (event) => {
|
|
||||||
const {action, data} = JSON.parse(event.data);
|
|
||||||
console.log('[IN]', action, data);
|
|
||||||
const routeFound = routers
|
|
||||||
.map(router => router(action, data))
|
|
||||||
.reduce((a, b) => a + b, 0);
|
|
||||||
if(routeFound === 0) {
|
|
||||||
console.warn(`route <${action}> not found`);
|
|
||||||
} else {
|
|
||||||
console.log(`routed to ${routeFound} elements`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.addEventListener('close', () => {
|
|
||||||
socket = null;
|
|
||||||
connect();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
connect();
|
|
||||||
|
|
||||||
export async function send(action: string, data?: any) {
|
|
||||||
if(socket === null) return;
|
|
||||||
if(socket && socket.readyState === socket.CONNECTING) {
|
|
||||||
try {
|
try {
|
||||||
await new Promise((resolve, reject) => {
|
connectionAttempts ++;
|
||||||
socket?.addEventListener('open', resolve);
|
console.log('connecting to', url);
|
||||||
socket?.addEventListener('close', reject);
|
socket = new WebSocket(url);
|
||||||
});
|
} catch (e) {
|
||||||
} catch(e) {
|
if(destroy) return;
|
||||||
|
if(connectionAttempts === 1)
|
||||||
|
connect();
|
||||||
|
else {
|
||||||
|
const seconds = 2 ** connectionAttempts;
|
||||||
|
console.log(`waiting ${seconds} seconds before reconnecting`);
|
||||||
|
setTimeout(connect, 1000 * seconds);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if(socket.readyState !== socket.OPEN) return;
|
|
||||||
|
socket.addEventListener('open', () => {
|
||||||
|
if(socket === null) return;
|
||||||
|
connectionAttempts = 0;
|
||||||
|
// socket.send('Hello Server!');
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.addEventListener('message', (event) => {
|
||||||
|
const {action, data} = JSON.parse(event.data);
|
||||||
|
console.log('[IN]', action, data);
|
||||||
|
const routeFound = routers
|
||||||
|
.map(router => router(action, data))
|
||||||
|
.reduce((a, b) => a + b, 0);
|
||||||
|
if(routeFound === 0) {
|
||||||
|
console.warn(`route <${action}> not found`);
|
||||||
|
} else {
|
||||||
|
console.log(`routed to ${routeFound} elements`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.addEventListener('close', () => {
|
||||||
|
if(destroy) return;
|
||||||
|
socket = null;
|
||||||
|
connect();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
connect();
|
||||||
|
|
||||||
|
async function send(action: string, data?: any) {
|
||||||
|
if(socket === null) return;
|
||||||
|
if(socket && socket.readyState === socket.CONNECTING) {
|
||||||
|
try {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
socket?.addEventListener('open', resolve);
|
||||||
|
socket?.addEventListener('close', reject);
|
||||||
|
});
|
||||||
|
} catch(e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(socket.readyState !== socket.OPEN) return;
|
||||||
|
}
|
||||||
|
const message = JSON.stringify({ action, data });
|
||||||
|
socket.send(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerRouter(router: any) {
|
||||||
|
routers.push(router);
|
||||||
|
}
|
||||||
|
|
||||||
|
function unregisterRouter(router: any) {
|
||||||
|
routers = routers.filter(r => r !== router);
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
destroy = true;
|
||||||
|
if(socket) {
|
||||||
|
socket.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
registerRouter,
|
||||||
|
unregisterRouter,
|
||||||
|
send,
|
||||||
|
destroy: close
|
||||||
}
|
}
|
||||||
const message = JSON.stringify({ action, data });
|
|
||||||
socket.send(message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function router(routes: any) {
|
export interface RouterObject {
|
||||||
|
[route: string]: (data: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Router = (route: string, data: any) => boolean
|
||||||
|
|
||||||
|
export function router(routes: RouterObject): Router {
|
||||||
return function(route: string, data: any) {
|
return function(route: string, data: any) {
|
||||||
if(route in routes) {
|
if(route in routes) {
|
||||||
routes[route](data);
|
routes[route](data);
|
||||||
|
|
@ -77,11 +106,3 @@ export function router(routes: any) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function registerRouter(router: any) {
|
|
||||||
routers.push(router);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unregisterRouter(router: any) {
|
|
||||||
routers = routers.filter(r => r !== router);
|
|
||||||
}
|
|
||||||
|
|
@ -3,13 +3,9 @@ import * as preload from '#preload';
|
||||||
console.log('#preload', preload);
|
console.log('#preload', preload);
|
||||||
|
|
||||||
const functions: any = (function() {
|
const functions: any = (function() {
|
||||||
const electron = !!preload.getClientId;
|
const electron = !!preload.versions;
|
||||||
const cordova = 'cordova' in globalThis;
|
const cordova = 'cordova' in globalThis;
|
||||||
|
|
||||||
console.log(preload);
|
|
||||||
|
|
||||||
// alert('Electron: ' + electron + '\nCordova: ' + cordova);
|
|
||||||
|
|
||||||
if(electron) {
|
if(electron) {
|
||||||
return preload;
|
return preload;
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -39,3 +35,5 @@ export const getClientId = functions.getClientId;
|
||||||
export const setClientId = functions.setClientId;
|
export const setClientId = functions.setClientId;
|
||||||
export const getHomeServer = functions.getHomeServer;
|
export const getHomeServer = functions.getHomeServer;
|
||||||
export const setHomeServer = functions.setHomeServer;
|
export const setHomeServer = functions.setHomeServer;
|
||||||
|
export const getSessionToken = functions.getSessionToken;
|
||||||
|
export const setSessionToken = functions.setSessionToken;
|
||||||
|
|
@ -1,16 +1,19 @@
|
||||||
import { useEffect } from 'react';
|
import { useContext, useEffect } from 'react';
|
||||||
import { registerRouter, router, send, unregisterRouter } from './api';
|
import { ServerConnectionContext } from '../components/ServerConnection';
|
||||||
|
import { Router, router, RouterObject } from './api';
|
||||||
|
|
||||||
export function useApi(actions: Function | object, deps: any[]) {
|
export function useApi(actions: Router | RouterObject, deps: any[]) {
|
||||||
|
const connection = useContext(ServerConnectionContext);
|
||||||
const _router = typeof actions === 'object' ? router(actions) : actions;
|
const _router = typeof actions === 'object' ? router(actions) : actions;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
registerRouter(_router);
|
connection.registerRouter(_router);
|
||||||
return () => {
|
return () => {
|
||||||
unregisterRouter(_router);
|
connection.unregisterRouter(_router);
|
||||||
};
|
};
|
||||||
}, deps);
|
}, deps);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
send: send,
|
send: connection.send,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,14 @@ import {
|
||||||
getClientId,
|
getClientId,
|
||||||
setClientId,
|
setClientId,
|
||||||
getHomeServer,
|
getHomeServer,
|
||||||
setHomeServer
|
setHomeServer,
|
||||||
|
getSessionToken,
|
||||||
|
setSessionToken
|
||||||
} from '../lib/native';
|
} from '../lib/native';
|
||||||
import { useApi } from '../lib/useApi';
|
import { useApi } from '../lib/useApi';
|
||||||
import Sidebar from '../components/Sidebar';
|
import Sidebar from '../components/Sidebar';
|
||||||
import NewAccount from './NewAccount';
|
import NewAccount from './NewAccount';
|
||||||
|
import ServerConnection from '../components/ServerConnection';
|
||||||
|
|
||||||
export const ChannelContext = createContext<{
|
export const ChannelContext = createContext<{
|
||||||
channel: string | null,
|
channel: string | null,
|
||||||
|
|
@ -32,6 +35,13 @@ export const HomeServerContext = createContext<{
|
||||||
homeServer: null,
|
homeServer: null,
|
||||||
setHomeServer: () => {}
|
setHomeServer: () => {}
|
||||||
});
|
});
|
||||||
|
export const SessionTokenContext = createContext<{
|
||||||
|
sessionToken: string | null,
|
||||||
|
setSessionToken: (token: string) => void
|
||||||
|
}>({
|
||||||
|
sessionToken: null,
|
||||||
|
setSessionToken() {}
|
||||||
|
})
|
||||||
export const TransparencyContext = createContext<(transparent: boolean) => void>(() => {});
|
export const TransparencyContext = createContext<(transparent: boolean) => void>(() => {});
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
|
|
@ -39,7 +49,7 @@ export default function App() {
|
||||||
const [clientId, setCachedClientId] = useState(getClientId());
|
const [clientId, setCachedClientId] = useState(getClientId());
|
||||||
const [homeServer, setCachedHomeServer] = useState<string | null>(getHomeServer());
|
const [homeServer, setCachedHomeServer] = useState<string | null>(getHomeServer());
|
||||||
const channelContextValue = { channel, setChannel }
|
const channelContextValue = { channel, setChannel }
|
||||||
|
const [cachedSessionToken, setCachedSessionToken] = useState<string | null>(null);
|
||||||
const [transparent, setTransparent] = useState(false);
|
const [transparent, setTransparent] = useState(false);
|
||||||
|
|
||||||
const setHomeServerCallback = useCallback((url: string | null) => {
|
const setHomeServerCallback = useCallback((url: string | null) => {
|
||||||
|
|
@ -61,16 +71,28 @@ export default function App() {
|
||||||
setClientId(clientId);
|
setClientId(clientId);
|
||||||
}, [clientId]);
|
}, [clientId]);
|
||||||
|
|
||||||
const { send } = useApi({
|
const updateCachedSessionToken = useCallback((token?: string) => {
|
||||||
'client:new'(data: string) {
|
setSessionToken(token ?? '');
|
||||||
setCachedClientId(data);
|
setCachedSessionToken(getSessionToken());
|
||||||
},
|
}, []);
|
||||||
}, [setCachedClientId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const SessionTokenContextValue = useMemo(() => {
|
||||||
if(clientId !== null) return;
|
return {
|
||||||
send('client:new');
|
sessionToken: cachedSessionToken,
|
||||||
}, [clientId]);
|
setSessionToken: updateCachedSessionToken
|
||||||
|
}
|
||||||
|
}, [cachedSessionToken, updateCachedSessionToken])
|
||||||
|
|
||||||
|
// const { send } = useApi({
|
||||||
|
// 'client:new'(data: string) {
|
||||||
|
// setCachedClientId(data);
|
||||||
|
// },
|
||||||
|
// }, [setCachedClientId]);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// if(clientId !== null) return;
|
||||||
|
// send('client:new');
|
||||||
|
// }, [clientId]);
|
||||||
|
|
||||||
const clientIdContextValue = { clientId, setClientId: setCachedClientId };
|
const clientIdContextValue = { clientId, setClientId: setCachedClientId };
|
||||||
|
|
||||||
|
|
@ -82,36 +104,61 @@ export default function App() {
|
||||||
// background: #282a36;
|
// background: #282a36;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ClientIdContext.Provider value={clientIdContextValue}>
|
<>
|
||||||
<ChannelContext.Provider value={channelContextValue}>
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<HomeServerContext.Provider value={homeServerContextValue}>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin='' />
|
||||||
<TransparencyContext.Provider value={setTransparent}>
|
<link href={"https://fonts.googleapis.com/css2?family=Fira+Sans&family=Josefin+Sans&family=Lato&family=Radio+Canada&family=Readex+Pro&family=Red+Hat+Text&family=Rubik&family=Signika&family=Telex&display=swap"} rel="stylesheet" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<style>{`
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin='' />
|
html {
|
||||||
<link href={"https://fonts.googleapis.com/css2?family=Fira+Sans&family=Josefin+Sans&family=Lato&family=Radio+Canada&family=Readex+Pro&family=Red+Hat+Text&family=Rubik&family=Signika&family=Telex&display=swap"} rel="stylesheet" />
|
--background: #282a36;
|
||||||
<div style={{
|
--current-line: #44475a;
|
||||||
background: transparent ? 'rgba(0, 0, 0, 0)' : '#282a36',
|
--foreground: #f8f8f2;
|
||||||
color: transparent ? 'black' : '#f8f8f2',
|
--comment: #6272a4;
|
||||||
fontSize: '16px',
|
--cyan: #8be9fd;
|
||||||
fontFamily: "'Red Hat Text', sans-serif",
|
--green: #50fa7b;
|
||||||
width: '100%',
|
--orange: #ffb86c;
|
||||||
height: '100%'
|
--pink: #ff79c6;
|
||||||
}}>
|
--purple: #bd93f9;
|
||||||
{homeServer === null && (
|
--red: #ff5555;
|
||||||
<NewAccount></NewAccount>
|
--yellow: #f1fa8c;
|
||||||
) || (
|
--primary: var(--purple);
|
||||||
<Sidebar
|
}
|
||||||
threshold={800}
|
a {
|
||||||
sidebar={300}
|
color: var(--cyan);
|
||||||
>
|
}
|
||||||
<Channels></Channels>
|
`}</style>
|
||||||
<Chat></Chat>
|
<ClientIdContext.Provider value={clientIdContextValue}>
|
||||||
</Sidebar>
|
<ChannelContext.Provider value={channelContextValue}>
|
||||||
)}
|
<HomeServerContext.Provider value={homeServerContextValue}>
|
||||||
</div>
|
<TransparencyContext.Provider value={setTransparent}>
|
||||||
</TransparencyContext.Provider>
|
<SessionTokenContext.Provider value={SessionTokenContextValue}>
|
||||||
</HomeServerContext.Provider>
|
<div style={{
|
||||||
</ChannelContext.Provider>
|
background: transparent ? 'rgba(0, 0, 0, 0)' : 'var(--background)',
|
||||||
</ClientIdContext.Provider>
|
color: transparent ? 'black' : 'var(--foreground)',
|
||||||
|
fontSize: '16px',
|
||||||
|
fontFamily: "'Red Hat Text', sans-serif",
|
||||||
|
width: '100%',
|
||||||
|
height: '100%'
|
||||||
|
}}>
|
||||||
|
{(cachedSessionToken === null || homeServer === null) ? (
|
||||||
|
<NewAccount></NewAccount>
|
||||||
|
) : (
|
||||||
|
<ServerConnection url={homeServer}>
|
||||||
|
<Sidebar
|
||||||
|
threshold={800}
|
||||||
|
sidebar={300}
|
||||||
|
>
|
||||||
|
<Channels></Channels>
|
||||||
|
<Chat></Chat>
|
||||||
|
</Sidebar>
|
||||||
|
</ServerConnection>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</SessionTokenContext.Provider>
|
||||||
|
</TransparencyContext.Provider>
|
||||||
|
</HomeServerContext.Provider>
|
||||||
|
</ChannelContext.Provider>
|
||||||
|
</ClientIdContext.Provider>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -50,7 +50,7 @@ export default function Channels() {
|
||||||
}, [channels, unreads]);
|
}, [channels, unreads]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('unreads', unreads);
|
// console.log('unreads', unreads);
|
||||||
}, [unreads]);
|
}, [unreads]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -60,10 +60,10 @@ export default function Channels() {
|
||||||
}, [channels]);
|
}, [channels]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(channel, channels);
|
// console.log(channel, channels);
|
||||||
if(channels.length === 0) return;
|
if(channels.length === 0) return;
|
||||||
if(channel !== null) return;
|
if(channel !== null) return;
|
||||||
console.log('this is what setChannel is', setChannel);
|
// console.log('this is what setChannel is', setChannel);
|
||||||
setChannel(channels[0].uid);
|
setChannel(channels[0].uid);
|
||||||
}, [channel, channels]);
|
}, [channel, channels]);
|
||||||
|
|
||||||
|
|
@ -143,8 +143,8 @@ export default function Channels() {
|
||||||
}}>ADD</button>
|
}}>ADD</button>
|
||||||
<NameTextbox></NameTextbox><br></br>
|
<NameTextbox></NameTextbox><br></br>
|
||||||
<button onClick={() => setHomeServer(null)}>leave</button><br></br>
|
<button onClick={() => setHomeServer(null)}>leave</button><br></br>
|
||||||
<LoginQR></LoginQR>
|
{/* <LoginQR></LoginQR> */}
|
||||||
<Totp></Totp>
|
{/* <Totp></Totp> */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -4,7 +4,7 @@ import { useApi } from '../lib/useApi';
|
||||||
import { ChannelContext, ClientIdContext } from './App';
|
import { ChannelContext, ClientIdContext } from './App';
|
||||||
import type { IMessage} from './Message';
|
import type { IMessage} from './Message';
|
||||||
import { Message } from './Message';
|
import { Message } from './Message';
|
||||||
import { MdSend } from 'react-icons/md'
|
import { MdSend } from 'react-icons/md';
|
||||||
|
|
||||||
function createMessage(from: string, text: string,
|
function createMessage(from: string, text: string,
|
||||||
channel: string, t = 0): IMessage {
|
channel: string, t = 0): IMessage {
|
||||||
|
|
@ -41,7 +41,6 @@ export default () => {
|
||||||
}, [messages]);
|
}, [messages]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('sending recents request');
|
|
||||||
send('message:recent', { channel });
|
send('message:recent', { channel });
|
||||||
}, [channel]);
|
}, [channel]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { useContext, useEffect, useState } from "react";
|
||||||
import { ClientIdContext, HomeServerContext } from "./App";
|
import { ClientIdContext, HomeServerContext } from "./App";
|
||||||
import QR from 'qrcode';
|
import QR from 'qrcode';
|
||||||
|
|
||||||
export default function() {
|
export default function LoginQR() {
|
||||||
const { homeServer } = useContext(HomeServerContext);
|
const { homeServer } = useContext(HomeServerContext);
|
||||||
const { clientId } = useContext(ClientIdContext);
|
const { clientId } = useContext(ClientIdContext);
|
||||||
const [qr, setQr] = useState<string | null>(null);
|
const [qr, setQr] = useState<string | null>(null);
|
||||||
|
|
|
||||||
|
|
@ -1,78 +1,207 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useCallback, useContext, useRef } from "react"
|
import { useCallback, useContext, useRef } from "react"
|
||||||
import { ClientIdContext, HomeServerContext, TransparencyContext } from "./App"
|
import ServerConnection from "../components/ServerConnection";
|
||||||
|
import { useApi } from "../lib/useApi";
|
||||||
|
import { ClientIdContext, HomeServerContext, SessionTokenContext, TransparencyContext } from "./App"
|
||||||
|
import QR from 'qrcode';
|
||||||
|
|
||||||
export default function NewAccount() {
|
export default function NewAccount() {
|
||||||
|
|
||||||
const [data, setData] = useState('');
|
// const [data, setData] = useState('');
|
||||||
const [scanning, setScanning] = useState(false);
|
// const [scanning, setScanning] = useState(false);
|
||||||
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
const { setHomeServer } = useContext(HomeServerContext);
|
|
||||||
const { setClientId } = useContext(ClientIdContext);
|
|
||||||
|
|
||||||
const setTransparent = useContext(TransparencyContext);
|
// const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
// const { setHomeServer } = useContext(HomeServerContext);
|
||||||
|
// const { setClientId } = useContext(ClientIdContext);
|
||||||
|
|
||||||
useEffect(() => {
|
// const setTransparent = useContext(TransparencyContext);
|
||||||
setTransparent(scanning);
|
|
||||||
}, [scanning, setTransparent]);
|
|
||||||
|
|
||||||
const go = useCallback(() => {
|
// useEffect(() => {
|
||||||
if(inputRef.current === null) return;
|
// setTransparent(scanning);
|
||||||
setHomeServer(inputRef.current.value)
|
// }, [scanning, setTransparent]);
|
||||||
}, [HomeServerContext]);
|
|
||||||
|
|
||||||
const scanQr = useCallback(() => {
|
// const go = useCallback(() => {
|
||||||
//@ts-ignore
|
// if(inputRef.current === null) return;
|
||||||
window.QRScanner.prepare((err: any, status: any) => {
|
// setHomeServer(inputRef.current.value)
|
||||||
if(!err && status.authorized) {
|
// }, [HomeServerContext]);
|
||||||
setScanning(true);
|
|
||||||
//@ts-ignore
|
|
||||||
window.QRScanner.hide();
|
|
||||||
//@ts-ignore
|
|
||||||
window.QRScanner.scan((err, text) => {
|
|
||||||
if (err) return alert(err);
|
|
||||||
// alert(text);
|
|
||||||
setData(text);
|
|
||||||
setScanning(false);
|
|
||||||
//@ts-ignore
|
|
||||||
window.QRScanner.show();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
// const scanQr = useCallback(() => {
|
||||||
// this avoids a timing issue whereby the component
|
// //@ts-ignore
|
||||||
// gets removed before it has a chance to clean up
|
// window.QRScanner.prepare((err: any, status: any) => {
|
||||||
// its setting of transparency...
|
// if(!err && status.authorized) {
|
||||||
if(scanning) return;
|
// setScanning(true);
|
||||||
if(!data) return;
|
// //@ts-ignore
|
||||||
const [action, homeServer, clientId] = data.split('|');
|
// window.QRScanner.hide();
|
||||||
switch(action) {
|
// //@ts-ignore
|
||||||
case 'loginv1': {
|
// window.QRScanner.scan((err, text) => {
|
||||||
setHomeServer(homeServer);
|
// if (err) return alert(err);
|
||||||
setClientId(clientId);
|
// // alert(text);
|
||||||
break;
|
// setData(text);
|
||||||
}
|
// setScanning(false);
|
||||||
|
// //@ts-ignore
|
||||||
|
// window.QRScanner.show();
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }, [data]);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// // this avoids a timing issue whereby the component
|
||||||
|
// // gets removed before it has a chance to clean up
|
||||||
|
// // its setting of transparency...
|
||||||
|
// if(scanning) return;
|
||||||
|
// if(!data) return;
|
||||||
|
// const [action, homeServer, clientId] = data.split('|');
|
||||||
|
// switch(action) {
|
||||||
|
// case 'loginv1': {
|
||||||
|
// setHomeServer(homeServer);
|
||||||
|
// setClientId(clientId);
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }, [data, scanning])
|
||||||
|
|
||||||
|
const [returning, setReturning] = useState(true);
|
||||||
|
const homeServerInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [homeServer, setHomeServer] = useState<string | null>(null);
|
||||||
|
const [connection, setConnection] = useState<WebSocket | null>(null);
|
||||||
|
const [connecting, setConnecting] = useState(false);
|
||||||
|
const [connectionError, setConnectionError] = useState('');
|
||||||
|
|
||||||
|
const connect = useCallback((url: string) => {
|
||||||
|
if(connecting) return;
|
||||||
|
setHomeServer(url);
|
||||||
|
setConnecting(true);
|
||||||
|
try {
|
||||||
|
const ws = new WebSocket(url);
|
||||||
|
|
||||||
|
ws.addEventListener('open', () => {
|
||||||
|
setConnecting(false);
|
||||||
|
setConnection(ws);
|
||||||
|
setConnectionError('');
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener('close', (e) => {
|
||||||
|
setConnecting(false);
|
||||||
|
setConnection(null);
|
||||||
|
console.log(e)
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener('error', (e) => {
|
||||||
|
setConnectionError('Couldn\'t connect to ' + url)
|
||||||
|
});
|
||||||
|
} catch(e: any) {
|
||||||
|
console.log('ASDFASDFASDF');
|
||||||
}
|
}
|
||||||
}, [data, scanning])
|
}, [connecting])
|
||||||
|
|
||||||
return <div style={{
|
return (
|
||||||
display: 'grid',
|
<div style={{
|
||||||
placeContent: 'center center',
|
display: 'grid',
|
||||||
height: '100%',
|
placeContent: 'center center',
|
||||||
}}>
|
height: '100%',
|
||||||
Create New Account!! <br />
|
}}>
|
||||||
Enter Home Server URL <br />
|
{returning ? (
|
||||||
<input defaultValue="wss://dev.valnet.xyz" ref={inputRef}></input> <br />
|
<>
|
||||||
<button onClick={go}> GO </button> <br />
|
Login
|
||||||
<br />
|
<a href="#" onClick={() => setReturning(false)}>Sign up</a>
|
||||||
or scan a QR! <br />
|
</>
|
||||||
<button onClick={scanQr}>SCAN</button><br></br>
|
) : (
|
||||||
<pre>
|
<>
|
||||||
{data}
|
<input ref={homeServerInputRef} defaultValue="wss://macos.valnet.xyz" disabled={connection !== null}></input>
|
||||||
{scanning ? 'SCANNING' : 'NOT SCANNING'}
|
<button onClick={() => connect(homeServerInputRef.current?.value ?? '')} disabled={connection !== null}>Next</button>
|
||||||
</pre>
|
{connecting ? `Connecting to ${homeServer}` : connectionError}
|
||||||
</div>
|
<br></br>
|
||||||
|
{connection !== null && (
|
||||||
|
<ServerConnection url={homeServer ?? ''}>
|
||||||
|
<SignUp>
|
||||||
|
</SignUp>
|
||||||
|
</ServerConnection>
|
||||||
|
)}
|
||||||
|
{/* Create New Account!! <br />
|
||||||
|
Enter Home Server URL <br />
|
||||||
|
<input defaultValue="wss://dev.valnet.xyz" ref={inputRef}></input> <br />
|
||||||
|
<button onClick={go}> GO </button> <br />
|
||||||
|
<br />
|
||||||
|
or scan a QR! <br />
|
||||||
|
<button onClick={scanQr}>SCAN</button><br></br>
|
||||||
|
<pre>
|
||||||
|
{data}
|
||||||
|
{scanning ? 'SCANNING' : 'NOT SCANNING'}
|
||||||
|
</pre> */}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const SignUp = (props: any) => {
|
||||||
|
|
||||||
|
const usernameRef = useRef<HTMLInputElement>(null);
|
||||||
|
const displayNameRef = useRef<HTMLInputElement>(null);
|
||||||
|
const totpRef = useRef<HTMLInputElement>(null);
|
||||||
|
const [clientId, setClientId] = useState<string | null>(null);
|
||||||
|
// const [totpToken, setTotpToken] = useState<string | null>(null);
|
||||||
|
const [qr, setQr] = useState<string | null>(null);
|
||||||
|
const { setSessionToken } = useContext(SessionTokenContext)
|
||||||
|
|
||||||
|
const { send } = useApi({
|
||||||
|
'client:new'(data: any) {
|
||||||
|
setClientId(data);
|
||||||
|
},
|
||||||
|
async 'totp:propose'(data: any) {
|
||||||
|
setQr(await QR.toDataURL(
|
||||||
|
'otpauth://totp/' +
|
||||||
|
(usernameRef.current?.value ?? '') +
|
||||||
|
'?secret=' +
|
||||||
|
data +
|
||||||
|
'&issuer=valnet-corner'
|
||||||
|
));
|
||||||
|
},
|
||||||
|
'totp:confirm'(data: any) {
|
||||||
|
setSessionToken(data.token);
|
||||||
|
console.log(data);
|
||||||
|
}
|
||||||
|
}, [setSessionToken]);
|
||||||
|
|
||||||
|
const createAccount = useCallback(() => {
|
||||||
|
send('client:new', {
|
||||||
|
username: usernameRef.current?.value,
|
||||||
|
displayName: displayNameRef.current?.value,
|
||||||
|
})
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(clientId === null) return;
|
||||||
|
send('totp:propose', clientId);
|
||||||
|
}, [clientId]);
|
||||||
|
|
||||||
|
const changeTotp = useCallback(() => {
|
||||||
|
const value = totpRef.current?.value ?? '';
|
||||||
|
if(!(/[0-9]{6}/.test(value))) return;
|
||||||
|
send('totp:confirm', {
|
||||||
|
clientId,
|
||||||
|
code: value
|
||||||
|
})
|
||||||
|
}, [clientId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<label>Username</label>
|
||||||
|
<input defaultValue={'Test' + Math.floor(Math.random() * 1000)} disabled={clientId !== null} ref={usernameRef}></input>
|
||||||
|
<label>Display Name</label>
|
||||||
|
<input defaultValue="Val" disabled={clientId !== null} ref={displayNameRef}></input>
|
||||||
|
<button disabled={clientId !== null} onClick={createAccount}>Next</button>
|
||||||
|
{clientId && (
|
||||||
|
<>
|
||||||
|
<br></br>
|
||||||
|
<img src={qr ?? ''}></img>
|
||||||
|
<br></br>
|
||||||
|
<label>TOTP Code</label>
|
||||||
|
<input onChange={changeTotp} ref={totpRef}></input>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -22,4 +22,7 @@ CREATE TABLE `sessions` (
|
||||||
);
|
);
|
||||||
|
|
||||||
ALTER TABLE `sessions`
|
ALTER TABLE `sessions`
|
||||||
ADD FOREIGN KEY (`client_uid`) REFERENCES `clients` (`uid`)
|
ADD FOREIGN KEY (`client_uid`) REFERENCES `clients` (`uid`);
|
||||||
|
|
||||||
|
ALTER TABLE `sessions`
|
||||||
|
CHANGE `id` `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST;
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
DROP PROCEDURE IF EXISTS new_client;
|
||||||
|
|
||||||
|
CREATE PROCEDURE new_client (IN name TINYTEXT, IN username VARCHAR(256)) BEGIN
|
||||||
|
DECLARE client_id INT UNSIGNED DEFAULT 0;
|
||||||
|
INSERT INTO clients (uid, name, username) VALUES (UUID(), name, username);
|
||||||
|
SET client_id = last_insert_id();
|
||||||
|
UPDATE clients
|
||||||
|
SET clients.name=name
|
||||||
|
WHERE clients.id=client_id;
|
||||||
|
|
||||||
|
SELECT clients.uid, clients.name, clients.username FROM clients WHERE clients.id=client_id;
|
||||||
|
END;
|
||||||
|
|
@ -1 +1 @@
|
||||||
CALL new_client("Anonymous");
|
CALL new_client(?, ?);
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import router from "../lib/router";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export default router({
|
||||||
|
async create() {
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
@ -7,8 +7,12 @@ import _get from '../db/snippets/client/get.sql';
|
||||||
import rename from '../db/snippets/client/rename.sql';
|
import rename from '../db/snippets/client/rename.sql';
|
||||||
|
|
||||||
export default router({
|
export default router({
|
||||||
async 'new'() {
|
async 'new'(data: any) {
|
||||||
const response = await query(_new);
|
const response = await query(
|
||||||
|
_new,
|
||||||
|
data.displayName,
|
||||||
|
data.username,
|
||||||
|
);
|
||||||
if(response === null) return;
|
if(response === null) return;
|
||||||
return reply(response[0][0].uid);
|
return reply(response[0][0].uid);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,29 @@ import { randomBytes } from 'crypto';
|
||||||
import getToken from 'totp-generator';
|
import getToken from 'totp-generator';
|
||||||
import query from "../db/query";
|
import query from "../db/query";
|
||||||
import confirm from '../db/snippets/totp/confirm.sql';
|
import confirm from '../db/snippets/totp/confirm.sql';
|
||||||
|
import addSessionToken from '../db/snippets/session/new.sql';
|
||||||
|
|
||||||
|
const validateTotp = (key: string, code: string) => {
|
||||||
|
return [
|
||||||
|
getToken(key, { timestamp: Date.now() }),
|
||||||
|
getToken(key, { timestamp: Date.now() - 30 * 1000 }),
|
||||||
|
getToken(key, { timestamp: Date.now() - 2 * 30 * 1000})
|
||||||
|
].includes(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateSessionToken = async (clientId: string) => {
|
||||||
|
let token = '';
|
||||||
|
for(let i = 0; i < 64; i ++) {
|
||||||
|
token += rb32()
|
||||||
|
}
|
||||||
|
console.log('created session token', clientId, token);
|
||||||
|
// scnd min hr day year
|
||||||
|
const year = 1000 * 60 * 60 * 24 * 365;
|
||||||
|
const expiration = Date.now() + year;
|
||||||
|
await query(addSessionToken, clientId, expiration, token);
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
// 0 1 2 3 4
|
// 0 1 2 3 4
|
||||||
// | | | | | |
|
// | | | | | |
|
||||||
// 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0
|
// 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0
|
||||||
|
|
@ -15,7 +38,7 @@ const mask = (len: number) => Math.pow(2, len) - 1;
|
||||||
const manipulate = (b: number, start: number, len: number, end: number) =>
|
const manipulate = (b: number, start: number, len: number, end: number) =>
|
||||||
(((b >> start) & mask(len)) << end) & (mask(len) << end)
|
(((b >> start) & mask(len)) << end) & (mask(len) << end)
|
||||||
|
|
||||||
const dict = (n: number): string => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'.at(n) as string;
|
const dict = (n: number): string => 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'[n] as string;
|
||||||
|
|
||||||
function rb32() {
|
function rb32() {
|
||||||
const bytes = randomBytes(5);
|
const bytes = randomBytes(5);
|
||||||
|
|
@ -51,8 +74,7 @@ export default router({
|
||||||
async 'confirm'(data: any) {
|
async 'confirm'(data: any) {
|
||||||
const { clientId, code } = data;
|
const { clientId, code } = data;
|
||||||
const key = proposals[clientId];
|
const key = proposals[clientId];
|
||||||
const trueCode = getToken(key);
|
if(!validateTotp(key, code)) return reply({
|
||||||
if(trueCode !== code) return reply({
|
|
||||||
err: 'codes did not match!'
|
err: 'codes did not match!'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -64,6 +86,7 @@ export default router({
|
||||||
});
|
});
|
||||||
|
|
||||||
return reply({
|
return reply({
|
||||||
|
token: await generateSessionToken(clientId),
|
||||||
err: null
|
err: null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Reference in New Issue