update sockets, account, auth

This commit is contained in:
riqo 2021-06-14 08:52:01 -05:00
commit d851db2fcc
20 changed files with 411 additions and 326 deletions

View file

@ -1,79 +0,0 @@
# Do not edit. File was generated by node-gyp's "configure" step
{
"target_defaults": {
"cflags": [],
"default_configuration": "Release",
"defines": [],
"include_dirs": [],
"libraries": []
},
"variables": {
"asan": 0,
"build_v8_with_gn": "false",
"coverage": "false",
"dcheck_always_on": 0,
"debug_nghttp2": "false",
"debug_node": "false",
"enable_lto": "false",
"enable_pgo_generate": "false",
"enable_pgo_use": "false",
"error_on_warn": "false",
"force_dynamic_crt": 0,
"host_arch": "x64",
"icu_data_in": "../../deps/icu-tmp/icudt67l.dat",
"icu_endianness": "l",
"icu_gyp_path": "tools/icu/icu-generic.gyp",
"icu_path": "deps/icu-small",
"icu_small": "false",
"icu_ver_major": "67",
"is_debug": 0,
"llvm_version": "0.0",
"napi_build_version": "6",
"node_byteorder": "little",
"node_debug_lib": "false",
"node_enable_d8": "false",
"node_install_npm": "true",
"node_module_version": 83,
"node_no_browser_globals": "false",
"node_prefix": "/",
"node_release_urlbase": "https://nodejs.org/download/release/",
"node_shared": "false",
"node_shared_brotli": "false",
"node_shared_cares": "false",
"node_shared_http_parser": "false",
"node_shared_libuv": "false",
"node_shared_nghttp2": "false",
"node_shared_openssl": "false",
"node_shared_zlib": "false",
"node_tag": "",
"node_target_type": "executable",
"node_use_bundled_v8": "true",
"node_use_dtrace": "true",
"node_use_etw": "false",
"node_use_node_code_cache": "true",
"node_use_node_snapshot": "true",
"node_use_openssl": "true",
"node_use_v8_platform": "true",
"node_with_ltcg": "false",
"node_without_node_options": "false",
"openssl_fips": "",
"openssl_is_fips": "false",
"shlib_suffix": "83.dylib",
"target_arch": "x64",
"v8_enable_31bit_smis_on_64bit_arch": 0,
"v8_enable_gdbjit": 0,
"v8_enable_i18n_support": 1,
"v8_enable_inspector": 1,
"v8_enable_pointer_compression": 0,
"v8_no_strict_aliasing": 1,
"v8_optimized_debug": 1,
"v8_promise_internal_field_count": 1,
"v8_random_seed": 0,
"v8_trace_maps": 0,
"v8_use_siphash": 1,
"want_separate_host_toolset": 0,
"xcode_version": "11.0",
"nodedir": "/Users/Enrique/Library/Caches/node-gyp/14.4.0",
"standalone_static_library": 1
}
}

View file

@ -12,7 +12,7 @@
"postinstall": "electron-builder install-app-deps",
"postuninstall": "electron-builder install-app-deps"
},
"main": "background.js",
"main": "init.js",
"dependencies": {
"@google-cloud/speech": "^4.2.0",
"@types/animejs": "^3.1.2",
@ -73,7 +73,7 @@
"lintOnSave": false,
"pluginOptions": {
"electronBuilder": {
"preload": "src/preload.ts",
"preload": "src/renderer/preload.ts",
"builderOptions": {
"appId": "com.crimata.ElectronUpdaterApp",
"artifactName": "${productName}-${version}.${ext}",

View file

@ -11,11 +11,7 @@
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<!-- <h1>Hello World!</h1>
We are using Node.js <span id="node-version"></span>, Chromium
<span id="chrome-version"></span>, and Electron
<span id="electron-version"></span>. -->
<div id="app"></div>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

105
src/account.ts Normal file
View file

@ -0,0 +1,105 @@
import { postAuth, postLogin, postLogout } from "@/api/account";
import { endSession, launchSession } from "@/session";
import { getToken, clearToken, setToken } from "@/composables/store";
import { parseAuthRes } from "./auth";
export const accountAuth = async (): Promise<Error | AuthState> => {
/* attempt to get a login token from the store */
const token = getToken();
/* try to login with it, returns platform secret and new token on success */
if (token) {
try {
const res = await postAuth(token);
const parsed = parseAuthRes(res);
setToken(parsed.token)
return {
profile: parsed.profile,
token: parsed.token
};
} catch(e) {
console.log('[ACCOUNT]', e);
clearToken();
throw(new Error('Failed to authenticate.'));
}
} else {
throw(new Error('Unable to authenticate.'));
}
};
export const accountLogin: IpcHandlerCallback<AccountCredentials, Profile> = async (payload) => {
const account = payload as AccountCredentials;
try {
// attempt login with email password
const res = await postLogin(account.email, account.password);
const parsed = parseAuthRes(res);
// save jwt token and profile
setToken(parsed.token)
// launch session
launchSession(parsed.token)
// return profile to renderer
return parsed.profile;
} catch(e) {
throw e;
}
}
// export const accountLogin = async (account: Account): Promise<Error | Profile> => {
//
// try {
//
// // attempt login with email password
// const res = await postLogin(account.email, account.password);
// const parsed = parseAuthRes(res);
//
// // save jwt token and profile
// setToken(parsed.token)
//
// // launch session
// launchSession(parsed.token)
//
// // return profile to renderer
// return parsed.profile;
//
// } catch(e) {
// console.log('[ACCOUNT]', e);
// throw (new Error('Failed to authenticate'));
// }
//
// }
export const accountLogout = async (): Promise<Error | void> => {
try {
// post logout to backend
await postLogout();
// remove key and crimataId
clearToken();
// kill crimata platform session
endSession();
return;
} catch(e) {
console.log('[ACCOUNT]', e);
return (new Error('Failed to logout. Please try again.'));
}
}

View file

@ -1,36 +1,31 @@
import { useHttp } from "@/composables/http";
import axios from "axios";
import {config} from "@/config";
const { post } = useHttp();
export const usrPwdAuth = async (email: string, password: string) => {
try {
return await post('/account/login', { email, password })
} catch (e) {
return null;
}
}
export const tokenAuth = async (cid: string, token: string) => {
try {
return await axios({
url: "http://127.0.0.1:3000/api/account/profile",
headers: {
Cookie: `jwt=${token}`
},
method: 'GET',
data: {
cid,
}
export const postAuth = async (token: string) => (
await axios({
url: config.BUSINESS_URL + config.BUSINESS_PREFIX + '/account/authenticate',
headers: {
Cookie: `jwt=${token}`
},
method: 'POST',
})
).data;
export const postLogin = async (email: string, password: string) => (
await post('/account/login', { email, password })
).data;
export const postLogout =
async (): Promise<null | Error> => (await post('/account/logout'));
} catch (e) {
return null;
}
}

13
src/auth.ts Normal file
View file

@ -0,0 +1,13 @@
export const parseAuthRes = (authRes: any) => {
const token = authRes.headers['set-cookie'][0].split(";")[0].split("=")[1] as string;
const profile = authRes.data as Profile;
return {
token,
profile
}
};

View file

@ -1,10 +1,8 @@
import axios, { AxiosRequestConfig } from 'axios';
import {config} from "@/config";
const preFix = '/api';
const baseURL = "http://127.0.0.1:3000" + preFix;
const baseURL = config.BUSINESS_URL + config.BUSINESS_PREFIX;
interface Request {
endpoint: string;
@ -12,7 +10,6 @@ interface Request {
config?: Record<string, any>;
}
const makeQuery = (reqQuery: Record<string, any>) => {
let result = '';

View file

@ -0,0 +1,50 @@
import { ipcMain, IpcMainInvokeEvent } from "electron";
export class IpcHandler<InputType, ReturnType> implements IIpcHandler<InputType, ReturnType> {
readonly channel: string;
readonly _handlerCallback: IpcHandlerCallback<InputType, ReturnType>;
constructor(options: {
channel: string;
handlerCallback: IpcHandlerCallback<InputType, ReturnType>;
}) {
this.channel = options.channel;
this._handlerCallback = options.handlerCallback;
}
handle() {
ipcMain.handle(this.channel, this._onInvoke);
}
remove() {
ipcMain.removeHandler(this.channel);
}
private async _onInvoke(_e: IpcMainInvokeEvent, payload?: string | null): Promise<Error | ReturnType> {
return new Promise(async (resolve, reject) => {
console.log(`[IPC]:${this.channel}`);
try {
const params = payload ? JSON.parse(payload) : null;
const res = await this._handlerCallback(params);
resolve(res as unknown as ReturnType);
} catch(e) {
console.log(`[IPC]:${this.channel}`, e);
reject(e);
}
});
}
}

View file

@ -1,9 +1,13 @@
import {config} from "@/config";
import fs from 'fs';
export const saveToJson = (fileName: string, data: any) => {
fs.writeFile(configPath + fileName, JSON.stringify(data), (err) => {
fs.writeFile(config.configPath + fileName, JSON.stringify(data), (err) => {
if (err) {
console.log("Error when saving to json.")
}
}
})
}
}

19
src/composables/store.ts Normal file
View file

@ -0,0 +1,19 @@
const Store = require('electron-store');
const schema = {
key: {
type: 'string',
},
};
const store = new Store({
schema,
encryptionKey: "super user test"
});
export const getToken = (): string | undefined => (store.get("token"));
export const clearToken = (): void => (store.delete("token"));
export const setToken = (token: string): void => (store.set('token', token));

View file

@ -3,9 +3,18 @@
import WebSocket from 'ws';
export default function useWebSockets(onMessageCallback: (s: string) => void) {
let socket: WebSocket | null = null;
const _connectionCheckTimeout = 4000;
const _reconnectTimeout = 1000;
let _connectionCheckInterval: ReturnType<typeof setTimeout>;
export default function useWebSockets(
messageCallback: (message: string) => void,
connectionStatusCallback: (alive: boolean) => void,
) {
let socket: WebSocket;
const send = async (data: Record<string, any>): Promise<boolean> => {
return new Promise((resolve, reject) => {
@ -21,30 +30,52 @@ export default function useWebSockets(onMessageCallback: (s: string) => void) {
const connect = (socketUrl: string, secret: string) => {
// avoid setting multiple interval;
if (_connectionCheckInterval) clearInterval(_connectionCheckInterval);
/* create a new socket */
socket = new WebSocket(socketUrl)
socket = new WebSocket(socketUrl);
/* add event listeners */
socket.on("open", () => {
if (socket)
socket.send(secret);
// ping server
_connectionCheckInterval = setInterval(() => {
socket.ping(null, true, (e: Error) => {
if (e) {
socket.close();
connectionStatusCallback(false);
setTimeout(() => connect(socketUrl, secret), _reconnectTimeout);
}
});
}, _connectionCheckTimeout);
});
socket.on("message", (event: WebSocket.MessageEvent) => {
onMessageCallback(event.data.toString())
messageCallback(event.data.toString())
});
socket.on("close", () => {
return
socket.on("close", (event: WebSocket.CloseEvent) => {
connectionStatusCallback(false);
clearInterval(_connectionCheckInterval);
if (!event.wasClean) {
setTimeout(() => connect(socketUrl, secret), _reconnectTimeout);
}
});
socket.on("pong", () => connectionStatusCallback(true));
}
const close = () => {
if (socket) {
socket.close();
socket = null;
}
}

17
src/config.ts Normal file
View file

@ -0,0 +1,17 @@
import { app } from "electron";
const env = process.env;
const PLATFORM_PORT = env.PLATFORM_PORT || 8760;
const PLATFORM_IP = env.PLATFORM_IP || 'http://127.0.0.1';
const BUSINESS_PORT = env.BUSINESS_PORT || 3010;
const BUSINESS_IP = env.BUSINESS_IP || 'http://127.0.0.1';
export const config = {
PLATFORM_URL: `${PLATFORM_IP}:${PLATFORM_PORT}`,
BUSINESS_URL: `${BUSINESS_IP}:${BUSINESS_PORT}`,
BUSINESS_PREFIX: '/api',
configPath: app.getPath('userData')
}

View file

@ -9,8 +9,6 @@ import { app, protocol } from "electron";
import createWindow from "./window";
import main from "./main";
require('dotenv').config();
console.log('Starting Crimata electron app.');
// Scheme must be registered before the app is ready
@ -43,4 +41,4 @@ if (isDev) {
process.on("SIGTERM", () => {
app.quit();
});
}
}

View file

@ -1,126 +1,32 @@
"use strict";
import { submit, fetchProfile, logout } from "../api/account";
import { ipcMain, IpcMainInvokeEvent } from "electron";
import { store } from "@/composables/store";
import { accountLogin, accountLogout } from "@/account";
import {IpcHandler} from "@/composables/ipcHandler";
const LOGIN_CHANNEL = "account-login";
const LOGOUT_CHANNEL = "account-logout";
const parseAuthRes = (authRes: any) => {
const token = authRes.headers['set-cookie'][0].split(";")[0].split("=")[1] as string;
const profile = authRes.data as Profile;
return {
token,
profile
}
};
const loginHandler = new IpcHandler({
channel: LOGIN_CHANNEL,
handlerCallback: accountLogin
});
const logoutHandler = new IpcHandler({
channel: LOGOUT_CHANNEL,
handlerCallback: accountLogout
});
const handlers = [loginHandler, logoutHandler];
export default handlers;
/**
* Get user profile from store and try to login with it.
*/
const onTokenLogin = async (
_event: IpcMainInvokeEvent,
_payload: null
): Promise<Profile | Error> => (
new Promise(async (resolve, reject) => {
console.log('[IPC]: user-profile');
// get jwt token and crimataId from store
const token = store.get('key');
const crimataId = store.get('crimataId');
// authenticate and fetch profile
try {
// attempt login with email token
const res = await fetchProfile(crimataId, token);
const parsed = parseAuthRes(res);
// return profile to renderer
resolve(parsed.profile);
} catch(e) {
reject(new Error('Failed to fetch profile.'));
}
})
)
const onLogin = async (
_event: IpcMainInvokeEvent,
payload: string
): Promise<Profile | Error> => (
new Promise(async (resolve, reject) => {
console.log('[IPC]: user-login');
const account = JSON.parse(payload);
if ( account.password && account.email ) {
try {
// attempt login with email password
const res = await submit(account.email, account.password);
const parsed = parseAuthRes(res);
// save jwt token and profile
store.set('key', parsed.token);
store.set('crimataId', parsed.profile.crimataId);
// init session
// return profile to renderer
resolve(parsed.profile);
} catch(e) {
console.log('[API]', e);
reject(new Error('Failed to authenticate'));
}
}
})
)
const onLogout = async (
_event: IpcMainInvokeEvent,
_payload: null
): Promise<void> => (
new Promise(async (resolve, reject) => {
console.log('[IPC]: user-logout');
try {
// post logout to backend
await logout();
// remove key and crimataId
store.delete('key');
store.delete('crimataId');
// TODO: kill crimata platform session
// endSession();
resolve();
} catch(e) {
reject(new Error('Failed to logout. Please try again.'));
}
})
)
export default function useAccountListeners(): void {
ipcMain.removeHandler("user-profile");
ipcMain.handle("user-profile", onProfile);
ipcMain.removeHandler("user-login");
ipcMain.handle("user-login", onLogin);
ipcMain.removeHandler("user-logout");
ipcMain.handle("user-logout", onLogout);
}
// export default function useAccountListeners(): void {
//
// ipcMain.removeHandler(LOGIN_HANDLER);
// ipcMain.handle(LOGIN_HANDLER, onLogin);
//
// ipcMain.removeHandler(LOGOUT_HANDLER);
// ipcMain.handle(LOGOUT_HANDLER, onLogout);
//
// }

View file

@ -1,17 +1,36 @@
"use strict";
import useAccountListeners from "./account";
import { IpcHandler } from "@/composables/ipcHandler";
import handlers from "./account";
import useSessionListeners from "./session";
// import useAudioListeners from "./audio";
interface IPCHandlers {
[channel: string]: IpcHandler<any, any>;
}
const ipcHandlers: IPCHandlers = {};
const _initHandlers = () => {
handlers.forEach((h) => {
if (!(h.channel in ipcHandlers)) {
ipcHandlers[h.channel] = h;
h.handle();
}
});
}
export default function useIpc(): void {
useAccountListeners();
_initHandlers();
// useAccountListeners();
useSessionListeners();
// useAudioListeners();
}

View file

@ -10,11 +10,11 @@ function onSendMessage(_event: IpcMainEvent, payload: Message): void {
}
// Login attempt, returns success or not.
function onLogin(_event: IpcMainEvent, payload: LoginPayload) => {
authenticate(payload.email, payload.password);
function onLogin(_event: IpcMainEvent, payload: LoginPayload): void {
console.log('hello')
}
export default function useSessionListeners(): void {
ipcMain.removeAllListeners("client-message");
ipcMain.on("client-message", onSendMessage);
}
}

View file

@ -1,41 +1,21 @@
/**
* Where the background logic really begins, gets called by app.onReady().
*
*
* Handles authentication. If profile is set, we launch a session, which consis
* of opening a connection with the platform, initializing the audio streams.
*
*
* The session is primarily an interface between the frontend and the platform,
* relaying messages from one to the other.
*
*
*/
import { tokenAuth, usrPwdAuth } from "@/api/account";
import { launchSession, endSession } from "@/session";
import useIpc from "@/ipc/index";
import store from "@/composables/store";
import { accountAuth } from "./account";
import { launchSession } from "./session";
import ipcEmit from "./composables/emitter";
import createWindow from "./window";
/* authenticate the user */
export async function authenticate(email: string, password: string) {
/* attempt normal login */
const platformKey, token, crimataId = await usrPwdAuth(email, password);
/* launch if successful */
if (platformKey)
launchSession(platformKey, crimataId);
/* save the token */
store.set("token", token);
}
/* logout the user, end the session */
export function deauthenticate() {
/* terminate the session */
endSession();
}
let authState: AuthState | null;
export default async function main() {
@ -45,18 +25,14 @@ export default async function main() {
/* launch browser window */
await createWindow();
/* attempt to get a login token from the store */
const token = store.get("token");
try {
authState = await accountAuth() as AuthState;
} catch(e) {
console.log('AUTH:', e);
authState = null;
} finally {
if (authState) launchSession(authState.token as string);
ipcEmit("set-profile", authState?.profile);
}
/* try to login with it, returns platform secret and new token on success */
if (token)
const newToken, profile = await tokenAuth(token);
/* if secret, we launch a session */
if (newToken)
launchSession(newToken, profile);
/* finally, save the most recent token */
store.set("token", newToken);
}
}

View file

@ -2,24 +2,34 @@
import useAudio from "@/audio";
import Canvas from "@/composables/convas";
// import useAudio from "@/audio";
import ipcEmit from "@/composables/emitter";
import useWebsockets from "./composables/websockets";
import {config} from "@/config";
/* data structure of messages that's tied to the UI */
let msgrState: UIState | null = null;
const uiState: any | null = null;
/* start and stop audio functionality */
const { initAudio, closeAudio } = useAudio();
// const { initAudio, closeAudio } = useAudio();
let isInitMessage: any;
let deauthenticate: any;
let isAddMessage: any
/**
* Controls for interfacing with the platform.
* Takes an onMessage callback which we define below.
*/
const { connect, send, close } = useWebsockets((content: any) => {
const onMessageCallback = (message: string) => {
const content = JSON.parse(message);
/* if the platform fails to authenticate, we must back down */
if (content === "auth_error") {
if (content === "CLOSE_AUTH_FAIL") {
deauthenticate();
return;
}
@ -28,8 +38,8 @@ const { connect, send, close } = useWebsockets((content: any) => {
if (isInitMessage(content)) {
uiState.set(content);
}
else if (isAddMessage(content)) {
uiState.add(content);
}
@ -37,10 +47,15 @@ const { connect, send, close } = useWebsockets((content: any) => {
else {
uiState.update(content);
}
}
});
const onConnectionStatusCallback = (alive: boolean) => {
ipcEmit('connection-state', alive);
}
/* send a message to the platform */
const { connect, send, close } = useWebsockets(onMessageCallback, onConnectionStatusCallback);
/* send a message to the platform */
export function sendMessage(message: Message) {
/* socket send */
@ -48,24 +63,22 @@ export function sendMessage(message: Message) {
}
/* launch a new session (the main process for authenticated users) */
export function launchSession(platformKey: string, crimata_id: string) {
export function launchSession(platformKey: string) {
/* connect to the platform */
connect(PLATFORM_URL, platformKey);
connect(config.PLATFORM_URL, platformKey);
/* initialize the audio streams */
initAudio();
/* push profile to window */
if (win)
ipcEmit("update-auth", crimata_id);
// initAudio();
}
export function endSession() {
closeAudio();
// closeAudio();
close();

View file

@ -1,3 +1,4 @@
interface Message {
text: boolean | string;
context: boolean | string;
@ -28,3 +29,25 @@ interface LoginPayload {
email: string;
password: string;
}
interface AuthState {
profile: Profile | null;
token: string | null;
}
interface AccountCredentials {
email: string;
password: string;
}
interface IpcHandlerCallback<I, O> {
(payload: I | null): Promise<Error | O>;
}
interface IIpcHandler<I, O> {
handle(): void;
remove(): void;
readonly _handlerCallback: IpcHandlerCallback<I, O>;
}

View file

@ -1,10 +1,12 @@
"use strict";
import { BrowserWindow, ipcMain } from "electron";
import { BrowserWindow, ipcMain, app } from "electron";
import { createProtocol } from "vue-cli-plugin-electron-builder/lib";
import { backgroundMitt } from './coposables/emitter';
import { saveToJson } from "./coposables/json";
import { backgroundMitt } from './composables/emitter';
import { saveToJson } from "./composables/json";
import * as path from "path";
import fs from 'fs';
import { config } from "@/config";
const { autoUpdater } = require('electron-updater');
interface IpcRendererPayload {
@ -19,8 +21,8 @@ const loadWinState = (fileName: string): WindowState => {
let state: WindowState;
try {
state = JSON.parse(fs.readFileSync(configPath + fileName).toString());
}
state = JSON.parse(fs.readFileSync(config.configPath + fileName).toString());
}
catch (error) {
state = {
@ -32,8 +34,8 @@ const loadWinState = (fileName: string): WindowState => {
}
return state
}
}
// Called when a NavBar button is pressed.
const onNavBar = (_event: any, action: string): void => {
@ -110,8 +112,8 @@ export default async function createWindow(): Promise<void> {
win = new BrowserWindow({
width: winState.width,
height: winState.height,
x: winState.x,
y: winState.y,
x: winState.x as number,
y: winState.y as number,
resizable: true,
backgroundColor: '#EBEBEB',
frame: false,