updating login

This commit is contained in:
Andrew Gundersen 2021-03-25 15:13:15 -05:00
commit bb9769e917
22 changed files with 316 additions and 362 deletions

View file

@ -1,5 +1,5 @@
<template>
<div id="app" v-if="ready">
<div id="app" v-if="(windowReady && sessionReady)">
<button class="titlebar" />
@ -28,55 +28,90 @@
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, ref } from "vue";
import { IpcRendererEvent } from "electron";
import { authRequest } from '@/modules/message';
import { useIpc } from "@/modules/ipc";
import Splash from "@/components/splash.vue"
import Splash from "@/components/splash.vue";
import Messenger from "@/components/messenger.vue";
import Login from "@/components/login.vue";
export default defineComponent({
components: { Splash },
components: {
Splash,
Messenger,
Login
},
setup() {
const { post, invoke } = useIpc();
const ready = ref(false);
const auth = ref(false);
const windowReady = ref(false);
const sessionReady = ref(false);
// Whether user is logged in.
let auth = ref(false);
// When connection is established, we send key over.
const onOpen = (_event: IpcRendererEvent, payload: any) => {
console.log(`Connected to Crimata.`)
const key = window.localStorage.getItem("key");
if (key) {
console.log(`Sending key: ${key}`)
post("client-message", authRequest(key, false, false))
}
else {
console.log("No key, session ready.")
sessionReady.value = true
}
// Attempt token login
const key = window.localStorage.getItem(AUTH_KEY);
if (key) {
post("client-message", key)
}
// onAuthResponse
const onAuthResponse = (_event: IpcMainEvent, payload: Auth) => {
if (payload.token) {
// Update authenticate state on auth message from server.
const onAuthResponse = (_event: IpcRendererEvent, payload: any) => {
const key = payload.message.key
const usr = payload.message.usr
console.log(`Received auth response: ${usr}, ${key}`)
if (key) {
console.log(`Auth success, saving key: ${key}.`)
window.localStorage.setItem("key", payload.message.key)
auth.value = true;
}
else {
console.log(`Auth failed, clearing local storage.`)
window.localStorage.clear()
auth.value = false;
}
console.log("Session is ready.")
sessionReady.value = true
}
// Post navbar action to backend.
const callNavbar = (action: string) => {
post('nav-bar', action);
invoke("nav-bar", action);
}
onMounted(() => {
window.ipcRenderer.on("auth-response", onAuthResponse)
window.ipcRenderer.on("window-ready", () => ready.value = true);
window.ipcRenderer.on("on-connect", onOpen)
window.ipcRenderer.on("auth-response", onAuthResponse)
window.ipcRenderer.on("window-ready", () => windowReady.value = true);
});
onUnmounted(() => {
window.ipcRenderer.removeAllListeners("window-ready")
window.ipcRenderer.removeAllListeners("auth-response")
window.ipcRenderer.removeAllListeners("on-connect")
window.ipcRenderer.removeAllListeners("window-ready")
window.ipcRenderer.removeAllListeners("auth-response")
})
return {
callNavbar,
ready,
auth,
sessionReady,
windowReady,
auth
}
}
})

View file

@ -18,8 +18,6 @@ const audioContainer = {
input: '',
}
const encoding = "hex";
const audioOptions = {
channelCount: 1,
sampleFormat: 16,
@ -28,21 +26,73 @@ const audioOptions = {
closeOnError: false,
}
// callback run on space bar key up and down.
const updateRecorder = (_event, payload: { isRecording: boolean; uid: string | null;
}): void => {
record = payload.isRecording;
if (!record) {
if (payload.uid) sendAudio(audioContainer.input, payload.uid);
}
// Returns recorded audio to frontend and sets record to false.
const onRecordingEnd = async (_event: any, payload: any) => {
return new Promise((resolve, reject) => {
try {
resolve(audioContainer.input);
record = false;
} catch (e) {
reject()
}
});
};
// listen for space key up/down event.
ipcMain.removeAllListeners('update-recorder');
ipcMain.on('update-recorder', updateRecorder);
// Main audio function run by run.ts module.
export function initAudioIO(): void {
console.log("AUDIO:Starting io streams.")
if (!ai) {
// Initialize and start input stream.
ai = new portAudio.AudioIO({ inOptions: audioOptions });
ai.setEncoding("hex");
ai.start();
// On each data chunk...
ai.on('data', (chunk: string) => {
// If recording, we capture the data.
if (record) {
console.log('Recording...')
audioContainer.input += chunk;
}
// Else, we don't capture and also clear audioContainer.
else {
if (audioContainer.input.length) {
audioContainer.input = "";
}
}
});
}
if (!ao) {
// Initialize and start input stream.
ao = new portAudio.AudioIO({ outOptions: audioOptions });
ao.start();
}
// Listen to record.
console.log("AUDIO:Adding recording listeners.")
ipcMain.removeAllListeners('start-recording');
ipcMain.on('start-recording', () => record = true);
ipcMain.removeHandler('stop-recording');
ipcMain.handle('stop-recording', onRecordingEnd);
}
// ---Audio playback--------------------------------------------
// Split Buffer into an array of len-sized Buffers.
function bufSplit(buf: Buffer, len: number): Array<Buffer> {
@ -59,32 +109,6 @@ function bufSplit(buf: Buffer, len: number): Array<Buffer> {
return chunks;
}
// Main audio function run by run.ts module.
export function initAudioIO(): void {
if (!ai) {
ai = new portAudio.AudioIO({ inOptions: audioOptions });
// base64 encoding needed for google speech to text.
ai.setEncoding(encoding);
ai.start();
ai.on('data', (chunk: string) => {
if (record) {
console.log('recording...')
audioContainer.input += chunk;
} else {
if (audioContainer.input.length) audioContainer.input = "";
}
});
}
if (!ao) {
ao = new portAudio.AudioIO({ outOptions: audioOptions });
ao.start();
}
}
// Audio playback.
export function play(input: Buffer): void {
let i = 0;
@ -128,8 +152,11 @@ export function play(input: Buffer): void {
}
}
// -------------------------------------------------------------
// Get's called on window close.
export function stopStream() {
console.log("AUDIO:Stopping audio stream.")
if(ai != null) {
ai.quit();
ai = null;
@ -140,3 +167,32 @@ export function stopStream() {
}
}
// // Returns recorded audio to frontend.
// const getAudio = async (_event: any, payload: any) => {
// return new Promise((resolve, reject) => {
// // Handle auth response from server.
// backgroundMitt.once('auth-res', (res: string) => {
// if (res == "locked") {
// reject(res);
// } else {
// console.log('Success!');
// auth = true;
// resolve(res);
// }
// });
// if (typeof payload !== "string") {
// payload = JSON.stringify(payload)
// }
// // Send token to backend
// socket.send(payload);
// });
// };

View file

@ -2,34 +2,59 @@
"use strict";
import { app } from "electron";
import { main } from './run';
import { backgroundMitt } from './emitter';
import { stopStream } from './audio';
import { createWindow } from './window';
import { initSession } from './session';
import { initAudioIO, stopStream } from './audio';
import { backgroundMitt } from '@/modules/emitter';
let winActive: boolean;
let win: boolean;
// Listen for window creation.
backgroundMitt.on('window-active', (state: boolean) => {
winActive = state;
win = state;
});
// Run when electron app is initialized.
async function main() {
console.log("MAIN:Initializing Electron App.")
// Must wait til window is created.
await createWindow();
// Instantiate socket session with crimata-platorm.
initSession();
// Begin audio stream.
initAudioIO();
}
// Root function of app.
export function initApp(dev: boolean): void {
// On initial startup.
app.on("ready", () => {
main();
main()
});
// Quit when all windows are closed.
// Quit app on window closed.
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
stopStream();
app.quit();
}
console.log("MAIN:Quitting app.")
app.quit()
});
// Shutdown audio streams peacefully.
app.on("before-quit", () => {
stopStream()
});
// When user clicks app icon (re-open)
app.on("activate", () => {
if (winActive === false) {
main();
if (!win) {
createWindow();
}
});
// Exit cleanly on request from parent process in development mode.

View file

@ -1,23 +0,0 @@
"use strict";
import { createWindow } from './window';
import agentInterface from './session';
import { initAudioIO } from './audio';
/*
* The main function will be run after electron app is ready.
*/
export async function main() {
const { initSession } = agentInterface()
// create main window.
await createWindow();
// Instantiate socket session with crimata-platorm.
initSession();
// Begin audio stream.
initAudioIO();
}

View file

@ -1,3 +1,12 @@
/*
* Creates a websocket session with Crimata Servers.
*
* Connects to Servers and attempts token authentication. Will send the result
* of the authentication to the window. It will then serve as a communication
* interface between the window and the servers. It will automatically try to
* reconnect on websocket disconnect.
*/
import { backgroundMitt } from './emitter';
import { ipcMain, IpcMainEvent } from "electron";
@ -5,13 +14,18 @@ import useWebSockets from "@/modules/ws";
import { handleAuthMessage, handleStandardMessage, handleProfileMessage, handleAnnotation } from "./handlers";
// Key for key-based auth.
let key: string;
// Calls appropriate endpoint for a server message.
const onServerMessage = (data: string): void => {
const onMessage = (data: string) => {
const message = JSON.parse(data)
if (message.key) {
backgroundMitt.emit('auth-response', message);
if (message.hasOwnProperty("key")) {
handleAuthMessage(message)
}
else if (message.content) {
@ -28,27 +42,43 @@ const onServerMessage = (data: string): void => {
};
const { createSocket, sendMessage } = useWebSockets(onServerMessage);
// Handle messages from client.
const onClientMessage = (_event: IpcMainEvent, payload: any) {
sendMessage(JSON.stringify(payload))
// Attempt token authentication onOpen.
const onOpen = () => {
if (key) {
sendMessage({
"key": key,
"usr": false,
"pwd": false
})
}
}
// Routes messages to and from Crimata Servers.
export default function agentInterface() {
const initSession = () => {
const { createSocket, sendMessage } = useWebSockets(onMessage, onOpen);
ipcMain.removeAllListeners()
// Handle messages from window/client.
const onMessageFromWindow = (_event: IpcMainEvent, payload: any) => {
console.log("New client message")
ipcMain.on("client-message", onClientMessage);
const success = sendMessage(payload)
createSocket()
if (!success) {
console.log("Unable to send message: ", payload)
}
}
return {
initSession
}
// Call this to initialize session with Crimata servers.
export const initSession = (key: string) => {
// Set the key.
key = key;
// Open socket connection.
createSocket()
// Attach listeners for frontend.
ipcMain.removeAllListeners("client-message")
ipcMain.on("client-message", onMessageFromWindow);
}

View file

@ -16,7 +16,6 @@ let win: BrowserWindow | null;
// Called when a NavBar button is pressed.
const onNavBar = (_event: any, action: string): void => {
console.log("onNavBar")
if (win) {
if (action === "close") {
win.close()
@ -59,18 +58,25 @@ const saveWindowState = () => {
// Do this on window mount.
const onWindowMount = (): void => {
console.log("BW:Adding window listeners. ")
// Must tell initApp that window exists.
backgroundMitt.emit('window-active', true);
// Handle win nav-bar event.
ipcMain.removeHandler('nav-bar'); // avoid setting duplicate handlers
ipcMain.handle('nav-bar', onNavBar);
ipcMain.removeHandler("nav-bar") // avoid setting duplicate handlers
ipcMain.handle("nav-bar", onNavBar);
// Gateway for messages to the frontend.
backgroundMitt.removeAllListeners('ipc-renderer')
backgroundMitt.on('ipc-renderer', renderMessage);
console.log("BW:Listeners created.")
}
// Do this on window dismount (close).
const onWindowDismount = (): void => {
console.log("BW:Window closed.")
win = null;
backgroundMitt.emit('window-active', false);
}
@ -78,6 +84,7 @@ const onWindowDismount = (): void => {
// function used by run.ts to create the main window.
export async function createWindow(): Promise<void> {
return new Promise((resolve, _reject) => {
console.log("BW:Creating browser window. ")
// avoid creating duplicate windows.
if (win) resolve();
@ -122,6 +129,7 @@ export async function createWindow(): Promise<void> {
if (win) win.show()
})
// For showing/hiding the splash screen.
win.webContents.on('did-finish-load', () => {
if (win) win.webContents.send('window-ready', {
message: true

View file

@ -3,7 +3,7 @@ import { onMounted, onUnmounted, ref, Ref } from "vue";
import useMitt from "@/modules/mitt";
import { useIpc } from '@/modules/ipc';
import keyboardNameMap from "../keyBoardMaps/keyboardNameMap";
import { renderMessage } from '@/modules/message';
import { renderMessage, clientMessage } from '@/modules/message';
function showRecIcon () {
@ -34,7 +34,7 @@ function hideRecIcon () {
export default function useAudioInputController (typing: Ref) {
// For sending messages.
const { post } = useIpc();
const { post, invoke } = useIpc();
const { emitter } = useMitt();
// Keepp track of when we are recording.
@ -49,10 +49,7 @@ export default function useAudioInputController (typing: Ref) {
// Start recording on space bar.
if (cmd == "SPACE" && !recording.value && !typing.value) {
post('update-recorder', {
isRecording: true,
uid: null
});
post('start-recording', "");
showRecIcon()
recording.value = true;
@ -62,7 +59,7 @@ export default function useAudioInputController (typing: Ref) {
}
const onKeyUp = (e: KeyboardEvent) => {
const onKeyUp = async (e: KeyboardEvent) => {
const cmd = keyboardNameMap[e.keyCode];
// Stop recording on space up.
@ -72,10 +69,12 @@ export default function useAudioInputController (typing: Ref) {
const message = renderMessage("", "", "", "sf")
emitter.emit("self-message", message);
const audio = invoke('update-recorder', {
isRecording: false,
uid: message.uid
});
// Stop recording and get audio from recorder.
const audio = await invoke('stop-recording', "");
// Send message to the backend for processing.
const clientM = clientMessage("", audio, message.uid);
post('client-message', clientM);
hideRecIcon()
recording.value = false;

View file

@ -1,5 +1,5 @@
<template>
<form id="login" @submit.prevent="submit">
<form id="login" @submit.prevent="submitForm">
<!-- login title -->
@ -11,24 +11,21 @@
<div id="loginBox">
<!-- username -->
<div class="inputBox">
<input class="input" type="text" v-model="email" />
<input class="input" type="text" v-model="usr" />
<div class="inputModifier">Email</div>
</div>
<!-- password -->
<div class="inputBox">
<input class="input" type="password" v-model="password" />
<input class="input" type="password" v-model="pwd" />
<div class="inputModifier">Password</div>
</div>
</div>
<!-- submit button; position: fixed -->
<button class="submitButton button" type="submitForm">Submit</button>
<div class="invalid" v-if="invalid">
Incorrect Credentials
</div>
<button class="submitButton button" type="submit">Submit</button>
</form>
<!-- back to login button: position: fixed -->
@ -40,8 +37,8 @@
import { defineComponent, ref } from "vue";
import { useAuth } from "@/modules/auth";
import { useIpc } from "@/modules/ipc";
import { useRouter } from "vue-router";
import useMessages from "@/modules/messages";
import { authRequest } from '@/modules/message';
export default defineComponent({
name: "Login",
@ -52,20 +49,18 @@ export default defineComponent({
const { setToken } = useAuth();
const { resetMessages } = useMessages();
const router = useRouter();
const form = ref({
email: "",
password: "",
})
let usr = ref("");
let pwd = ref("");
// Submit login credentials to the backend.
const submitForm = () => {
post("auth-message", form)
console.log(`Submitting login form: ${usr.value}, ${pwd.value}`)
post("client-message", authRequest(false, usr.value, pwd.value))
}
return {
form,
usr,
pwd,
submitForm
}

View file

@ -28,8 +28,7 @@
import { defineComponent, ref } from "vue";
import { useAuth } from "@/modules/auth";
import { useIpc } from "@/modules/ipc";
import { useRouter } from "vue-router";
import { logoutRequest } from '@/modules/message';
export default defineComponent({
name: "Settings",
@ -38,8 +37,6 @@
const toggleSettings = ref(false);
const { post } = useIpc();
const { logout } = useAuth();
const router = useRouter();
// Listen for escape key to close settings.
const onEscape = (e: any) => {
@ -57,7 +54,8 @@
// We ask server to log us out.
const onLogout = () => {
post("auth-request", "logout")
console.log("Submitting logout request.")
post("client-message", logoutRequest())
}
return {

View file

@ -1,4 +1,4 @@
import { RenderMessage, ClientMessage } from "@/types/message/index";
import { RenderMessage, ClientMessage, AuthRequest, LogoutRequest } from "@/types/message/index";
import { v4 as uuidv4 } from 'uuid';
function getTimeStamp(): number {
@ -27,4 +27,18 @@ export const clientMessage = (text: string, audio: string | boolean, uid: string
text,
uid
}
)
export const authRequest = (key: boolean | string, usr: boolean | string, pwd: boolean | string): AuthRequest => (
{
key,
usr,
pwd
}
)
export const logoutRequest = (): LogoutRequest => (
{
logout: true
}
)

View file

@ -7,40 +7,49 @@ let socket: WebSocket;
// Run every time we want to connect to backend.
export default function useWebSockets(receiveCallback: (s: string) => any) {
export default function useWebSockets(receiveCallback: (s: string) => any, openCallback: () => any) {
const sendMessage = (data: string) => {
// Returns bool (sucess or fail).
const sendMessage = (data: any) => {
console.log("WS:Sending message: ", data)
console.log("Sending message: ", data)
socket.send(data)
if (socket.readyState !== 1) {
return false
}
else {
socket.send(JSON.stringify(data))
return true
}
}
const onOpen = (event: WebSocket.OpenEvent) => {
console.log('Connected to Server!');
console.log("WS:Connected to WS Server!");
openCallback()
}
const onServerMessage = (event: WebSocket.MessageEvent) => {
console.log('Message received: ', event);
console.log("WS:Message received: ", event.data);
receiveCallback(event.data.toString())
}
const onClose = (event: WebSocket.CloseEvent) => {
console.log("Socket closed normally.")
console.log("WS:Socket closed normally.")
}
// Reconnect automatically on error.
const onError = (event: WebSocket.ErrorEvent) => {
console.log('WebSocket error: ', event);
console.log("WS:WebSocket error: ", event.message);
console.log("Reconnecting...")
createSocket()
console.log("Attempting reconnect in 1s.")
setTimeout(createSocket, 1000)
}
const createSocket = () => {
@ -50,9 +59,9 @@ export default function useWebSockets(receiveCallback: (s: string) => any) {
socket.addEventListener("open", onOpen)
socket.addEventListener("message", onServerMessage)
socket.addEventListener("close", onClose)
socket.addEventListener("error", createSocket)
socket.addEventListener("error", onError)
console.log("New socket created.")
console.log("WS:New socket created.")
}
return {

View file

@ -16,9 +16,14 @@ export interface ClientMessage {
uid: string;
}
export interface Creds {
email: string;
password: string;
export interface AuthRequest {
key: boolean | string;
usr: boolean | string;
pwd: boolean | string;
}
export interface LogoutRequest {
logout: boolean;
}
// Possible messages from server:

View file

@ -1,197 +0,0 @@
<template>
<div id="register">
<!-- login title -->
<div id="registerTitle">
<div>Register</div>
</div>
<!-- username and password forms -->
<form id="registerBox" @submit.prevent="submit">
<!-- username -->
<input class="input" type="text" v-model="email" placeholder="email" required/>
<!-- password -->
<input class="input" type="password" v-model="password" placeholder="Password" required/>
<!-- re-enter passoword -->
<input class="input" type="password" v-model="password2" placeholder="Re-enter password" required/>
<!-- submit button; position: fixed -->
<button class="button" type="submit">Submit</button>
<div v-if="invalidEmail" class="invalid">Invalid Email.</div>
<div v-else-if="weakPass" class="invalid">Password must be 8-15 characters long.
<br>Password must have at least one:
<br>&emsp; Upper case letter
<br>&emsp; Lower case letter
<br>&emsp; One numeric digit
<br>&emsp; One special character
</div>
<div v-else-if="passUnMatch" class="invalid">Passwords dont match.</div>
</form>
<!-- back to login button: position: fixed -->
<button class="button2" @click.prevent="switchView">Back to login</button>
</div>
</template>
<script lang="ts">
import {
defineComponent,
ref,
} from "vue";
import { useRouter } from "vue-router";
import { useIpc } from "@/modules/ipc";
import { useAuth } from "@/modules/auth";
interface RegisterPayload {
request: string;
email: string;
password: string;
}
export default defineComponent({
name: "Register",
setup() {
const router = useRouter();
const { invoke } = useIpc();
const { setToken } = useAuth();
const email = ref("");
const password = ref("");
const password2 = ref("");
const invalidEmail = ref(false);
const weakPass = ref(false);
const passUnMatch = ref(false);
// emai must be in '@email.com' format
const emailReg = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i;
// password must be 8-15 chars, have one upper, lower, number, and special char
const pwReg = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,15}$/;
// call this function on form submit.
const submit = async () => {
// reset invalid credentials references.
invalidEmail.value = false;
weakPass.value = false;
passUnMatch.value = false;
// validate email.
if (!emailReg.test(email.value)) {
invalidEmail.value = true;
return;
}
// validate password strength.
if (!pwReg.test(password.value)) {
weakPass.value = true;
return;
}
// check password match
if (password.value !== password2.value) {
passUnMatch.value = true;
return;
}
const payload: RegisterPayload = {
request: "register",
email: email.value,
password: password.value,
};
try {
const res = await invoke('auth-session', payload);
setToken(res);
router.push({ name: "home" });
} catch(e) {
console.log('Failed to register.')
}
};
const switchView = () => {
router.push({ name: "login" });
};
return {
email,
password,
password2,
switchView,
submit,
invalidEmail,
weakPass,
passUnMatch,
};
},
});
</script>
<style lang="scss" scoped>
#register {
width: 350px;
height: 500px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #e6e6e6;
}
#registerTitle {
min-width: 181px;
font-size: 24px;
font-weight: bold;
text-align: left;
}
#registerBox {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.input {
background-color: #B9B9B9;
border: none;
color: white;
padding: 16px;
text-decoration: none;
margin: 4px 2px;
cursor: pointer;
border-radius: 10px;
}
.button {
position: fixed;
margin-top: 165px;
background-color: #58C4FD;
border: none;
color: white;
padding: 10px 20px;
text-decoration: none;
border-radius: 20px;
font-weight: bold;
}
.button2 {
position: fixed;
margin-top: 225px;
border: none;
background-color: #e6e6e6;
text-decoration: none;
color: #58C4FD;
font-weight: bold;
}
.invalid {
font-family: "SF Compact Display";
font-weight: bold;
font-size: 14px;
color: #F53737;
}
</style>

View file

@ -1 +1 @@
{"w":350,"h":699,"x":649,"y":168}
{"w":629,"h":500,"x":1423,"y":657}