From 93ad0dfb1a325f73b3df26cde7fbe9b9f394715e Mon Sep 17 00:00:00 2001 From: Bronwen Date: Fri, 29 Jul 2022 00:01:01 -0400 Subject: [PATCH] session tokens! --- packages/preload/src/index.ts | 6 +- packages/preload/src/settings.ts | 15 + packages/renderer/index.html | 6 + .../src/components/ServerConnection.tsx | 37 +++ packages/renderer/src/components/Totp.tsx | 13 +- .../renderer/src/components/usePrevious.tsx | 14 + packages/renderer/src/index.tsx | 13 +- packages/renderer/src/lib/api.ts | 163 ++++++----- packages/renderer/src/lib/native.ts | 10 +- packages/renderer/src/lib/useApi.tsx | 15 +- packages/renderer/src/pages/App.tsx | 131 ++++++--- packages/renderer/src/pages/Channels.tsx | 10 +- packages/renderer/src/pages/Chat.tsx | 3 +- packages/renderer/src/pages/LoginQR.tsx | 2 +- packages/renderer/src/pages/NewAccount.tsx | 257 +++++++++++++----- .../server/public/migrations/8-sessions.sql | 5 +- .../migrations/9-new-client-with-username.sql | 12 + .../server/src/db/snippets/client/new.sql | 2 +- packages/server/src/routers/account.ts | 10 + packages/server/src/routers/client.ts | 8 +- packages/server/src/routers/totp.ts | 29 +- 21 files changed, 538 insertions(+), 223 deletions(-) create mode 100644 packages/renderer/src/components/ServerConnection.tsx create mode 100644 packages/renderer/src/components/usePrevious.tsx create mode 100644 packages/server/public/migrations/9-new-client-with-username.sql create mode 100644 packages/server/src/routers/account.ts diff --git a/packages/preload/src/index.ts b/packages/preload/src/index.ts index 709e13c..c36a0ee 100644 --- a/packages/preload/src/index.ts +++ b/packages/preload/src/index.ts @@ -6,6 +6,8 @@ export { getClientId, setClientId, getHomeServer, - setHomeServer + setHomeServer, + getSessionToken, + setSessionToken } from './settings'; -export {versions} from './versions'; +export {versions} from './versions'; \ No newline at end of file diff --git a/packages/preload/src/settings.ts b/packages/preload/src/settings.ts index ac9adfb..179ba25 100644 --- a/packages/preload/src/settings.ts +++ b/packages/preload/src/settings.ts @@ -15,6 +15,7 @@ const appdataPath = process.env.APPDATA || // windows const cornerDataPath = resolve(appdataPath, 'corner'); const clientIdPath = resolve(cornerDataPath, 'clientId'); const homeServerPath = resolve(cornerDataPath, 'homeServer'); +const sessionTokenPath = resolve(cornerDataPath, 'sessionToken'); // --- setup --- @@ -22,6 +23,10 @@ if(!existsSync(cornerDataPath)) mkdirSync(cornerDataPath); if(!existsSync(clientIdPath)) writeFileSync(clientIdPath, ''); +if(!existsSync(homeServerPath)) + writeFileSync(homeServerPath, ''); +if(!existsSync(sessionTokenPath)) + writeFileSync(sessionTokenPath, ''); // --- helpers --- @@ -59,4 +64,14 @@ export function setHomeServer(url: string) { return null } 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); } \ No newline at end of file diff --git a/packages/renderer/index.html b/packages/renderer/index.html index 122fe48..557725c 100644 --- a/packages/renderer/index.html +++ b/packages/renderer/index.html @@ -3,6 +3,12 @@ + Vite App diff --git a/packages/renderer/src/components/ServerConnection.tsx b/packages/renderer/src/components/ServerConnection.tsx new file mode 100644 index 0000000..9ef6940 --- /dev/null +++ b/packages/renderer/src/components/ServerConnection.tsx @@ -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>({ + 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 + {props.children} + +} \ No newline at end of file diff --git a/packages/renderer/src/components/Totp.tsx b/packages/renderer/src/components/Totp.tsx index f52aa7e..993c350 100644 --- a/packages/renderer/src/components/Totp.tsx +++ b/packages/renderer/src/components/Totp.tsx @@ -3,18 +3,7 @@ import 'reactjs-popup/dist/index.css'; import { useApi } from '../lib/useApi'; import { ClientIdContext } from '../pages/App'; import QR from 'qrcode'; - -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(); - // 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; -} +import { usePrevious } from './usePrevious'; export default function Totp () { diff --git a/packages/renderer/src/components/usePrevious.tsx b/packages/renderer/src/components/usePrevious.tsx new file mode 100644 index 0000000..385ef4c --- /dev/null +++ b/packages/renderer/src/components/usePrevious.tsx @@ -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(); + // 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; +} diff --git a/packages/renderer/src/index.tsx b/packages/renderer/src/index.tsx index b551cd6..81bfd83 100644 --- a/packages/renderer/src/index.tsx +++ b/packages/renderer/src/index.tsx @@ -1,9 +1,12 @@ import React from 'react'; -import ReactDOM from 'react-dom'; +import ReactDOM from 'react-dom/client'; import Sidebar from './components/Sidebar'; import App from './pages/App'; -ReactDOM.render( - , - document.getElementById('app'), -); \ No newline at end of file +const container = document.getElementById('app'); +if(container !== null) { + const root = ReactDOM.createRoot(container) + root.render(); +} else { + throw new Error('Failed to initialize app, container not found!'); +} \ No newline at end of file diff --git a/packages/renderer/src/lib/api.ts b/packages/renderer/src/lib/api.ts index 232f90d..17e6feb 100644 --- a/packages/renderer/src/lib/api.ts +++ b/packages/renderer/src/lib/api.ts @@ -1,73 +1,102 @@ - - -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) { +export function connectApi(url: string) { + let socket: WebSocket | null = null; + let connectionAttempts = 0; + let destroy = false; + let routers: any[] = []; + + const connect = async () => { try { - await new Promise((resolve, reject) => { - socket?.addEventListener('open', resolve); - socket?.addEventListener('close', reject); - }); - } catch(e) { + connectionAttempts ++; + console.log('connecting to', url); + socket = new WebSocket(url); + } 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; } - 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) { if(route in routes) { routes[route](data); @@ -76,12 +105,4 @@ export function router(routes: any) { return false; } }; -} - -export function registerRouter(router: any) { - routers.push(router); -} - -export function unregisterRouter(router: any) { - routers = routers.filter(r => r !== router); } \ No newline at end of file diff --git a/packages/renderer/src/lib/native.ts b/packages/renderer/src/lib/native.ts index 35ad94a..9b75937 100644 --- a/packages/renderer/src/lib/native.ts +++ b/packages/renderer/src/lib/native.ts @@ -3,13 +3,9 @@ import * as preload from '#preload'; console.log('#preload', preload); const functions: any = (function() { - const electron = !!preload.getClientId; + const electron = !!preload.versions; const cordova = 'cordova' in globalThis; - console.log(preload); - - // alert('Electron: ' + electron + '\nCordova: ' + cordova); - if(electron) { return preload; } else { @@ -38,4 +34,6 @@ console.log('native functions loaded', functions); export const getClientId = functions.getClientId; export const setClientId = functions.setClientId; export const getHomeServer = functions.getHomeServer; -export const setHomeServer = functions.setHomeServer; \ No newline at end of file +export const setHomeServer = functions.setHomeServer; +export const getSessionToken = functions.getSessionToken; +export const setSessionToken = functions.setSessionToken; \ No newline at end of file diff --git a/packages/renderer/src/lib/useApi.tsx b/packages/renderer/src/lib/useApi.tsx index 0dcab57..94e565d 100644 --- a/packages/renderer/src/lib/useApi.tsx +++ b/packages/renderer/src/lib/useApi.tsx @@ -1,16 +1,19 @@ -import { useEffect } from 'react'; -import { registerRouter, router, send, unregisterRouter } from './api'; +import { useContext, useEffect } from 'react'; +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; + useEffect(() => { - registerRouter(_router); + connection.registerRouter(_router); return () => { - unregisterRouter(_router); + connection.unregisterRouter(_router); }; }, deps); return { - send: send, + send: connection.send, }; } diff --git a/packages/renderer/src/pages/App.tsx b/packages/renderer/src/pages/App.tsx index f86c817..f785317 100644 --- a/packages/renderer/src/pages/App.tsx +++ b/packages/renderer/src/pages/App.tsx @@ -5,11 +5,14 @@ import { getClientId, setClientId, getHomeServer, - setHomeServer + setHomeServer, + getSessionToken, + setSessionToken } from '../lib/native'; import { useApi } from '../lib/useApi'; import Sidebar from '../components/Sidebar'; import NewAccount from './NewAccount'; +import ServerConnection from '../components/ServerConnection'; export const ChannelContext = createContext<{ channel: string | null, @@ -32,6 +35,13 @@ export const HomeServerContext = createContext<{ homeServer: null, setHomeServer: () => {} }); +export const SessionTokenContext = createContext<{ + sessionToken: string | null, + setSessionToken: (token: string) => void +}>({ + sessionToken: null, + setSessionToken() {} +}) export const TransparencyContext = createContext<(transparent: boolean) => void>(() => {}); export default function App() { @@ -39,7 +49,7 @@ export default function App() { const [clientId, setCachedClientId] = useState(getClientId()); const [homeServer, setCachedHomeServer] = useState(getHomeServer()); const channelContextValue = { channel, setChannel } - + const [cachedSessionToken, setCachedSessionToken] = useState(null); const [transparent, setTransparent] = useState(false); const setHomeServerCallback = useCallback((url: string | null) => { @@ -61,16 +71,28 @@ export default function App() { setClientId(clientId); }, [clientId]); - const { send } = useApi({ - 'client:new'(data: string) { - setCachedClientId(data); - }, - }, [setCachedClientId]); + const updateCachedSessionToken = useCallback((token?: string) => { + setSessionToken(token ?? ''); + setCachedSessionToken(getSessionToken()); + }, []); - useEffect(() => { - if(clientId !== null) return; - send('client:new'); - }, [clientId]); + const SessionTokenContextValue = useMemo(() => { + return { + sessionToken: cachedSessionToken, + 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 }; @@ -82,36 +104,61 @@ export default function App() { // background: #282a36; return ( - - - - - - - -
- {homeServer === null && ( - - ) || ( - - - - - )} -
-
-
-
-
+ <> + + + + + + + + + +
+ {(cachedSessionToken === null || homeServer === null) ? ( + + ) : ( + + + + + + + )} +
+
+
+
+
+
+ ); } \ No newline at end of file diff --git a/packages/renderer/src/pages/Channels.tsx b/packages/renderer/src/pages/Channels.tsx index e5ef6d0..b1c344e 100644 --- a/packages/renderer/src/pages/Channels.tsx +++ b/packages/renderer/src/pages/Channels.tsx @@ -50,7 +50,7 @@ export default function Channels() { }, [channels, unreads]); useEffect(() => { - console.log('unreads', unreads); + // console.log('unreads', unreads); }, [unreads]); useEffect(() => { @@ -60,10 +60,10 @@ export default function Channels() { }, [channels]); useEffect(() => { - console.log(channel, channels); + // console.log(channel, channels); if(channels.length === 0) 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); }, [channel, channels]); @@ -143,8 +143,8 @@ export default function Channels() { }}>ADD



- - + {/* */} + {/* */} ); } \ No newline at end of file diff --git a/packages/renderer/src/pages/Chat.tsx b/packages/renderer/src/pages/Chat.tsx index dec7289..054a34c 100644 --- a/packages/renderer/src/pages/Chat.tsx +++ b/packages/renderer/src/pages/Chat.tsx @@ -4,7 +4,7 @@ import { useApi } from '../lib/useApi'; import { ChannelContext, ClientIdContext } from './App'; import type { IMessage} from './Message'; import { Message } from './Message'; -import { MdSend } from 'react-icons/md' +import { MdSend } from 'react-icons/md'; function createMessage(from: string, text: string, channel: string, t = 0): IMessage { @@ -41,7 +41,6 @@ export default () => { }, [messages]); useEffect(() => { - console.log('sending recents request'); send('message:recent', { channel }); }, [channel]); diff --git a/packages/renderer/src/pages/LoginQR.tsx b/packages/renderer/src/pages/LoginQR.tsx index 4bbed25..8a0ed29 100644 --- a/packages/renderer/src/pages/LoginQR.tsx +++ b/packages/renderer/src/pages/LoginQR.tsx @@ -2,7 +2,7 @@ import { useContext, useEffect, useState } from "react"; import { ClientIdContext, HomeServerContext } from "./App"; import QR from 'qrcode'; -export default function() { +export default function LoginQR() { const { homeServer } = useContext(HomeServerContext); const { clientId } = useContext(ClientIdContext); const [qr, setQr] = useState(null); diff --git a/packages/renderer/src/pages/NewAccount.tsx b/packages/renderer/src/pages/NewAccount.tsx index 8bbd1cb..e3e74a0 100644 --- a/packages/renderer/src/pages/NewAccount.tsx +++ b/packages/renderer/src/pages/NewAccount.tsx @@ -1,78 +1,207 @@ import { useEffect, useState } 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() { - const [data, setData] = useState(''); - const [scanning, setScanning] = useState(false); + // const [data, setData] = useState(''); + // const [scanning, setScanning] = useState(false); - const inputRef = useRef(null); - const { setHomeServer } = useContext(HomeServerContext); - const { setClientId } = useContext(ClientIdContext); - const setTransparent = useContext(TransparencyContext); + // const inputRef = useRef(null); + // const { setHomeServer } = useContext(HomeServerContext); + // const { setClientId } = useContext(ClientIdContext); - useEffect(() => { - setTransparent(scanning); - }, [scanning, setTransparent]); + // const setTransparent = useContext(TransparencyContext); - const go = useCallback(() => { - if(inputRef.current === null) return; - setHomeServer(inputRef.current.value) - }, [HomeServerContext]); + // useEffect(() => { + // setTransparent(scanning); + // }, [scanning, setTransparent]); - const scanQr = useCallback(() => { - //@ts-ignore - window.QRScanner.prepare((err: any, status: any) => { - if(!err && status.authorized) { - 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]); + // const go = useCallback(() => { + // if(inputRef.current === null) return; + // setHomeServer(inputRef.current.value) + // }, [HomeServerContext]); - 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; - } + // const scanQr = useCallback(() => { + // //@ts-ignore + // window.QRScanner.prepare((err: any, status: any) => { + // if(!err && status.authorized) { + // 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(() => { + // // 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(null); + const [homeServer, setHomeServer] = useState(null); + const [connection, setConnection] = useState(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
- Create New Account!!
- Enter Home Server URL
-
-
-
- or scan a QR!
-

-
-      {data}
-      {scanning ? 'SCANNING' : 'NOT SCANNING'}
-    
-
+ return ( +
+ {returning ? ( + <> + Login + setReturning(false)}>Sign up + + ) : ( + <> + + + {connecting ? `Connecting to ${homeServer}` : connectionError} +

+ {connection !== null && ( + + + + + )} + {/* Create New Account!!
+ Enter Home Server URL
+
+
+
+ or scan a QR!
+

+
+            {data}
+            {scanning ? 'SCANNING' : 'NOT SCANNING'}
+          
*/} + + )} +
+ ); +} + +const SignUp = (props: any) => { + + const usernameRef = useRef(null); + const displayNameRef = useRef(null); + const totpRef = useRef(null); + const [clientId, setClientId] = useState(null); + // const [totpToken, setTotpToken] = useState(null); + const [qr, setQr] = useState(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 ( + <> + + + + + + {clientId && ( + <> +

+ +

+ + + + )} + + ) } \ No newline at end of file diff --git a/packages/server/public/migrations/8-sessions.sql b/packages/server/public/migrations/8-sessions.sql index 4a2d68d..6ed26a1 100644 --- a/packages/server/public/migrations/8-sessions.sql +++ b/packages/server/public/migrations/8-sessions.sql @@ -22,4 +22,7 @@ CREATE TABLE `sessions` ( ); ALTER TABLE `sessions` -ADD FOREIGN KEY (`client_uid`) REFERENCES `clients` (`uid`) \ No newline at end of file +ADD FOREIGN KEY (`client_uid`) REFERENCES `clients` (`uid`); + +ALTER TABLE `sessions` +CHANGE `id` `id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY FIRST; \ No newline at end of file diff --git a/packages/server/public/migrations/9-new-client-with-username.sql b/packages/server/public/migrations/9-new-client-with-username.sql new file mode 100644 index 0000000..59c469d --- /dev/null +++ b/packages/server/public/migrations/9-new-client-with-username.sql @@ -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; \ No newline at end of file diff --git a/packages/server/src/db/snippets/client/new.sql b/packages/server/src/db/snippets/client/new.sql index 08d560e..a77007e 100644 --- a/packages/server/src/db/snippets/client/new.sql +++ b/packages/server/src/db/snippets/client/new.sql @@ -1 +1 @@ -CALL new_client("Anonymous"); \ No newline at end of file +CALL new_client(?, ?); \ No newline at end of file diff --git a/packages/server/src/routers/account.ts b/packages/server/src/routers/account.ts new file mode 100644 index 0000000..979c0e3 --- /dev/null +++ b/packages/server/src/routers/account.ts @@ -0,0 +1,10 @@ +import router from "../lib/router"; + + + + +export default router({ + async create() { + + } +}) \ No newline at end of file diff --git a/packages/server/src/routers/client.ts b/packages/server/src/routers/client.ts index 42234fc..6c26bac 100644 --- a/packages/server/src/routers/client.ts +++ b/packages/server/src/routers/client.ts @@ -7,8 +7,12 @@ import _get from '../db/snippets/client/get.sql'; import rename from '../db/snippets/client/rename.sql'; export default router({ - async 'new'() { - const response = await query(_new); + async 'new'(data: any) { + const response = await query( + _new, + data.displayName, + data.username, + ); if(response === null) return; return reply(response[0][0].uid); }, diff --git a/packages/server/src/routers/totp.ts b/packages/server/src/routers/totp.ts index f26618d..fac335c 100644 --- a/packages/server/src/routers/totp.ts +++ b/packages/server/src/routers/totp.ts @@ -4,6 +4,29 @@ import { randomBytes } from 'crypto'; import getToken from 'totp-generator'; import query from "../db/query"; 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 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) => (((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() { const bytes = randomBytes(5); @@ -51,8 +74,7 @@ export default router({ async 'confirm'(data: any) { const { clientId, code } = data; const key = proposals[clientId]; - const trueCode = getToken(key); - if(trueCode !== code) return reply({ + if(!validateTotp(key, code)) return reply({ err: 'codes did not match!' }); @@ -64,6 +86,7 @@ export default router({ }); return reply({ + token: await generateSessionToken(clientId), err: null }); }