session tokens!

main
Bronwen 2022-07-29 00:01:01 -04:00
parent 30ebc0fcf1
commit 93ad0dfb1a
21 changed files with 538 additions and 223 deletions

View File

@ -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';

View File

@ -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 ---
@ -59,4 +64,14 @@ export function setHomeServer(url: string) {
return null return null
} }
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);
} }

View File

@ -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>

View File

@ -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>
}

View File

@ -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 () {

View File

@ -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;
}

View File

@ -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!');
}

View File

@ -1,73 +1,102 @@
export function connectApi(url: string) {
let socket: WebSocket | null = null;
let socket: WebSocket | null = null; let connectionAttempts = 0;
let connectionAttempts = 0; let destroy = false;
const url = 'wss://dev.valnet.xyz'; let routers: any[] = [];
let routers: any[] = []; const connect = async () => {
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);
@ -76,12 +105,4 @@ export function router(routes: any) {
return false; return false;
} }
}; };
}
export function registerRouter(router: any) {
routers.push(router);
}
export function unregisterRouter(router: any) {
routers = routers.filter(r => r !== router);
} }

View File

@ -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 {
@ -38,4 +34,6 @@ console.log('native functions loaded', functions);
export const getClientId = functions.getClientId; 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;

View File

@ -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,
}; };
} }

View File

@ -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>
</>
); );
} }

View File

@ -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>
); );
} }

View File

@ -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]);

View File

@ -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);

View File

@ -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>
</>
)}
</>
)
} }

View File

@ -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;

View File

@ -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;

View File

@ -1 +1 @@
CALL new_client("Anonymous"); CALL new_client(?, ?);

View File

@ -0,0 +1,10 @@
import router from "../lib/router";
export default router({
async create() {
}
})

View File

@ -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);
}, },

View File

@ -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
}); });
} }