login
parent
98a1906860
commit
f3c8a2e482
|
|
@ -8,7 +8,6 @@ export function connectApi(url: string) {
|
|||
const connect = async () => {
|
||||
try {
|
||||
connectionAttempts ++;
|
||||
// console.log('connecting to', url);
|
||||
socket = new WebSocket(url);
|
||||
} catch (e) {
|
||||
if(destroy) return;
|
||||
|
|
@ -25,6 +24,7 @@ export function connectApi(url: string) {
|
|||
socket.addEventListener('open', () => {
|
||||
if(socket === null) return;
|
||||
connectionAttempts = 0;
|
||||
console.log('connected to', url);
|
||||
// socket.send('Hello Server!');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@ import { FaUserPlus } from 'react-icons/fa';
|
|||
import ServerConnection from "../components/ServerConnection";
|
||||
import useHomeServer from "../contexts/PersistentState/useHomeServerNative";
|
||||
import { BigButton } from "./BigButton";
|
||||
import { SignUp } from "./SignUp";
|
||||
import { MdOutlineNavigateNext } from 'react-icons/md';
|
||||
import useHover from "../hooks/useHover";
|
||||
import { AiOutlineEdit } from "react-icons/ai";
|
||||
import { useApi } from "../lib/useApi";
|
||||
import useSessionToken from "../hooks/useSessionToken";
|
||||
import useClientId from "../hooks/useClientId";
|
||||
|
||||
export default function NewAccount() {
|
||||
|
||||
|
|
@ -70,11 +72,8 @@ export default function NewAccount() {
|
|||
// const homeServerInputRef = useRef<HTMLInputElement>(null);
|
||||
const { setHomeServer, homeServer } = useHomeServer();
|
||||
const [homeServerInput, setHomeServerInput] = useState<string>(homeServer ?? '');
|
||||
const [usernameInput, setUsernameInput] = useState('');
|
||||
const [authCodeInput, setAuthCodeInput] = useState('');
|
||||
const [returning, setReturning] = useState(true);
|
||||
const [connection, setConnection] = useState<WebSocket | null>(null);
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
// const [connection, setConnection] = useState<WebSocket | null>(null);
|
||||
const [connectionError, setConnectionError] = useState('');
|
||||
const [edittingHomeServer, setEdittingHomeServer] = useState(false);
|
||||
const [homeServerInputRef, homeServerHovered] = useHover<HTMLInputElement>();
|
||||
|
|
@ -88,29 +87,43 @@ export default function NewAccount() {
|
|||
}
|
||||
}, [homeServer]);
|
||||
|
||||
const connect = useCallback((url: string) => {
|
||||
const [connecting, setConnecting] = useState(false);
|
||||
const [connectionSucceeded, setConnectionSucceeded] = useState(false);
|
||||
|
||||
const connect = useCallback(() => {
|
||||
if(connecting) return;
|
||||
setHomeServer(url);
|
||||
const url = homeServerInput;
|
||||
setConnecting(true);
|
||||
|
||||
const ws = new WebSocket(url);
|
||||
try {
|
||||
const ws = new WebSocket(url);
|
||||
|
||||
ws.addEventListener('open', () => {
|
||||
setConnecting(false);
|
||||
setConnectionSucceeded(true);
|
||||
setHomeServer(homeServerInput);
|
||||
setEdittingHomeServer(false);
|
||||
});
|
||||
|
||||
ws.addEventListener('error', (e) => {
|
||||
setConnecting(false);
|
||||
setConnectionSucceeded(false);
|
||||
setConnectionError('Connection failed')
|
||||
});
|
||||
} catch (e) {
|
||||
setConnecting(false)
|
||||
setConnectionSucceeded(false);
|
||||
setConnectionError('Connection failed in catch');
|
||||
}
|
||||
}, [connecting, homeServerInput]);
|
||||
|
||||
ws.addEventListener('open', () => {
|
||||
setConnecting(false);
|
||||
setConnection(ws);
|
||||
setConnectionError('');
|
||||
});
|
||||
|
||||
ws.addEventListener('close', (e) => {
|
||||
setConnecting(false);
|
||||
setConnection(null);
|
||||
console.log(e)
|
||||
});
|
||||
|
||||
ws.addEventListener('error', (e) => {
|
||||
setConnectionError('Connection failed')
|
||||
});
|
||||
}, [connecting]);
|
||||
const next = useCallback(() => {
|
||||
// debugger;
|
||||
if(edittingHomeServer) {
|
||||
connect()
|
||||
} else {
|
||||
console.log('do login');
|
||||
}
|
||||
}, [homeServer, homeServerInput, edittingHomeServer, connect])
|
||||
|
||||
// return (
|
||||
// <div style={{
|
||||
|
|
@ -181,7 +194,8 @@ export default function NewAccount() {
|
|||
background: 'var(--neutral-3)',
|
||||
}}>
|
||||
<div style={{
|
||||
width: '450px',
|
||||
width: 'calc(100% - 40px)',
|
||||
maxWidth: '450px',
|
||||
background: 'var(--neutral-4)',
|
||||
boxShadow: '0px 4px 20px 0px var(--neutral-1)',
|
||||
borderRadius: '8px',
|
||||
|
|
@ -254,57 +268,173 @@ export default function NewAccount() {
|
|||
onClick={(e) => {
|
||||
setEdittingHomeServer(true);
|
||||
}}
|
||||
onKeyPress={(e: any) => {
|
||||
if(e.code === 'Enter') {
|
||||
if(homeServer === homeServerInput)
|
||||
return setEdittingHomeServer(false);
|
||||
setHomeServer(homeServerInput)
|
||||
}
|
||||
}}
|
||||
onKeyPress={(e: any) => e.code === 'Enter' && next()}
|
||||
></Input>
|
||||
<div style={{
|
||||
paddingLeft: '16px'
|
||||
}}>
|
||||
{(connecting) ? (
|
||||
<div style={{ color: 'var(--neutral-7)'}}>
|
||||
Connecting...
|
||||
</div>
|
||||
) : (
|
||||
(!connectionSucceeded) && (
|
||||
<div style={{ color: 'var(--red)'}}>
|
||||
{connectionError}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Label>Username</Label>
|
||||
<div style={{
|
||||
transform: 'skew(6deg, 0deg)',
|
||||
margin: '8px',
|
||||
}}>
|
||||
<Input
|
||||
disabled={edittingHomeServer}
|
||||
value={usernameInput}
|
||||
setValue={setUsernameInput}
|
||||
></Input>
|
||||
</div>
|
||||
<Label>Auth Code</Label>
|
||||
<div style={{
|
||||
transform: 'skew(6deg, 0deg)',
|
||||
margin: '8px',
|
||||
}}>
|
||||
<Input
|
||||
disabled={edittingHomeServer}
|
||||
value={authCodeInput}
|
||||
setValue={setAuthCodeInput}
|
||||
></Input>
|
||||
</div>
|
||||
<div style={{
|
||||
transform: 'skew(6deg, 0deg)',
|
||||
margin: '8px',
|
||||
textAlign: 'right'
|
||||
}}>
|
||||
<BigButton
|
||||
icon={MdOutlineNavigateNext}
|
||||
text="Next"
|
||||
selected={false}
|
||||
angle={6}
|
||||
width="auto"
|
||||
inline={true}
|
||||
onClick={() => {}}
|
||||
></BigButton>
|
||||
</div>
|
||||
<ServerConnection url={homeServer ?? ''}>
|
||||
{(returning) ? (
|
||||
<Login disabled={edittingHomeServer}></Login>
|
||||
) : (
|
||||
<SignUp disabled={edittingHomeServer}></SignUp>
|
||||
)}
|
||||
</ServerConnection>
|
||||
{edittingHomeServer && <Next onClick={next}></Next>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Next(props: {
|
||||
onClick?: (e: any) => void
|
||||
}) {
|
||||
return (
|
||||
<div style={{
|
||||
transform: 'skew(6deg, 0deg)',
|
||||
margin: '8px',
|
||||
textAlign: 'right'
|
||||
}}>
|
||||
<BigButton
|
||||
icon={MdOutlineNavigateNext}
|
||||
text="Next"
|
||||
selected={false}
|
||||
angle={6}
|
||||
width="auto"
|
||||
inline={true}
|
||||
onClick={props.onClick}
|
||||
></BigButton>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface LoginProps {
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
function Login(props: LoginProps) {
|
||||
const [usernameInput, setUsernameInput] = useState('');
|
||||
const [authCodeInput, setAuthCodeInput] = useState('');
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [success, setSuccess] = useState<string | null>(null);
|
||||
|
||||
const { setSessionToken } = useSessionToken();
|
||||
const { setClientId } = useClientId();
|
||||
|
||||
const { send } = useApi({
|
||||
'session:login'({ err, sessionToken, clientId }) {
|
||||
if(err) {
|
||||
setSuccess(null);
|
||||
setError(err);
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
setSuccess('Success!');
|
||||
setTimeout(() => {
|
||||
setClientId(clientId);
|
||||
setSessionToken(sessionToken);
|
||||
}, 1000)
|
||||
}
|
||||
})
|
||||
|
||||
const next = () => {
|
||||
send('session:login', {
|
||||
username: usernameInput,
|
||||
totp: authCodeInput
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Label>Username</Label>
|
||||
<div style={{
|
||||
transform: 'skew(6deg, 0deg)',
|
||||
margin: '8px',
|
||||
}}>
|
||||
<Input
|
||||
disabled={props.disabled}
|
||||
value={usernameInput}
|
||||
setValue={setUsernameInput}
|
||||
focusOnEenable={true}
|
||||
></Input>
|
||||
</div>
|
||||
<Label>Auth Code</Label>
|
||||
<div style={{
|
||||
transform: 'skew(6deg, 0deg)',
|
||||
margin: '8px',
|
||||
}}>
|
||||
<Input
|
||||
disabled={props.disabled}
|
||||
value={authCodeInput}
|
||||
setValue={setAuthCodeInput}
|
||||
></Input>
|
||||
</div>
|
||||
{error && <div style={{ color: 'var(--red)', textAlign: 'center' }}>
|
||||
{error}
|
||||
</div>}
|
||||
{success && <div style={{ color: 'var(--green)', textAlign: 'center' }}>
|
||||
{success}
|
||||
</div>}
|
||||
{!props.disabled && <Next onClick={next}></Next>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface SignUpProps {
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
function SignUp(props: SignUpProps) {
|
||||
const [usernameInput, setUsernameInput] = useState('');
|
||||
const [authCodeInput, setAuthCodeInput] = useState('');
|
||||
|
||||
const next = () => {
|
||||
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Label>Username</Label>
|
||||
<div style={{
|
||||
transform: 'skew(6deg, 0deg)',
|
||||
margin: '8px',
|
||||
}}>
|
||||
<Input
|
||||
disabled={props.disabled}
|
||||
value={usernameInput}
|
||||
setValue={setUsernameInput}
|
||||
focusOnEenable={true}
|
||||
></Input>
|
||||
</div>
|
||||
<Label>Auth Code</Label>
|
||||
<div style={{
|
||||
transform: 'skew(6deg, 0deg)',
|
||||
margin: '8px',
|
||||
}}>
|
||||
<Input
|
||||
disabled={props.disabled}
|
||||
value={authCodeInput}
|
||||
setValue={setAuthCodeInput}
|
||||
></Input>
|
||||
</div>
|
||||
{!props.disabled && <Next onClick={next}></Next>}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function Label(props: any) {
|
||||
return <label style={{
|
||||
paddingLeft: '24px',
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
SELECT name, username, uid FROM clients WHERE username=?
|
||||
|
|
@ -0,0 +1 @@
|
|||
SELECT totp FROM clients WHERE uid=?;
|
||||
|
|
@ -19,7 +19,7 @@ export function expose(router: Function, port: number) {
|
|||
}
|
||||
const {action, data} = message;
|
||||
try {
|
||||
if(typeof data === 'object' && 'sessionToken' in data) {
|
||||
if(typeof data === 'object' && 'sessionToken' in data && data.sessionToken !== null) {
|
||||
const auth = await validateSessionToken(data.sessionToken);
|
||||
data.$sessionToken = data.sessionToken;
|
||||
delete data['sessionToken'];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
import query from "../db/query";
|
||||
import addSessionToken from '../db/snippets/session/new.sql';
|
||||
import { rb32 } from "../lib/rb32";
|
||||
|
||||
export 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,0 +1,3 @@
|
|||
import { rb32 } from "../lib/rb32";
|
||||
|
||||
export const generateTotpKey = () => rb32() + rb32();
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import query from '../db/query'
|
||||
import getByUsername from '../db/snippets/client/getByUsername.sql'
|
||||
|
||||
export async function getClientIdByUsername(username: string) {
|
||||
const res = await query(getByUsername, username);
|
||||
if(res === null || res.length !== 1) return null;
|
||||
return res[0].uid;
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { randomBytes } from 'crypto';
|
||||
|
||||
// 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 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0
|
||||
// | | | | | | | | |
|
||||
// a b c d e f g h
|
||||
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'[n] as string;
|
||||
export function rb32() {
|
||||
const bytes = randomBytes(5);
|
||||
|
||||
const a = manipulate(bytes[0], 3, 5, 0);
|
||||
const b = manipulate(bytes[0], 0, 3, 2) | manipulate(bytes[1], 6, 2, 0);
|
||||
const c = manipulate(bytes[1], 1, 5, 0);
|
||||
const d = manipulate(bytes[1], 0, 1, 4) | manipulate(bytes[2], 4, 4, 0);
|
||||
const e = manipulate(bytes[2], 0, 4, 1) | manipulate(bytes[3], 7, 1, 0);
|
||||
const f = manipulate(bytes[3], 2, 5, 0);
|
||||
const g = manipulate(bytes[3], 0, 2, 3) | manipulate(bytes[4], 5, 3, 0);
|
||||
const h = manipulate(bytes[4], 0, 5, 0);
|
||||
|
||||
return dict(a) + dict(b) + dict(c) + dict(d) + dict(e) + dict(f) + dict(g) + dict(h);
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import query from '../db/query'
|
||||
import getTotpKey from '../db/snippets/client/getTotpKeyByClientId.sql'
|
||||
import { validateTotp } from './validateTotp';
|
||||
|
||||
export async function validateClientTotp(clientId: string, code: string) {
|
||||
const res = await query(getTotpKey, clientId);
|
||||
if(res === null || res.length !== 1) return false;
|
||||
return validateTotp(res[0].totp, code);
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import getToken from 'totp-generator';
|
||||
|
||||
export 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);
|
||||
};
|
||||
|
|
@ -4,6 +4,12 @@ import { reply } from "../lib/WebSocketServer"
|
|||
import invalidate from '../db/snippets/session/invalidate.sql'
|
||||
import _get from '../db/snippets/session/get.sql'
|
||||
import query from "../db/query";
|
||||
import { getClientIdByUsername } from "../lib/getClientIdByUsername";
|
||||
import { generateSessionToken } from "../lib/generateSessionToken";
|
||||
import { validateTotp } from "../lib/validateTotp";
|
||||
import { validateClientTotp } from "../lib/validateClientTotp";
|
||||
|
||||
const randomWait = async () => await new Promise(res => setTimeout(res, Math.random() * 300));
|
||||
|
||||
export default router({
|
||||
async 'invalidate'(data: any) {
|
||||
|
|
@ -11,6 +17,25 @@ export default router({
|
|||
return reply({
|
||||
err: null
|
||||
})
|
||||
},
|
||||
async 'login'(data: any) {
|
||||
await randomWait();
|
||||
const { username, totp } = data;
|
||||
const clientId = await getClientIdByUsername(username);
|
||||
if(clientId === null) return reply({
|
||||
err: 'Incorrect username or auth code'
|
||||
});
|
||||
const validTotp = await validateClientTotp(clientId, totp);
|
||||
console.log(username, clientId, validTotp);
|
||||
if(!validTotp) return reply({
|
||||
err: 'Incorrect username or auth code'
|
||||
});
|
||||
const sessionToken = await generateSessionToken(clientId);
|
||||
return reply({
|
||||
err: null,
|
||||
sessionToken,
|
||||
clientId
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,68 +1,17 @@
|
|||
import router from "../lib/router"
|
||||
import { reply } from "../lib/WebSocketServer"
|
||||
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
|
||||
// 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0 1 2 3 4 0
|
||||
// | | | | | | | | |
|
||||
// a b c d e f g h
|
||||
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'[n] as string;
|
||||
|
||||
function rb32() {
|
||||
const bytes = randomBytes(5);
|
||||
|
||||
const a = manipulate(bytes[0], 3, 5, 0);
|
||||
const b = manipulate(bytes[0], 0, 3, 2) | manipulate(bytes[1], 6, 2, 0);
|
||||
const c = manipulate(bytes[1], 1, 5, 0);
|
||||
const d = manipulate(bytes[1], 0, 1, 4) | manipulate(bytes[2], 4, 4, 0);
|
||||
const e = manipulate(bytes[2], 0, 4, 1) | manipulate(bytes[3], 7, 1, 0);
|
||||
const f = manipulate(bytes[3], 2, 5, 0);
|
||||
const g = manipulate(bytes[3], 0, 2, 3) | manipulate(bytes[4], 5, 3, 0);
|
||||
const h = manipulate(bytes[4], 0, 5, 0);
|
||||
|
||||
return dict(a) + dict(b) + dict(c) + dict(d) + dict(e) + dict(f) + dict(g) + dict(h);
|
||||
}
|
||||
|
||||
const totpKey = () => rb32() + rb32();
|
||||
import { validateTotp } from "../lib/validateTotp";
|
||||
import { generateSessionToken } from "../lib/generateSessionToken";
|
||||
import { generateTotpKey } from "../lib/generateTotpKey";
|
||||
|
||||
const proposals: any = {}
|
||||
|
||||
export default router({
|
||||
'propose'(clientId: string) {
|
||||
if(clientId in proposals) return reply(proposals[clientId]);
|
||||
const key = totpKey();
|
||||
const key = generateTotpKey();
|
||||
proposals[clientId] = key;
|
||||
console.log(clientId, proposals)
|
||||
setTimeout(() => {
|
||||
|
|
|
|||
Reference in New Issue