aboutsummaryrefslogtreecommitdiff
path: root/lib/vnlib.browser
diff options
context:
space:
mode:
Diffstat (limited to 'lib/vnlib.browser')
-rw-r--r--lib/vnlib.browser/LICENSE.txt21
-rw-r--r--lib/vnlib.browser/README.md11
-rw-r--r--lib/vnlib.browser/Taskfile.yaml36
-rw-r--r--lib/vnlib.browser/src/axios/index.ts118
-rw-r--r--lib/vnlib.browser/src/binhelpers.ts72
-rw-r--r--lib/vnlib.browser/src/globalState/index.ts139
-rw-r--r--lib/vnlib.browser/src/globalState/storage.ts80
-rw-r--r--lib/vnlib.browser/src/helpers/apiCall.ts276
-rw-r--r--lib/vnlib.browser/src/helpers/autoHeartbeat.ts63
-rw-r--r--lib/vnlib.browser/src/helpers/confirm.ts30
-rw-r--r--lib/vnlib.browser/src/helpers/envSize.ts49
-rw-r--r--lib/vnlib.browser/src/helpers/lastPage.ts109
-rw-r--r--lib/vnlib.browser/src/helpers/message.ts56
-rw-r--r--lib/vnlib.browser/src/helpers/pageGuard.ts49
-rw-r--r--lib/vnlib.browser/src/helpers/serverObjectBuffer.ts149
-rw-r--r--lib/vnlib.browser/src/helpers/validation.ts113
-rw-r--r--lib/vnlib.browser/src/helpers/wait.ts55
-rw-r--r--lib/vnlib.browser/src/index.ts75
-rw-r--r--lib/vnlib.browser/src/mfa/config.ts89
-rw-r--r--lib/vnlib.browser/src/mfa/login.ts228
-rw-r--r--lib/vnlib.browser/src/mfa/pki.ts150
-rw-r--r--lib/vnlib.browser/src/session/cookies.ts69
-rw-r--r--lib/vnlib.browser/src/session/index.ts50
-rw-r--r--lib/vnlib.browser/src/session/internal.ts247
-rw-r--r--lib/vnlib.browser/src/session/types.ts110
-rw-r--r--lib/vnlib.browser/src/social/index.ts232
-rw-r--r--lib/vnlib.browser/src/toast/index.ts76
-rw-r--r--lib/vnlib.browser/src/toast/toaster.ts83
-rw-r--r--lib/vnlib.browser/src/toast/types.ts48
-rw-r--r--lib/vnlib.browser/src/types.ts56
-rw-r--r--lib/vnlib.browser/src/user/index.ts42
-rw-r--r--lib/vnlib.browser/src/user/internal.ts190
-rw-r--r--lib/vnlib.browser/src/user/types.ts95
-rw-r--r--lib/vnlib.browser/src/util.ts28
-rw-r--r--lib/vnlib.browser/src/webcrypto.ts97
35 files changed, 3391 insertions, 0 deletions
diff --git a/lib/vnlib.browser/LICENSE.txt b/lib/vnlib.browser/LICENSE.txt
new file mode 100644
index 0000000..581ced4
--- /dev/null
+++ b/lib/vnlib.browser/LICENSE.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Vaughn Nugent
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE. \ No newline at end of file
diff --git a/lib/vnlib.browser/README.md b/lib/vnlib.browser/README.md
new file mode 100644
index 0000000..7182de6
--- /dev/null
+++ b/lib/vnlib.browser/README.md
@@ -0,0 +1,11 @@
+# @vnuge/vnlib.browser
+
+This repo contains the client side JavaScript library for interacting with the VNLib.Plugins.Essentials.Accounts user account api and security system. This library was also configured for use in web-extension context.
+
+## Getting started
+You can find a startup and installation guide on my website
+
+[Docs and guides](https://www.vaughnnugent.com/resources/software/articles?tags=docs,_vnlib.browser)
+
+## License
+All source files in this repository is licensed under the MIT License. See the LICENSE files for more information. \ No newline at end of file
diff --git a/lib/vnlib.browser/Taskfile.yaml b/lib/vnlib.browser/Taskfile.yaml
new file mode 100644
index 0000000..bdac379
--- /dev/null
+++ b/lib/vnlib.browser/Taskfile.yaml
@@ -0,0 +1,36 @@
+# https://taskfile.dev
+
+#Called by the vnbuild system to produce builds for my website
+#https://www.vaughnnugent.com/resources/software
+
+
+#this file must be in the same directory as the solution file
+
+version: '3'
+
+tasks:
+
+#called by build pipeline to build module
+ build:
+ cmds:
+ - echo "building module {{.MODULE_NAME}}"
+
+ #install dependencies and build
+ - npm install
+ - npm run build
+
+ postbuild_success:
+ cmds:
+ - powershell -Command "mkdir bin -Force"
+ #tgz the dist folder
+ - tar --exclude="./node_modules" --exclude="./src" --exclude="./.git" --exclude="./bin" --exclude=".gitignore" --exclude="*.yaml" --exclude="*.yml" -czf bin/release.tgz .
+
+
+#called by build pipeline to clean module
+ clean:
+ ignore_error: true
+ cmds:
+ #delete dist folder
+ - cmd: powershell -Command "Remove-Item -Recurse node_modules"
+ - cmd: powershell -Command "Remove-Item -Recurse dist"
+ - cmd: powershell -Command "Remove-Item -Recurse -Force bin" \ No newline at end of file
diff --git a/lib/vnlib.browser/src/axios/index.ts b/lib/vnlib.browser/src/axios/index.ts
new file mode 100644
index 0000000..644011e
--- /dev/null
+++ b/lib/vnlib.browser/src/axios/index.ts
@@ -0,0 +1,118 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { computed, MaybeRef, type Ref } from 'vue'
+import { cloneDeep, merge, isObjectLike, defaultTo } from 'lodash-es'
+import axios, { type Axios, type AxiosRequestConfig, type AxiosResponse } from 'axios'
+import { get, toReactive } from '@vueuse/core';
+import { useSession, type ISession } from '../session'
+import { getGlobalStateInternal } from '../globalState';
+
+const configureAxiosInternal = (instance: Axios, session: ISession, tokenHeader: Ref<string | undefined>) => {
+
+ const { loggedIn, generateOneTimeToken } = session;
+
+ //Add request interceptor to add the token to the request
+ instance.interceptors.request.use(async (config) => {
+
+ //Get the current global config/token header value
+ const tokenHeaderValue = get(tokenHeader);
+
+ // See if the current session is logged in
+ if (tokenHeaderValue && loggedIn.value) {
+ // Get an otp for the request
+ config.headers[tokenHeaderValue] = await generateOneTimeToken()
+ }
+ // Return the config
+ return config
+ }, function (error) {
+ // Do something with request error
+ return Promise.reject(error)
+ })
+
+ //Add response interceptor to add a function to the response to get the result or throw an error to match the WebMessage server message
+ instance.interceptors.response.use((response: AxiosResponse) => {
+
+ //Add a function to the response to get the result or throw an error
+ if (isObjectLike(response.data)) {
+ response.data.getResultOrThrow = () => {
+ if (response.data.success) {
+ return response.data.result;
+ } else {
+ //Throw in apicall format to catch in the catch block
+ throw { response };
+ }
+ }
+ }
+ return response;
+ })
+
+ return instance;
+}
+
+/**
+ * Gets a reactive axios instance with the default configuration
+ * @param config Optional Axios instance configuration to apply, will be merged with the default config
+ * @returns A reactive ref to an axios instance
+ */
+export const useAxiosInternal = (() => {
+
+ //Get the session and utils
+ const { axios: _axiosConfig } = getGlobalStateInternal();
+
+ const tokenHeader = computed(() => defaultTo(_axiosConfig.value.tokenHeader, ''));
+ const session = useSession();
+
+ return (config?: MaybeRef<AxiosRequestConfig | undefined | null>): Readonly<Ref<Axios>> => {
+
+ /**
+ * Computed config, merges the default config with the passed config. When
+ * the fallback config is updated, it will compute the merged config
+ */
+ const mergedConfig = config ?
+ computed(() => merge(cloneDeep(_axiosConfig.value), get(config)))
+ : _axiosConfig
+
+ /**
+ * Computes a new axios insance when the config changes
+ */
+ const computedAxios = computed(() => {
+ const instance = axios.create(mergedConfig.value);
+ return configureAxiosInternal(instance, session, tokenHeader);
+ });
+
+ return computedAxios;
+ }
+})();
+
+/**
+ * Gets a reactive axios instance that merges the supplied config with the global config
+ * @param config Optional Axios instance configuration to apply, will be merged with the default config
+ * @returns The axios instance
+ */
+export const useAxios = (config: MaybeRef<AxiosRequestConfig | undefined | null>): Axios => {
+
+ const axiosRef = useAxiosInternal(config);
+
+ /**
+ * Return a reactive axios instance. When updates are made to the config,
+ * the instance will be updated without the caller needing to re-request it.
+ */
+ return toReactive(axiosRef);
+}
diff --git a/lib/vnlib.browser/src/binhelpers.ts b/lib/vnlib.browser/src/binhelpers.ts
new file mode 100644
index 0000000..c6ad274
--- /dev/null
+++ b/lib/vnlib.browser/src/binhelpers.ts
@@ -0,0 +1,72 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+export const LongToArray = function (long : number) {
+ // Empty array
+ const byteArray = Array(8).fill(0)
+ for (let index = 0; index < 8; index++) {
+ const byte = long & 0xff
+ byteArray[index] = byte
+ long = (long - byte) / 256
+ }
+ return byteArray
+}
+
+export const IntToArray = function(int : number) {
+ // Empty array
+ const byteArray = Array(4).fill(0)
+ for (let index = 0; index < 4; index++) {
+ const byte = int & 0xff
+ byteArray[index] = byte
+ int = (int - byte) / 256
+ }
+ return byteArray
+}
+
+export const Base64ToArray = function (b64string : string) : Array<number> {
+ // Recover the encoded data
+ const decData = atob(b64string)
+ // Convert to array
+ return Array.from(decData, c => c.charCodeAt(0))
+}
+
+export const Base64ToUint8Array = function (b64string : string) : Uint8Array {
+ // Recover the encoded data
+ const decData = atob(b64string)
+ // Convert to array
+ return Uint8Array.from(decData, c => c.charCodeAt(0))
+}
+
+export const Utf8StringToBuffer = function (str : string) : Array<number> {
+ // encode the string to utf8 binary
+ const enc = new TextEncoder().encode(str)
+ return Array.from(enc);
+}
+
+export const ArrayBuffToBase64 = function(e : ArrayBuffer) : string {
+ const arr = Array.from(new Uint8Array(e))
+ return btoa(String.fromCharCode.apply(null, arr))
+}
+
+export const ArrayToHexString = function(buffer : Array<number> | ArrayBuffer) : string {
+ return Array.prototype.map.call(buffer, function (byte : number) {
+ return ('0' + (byte & 0xFF).toString(16)).slice(-2)
+ }).join('')
+}
diff --git a/lib/vnlib.browser/src/globalState/index.ts b/lib/vnlib.browser/src/globalState/index.ts
new file mode 100644
index 0000000..4cdbea3
--- /dev/null
+++ b/lib/vnlib.browser/src/globalState/index.ts
@@ -0,0 +1,139 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { merge } from "lodash-es";
+import { ref, type ToRefs, type Ref } from "vue";
+import { toRefs, type StorageLike, set } from "@vueuse/core";
+import { toReactive, createStorageRef } from "./storage";
+import type { SessionConfig } from "../session";
+import type { UserConfig } from "../user";
+import type { AxiosRequestConfig } from "axios";
+
+export interface GlobalSessionConfig extends SessionConfig {
+ readonly cookiesEnabled: boolean;
+ readonly loginCookieName: string;
+}
+
+export interface GlobalAxiosConfig extends AxiosRequestConfig {
+ tokenHeader: string;
+}
+
+export interface GlobalApiConfig {
+ readonly session: GlobalSessionConfig;
+ readonly axios: GlobalAxiosConfig;
+ readonly user: UserConfig;
+ readonly storage: StorageLike;
+}
+
+export interface GlobalConfigUpdate {
+ readonly session?: Partial<GlobalSessionConfig>;
+ readonly axios?: Partial<GlobalAxiosConfig>;
+ readonly user?: Partial<UserConfig>;
+ readonly storage?: StorageLike;
+}
+
+export enum StorageKey {
+ Session = '_vn-session',
+ Keys = "_vn-keys",
+ User = '_vn-user'
+}
+
+
+/**
+ * Gets the default/fallback axios configuration
+ * @returns The default axios configuration
+ */
+const getDefaultAxiosConfig = (): GlobalAxiosConfig => {
+ return {
+ baseURL: '/',
+ timeout: 60 * 1000,
+ withCredentials: false,
+ tokenHeader: 'X-Web-Token'
+ }
+}
+
+/**
+ * Gets the default/fallback session configuration
+ * @returns The default session configuration
+ */
+const getDefaultSessionConfig = (): GlobalSessionConfig & SessionConfig => {
+ return {
+ browserIdSize: 32,
+ signatureAlgorithm: 'HS256',
+
+ cookiesEnabled: navigator?.cookieEnabled === true,
+ loginCookieName: 'li',
+
+ keyAlgorithm: {
+ name: 'RSA-OAEP',
+ modulusLength: 4096,
+ publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
+ hash: { name: 'SHA-256' },
+ } as RsaHashedKeyAlgorithm,
+ }
+}
+
+/**
+ * Get the default/fallback user configuration
+ * @returns The default user configuration
+ */
+const getDefaultUserConfig = (): UserConfig => {
+ return {
+ accountBasePath: '/account',
+ }
+};
+
+const _globalState = ref<GlobalApiConfig>({
+ axios: getDefaultAxiosConfig(),
+ session: getDefaultSessionConfig(),
+ user: getDefaultUserConfig(),
+ storage: localStorage
+});
+
+//Get refs to the state
+const _refs = toRefs(_globalState);
+
+//Store reactive storage
+const rStorage = toReactive(_refs.storage);
+
+export const getGlobalStateInternal = (): Readonly<ToRefs<GlobalApiConfig>> => _refs
+
+/**
+ * Gets a reactive storage slot that will work from the
+ * global configuration storage
+ */
+export const createStorageSlot = <T>(key: StorageKey, defaultValue: T): Ref<T> => createStorageRef(rStorage, key, defaultValue);
+
+/**
+ * Sets the global api configuration
+ * @param config The new configuration
+ */
+export const setApiConfigInternal = (config: GlobalConfigUpdate): void => {
+
+ //merge with defaults
+ const newConfig = {
+ axios: merge(getDefaultAxiosConfig(), config.axios),
+ session: merge(getDefaultSessionConfig(), config.session),
+ user: merge(getDefaultUserConfig(), config.user),
+ storage: config.storage
+ }
+
+ //Update the global state
+ set(_globalState, newConfig)
+} \ No newline at end of file
diff --git a/lib/vnlib.browser/src/globalState/storage.ts b/lib/vnlib.browser/src/globalState/storage.ts
new file mode 100644
index 0000000..b3fb1bb
--- /dev/null
+++ b/lib/vnlib.browser/src/globalState/storage.ts
@@ -0,0 +1,80 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { ref, type Ref } from 'vue';
+import { defer, isEqual } from 'lodash-es';
+import { watchDebounced, type StorageLike } from '@vueuse/core'
+
+export interface ReactiveStorageLike extends StorageLike {
+ onStorageChanged: (key: string, callback: () => void) => void;
+}
+
+export const toReactive = (storage: Ref<StorageLike | undefined>): ReactiveStorageLike => {
+
+ const getItem = (key: string): string | null => storage.value?.getItem(key) ?? null;
+ const setItem = (key: string, value: string): void => storage.value?.setItem(key, value);
+ const removeItem = (key: string): void => storage.value?.removeItem(key);
+
+ const onStorageChanged = (key: string, callback: () => void) => {
+ //Listen for storage changes on the window and invoke the callback for all changes
+ window?.addEventListener('storage', ev => ev.key === key ? callback() : null);
+
+ //Also register watcher for storage change events
+ watchDebounced(storage, callback, { debounce: 100, immediate:false })
+
+ //intial update
+ callback();
+ }
+
+ return {
+ getItem,
+ setItem,
+ removeItem,
+ onStorageChanged
+ }
+}
+
+export const createStorageRef = <T>(backend: ReactiveStorageLike, key: string, defaultValue: T): Ref<T> => {
+ //reactive storage element
+ const storage = ref<T>(defaultValue);
+
+ //Recovers data from storage
+ const onUpdate = () => {
+ const string = backend.getItem(key);
+ storage.value = JSON.parse(string || "{}");
+ }
+
+ //Watch storage changes
+ backend.onStorageChanged(key, onUpdate);
+
+ //Watch for reactive changes and write to storage
+ watchDebounced(storage, (value) => defer(() => {
+ //Convert to string and store
+ const string = JSON.stringify(value);
+ const oldValue = backend.getItem(key);
+ //Only write if the value has changed
+ if (isEqual(string, oldValue)) {
+ return;
+ }
+ //Write to storage
+ backend.setItem(key, string);
+ }), { deep: true, debounce: 100 })
+
+ return storage as Ref<T>;
+}
diff --git a/lib/vnlib.browser/src/helpers/apiCall.ts b/lib/vnlib.browser/src/helpers/apiCall.ts
new file mode 100644
index 0000000..b98fecb
--- /dev/null
+++ b/lib/vnlib.browser/src/helpers/apiCall.ts
@@ -0,0 +1,276 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { type MaybeRef, type Ref } from 'vue'
+import { AxiosError, type Axios } from 'axios';
+import { defaultTo, isArray, isNil, isEqual } from 'lodash-es';
+import { get, useConfirmDialog } from '@vueuse/core';
+import { useFormToaster, useToaster } from '../toast';
+import { useWait } from './wait';
+import { useAxiosInternal } from '../axios';
+import type { IErrorNotifier, CombinedToaster } from '../toast';
+
+export interface IApiHandle<T> {
+ /**
+ * Called to get the object to pass to apiCall is invoked
+ */
+ getCallbackObject(): T;
+
+ /**
+ * Called to get the notifier to use for the api call
+ */
+ getNotifier(): IErrorNotifier;
+
+ /**
+ * Called to set the waiting flag
+ */
+ setWaiting: (waiting: boolean) => void;
+}
+
+export interface IApiPassThrough {
+ readonly axios: Axios;
+ readonly toaster: CombinedToaster;
+}
+
+export interface IElevatedCallPassThrough extends IApiPassThrough {
+ readonly password: string;
+}
+
+export interface ApiCall<T> {
+ <TR>(callback: (data: T) => Promise<TR | undefined>): Promise<TR | undefined>;
+}
+
+export type CustomMessageHandler = (message: string) => void;
+
+const useApiCallInternal = <T>(args: IApiHandle<T>): ApiCall<T> => {
+
+ /**
+ * Provides a wrapper method for making remote api calls to a server
+ * while capturing context and errors and common api arguments.
+ * @param {*} callback The method to call within api request context
+ * @returns A promise that resolves to the result of the async function
+ */
+ return async <TR>(callback: (data: T) => Promise<TR | undefined>): Promise<TR | undefined> => {
+ const notifier = args.getNotifier();
+
+ // Set the waiting flag
+ args.setWaiting(true);
+
+ try {
+ //Close the current toast value
+ notifier.close();
+
+ const obj = args.getCallbackObject();
+
+ //Exec the async function
+ return await callback(obj);
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ } catch (errMsg: any) {
+ console.error(errMsg)
+ // See if the error has an axios response
+ if (isNil(errMsg.response)) {
+ if (errMsg.message === 'Network Error') {
+ notifier.notifyError('Please check your internet connection')
+ } else {
+ notifier.notifyError('An unknown error occured')
+ }
+ return
+ }
+ // Axios error message
+ const response = errMsg.response
+ const errors = response?.data?.errors
+ const hasErrors = isArray(errors) && errors.length > 0
+
+ const SetMessageWithDefault = (message: string) => {
+ if (hasErrors) {
+ const title = 'Please verify your ' + defaultTo(errors[0].property, 'form')
+ notifier.notifyError(title, errors[0].message)
+ } else {
+ notifier.notifyError(defaultTo(response?.data?.result, message))
+ }
+ }
+
+ switch (response.status) {
+ case 200:
+ SetMessageWithDefault('')
+ break
+ case 400:
+ SetMessageWithDefault('Bad Request')
+ break
+ case 422:
+ SetMessageWithDefault('The server did not accept the request')
+ break
+ case 401:
+ SetMessageWithDefault('You are not logged in.')
+ break
+ case 403:
+ SetMessageWithDefault('Please clear you cookies/cache and try again')
+ break
+ case 404:
+ SetMessageWithDefault('The requested resource was not found')
+ break
+ case 409:
+ SetMessageWithDefault('Please clear you cookies/cache and try again')
+ break
+ case 410:
+ SetMessageWithDefault('The requested resource has expired')
+ break
+ case 423:
+ SetMessageWithDefault('The requested resource is locked')
+ break
+ case 429:
+ SetMessageWithDefault('You have made too many requests, please try again later')
+ break
+ case 500:
+ SetMessageWithDefault('There was an error processing your request')
+ break
+ default:
+ SetMessageWithDefault('An unknown error occured')
+ break
+ }
+ } finally {
+ // Clear the waiting flag
+ args.setWaiting(false);
+ }
+ }
+}
+
+const creatApiHandle = (notifier: MaybeRef<IErrorNotifier>, axios: Ref<Axios>): IApiHandle<IApiPassThrough> => {
+
+ const toaster = useToaster();
+ const { setWaiting } = useWait();
+
+ const getCallbackObject = (): IApiPassThrough => ({ axios: get(axios), toaster })
+ const getNotifier = (): IErrorNotifier => get(notifier);
+
+ return { getCallbackObject, getNotifier, setWaiting }
+}
+
+/**
+ * Provides a wrapper method for making remote api calls to a server
+ * while capturing context and errors and common api arguments.
+ * @param {*} asyncFunc The method to call within api request context
+ * @returns A promise that resolves to the result of the async function
+ */
+export const apiCall = (() =>{
+
+ const axios = useAxiosInternal(null);
+ const errorNotifier = useFormToaster();
+
+ //Create the api call handle
+ const handle = creatApiHandle(errorNotifier, axios);
+ //Confiugre the api call to use global configuration
+ return useApiCallInternal(handle);
+})();
+
+/**
+ * Customizes the api call to use a custom error message
+ * @param msg The message to display when an error occurs
+ * @returns {Object} The api call object {apiCall: Promise }
+ */
+export const configureApiCall = (msg: CustomMessageHandler): { apiCall: ApiCall<IApiPassThrough> } =>{
+
+ const notifier = ((): IErrorNotifier => {
+ return{
+ notifyError: (t: string, m?: string) => {
+ msg(t);
+ return m;
+ },
+ close(id: string) {
+ msg('')
+ return id;
+ },
+ }
+ })()
+
+ const axios = useAxiosInternal(null);
+
+ //Create custom api handle
+ const handle = creatApiHandle(notifier, axios);
+
+ //Confiugre the api call to use global configuration
+ const apiCall = useApiCallInternal(handle);
+ return { apiCall }
+}
+
+/**
+ * Gets the shared password prompt object and the elevated api call method handler
+ * to allow for elevated api calls that require a password.
+ * @returns {Object} The password prompt configuration object, and the elevated api call method
+ */
+export const usePassConfirm = (() => {
+
+ //Shared confirm object
+ const confirm = useConfirmDialog();
+
+ /**
+ * Displays the password prompt and executes the api call with the password
+ * captured from the prompt. If the api call returns a 401 error, the password
+ * prompt is re-displayed and the server error message is displayed in the form
+ * error toaster.
+ * @param callback The async callback method that invokes the elevated api call.
+ * @returns A promise that resolves to the result of the async function
+ */
+ const elevatedApiCall = <T>(callback: (api: IElevatedCallPassThrough) => Promise<T>): Promise<T | undefined> => {
+ //Invoke api call method but handle 401 errors by re-displaying the password prompt
+ return apiCall<T>(async (api: IApiPassThrough) : Promise<T | undefined> => {
+ // eslint-disable-next-line no-constant-condition
+ while (1) {
+
+ //Display the password prompt
+ const { data, isCanceled } = await confirm.reveal()
+
+ if (isCanceled) {
+ break;
+ }
+
+ try {
+ //Execute the api call with prompt response
+ return await callback({...api, ...data });
+ }
+ //Catch 401 errors and re-display the password prompt, otherwise throw the error
+ catch (err) {
+ if(!(err instanceof AxiosError)){
+ throw err;
+ }
+
+ const { response } = err;
+
+ if(isNil(response)){
+ throw err;
+ }
+
+ //Check status code, if 401, re-display the password prompt
+ if (!isEqual(response?.status, 401)) {
+ throw err;
+ }
+
+ //Display the error message
+ api.toaster.form.error({ title: response.data.result });
+
+ //Re-display the password prompt
+ }
+ }
+ })
+ }
+
+ //Pass through confirm object and elevated api call
+ return () => ({ ...confirm, elevatedApiCall })
+})(); \ No newline at end of file
diff --git a/lib/vnlib.browser/src/helpers/autoHeartbeat.ts b/lib/vnlib.browser/src/helpers/autoHeartbeat.ts
new file mode 100644
index 0000000..459a33a
--- /dev/null
+++ b/lib/vnlib.browser/src/helpers/autoHeartbeat.ts
@@ -0,0 +1,63 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { isRef, type Ref } from "vue";
+import { useIntervalFn, syncRef, get, type MaybeRef } from "@vueuse/core";
+import { useUser } from "../user";
+import { useSession } from "../session";
+
+export interface IAutoHeartbeatControls {
+ /**
+ * The current state of the heartbeat interval
+ */
+ enabled: Ref<boolean>;
+ /**
+ * Enables the hearbeat interval if configured
+ */
+ enable: () => void;
+ /**
+ * Disables the heartbeat interval
+ */
+ disable: () => void;
+}
+
+/**
+* Configures shared controls for the heartbeat interval
+* @param enabled The a reactive value that may be used to enable/disable the heartbeat interval
+*/
+export const useAutoHeartbeat = (interval: Readonly<MaybeRef<number>>, enabled: MaybeRef<boolean>): IAutoHeartbeatControls =>{
+
+ //Get heartbeat method to invoke when the timer fires
+ const { heartbeat } = useUser();
+ const { loggedIn } = useSession();
+
+ //Setup the automatic heartbeat interval
+ const { isActive, pause, resume } = useIntervalFn(() => loggedIn.value ? heartbeat() : null, interval);
+
+ //Sync the enabled ref with the active state if it is a ref
+ if(isRef(enabled)){
+ //only sync from caller value to ref
+ syncRef(enabled, isActive, { direction: 'ltr' })
+ }
+
+ //Update timer immediately
+ get(enabled) ? resume() : pause();
+
+ return { enabled:isActive, enable:resume, disable:pause }
+} \ No newline at end of file
diff --git a/lib/vnlib.browser/src/helpers/confirm.ts b/lib/vnlib.browser/src/helpers/confirm.ts
new file mode 100644
index 0000000..77b92e0
--- /dev/null
+++ b/lib/vnlib.browser/src/helpers/confirm.ts
@@ -0,0 +1,30 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { useConfirmDialog } from "@vueuse/core"
+
+/**
+ * Gets the global confirm dialog interface for building application wide
+ * confirm dialog state
+ */
+export const useConfirm = (() => {
+ //Store a global confirm dialog singleton to share across components
+ const globalConfirm = useConfirmDialog()
+ return () => globalConfirm;
+})() \ No newline at end of file
diff --git a/lib/vnlib.browser/src/helpers/envSize.ts b/lib/vnlib.browser/src/helpers/envSize.ts
new file mode 100644
index 0000000..e078e5b
--- /dev/null
+++ b/lib/vnlib.browser/src/helpers/envSize.ts
@@ -0,0 +1,49 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { toSafeInteger } from 'lodash-es';
+import { ref, computed } from "vue"
+import { useElementSize } from '@vueuse/core'
+
+/**
+ * Setups a common state for the environment size
+ */
+export const useEnvSize = (() => {
+ //Element refs for app to watch
+ const header = ref(null);
+ const footer = ref(null);
+ const content = ref(null);
+
+ //Setup reactive element sizes
+ const headerSize = useElementSize(header);
+ const footerSize = useElementSize(footer);
+ const contentSize = useElementSize(content);
+
+ const footerHeight = computed(() => toSafeInteger(footerSize.height.value));
+ const headerHeight = computed(() => toSafeInteger(headerSize.height.value));
+ const contentHeight = computed(() => toSafeInteger(contentSize.height.value - headerSize.height.value - footerSize.height.value));
+
+
+ return (exportElements?: boolean) => {
+
+ return exportElements ?
+ { footerHeight, headerHeight, contentHeight, header, footer, content } :
+ { footerHeight, headerHeight, contentHeight }
+ }
+})(); \ No newline at end of file
diff --git a/lib/vnlib.browser/src/helpers/lastPage.ts b/lib/vnlib.browser/src/helpers/lastPage.ts
new file mode 100644
index 0000000..8be2116
--- /dev/null
+++ b/lib/vnlib.browser/src/helpers/lastPage.ts
@@ -0,0 +1,109 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { defaultTo } from "lodash-es";
+import { useRouter } from "vue-router"
+
+export interface ILastPage{
+ /**
+ * Pushes the current page into the last-page stack
+ */
+ push(): void;
+ /**
+ * Stores the current page and navigates to the desired route
+ * @param route The route to navigate to
+ */
+ pushAndNavigate(route: object): void;
+ /**
+ * Navigates to the last page if it exists
+ */
+ gotoLastPage(): void;
+}
+
+export interface ILastPageStorage {
+
+ /**
+ * Pushes the current page into the last-page stack
+ */
+ push(route: string): void;
+
+ /**
+ * Pops the last page from the last-page stack storage
+ * @returns {any} The last page route object stored
+ */
+ pop(): string | null;
+}
+
+const storageKey = "lastPage";
+
+//Storage impl
+const defaultStack = (): ILastPageStorage => {
+ const storage = sessionStorage;
+
+ const push = (route: string) => {
+ //Serialize the route data and store it
+ storage?.setItem(storageKey, route);
+ }
+
+ const pop = (): string | null => {
+ //Get the route data and deserialize it
+ const route = storage?.getItem(storageKey);
+ if (route) {
+ storage?.removeItem(storageKey);
+ return route;
+ }
+ return null;
+ }
+ return { push, pop }
+}
+
+/**
+ * Gets the configuration for the last page the user was on
+ * when the page guard was called. This is used to return to the
+ * last page after login.
+ * @returns { gotoLastPage: Function }
+ */
+export const useLastPage = (storage?: ILastPageStorage): ILastPage => {
+
+ //fallback to default storage
+ const _storage = defaultTo(storage, defaultStack());
+
+ //Get the current router instance
+ const router = useRouter();
+
+ //Store the current page to the last page stack
+ const push = () => _storage.push(router.currentRoute.value.fullPath);
+
+ const pushAndNavigate = (route: object) => {
+ //Store the current page to the last page stack
+ push();
+ //Navigate to the desired route
+ router.push(route);
+ };
+
+ const gotoLastPage = () => {
+ //Get the last stored page and navigate to it
+ const lp = _storage.pop();
+ if (lp) {
+ router.push(lp);
+ }
+ };
+
+ return { push, pushAndNavigate, gotoLastPage }
+} \ No newline at end of file
diff --git a/lib/vnlib.browser/src/helpers/message.ts b/lib/vnlib.browser/src/helpers/message.ts
new file mode 100644
index 0000000..3c79577
--- /dev/null
+++ b/lib/vnlib.browser/src/helpers/message.ts
@@ -0,0 +1,56 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { defaultTo } from 'lodash-es';
+import { useFormToaster } from '../toast'
+import type { IErrorNotifier } from "../toast";
+
+/**
+ * The message handler interface
+ */
+export interface IMessageHandler {
+ setMessage(title: string, text?: string): void;
+ clearMessage(): void;
+ onInput(): void;
+}
+
+/**
+ * Gets the message state subsystem for displaying errors
+ * to the user and clearing them
+ * @param notifier An optional notifier to use instead of the default
+ */
+export const useMessage = (notifier?: IErrorNotifier): IMessageHandler =>{
+
+ const _notifier = defaultTo(notifier, useFormToaster())
+
+ const clearMessage = (): void => _notifier.close()
+
+ const setMessage = (title: string, text = ''): void => {
+ //Setting a null value will also clear the message
+ title ? _notifier.notifyError(title, text) : clearMessage();
+ }
+
+ const onInput = clearMessage;
+
+ return {
+ setMessage,
+ clearMessage,
+ onInput
+ }
+} \ No newline at end of file
diff --git a/lib/vnlib.browser/src/helpers/pageGuard.ts b/lib/vnlib.browser/src/helpers/pageGuard.ts
new file mode 100644
index 0000000..c3a767b
--- /dev/null
+++ b/lib/vnlib.browser/src/helpers/pageGuard.ts
@@ -0,0 +1,49 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { watch } from 'vue'
+import { useSession } from "../session";
+import { useLastPage } from './lastPage';
+import type { ILastPageStorage } from "./lastPage";
+
+
+/**
+ * When called, configures the component to
+ * only be visible when the user is logged in. If the user is
+ * not logged in, the user is redirected to the login page.
+ * @remarks Once called, if the user is logged-in changes will be
+ * watch to redirect if the user becomes logged out.
+*/
+export const usePageGuard = (loginRoute = { name: 'Login' }, lpStorage?: ILastPageStorage): void => {
+ //Get the session state
+ const session = useSession();
+
+ //Get last page controller, fall back to default session storage stack
+ const { pushAndNavigate } = useLastPage(lpStorage)
+
+ // Initial check for logged in to guard the page
+ if (!session.loggedIn.value) {
+ //Store last route and redirect to login
+ pushAndNavigate(loginRoute);
+ }
+
+ // setup watcher on session login value
+ // If the login value changes to false, redirect to login page
+ watch(session.loggedIn, value => value === false ? pushAndNavigate(loginRoute) : null)
+}
diff --git a/lib/vnlib.browser/src/helpers/serverObjectBuffer.ts b/lib/vnlib.browser/src/helpers/serverObjectBuffer.ts
new file mode 100644
index 0000000..5595ec7
--- /dev/null
+++ b/lib/vnlib.browser/src/helpers/serverObjectBuffer.ts
@@ -0,0 +1,149 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { clone, isEqual, assign } from 'lodash-es'
+import { reactive, computed, type UnwrapNestedRefs, type Ref } from 'vue'
+
+/**
+ * Represents a server object that has been buffered for editing
+ * and can be reverted to the original state, or new state data
+ * applied from the server
+ */
+export interface ServerObjectBuffer<T> {
+ /**
+ * The data is a reactive clone of the initial data state. It is used to store the current state of the resource.
+ */
+ readonly data: UnwrapNestedRefs<T>;
+ /**
+ * The buffer is a reactive clone of the initial data state. It is used to track changes.
+ */
+ readonly buffer: UnwrapNestedRefs<T>;
+ /**
+ * A computed value that indicates if the buffer has been modified from the data
+ */
+ readonly modified: Readonly<Ref<boolean>>;
+ /**
+ * Applies the new server response data to the resource data and reverts the buffer to the resource data
+ * @param newData The new server response data to apply to the buffer
+ */
+ apply(newData: T): void;
+ /**
+ * Reverts the buffer to the resource data
+ */
+ revert(): void;
+}
+
+export type UpdateCallback<T, TResut> = (state: ServerObjectBuffer<T>) => Promise<TResut>;
+
+/**
+ * Represents a server object that has been buffered for editing
+ * and can be reverted to the original state or updated on the server
+ */
+export interface ServerDataBuffer<T, TResut> extends ServerObjectBuffer<T>{
+ /**
+ * Pushes buffered changes to the server if configured
+ */
+ update(): Promise<TResut>;
+}
+
+export interface UseDataBuffer {
+ /**
+ * Configures a helper type that represents a data buffer that reflects the state of a resource
+ * on the server. This is useful for editing forms, where the user may want to revert their
+ * changes, or reload them from the server after an edit;
+ * @param initialData The intiial data to wrap in the buffer
+ * @returns
+ */
+ <T extends object>(initialData: T): ServerObjectBuffer<T>;
+ /**
+ * Configures a helper type that represents a data buffer that reflects the state of a resource
+ * on the server. This is useful for editing forms, where the user may want to revert their
+ * changes, or reload them from the server after an edit;
+ * @param initialData
+ * @returns
+ */
+ <T extends object, TState = unknown>(initialData: T, onUpdate: UpdateCallback<T, TState>): ServerDataBuffer<T, TState>;
+}
+
+
+const createObjectBuffer = <T extends object>(initialData: T): ServerObjectBuffer<T> => {
+
+ /**
+ * The data is a reactive clone of the initial data state. It is used to store the current state of the resource.
+ */
+ const data = reactive(clone(initialData || {}))
+
+ /**
+ * The buffer is a reactive clone of the initial data state. It is used to track changes.
+ */
+ const buffer = reactive(clone(initialData || {}))
+
+ /**
+ * A computed value that indicates if the buffer has been modified from the data
+ * @returns {boolean} True if the buffer has been modified from the data
+ */
+ const modified = computed(() => !isEqual(buffer, data))
+
+ /**
+ * Applies the new server response data to the resource data and reverts the buffer to the resource data
+ * @param newData The new server response data to apply to the buffer
+ */
+ const apply = (newData: T): void => {
+ // Apply the new data to the buffer
+ assign(data, newData);
+ // Revert the buffer to the resource data
+ assign(buffer, data)
+ }
+
+ /**
+ * Reverts the buffer to the resource data
+ */
+ const revert = (): void => {
+ assign(buffer, data);
+ }
+
+ return { data, buffer, modified, apply, revert }
+}
+
+const createUpdatableBuffer = <T extends object, TState = unknown> (initialData: T, onUpdate: UpdateCallback<T, TState>): ServerDataBuffer<T, TState> => {
+ //Configure the initial data state and the buffer
+
+ const state: ServerObjectBuffer<T> = createObjectBuffer(initialData)
+
+ /**
+ * Pushes buffered changes to the server if configured
+ */
+ const update = async (): Promise<TState> => {
+ return onUpdate ? onUpdate(state) : Promise.resolve(undefined as unknown as TState);
+ }
+
+ return{ ...state, update }
+}
+
+const _useDataBuffer = <T extends object, TState = unknown>(initialData: T, onUpdate?: UpdateCallback<T, TState>)
+: ServerObjectBuffer<T> | ServerDataBuffer<T, TState> => {
+
+ return onUpdate ? createUpdatableBuffer<T, TState>(initialData, onUpdate) : createObjectBuffer<T>(initialData)
+}
+
+/**
+ * Cretes a buffer object that represents server data, tracks changes,
+ * can revert changes, and can optionally push buffered changes to the server
+ */
+export const useDataBuffer: UseDataBuffer = (_useDataBuffer as UseDataBuffer)
diff --git a/lib/vnlib.browser/src/helpers/validation.ts b/lib/vnlib.browser/src/helpers/validation.ts
new file mode 100644
index 0000000..e60563e
--- /dev/null
+++ b/lib/vnlib.browser/src/helpers/validation.ts
@@ -0,0 +1,113 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { defaultTo, first } from "lodash-es";
+import { get } from "@vueuse/core";
+import { ref, type MaybeRef } from "vue";
+import { useFormToaster } from "../toast";
+import type { IErrorNotifier } from "../toast";
+
+/**
+ * Represents a validator that performs validation and returns a boolean
+ * along with an error message if the validation fails
+ */
+export interface IValidator {
+ /**
+ * Performs asynchronous validation and returns a boolean
+ * indicating if the validation was successful
+ */
+ validate(): Promise<boolean>;
+ /**
+ * Returns the first error message in the validation list
+ */
+ firstError(): Error;
+}
+
+export interface ValidationWrapper {
+ /**
+ * Computes the validation of the wrapped validator and captures the results.
+ * If the validation fails, the first error message is displayed in a toast notification.
+ * @returns The result of the validation
+ */
+ validate: () => Promise<boolean>;
+}
+
+export interface VuelidateInstance {
+ $validate: () => Promise<boolean>;
+ $errors: Array<{ $message: string }>;
+}
+
+//Wrapper around a Vuelidate validator
+const createVuelidateWrapper = <T extends VuelidateInstance>(validator: Readonly<MaybeRef<T>>): IValidator => {
+ const validate = async (): Promise<boolean> => {
+ return get(validator).$validate();
+ }
+
+ const firstError = (): Error => {
+ const errs = get(validator).$errors;
+ return new Error(first(errs)?.$message);
+ }
+
+ return { validate, firstError }
+}
+
+const createValidator = <T extends IValidator>(validator: MaybeRef<T>, toaster: IErrorNotifier): ValidationWrapper => {
+ const validate = async (): Promise<boolean> => {
+ // Validate the form
+ const valid = await get(validator).validate();
+ // If the form is no valid set the error message
+ if (!valid) {
+ const first = get(validator).firstError();
+ // Set the error message to the first error in the form list
+ toaster.notifyError(first.message);
+ }
+ return valid
+ }
+
+ return { validate }
+}
+
+/**
+ * Wraps a validator with a toaster to display validation errors
+ * @param validator The validator to wrap
+ * @param toaster The toaster to use for validation errors
+ * @returns The validation toast wrapper
+ * @example returns { validate: Function<Promise<boolean>> }
+ * const { validate } = useValidationWrapper(validator, toaster)
+ * const result = await validate()
+ */
+export const useValidationWrapper = <T extends IValidator>(validator: Readonly<MaybeRef<T>>, toaster?: IErrorNotifier): ValidationWrapper => {
+ return createValidator(validator, toaster ?? useFormToaster());
+}
+
+/**
+ * Wraps a Vuelidate validator with a toaster to display validation errors
+ * @param validator The vuelidate instance to wrap
+ * @param toaster The toaster to use for validation errors
+ * @returns The validation toast wrapper
+ * @example returns { validate: Function<Promise<boolean>> }
+ * const { validate } = useValidationWrapper(validator, toaster)
+ * const result = await validate()
+ */
+export const useVuelidateWrapper = <T extends VuelidateInstance>(v$: Readonly<MaybeRef<T>>, toaster?: IErrorNotifier) => {
+ const wrapper = createVuelidateWrapper(v$);
+ //Vuelidate class wrapper around the validator
+ const validator = ref(wrapper);
+ return createValidator(validator, defaultTo(toaster, useFormToaster()));
+}
diff --git a/lib/vnlib.browser/src/helpers/wait.ts b/lib/vnlib.browser/src/helpers/wait.ts
new file mode 100644
index 0000000..0443199
--- /dev/null
+++ b/lib/vnlib.browser/src/helpers/wait.ts
@@ -0,0 +1,55 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { set } from "@vueuse/core";
+import { readonly, ref, type Ref } from "vue";
+
+export interface IWaitSingal{
+ /**
+ * The waiting flag ref (readonly)
+ */
+ readonly waiting: Readonly<Ref<boolean>>;
+ /**
+ * Sets the waiting flag to the value passed in
+ * @param waiting The value to set the waiting flag to
+ */
+ setWaiting(waiting: boolean): void;
+}
+
+const wait = (waiting: Ref<boolean>) : IWaitSingal => {
+ return{
+ waiting: readonly(waiting),
+ setWaiting: (w: boolean) => set(waiting, w)
+ }
+}
+
+export const useWait = (() => {
+ const waiting = ref(false)
+ /**
+ * Uses the internal waiting flag to determine if the component should be waiting
+ * based on pending apiCall() requests.
+ * @returns {Object} { waiting: Boolean, setWaiting: Function }
+ * @example //Waiting flag is reactive
+ * const { waiting, setWaiting } = useWait()
+ * setWaiting(true) //Manually set the waiting flag
+ * setWaiting(false) //Manually clear the waiting flag
+ */
+ return (): IWaitSingal => wait(waiting);
+})()
+
diff --git a/lib/vnlib.browser/src/index.ts b/lib/vnlib.browser/src/index.ts
new file mode 100644
index 0000000..e2ba952
--- /dev/null
+++ b/lib/vnlib.browser/src/index.ts
@@ -0,0 +1,75 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+/*************************
+ EXPORTS
+*************************/
+
+//Export the all util
+export * from './util';
+
+export type { WebMessage, ServerValidationError } from './types'
+
+//Mfa exports
+export * from './mfa/login'
+export * from './mfa/pki'
+export * from './mfa/config'
+
+//Social exports
+export * from './social'
+
+//Forward session public exports
+export * from './session'
+
+//Axios exports
+export { useAxios } from './axios'
+
+//User exports
+export * from './user'
+
+//Export toast apis directly
+export * from './toast'
+
+//Export helpers
+export * from './helpers/apiCall'
+export * from './helpers/autoHeartbeat'
+export * from './helpers/autoScroll'
+export * from './helpers/confirm'
+export * from './helpers/envSize'
+export * from './helpers/lastPage'
+export * from './helpers/message'
+export * from './helpers/pageGuard'
+export * from './helpers/serverObjectBuffer'
+export * from './helpers/validation'
+export * from './helpers/wait'
+
+/*************************
+ SETUP/LOCALS
+*************************/
+
+import { cloneDeep } from 'lodash-es';
+import { setApiConfigInternal, type GlobalConfigUpdate } from './globalState';
+export type { GlobalApiConfig } from './globalState';
+
+/**
+ * Configures the global api settings for the entire library,
+ * may be called at any time, but should be called in the main app component
+ * before other stateful components are mounted.
+ */
+export const configureApi = (config: GlobalConfigUpdate) => setApiConfigInternal(cloneDeep(config)); \ No newline at end of file
diff --git a/lib/vnlib.browser/src/mfa/config.ts b/lib/vnlib.browser/src/mfa/config.ts
new file mode 100644
index 0000000..d4bce35
--- /dev/null
+++ b/lib/vnlib.browser/src/mfa/config.ts
@@ -0,0 +1,89 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { get } from "@vueuse/core";
+import { type MaybeRef } from "vue";
+import { useAxiosInternal } from "../axios"
+import type { MfaMethod } from "./login"
+import type { WebMessage } from '../types'
+
+export type UserArg = object;
+
+/**
+ * Represents the server api for interacting with the user's
+ * mfa configuration
+ */
+export interface MfaApi{
+ /**
+ * disables the given mfa method
+ * @param type The mfa method to disable
+ * @param password The user's password
+ */
+ disableMethod(type : MfaMethod, password: string) : Promise<WebMessage>;
+
+ /**
+ * Initializes or updates the given mfa method configuration
+ * @param type The mfa method to initialize or update
+ * @param password The user's password
+ * @param userConfig Optional extended configuration for the mfa method. Gets passed to the server
+ */
+ initOrUpdateMethod<T>(type: MfaMethod, password: string, userConfig?: UserArg) : Promise<WebMessage<T>>;
+
+ /**
+ * Refreshes the enabled mfa methods
+ */
+ getMethods(): Promise<MfaMethod[]>;
+}
+
+/**
+ * Gets the api for interacting with the the user's mfa configuration
+ * @param mfaEndpoint The server mfa endpoint relative to the base url
+ * @returns An object containing the mfa api
+ */
+export const useMfaConfig = (mfaEndpoint: MaybeRef<string>): MfaApi =>{
+
+ const axios = useAxiosInternal(null)
+
+ const getMethods = async () => {
+ //Get the mfa methods
+ const { data } = await axios.value.get<MfaMethod[]>(get(mfaEndpoint));
+ return data
+ }
+
+ const disableMethod = async (type: MfaMethod, password: string) : Promise<WebMessage> => {
+ const { post } = get(axios);
+ //Disable the mfa using the post method
+ const { data } = await post<WebMessage>(get(mfaEndpoint), { type, password });
+ return data;
+ }
+
+ const initOrUpdateMethod = async <T>(type: MfaMethod, password: string, userConfig?: UserArg) : Promise<WebMessage<T>> => {
+ const { put } = get(axios);
+ //enable or update the mfa using the put method
+ const { data } = await put<WebMessage<T>>(get(mfaEndpoint), { type, password, ...userConfig });
+ return data;
+ }
+
+ return {
+ disableMethod,
+ initOrUpdateMethod,
+ getMethods
+ }
+}
+
diff --git a/lib/vnlib.browser/src/mfa/login.ts b/lib/vnlib.browser/src/mfa/login.ts
new file mode 100644
index 0000000..a2bf120
--- /dev/null
+++ b/lib/vnlib.browser/src/mfa/login.ts
@@ -0,0 +1,228 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { decodeJwt, type JWTPayload } from "jose";
+import { forEach, isNil } from 'lodash-es';
+import { get } from "@vueuse/core";
+import { debugLog } from "../util"
+import { useUser, type ExtendedLoginResponse } from "../user";
+import { AccountEndpoint, type IUserInternal } from "../user/internal";
+import { useAxiosInternal } from "../axios";
+import type { Ref } from "vue";
+import type { Axios } from "axios";
+import type { ITokenResponse } from "../session";
+import type { WebMessage } from "../types";
+
+export enum MfaMethod {
+ TOTP = 'totp'
+}
+
+export interface IMfaSubmission {
+ /**
+ * TOTP code submission
+ */
+ readonly code?: number;
+}
+
+export interface IMfaMessage extends JWTPayload {
+ /**
+ * The type of mfa upgrade
+ */
+ readonly type: MfaMethod;
+ /**
+ * The time in seconds that the mfa upgrade is valid for
+ */
+ readonly expires?: number;
+}
+
+export interface IMfaFlowContinuiation extends IMfaMessage {
+ /**
+ * Sumits the mfa message to the server and attempts to complete
+ * a login process
+ * @param message The mfa submission to send to the server
+ * @returns A promise that resolves to a login result
+ */
+ submit: <T>(message: IMfaSubmission) => Promise<WebMessage<T>>;
+}
+
+/**
+ * Interface for handling mfa upgrade submissions to the server
+ */
+export interface MfaSumissionHandler {
+ /**
+ * Submits an mfa upgrade submission to the server
+ * @param submission The mfa upgrade submission to send to the server to complete an mfa login
+ */
+ submit<T>(submission: IMfaSubmission): Promise<WebMessage<T>>;
+}
+
+/**
+ * Interface for processing mfa messages from the server of a given
+ * mfa type
+ */
+export interface IMfaTypeProcessor {
+ readonly type: MfaMethod;
+ /**
+ * Processes an MFA message payload of the registered mfa type
+ * @param payload The mfa message from the server as a string
+ * @param onSubmit The submission handler to use to submit the mfa upgrade
+ * @returns A promise that resolves to a Login request
+ */
+ processMfa: (payload: IMfaMessage, onSubmit : MfaSumissionHandler) => Promise<IMfaFlowContinuiation>
+}
+
+export interface IMfaLoginManager {
+ /**
+ * Logs a user in with the given username and password, and returns a login result
+ * or a mfa flow continuation depending on the login flow
+ * @param userName The username of the user to login
+ * @param password The password of the user to login
+ */
+ login(userName: string, password: string): Promise<WebMessage | IMfaFlowContinuiation>;
+}
+
+const getMfaProcessor = (user: IUserInternal, axios:Ref<Axios>) =>{
+
+ //Store handlers by their mfa type
+ const handlerMap = new Map<string, IMfaTypeProcessor>();
+
+ //Creates a submission handler for an mfa upgrade
+ const createSubHandler = (upgrade : string, finalize: (res: ITokenResponse) => Promise<void>) :MfaSumissionHandler => {
+
+ const submit = async<T>(submission: IMfaSubmission): Promise<WebMessage<T>> => {
+ const { post } = get(axios);
+
+ //All mfa upgrades use the account login endpoint
+ const ep = user.getEndpoint(AccountEndpoint.Login);
+
+ //Get the mfa type from the upgrade message
+ const { type } = decodeJwt(upgrade) as IMfaMessage;
+
+ //MFA upgrades currently use the login endpoint with a query string. The type that is captured from the upgrade
+ const endpoint = `${ep}?mfa=${type}`;
+
+ //Submit request
+ const response = await post<ITokenResponse>(endpoint, {
+ //Pass raw upgrade message back to server as its signed
+ upgrade,
+ //publish submission
+ ...submission,
+ //Local time as an ISO string of the current time
+ localtime: new Date().toISOString()
+ })
+
+ // If the server returned a token, complete the login
+ if (response.data.success && !isNil(response.data.token)) {
+ await finalize(response.data)
+ }
+
+ return response.data as WebMessage<T>;
+ }
+
+ return { submit }
+ }
+
+ const processMfa = (mfaMessage: string, finalize: (res: ITokenResponse) => Promise<void>) : Promise<IMfaFlowContinuiation> => {
+
+ //Mfa message is a jwt, decode it (unsecure decode)
+ const mfa = decodeJwt(mfaMessage) as IMfaMessage;
+ debugLog(mfa)
+
+ //Select the mfa handler
+ const handler = handlerMap.get(mfa.type);
+
+ //If no handler is found, throw an error
+ if(!handler){
+ throw new Error('Server responded with an unsupported two factor auth type, login cannot continue.')
+ }
+
+ //Init submission handler
+ const submitHandler = createSubHandler(mfaMessage, finalize);
+
+ //Process the mfa message
+ return handler.processMfa(mfa, submitHandler);
+ }
+
+ const registerHandler = (handler: IMfaTypeProcessor) => {
+ handlerMap.set(handler.type, handler);
+ }
+
+ return { processMfa, registerHandler }
+}
+
+/**
+ * Gets a pre-configured TOTP mfa flow processor
+ * @returns A pre-configured TOTP mfa flow processor
+ */
+export const totpMfaProcessor = (): IMfaTypeProcessor => {
+
+ const processMfa = async (payload: IMfaMessage, onSubmit: MfaSumissionHandler): Promise<IMfaFlowContinuiation> => {
+ return { ... payload, submit: onSubmit.submit }
+ }
+
+ return {
+ type: MfaMethod.TOTP,
+ processMfa
+ }
+}
+
+/**
+ * Gets the mfa login handler for the accounts backend
+ * @param handlers A list of mfa handlers to register
+ * @returns The configured mfa login handler
+ */
+export const useMfaLogin = (handlers : IMfaTypeProcessor[]): IMfaLoginManager => {
+
+ //get the user instance
+ const user = useUser() as IUserInternal
+
+ const axios = useAxiosInternal(null)
+
+ //Get new mfa processor
+ const mfaProcessor = getMfaProcessor(user, axios);
+
+ //Login that passes through logins with mfa
+ const login = async <T>(userName: string, password: string) : Promise<ExtendedLoginResponse<T> | IMfaFlowContinuiation> => {
+
+ //User-login with mfa response
+ const response = await user.login(userName, password);
+
+ const { mfa } = response as { mfa?: boolean }
+
+ //Get the mfa upgrade message from the server
+ if (mfa === true){
+
+ // Process the two factor auth message and add it to the response
+ const result = await mfaProcessor.processMfa(response.result as string, response.finalize);
+
+ return {
+ ...result
+ };
+ }
+
+ //If no mfa upgrade message is returned, the login is complete
+ return response as ExtendedLoginResponse<T>;
+ }
+
+ //Register all the handlers
+ forEach(handlers, mfaProcessor.registerHandler);
+
+ return { login }
+}
+
diff --git a/lib/vnlib.browser/src/mfa/pki.ts b/lib/vnlib.browser/src/mfa/pki.ts
new file mode 100644
index 0000000..f2f08c6
--- /dev/null
+++ b/lib/vnlib.browser/src/mfa/pki.ts
@@ -0,0 +1,150 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { decodeJwt } from "jose"
+import { get } from "@vueuse/core"
+import { type MaybeRef } from "vue";
+import { useAxiosInternal } from "../axios"
+import { useUser } from "../user"
+import { debugLog } from "../util"
+import type { WebMessage } from '../types'
+import type { IUserLoginRequest } from "../user/types"
+import type { ITokenResponse } from "../session"
+import type { UserArg } from "./config";
+
+
+/**
+ * Represents the server api for loging in with a signed OTP
+ */
+export interface PkiLogin{
+ /**
+ * Authenticates a user with a signed JWT one time password
+ * @param pkiJwt The user input JWT signed one time password for authentication
+ * @returns A promise that resolves to the login result
+ */
+ login<T>(pkiJwt: string): Promise<WebMessage<T>>
+}
+
+export interface PkiPublicKey {
+ readonly kid: string;
+ readonly alg: string;
+ readonly kty: string;
+ readonly crv: string;
+ readonly x: string;
+ readonly y: string;
+}
+
+/**
+ * A base, non-mfa integrated PKI endpoint adapter interface
+ */
+export interface PkiApi {
+ /**
+ * Initializes or updates the pki method for the current user
+ * @param publicKey The user's public key to initialize or update the pki method
+ * @param options Optional extended configuration for the pki method. Gets passed to the server
+ */
+ addOrUpdate(publicKey: PkiPublicKey, options?: UserArg): Promise<WebMessage>;
+ /**
+ * Disables the pki method for the current user and passes the given options to the server
+ */
+ disable(options?: UserArg): Promise<WebMessage>;
+ /**
+ * Gets all public keys for the current user
+ */
+ getAllKeys(): Promise<PkiPublicKey[]>;
+ /**
+ * Removes a single public key by it's id for the current user
+ */
+ removeKey(kid: string): Promise<WebMessage>;
+}
+
+interface PkiLoginRequest extends IUserLoginRequest{
+ login: string;
+}
+
+/**
+ * Creates a pki login api that allows for authentication with a signed JWT
+ */
+export const usePkiAuth = (pkiEndpoint: MaybeRef<string>): PkiLogin =>{
+
+ const axios = useAxiosInternal()
+ const { prepareLogin } = useUser()
+
+ const login = async <T>(pkiJwt: string): Promise<WebMessage<T>> => {
+
+ //try to decode the jwt to confirm its form is valid
+ const jwt = decodeJwt(pkiJwt)
+ debugLog(jwt)
+
+ //Prepare a login message
+ const loginMessage = await prepareLogin() as PkiLoginRequest;
+
+ //Set the 'login' field to the otp
+ loginMessage.login = pkiJwt;
+
+ const { post } = get(axios)
+ const { data } = await post<ITokenResponse>(get(pkiEndpoint), loginMessage)
+
+ data.getResultOrThrow();
+
+ //Finalize the login
+ await loginMessage.finalize(data);
+
+ return data as WebMessage<T>;
+ }
+
+ return { login }
+}
+
+/**
+ * Gets the api for interacting with the the user's pki configuration
+ * @param pkiEndpoint The server pki endpoint relative to the base url
+ * @returns An object containing the pki api
+ */
+export const usePkiConfig = (pkiEndpoint: MaybeRef<string>): PkiApi => {
+
+ const axios = useAxiosInternal(null)
+
+ const addOrUpdate = async (publicKey: PkiPublicKey, options?: UserArg): Promise<WebMessage> => {
+ const { patch } = get(axios);
+ const { data } = await patch<WebMessage>(get(pkiEndpoint), { ...publicKey, ...options });
+ return data;
+ }
+
+ const getAllKeys = async (): Promise<PkiPublicKey[]> => {
+ const { data } = await axios.value.get<WebMessage<PkiPublicKey[]>>(get(pkiEndpoint));
+ return data.getResultOrThrow();
+ }
+
+ const disable = async (options?: UserArg): Promise<WebMessage> => {
+ const { delete: del } = get(axios);
+ //emtpy delete request deletes all keys
+ const { data } = await del<WebMessage>(get(pkiEndpoint), options);
+ return data;
+ }
+
+ const removeKey = async (kid: string): Promise<WebMessage> => {
+ const { delete: del } = get(axios);
+ //Delete request with the id parameter deletes a single key
+ const { data } = await del<WebMessage>(`${get(pkiEndpoint)}?id=${kid}`);
+ return data;
+ }
+
+ return { addOrUpdate, disable, getAllKeys, removeKey }
+}
diff --git a/lib/vnlib.browser/src/session/cookies.ts b/lib/vnlib.browser/src/session/cookies.ts
new file mode 100644
index 0000000..0e3e613
--- /dev/null
+++ b/lib/vnlib.browser/src/session/cookies.ts
@@ -0,0 +1,69 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { defer, isEqual } from 'lodash-es'
+import { computed, ref, type Ref } from 'vue'
+import Cookies from 'universal-cookie'
+
+export interface CookieMonitor<T> {
+ readonly cookieValue: Readonly<Ref<T>>;
+ readonly enabled: Readonly<Ref<boolean>>;
+}
+
+export type CookieValueConveter<T> = (value: string) => T;
+
+/**
+ * Creates a reactive wrapper that monitors a single cookie value for changes
+ * @param enabled A ref that indicates if the cookie should be monitored
+ * @param cookieName The name of the cookie to monitor
+ * @param converter An optional converter function to convert the cookie value to a different type
+ */
+export const createCookieMonitor = <T = string>(enabled: Ref<boolean>, cookieName: Ref<string | undefined>, converter?: CookieValueConveter<T>) : CookieMonitor<T> => {
+
+ //Set default converter
+ converter ??= (value: string) => value as unknown as T;
+
+ const cookieJar = new Cookies();
+ const cookieRef = ref<string>('');
+ const cookieValue = computed<T>(() => converter!(cookieRef.value));
+
+ //Store last value to avoid unnecessary updates
+ let lastValue = '';
+
+ const get = (): string => cookieJar.get<string>(cookieName.value || '', { doNotParse: true });
+
+ const checkAndUpdateCookie = () => {
+ if (!enabled.value) {
+ return;
+ }
+ //Get the new cookeie value
+ const newValue = get()
+ if (!isEqual(newValue, lastValue)) {
+ lastValue = cookieRef.value = newValue
+ }
+ }
+
+ //Watch for changes to the cookie and update the ref only if its enabled
+ cookieJar.addChangeListener(() => defer(checkAndUpdateCookie))
+
+ //Initial cookie check
+ checkAndUpdateCookie()
+
+ return { cookieValue , enabled }
+} \ No newline at end of file
diff --git a/lib/vnlib.browser/src/session/index.ts b/lib/vnlib.browser/src/session/index.ts
new file mode 100644
index 0000000..d3fdc2b
--- /dev/null
+++ b/lib/vnlib.browser/src/session/index.ts
@@ -0,0 +1,50 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { computed } from 'vue';
+import { toSafeInteger } from 'lodash-es';
+import { toRefs } from "@vueuse/core";
+import { createCookieMonitor } from "./cookies";
+import { StorageKey, createStorageSlot, getGlobalStateInternal } from "../globalState";
+import { createSession, type IKeyStorage, type IStateStorage } from "./internal";
+import type { ISession } from "./types";
+
+//Export all internal types
+export type * from './types'
+
+/**
+ * Gets the global session api instance
+ * @returns The session api instance
+ */
+export const useSession = (() => {
+
+ //Use reactive config
+ const { session } = getGlobalStateInternal()
+ const { cookiesEnabled } = toRefs(session)
+
+ const liCookieName = computed<string | undefined>(() => session.value.loginCookieName)
+
+ const loginCookie = createCookieMonitor(cookiesEnabled, liCookieName, v => toSafeInteger(v));
+
+ //create reactive storage slots
+ const keyStore = createStorageSlot<IKeyStorage>(StorageKey.Keys, { priv: null, pub: null })
+ const sessionState = createStorageSlot<IStateStorage>(StorageKey.Session, { token: null, browserId: null })
+
+ return (): ISession => createSession(session, loginCookie, keyStore, sessionState);
+})();
diff --git a/lib/vnlib.browser/src/session/internal.ts b/lib/vnlib.browser/src/session/internal.ts
new file mode 100644
index 0000000..d7856c3
--- /dev/null
+++ b/lib/vnlib.browser/src/session/internal.ts
@@ -0,0 +1,247 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { defaults, isEmpty, isNil } from 'lodash-es';
+import { computed, watch, type Ref } from "vue";
+import { get, set, toRefs } from '@vueuse/core';
+import { SignJWT } from 'jose'
+import crypto, { decryptAsync, getRandomHex } from "../webcrypto";
+import { ArrayBuffToBase64, Base64ToUint8Array } from '../binhelpers'
+import { debugLog } from "../util";
+import type { CookieMonitor } from './cookies'
+import type { ISession, ISessionKeyStore, ITokenResponse, ClientCredential, SessionConfig } from './types'
+
+export interface IStateStorage {
+ token: string | null;
+ browserId: string | null;
+}
+export interface IKeyStorage {
+ priv: string | null;
+ pub: string | null;
+}
+
+interface IInternalKeyStore extends ISessionKeyStore {
+ getPublicKey(): Promise<string>;
+ clearKeys(): void;
+}
+
+
+enum ServerLiTokenValues{
+ NoToken = 0,
+ LocalAccount = 1,
+ ExternalAccount = 2
+}
+
+const createKeyStore = (storage: Ref<IKeyStorage>, keyAlg: Ref<AlgorithmIdentifier>): IInternalKeyStore => {
+
+ const { priv, pub } = toRefs(storage)
+
+ const getPublicKey = async (): Promise<string> => {
+ //Check if we have a public key
+ if (isNil(pub.value)) {
+ //If not, generate a new key pair
+ await checkAndSetKeysAsync();
+ return pub.value!;
+ }
+ return pub.value;
+ }
+
+ const setCredentialAsync = async (keypair: CryptoKeyPair): Promise<void> => {
+ // Store the private key
+ const newPrivRaw = await crypto.exportKey('pkcs8', keypair.privateKey);
+ const newPubRaw = await crypto.exportKey('spki', keypair.publicKey);
+
+ //Store keys as base64 strings
+ priv.value = ArrayBuffToBase64(newPrivRaw);
+ pub.value = ArrayBuffToBase64(newPubRaw);
+ }
+
+ const clearKeys = async (): Promise<void> => {
+ set(priv, null);
+ set(pub, null);
+ }
+
+ const checkAndSetKeysAsync = async (): Promise<void> => {
+ // Check if we have a key pair already
+ if (!isNil(priv.value) && !isNil(pub.value)) {
+ return;
+ }
+
+ // If not, generate a new key pair
+ const keypair = await crypto.generateKey(keyAlg.value, true, ['encrypt', 'decrypt']) as CryptoKeyPair;
+
+ //Set credential
+ await setCredentialAsync(keypair);
+
+ debugLog("Generated new client keypair, none were found")
+ }
+
+ const regenerateKeysAsync = (): Promise<void> => {
+ //Clear keys and generate new ones
+ clearKeys();
+ return checkAndSetKeysAsync();
+ }
+
+ const decryptDataAsync = async (data: string | ArrayBuffer): Promise<ArrayBuffer> => {
+ // Convert the private key to a Uint8Array from its base64 string
+ const keyData = Base64ToUint8Array(priv.value || "")
+
+ //import private key as pkcs8
+ const privKey = await crypto.importKey('pkcs8', keyData, keyAlg.value, false, ['decrypt'])
+
+ // Decrypt the data and return it
+ return await decryptAsync(keyAlg.value, privKey, data, false) as ArrayBuffer
+ }
+
+ const decryptAndHashAsync = async (data: string | ArrayBuffer): Promise<string> => {
+ // Decrypt the data
+ const decrypted = await decryptDataAsync(data)
+
+ // Hash the decrypted data
+ const hashed = await crypto.digest({ name: 'SHA-256' }, decrypted)
+
+ // Convert the hash to a base64 string
+ return ArrayBuffToBase64(hashed)
+ }
+
+ return {
+ getPublicKey,
+ clearKeys,
+ regenerateKeysAsync,
+ decryptDataAsync,
+ decryptAndHashAsync
+ }
+}
+
+const createUtil = (utilState: Ref<SessionConfig>, sessionStorage: Ref<IStateStorage>, keyStorage: Ref<IKeyStorage>) => {
+
+ const otpNonceSize = 16;
+ const { browserIdSize, signatureAlgorithm: sigAlg, keyAlgorithm: keyAlg } = toRefs(utilState);
+
+ const KeyStore = createKeyStore(keyStorage, keyAlg);
+
+ //Create session state and key store
+ const { browserId, token } = toRefs(sessionStorage);
+
+ const getBrowserId = (): string => {
+ // Check browser id
+ if (isNil(browserId.value)) {
+ // generate a new random value and store it
+ browserId.value = getRandomHex(browserIdSize.value);
+ debugLog("Generated new browser id, none was found")
+ }
+
+ return browserId.value;
+ }
+
+ const updateCredentials = async (response: ITokenResponse): Promise<void> => {
+ /*
+ * The server sends an encrypted HMAC key
+ * using our public key. We need to decrypt it
+ * and use it to sign messages to the server.
+ */
+ const decrypted = await KeyStore.decryptDataAsync(response.token)
+
+ // Convert the hash to a base64 string and store it
+ token.value = ArrayBuffToBase64(decrypted)
+ }
+
+ const generateOneTimeToken = async (): Promise<string | null> => {
+ //we need to get the shared key from storage and decode it, it may be null if not set
+ const sharedKey = token.value ? Base64ToUint8Array(token.value) : null
+
+ if (!sharedKey) {
+ return null;
+ }
+
+ //Inint jwt with a random nonce
+ const nonce = getRandomHex(otpNonceSize);
+
+ //Get the alg from the config
+ const alg = get(sigAlg);
+
+ const jwt = new SignJWT({ 'nonce': nonce })
+ //Set alg
+ jwt.setProtectedHeader({ alg })
+ //Iat is the only required claim at the current time utc
+ .setIssuedAt()
+ .setAudience(window.location.origin)
+
+ //Sign the jwt
+ const signedJWT = await jwt.sign(sharedKey)
+
+ return signedJWT;
+ }
+
+ const clearLoginState = (): void => {
+ set(browserId, null);
+ set(token, null);
+ KeyStore.clearKeys();
+ }
+
+ const getClientSecInfo = async (): Promise<ClientCredential> => {
+ //Generate and get the credential info
+ const publicKey = await KeyStore.getPublicKey();
+ const browserId = getBrowserId();
+ return { publicKey, browserId };
+ }
+
+ return {
+ KeyStore,
+ getClientSecInfo,
+ updateCredentials,
+ generateOneTimeToken,
+ clearLoginState
+ };
+}
+
+export const createSession = (
+ sessionConfig: Readonly<Ref<SessionConfig>>,
+ cookies: CookieMonitor<number>,
+ keys: Ref<IKeyStorage>,
+ state: Ref<IStateStorage>
+): ISession =>{
+
+ //assign defaults to storage slots before toRefs call
+ defaults(state.value, { token: null, browserId: null });
+ defaults(keys.value, { priv: null, pub: null });
+
+ //Create the session util
+ const util = createUtil(sessionConfig, state, keys);
+ const { token } = toRefs(state);
+ const isServerTokenSet = computed<boolean>(() => !isEmpty(token.value));
+
+ //Translate the cookie value to a LoginCookieValue
+ const { enabled, cookieValue } = cookies
+
+ //If cookies are disabled, only allow the user to be logged in if the token is set
+ const loggedIn = computed<boolean>(() => enabled.value ? cookieValue.value > 0 && isServerTokenSet.value : isServerTokenSet.value);
+
+ const isLocalAccount = computed<boolean>(() => cookieValue.value === ServerLiTokenValues.LocalAccount);
+
+ //Watch the logged in value and if it changes from true to false, clear the token
+ watch(loggedIn, value => value ? null : token.value = null);
+
+ return {
+ loggedIn,
+ isLocalAccount,
+ ...util
+ }
+}
+
diff --git a/lib/vnlib.browser/src/session/types.ts b/lib/vnlib.browser/src/session/types.ts
new file mode 100644
index 0000000..bbb5de6
--- /dev/null
+++ b/lib/vnlib.browser/src/session/types.ts
@@ -0,0 +1,110 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import type { Ref } from "vue";
+import type { WebMessage } from "../types";
+
+export interface SessionConfig {
+ readonly browserIdSize: number;
+ readonly signatureAlgorithm: string;
+ readonly keyAlgorithm: AlgorithmIdentifier;
+}
+
+
+export interface ISessionKeyStore {
+ /**
+ * Regenerates the credentials and stores them in the key store
+ */
+ regenerateKeysAsync(): Promise<void>;
+
+ /**
+ * Decrypts the server encrypted that conforms to the vnlib protocol
+ * @param data The data to encrypt, may be a string or an array buffer
+ */
+ decryptDataAsync(data: string | ArrayBuffer): Promise<ArrayBuffer>;
+
+ /**
+ * Decrypts and hashes the data that conforms to the vnlib protocol
+ * @param data The data to decrypt and hash, may be a string or an array buffer
+ */
+ decryptAndHashAsync(data: string | ArrayBuffer): Promise<string>;
+}
+
+/**
+ * Represents the current server/client session state
+ */
+export interface ISession {
+ /**
+ * A readonly reactive reference to the login status
+ * of the session.
+ */
+ readonly loggedIn: Readonly<Ref<boolean>>;
+
+ /**
+ * A readonly reactive reference indicating if the client
+ * is using a local account or a remote/oauth account
+ */
+ readonly isLocalAccount: Readonly<Ref<boolean>>;
+
+ /**
+ * The internal session key store
+ */
+ readonly KeyStore: ISessionKeyStore;
+
+ /**
+ * Updates session credentials from the server response
+ * @param response The raw response from the server
+ */
+ updateCredentials(response: ITokenResponse): Promise<void>;
+
+ /**
+ * Computes a one time key for a fetch request security header
+ * It is a signed jwt token that is valid for a short period of time
+ */
+ generateOneTimeToken(): Promise<string | null>;
+
+ /**
+ * Clears the session login status and removes all client side
+ * session data
+ */
+ clearLoginState(): void;
+
+ /**
+ * Gets the client's security info
+ */
+ getClientSecInfo(): Promise<ClientCredential>;
+}
+
+export interface ITokenResponse<T = unknown> extends WebMessage<T> {
+ readonly token: string;
+}
+
+/**
+ * Represents the browser's client credential
+ */
+export interface ClientCredential{
+ /**
+ * The browser id of the current client
+ */
+ readonly browserId: string;
+ /**
+ * The public key of the current client
+ */
+ readonly publicKey: string;
+} \ No newline at end of file
diff --git a/lib/vnlib.browser/src/social/index.ts b/lib/vnlib.browser/src/social/index.ts
new file mode 100644
index 0000000..a164eb8
--- /dev/null
+++ b/lib/vnlib.browser/src/social/index.ts
@@ -0,0 +1,232 @@
+import { find, isEqual } from "lodash-es";
+import { get } from "@vueuse/core";
+import { MaybeRef } from "vue";
+import Cookies from "universal-cookie";
+import { useUser } from "../user";
+import { useAxios } from "../axios";
+import { useSession, type ITokenResponse } from "../session";
+import { type WebMessage } from "../types";
+import { type AxiosRequestConfig } from "axios";
+
+export type SocialServerSetQuery = 'invalid' | 'expired' | 'authorized';
+
+export interface OAuthMethod{
+ /**
+ * Gets the url to the login endpoint for this method
+ */
+ readonly Id: string
+ /**
+ * The endpoint to submit the authentication request to
+ */
+ loginUrl(): string
+ /**
+ * Called when the login to this method was successful
+ */
+ onSuccessfulLogin?:() => void
+ /**
+ * Called when the logout to this method was successful
+ */
+ onSuccessfulLogout?:(responseData: unknown) => void
+ /**
+ * Gets the data to send to the logout endpoint, if this method
+ * is undefined, then the logout will be handled by the normal user logout
+ */
+ getLogoutData?: () => { readonly url: string; readonly args: unknown }
+}
+
+export interface SocialLoginApi<T>{
+ /**
+ * The collection of registred authentication methods
+ */
+ readonly methods: T[]
+ /**
+ * Begins an OAuth2 social web authentication flow against the server
+ * handling encryption and redirection of the browser
+ * @param method The desired method to use for login
+ */
+ beginLoginFlow(method: T): Promise<void>;
+ /**
+ * Completes a login flow if authorized, otherwise throws an error
+ * with the message from the server
+ * @returns A promise that resolves when the login is complete
+ */
+ completeLogin(): Promise<void>;
+ /**
+ * Logs out of the current session
+ * @returns A promise that resolves to true if the logout could be handled by
+ * the current method, otherwise false
+ */
+ logout(): Promise<boolean>;
+ /**
+ * Gets the active method for the current session if the
+ * user is logged in using a social login method that is defined
+ * in the methods collection
+ */
+ getActiveMethod(): T | undefined;
+}
+
+/**
+ * Creates a new social login api for the given methods
+ */
+export const useSocialOauthLogin = <T extends OAuthMethod>(methods: T[], axiosConfig?: Partial<AxiosRequestConfig>): SocialLoginApi<T> =>{
+
+ const cookieName = 'active-social-login';
+
+ const { loggedIn, KeyStore } = useSession();
+ const { prepareLogin, logout:userLogout } = useUser();
+ const axios = useAxios(axiosConfig);
+
+ //A cookie will hold the status of the current login method
+ const c = new Cookies(null, { path: '/', sameSite: 'strict', httpOnly:false });
+
+ const getNonceQuery = () => new URLSearchParams(window.location.search).get('nonce');
+ const getResultQuery = () => new URLSearchParams(window.location.search).get('result');
+ const selectMethodForCurrentUrl = () => find(methods, method => {
+ const loginUrl = method.loginUrl();
+ //Check for absolute url, then check if the path is the same
+ if(loginUrl.startsWith('http')){
+ const asUrl = new URL(loginUrl);
+ return isEqual(asUrl.pathname, window.location.pathname);
+ }
+ //Relative url
+ return isEqual(loginUrl, window.location.pathname);
+ })
+
+ const getActiveMethod = (): T | undefined => {
+ const methodName = c.get(cookieName)
+ return find(methods, method => isEqual(method.Id, methodName))
+ }
+
+ const beginLoginFlow = async (method: T): Promise<void> => {
+ //Prepare the login claim`
+ const claim = await prepareLogin()
+ const { data } = await axios.put<WebMessage<string>>(method.loginUrl(), claim)
+
+ const encDat = data.getResultOrThrow()
+ // Decrypt the result which should be a redirect url
+ const result = await KeyStore.decryptDataAsync(encDat)
+ // get utf8 text
+ const text = new TextDecoder('utf-8').decode(result)
+ // Recover url
+ const redirect = new URL(text)
+ // Force https
+ redirect.protocol = 'https:'
+ // redirect to the url
+ window.location.href = redirect.href
+ }
+
+ const completeLogin = async () => {
+
+ //Get auth result from query params
+ const result = getResultQuery();
+ switch(result){
+ case 'invalid':
+ throw new Error('The request was invalid, and you could not be logged in. Please try again.');
+ case 'expired':
+ throw new Error('The request has expired. Please try again.');
+
+ //Continue with login
+ case 'authorized':
+ break;
+
+ default:
+ throw new Error('There was an error processing the login request. Please try again.')
+ }
+
+ const method = selectMethodForCurrentUrl();
+
+ if(!method){
+ throw new Error('The current url is not a valid social login url');
+ }
+
+ //Recover the nonce from query params
+ const nonce = getNonceQuery();
+ if(!nonce){
+ throw new Error('The current session has not been initialized for social login');
+ }
+
+ //Prepare the session for a new login
+ const login = await prepareLogin();
+
+ //Send a post request to the endpoint to complete the login and pass the nonce argument
+ const { data } = await axios.post<ITokenResponse>(method.loginUrl(), { ...login, nonce })
+
+ //Verify result
+ data.getResultOrThrow()
+
+ //Complete login authorization
+ await login.finalize(data);
+
+ //Signal the method that the login was successful
+ if(method.onSuccessfulLogin){
+ method.onSuccessfulLogin();
+ }
+
+ //Set the cookie to the method id
+ c.set(cookieName, method.Id);
+ }
+
+ const logout = async (): Promise<boolean> => {
+ if(!get(loggedIn)){
+ return false;
+ }
+
+ //see if any methods are active
+ const method = getActiveMethod();
+
+ if(!method){
+ return false;
+ }
+
+ /**
+ * If no logout data method is defined, then the logout
+ * is handled by a normal account logout
+ */
+ if(!method.getLogoutData){
+ //Normal user logout
+ const result = await userLogout();
+
+ if(method.onSuccessfulLogout){
+ method.onSuccessfulLogout(result);
+ }
+
+ return true;
+ }
+
+ const { url, args } = method.getLogoutData();
+
+ //Exec logout post request against the url
+ const { data } = await axios.post(url, args);
+
+ //clear cookie on success
+ c.remove(cookieName);
+
+ //Signal the method that the logout was successful
+ if (method.onSuccessfulLogout) {
+ method.onSuccessfulLogout(data);
+ }
+
+ return true;
+ }
+
+ return{
+ beginLoginFlow,
+ completeLogin,
+ getActiveMethod,
+ logout,
+ methods
+ }
+}
+
+/**
+ * Creates a new simple social OAuth method used for login
+ * @example
+ * const google = createSocialMethod('google', 'https://accounts.google.com/o/oauth2/v2/auth')
+ * const facebook = createSocialMethod('facebook', 'https://www.facebook.com/v2.10/dialog/oauth')
+ */
+export const createSocialMethod = (id: string, path: MaybeRef<string>): OAuthMethod => {
+ return{
+ Id: id,
+ loginUrl: () => get(path),
+ }
+} \ No newline at end of file
diff --git a/lib/vnlib.browser/src/toast/index.ts b/lib/vnlib.browser/src/toast/index.ts
new file mode 100644
index 0000000..054c87e
--- /dev/null
+++ b/lib/vnlib.browser/src/toast/index.ts
@@ -0,0 +1,76 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { shallowRef, type MaybeRef } from 'vue'
+import { set } from '@vueuse/core'
+import { debugLog } from '../util'
+import { createFormToaster, createGeneralToaster } from './toaster'
+import type { INotifier, CombinedToaster, ToasterNotifier } from './types'
+
+export type * from './types'
+
+class DefaultNotifier implements INotifier {
+ notify(config: { title:string, text:string }): void {
+ debugLog(`Notification: ${config.title} - ${config.text}`)
+ }
+ close(id: string): void {
+ debugLog(`Notification closed: ${id}`)
+ }
+}
+
+//Combined toaster impl
+const createCombinedToaster = (notifier: MaybeRef<INotifier>) : CombinedToaster => {
+ const form = createFormToaster(notifier);
+ const general = createGeneralToaster(notifier);
+
+ const close = (id?: string) => {
+ form.close(id);
+ general.close(id);
+ }
+
+ return Object.freeze({ form, general, close });
+}
+
+// The program handler for the notification
+const _global = shallowRef<INotifier>(new DefaultNotifier())
+
+/**
+ * Configures the notification handler.
+ * @param {*} notifier The method to call when a notification is to be displayed
+ * @returns The notifier
+ */
+export const configureNotifier = (notifier : INotifier) => set(_global, notifier)
+
+/**
+ * Gets the default toaster for general notifications
+ * and the form toaster for form notifications
+ * @returns {Object} The toaster contianer object
+ */
+export const useToaster = (notifier?: MaybeRef<INotifier>): CombinedToaster => createCombinedToaster(notifier ?? _global);
+
+/**
+ * Gets the default toaster for from notifications
+ */
+export const useFormToaster = (notifier?: MaybeRef<INotifier>): ToasterNotifier => createFormToaster(notifier ?? _global);
+
+/**
+ * Gets the default toaster for general notifications
+ * @returns {Object} The toaster contianer object
+ */
+export const useGeneralToaster = (notifier?: MaybeRef<INotifier>): ToasterNotifier => createGeneralToaster(notifier ?? _global); \ No newline at end of file
diff --git a/lib/vnlib.browser/src/toast/toaster.ts b/lib/vnlib.browser/src/toast/toaster.ts
new file mode 100644
index 0000000..0f178f8
--- /dev/null
+++ b/lib/vnlib.browser/src/toast/toaster.ts
@@ -0,0 +1,83 @@
+
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { assign } from 'lodash-es'
+import { get } from '@vueuse/core'
+import type { MaybeRef } from 'vue'
+import type { INotifier, ToasterNotifier } from './types'
+
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+export const createToaster = (notifier: MaybeRef<INotifier>, fallback: object): ToasterNotifier => {
+
+ const success = (config: any): void => {
+ config.type = 'success'
+ assign(config, fallback)
+ get(notifier).notify(config)
+ }
+
+ const error = (config: any): void => {
+ config.type = 'error'
+ assign(config, fallback)
+ get(notifier).notify(config)
+ }
+
+ const info = (config: any): void => {
+ config.type = 'info'
+ assign(config, fallback)
+ get(notifier).notify(config)
+ }
+
+ const close = (id: string): void => get(notifier).close(id);
+
+ const notifyError = (title: string, message?: string): void => error({ title, text: message });
+
+ return { success, error, info, close, notifyError }
+}
+
+/**
+ * Creates a toaster for form notifications
+ * @param notifier The send to write the notifications to
+ * @returns A toaster to send form notifications to
+ */
+export const createFormToaster = (notifier: MaybeRef<INotifier>) => {
+
+ const formConfig = Object.freeze({
+ group: 'form',
+ ignoreDuplicates: true,
+ // Froms only allow for one notification at a time
+ max: 1,
+ // Disable close on click
+ closeOnClick: false
+ });
+
+ return createToaster(notifier, formConfig);
+}
+
+/**
+ * The general toaster implementation of the IToaster interface
+ */
+export const createGeneralToaster = (notifier: MaybeRef<INotifier>) => {
+ //Setup config for the general toaster w/ the group set to general
+ const config = Object.freeze({
+ group: 'general'
+ });
+ return createToaster(notifier, config);
+} \ No newline at end of file
diff --git a/lib/vnlib.browser/src/toast/types.ts b/lib/vnlib.browser/src/toast/types.ts
new file mode 100644
index 0000000..be74301
--- /dev/null
+++ b/lib/vnlib.browser/src/toast/types.ts
@@ -0,0 +1,48 @@
+
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+export interface ToastEvent{
+
+}
+
+export interface IToaster{
+ success(config : object) : void;
+ error(config : object) : void;
+ info(config : object) : void;
+ close(id? : string) : void;
+}
+
+export interface INotifier{
+ notify(config : object) : void;
+ close(id : string) : void;
+}
+
+export interface IErrorNotifier {
+ notifyError(title: string, message?: string): void;
+ close(id? : string): void;
+}
+
+export interface ToasterNotifier extends IToaster, IErrorNotifier{
+}
+
+export interface CombinedToaster {
+ readonly form: IToaster;
+ readonly general: IToaster;
+}
diff --git a/lib/vnlib.browser/src/types.ts b/lib/vnlib.browser/src/types.ts
new file mode 100644
index 0000000..9e2b670
--- /dev/null
+++ b/lib/vnlib.browser/src/types.ts
@@ -0,0 +1,56 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+/**
+ * Represents a uniform message from the server
+ */
+export interface WebMessage<T = unknown> {
+ /**
+ * The result of the operation, or an error message
+ * if the sucess value is false
+ */
+ readonly result : T | string;
+ /**
+ * True if the operation was successful, false otherwise
+ */
+ readonly success : boolean;
+ /**
+ * Validation errors, if any occured
+ */
+ readonly errors?: ServerValidationError[]
+
+ /**
+ * Returns the result, or throws an error if the operation was not successful
+ */
+ getResultOrThrow() : T;
+}
+
+/**
+ * Represents a validation error from the server for a specific property
+ */
+export interface ServerValidationError{
+ /**
+ * The property that failed validation
+ */
+ readonly property : string;
+ /**
+ * The error message
+ */
+ readonly message: string;
+} \ No newline at end of file
diff --git a/lib/vnlib.browser/src/user/index.ts b/lib/vnlib.browser/src/user/index.ts
new file mode 100644
index 0000000..8b037ba
--- /dev/null
+++ b/lib/vnlib.browser/src/user/index.ts
@@ -0,0 +1,42 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { useSession } from "../session";
+import { useAxiosInternal } from '../axios';
+import { createUser } from './internal'
+import { StorageKey, getGlobalStateInternal, createStorageSlot } from '../globalState';
+import type { UserState } from './types';
+
+//Export public types
+export type * from './types'
+
+/**
+ * Gets the global user interface
+ * @returns The users api instance
+ */
+export const useUser = (() => {
+ const _userStorage = createStorageSlot<UserState>(StorageKey.User, { userName: undefined });
+ const { user } = getGlobalStateInternal();
+ //Use global axios instance
+ const _axios = useAxiosInternal();
+ //Use global session
+ const _session = useSession();
+
+ return () => createUser(user, _axios, _session, _userStorage);
+})()
diff --git a/lib/vnlib.browser/src/user/internal.ts b/lib/vnlib.browser/src/user/internal.ts
new file mode 100644
index 0000000..4a5aed6
--- /dev/null
+++ b/lib/vnlib.browser/src/user/internal.ts
@@ -0,0 +1,190 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { isNil, defaultTo, defaults } from 'lodash-es'
+import { computed, watch, type Ref } from "vue"
+import { get, set, toRefs } from '@vueuse/core'
+import type { Axios, AxiosResponse } from "axios"
+import type { ISession, ITokenResponse } from '../session'
+import type { WebMessage } from '../types'
+import type { User, UserConfig, UserProfile, ExtendedLoginResponse, UserState } from './types'
+
+export enum AccountEndpoint {
+ Login = "login",
+ Logout = "logout",
+ Register = "register",
+ Reset = "reset",
+ Profile = "profile",
+ HeartBeat = "keepalive"
+}
+
+export interface IUserInternal extends User {
+ getEndpoint: (endpoint: AccountEndpoint) => string;
+}
+
+export const createUser = (
+ config: Readonly<Ref<UserConfig>>,
+ axios:Readonly<Ref<Axios>>,
+ session: ISession,
+ state: Ref<UserState>
+): IUserInternal => {
+
+
+ //always set default value before call to toRefs
+ defaults(state.value, { userName: undefined })
+
+ const { accountBasePath } = toRefs(config);
+ const { userName } = toRefs(state);
+
+ const getEndpoint = (endpoint: AccountEndpoint) => `${get(accountBasePath)}/${endpoint}`;
+
+ const prepareLogin = async () => {
+ //Store a copy of the session data and the current time for the login request
+ const finalize = async (response: ITokenResponse): Promise<void> => {
+ //Update the session with the new credentials
+ await session.updateCredentials(response);
+
+ //Update the user state with the new username
+ set(userName, (response as { email? : string }).email);
+ }
+
+ //Get or regen the client public key
+ const { publicKey, browserId } = await session.getClientSecInfo();
+
+ return {
+ clientid: browserId,
+ pubkey: publicKey,
+ localtime: new Date().toISOString(),
+ locallanguage: navigator.language,
+ username: '',
+ password: '',
+ finalize
+ }
+ }
+
+ //Expose the logged in state behind multiple refs
+ const loggedIn = computed(() => session.loggedIn.value);
+
+ //We want to watch the loggin ref and if it changes to false, clear the username
+ watch(loggedIn, value => value === false ? set(userName, undefined) : undefined)
+
+ const logout = async (): Promise<WebMessage> => {
+ //Get axios with logout endpoint
+ const { post } = get(axios);
+ const ep = getEndpoint(AccountEndpoint.Logout);
+
+ // Send a post to the account logout endpoint to logout
+ const { data } = await post<WebMessage>(ep, {});
+
+ //regen session credentials on successful logout
+ await session.KeyStore.regenerateKeysAsync()
+
+ // return the response
+ return data
+ }
+
+ const login = async <T>(userName: string, password: string): Promise<ExtendedLoginResponse<T>> => {
+ //Get axios and the login endpoint
+ const { post } = get(axios);
+ const ep = getEndpoint(AccountEndpoint.Login);
+
+ const prepped = await prepareLogin();
+
+ //Set the username and password
+ prepped.username = userName;
+ prepped.password = password;
+
+ //Send the login request
+ const { data } = await post<ITokenResponse<T>>(ep, prepped);
+
+ // Check the response
+ if(data.success === true) {
+
+ // If the server returned a token, complete the login
+ if (!isNil(data.token)) {
+ await prepped.finalize(data)
+ }
+ }
+
+ return {
+ ...data,
+ finalize: prepped.finalize
+ }
+ }
+
+ const getProfile = async <T extends UserProfile>(): Promise <T> => {
+ //Get axios and the profile endpoint
+ const ax = get(axios)
+ const ep = getEndpoint(AccountEndpoint.Profile);
+
+ // Get the user's profile from the profile endpoint
+ const response = await ax.get<T>(ep);
+
+ //Update the internal username if it was set by the server
+ const newUsername = defaultTo(response.data.email, get(userName));
+
+ //Update the user state with the new username from the server
+ set(userName, newUsername);
+
+ // return response data
+ return response.data
+ }
+
+ const resetPassword = async (current: string, newPass: string, args: object): Promise<WebMessage> => {
+ //Get axios and the reset password endpoint
+ const { post } = get(axios);
+ const ep = getEndpoint(AccountEndpoint.Reset);
+
+ // Send a post to the reset password endpoint
+ const { data } = await post<WebMessage>(ep, {
+ current,
+ new_password: newPass,
+ ...args
+ })
+
+ return data
+ }
+
+ const heartbeat = async (): Promise <AxiosResponse> => {
+ //Get axios and the heartbeat endpoint
+ const { post } = get(axios);
+ const ep = getEndpoint(AccountEndpoint.HeartBeat);
+
+ // Send a post to the heartbeat endpoint
+ const response = await post<ITokenResponse>(ep);
+
+ //If success flag is set, update the credentials
+ if(response.data.success){
+ //Update credential
+ await session.updateCredentials(response.data);
+ }
+ return response;
+ }
+
+ return{
+ userName,
+ prepareLogin,
+ logout,
+ login,
+ getProfile,
+ resetPassword,
+ heartbeat,
+ getEndpoint,
+ }
+}
diff --git a/lib/vnlib.browser/src/user/types.ts b/lib/vnlib.browser/src/user/types.ts
new file mode 100644
index 0000000..37d2829
--- /dev/null
+++ b/lib/vnlib.browser/src/user/types.ts
@@ -0,0 +1,95 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import type { AxiosResponse } from "axios"
+import type { Ref } from "vue"
+import type { WebMessage } from "../types"
+import type { ITokenResponse } from "../session"
+
+/**
+ * Represents the user/account server api
+ */
+export interface User {
+ /**
+ * A reactive ref to the username of the current user.
+ * Its updated on calls to getProfile
+ */
+ readonly userName: Ref<string | undefined>
+ /**
+ * Prepares a login request for the server
+ */
+ prepareLogin(): Promise<IUserLoginRequest>
+ /**
+ * Attempts to log the user out
+ */
+ logout(): Promise<WebMessage>
+ /**
+ * Attempts to log the user in with a possible mfa upgrade result
+ * @param userName The username to login with
+ * @param password The password to login with
+ * @returns A promise that resolves to the login result
+ */
+ login<T>(userName: string, password: string): Promise<ExtendedLoginResponse<T>>
+ /**
+ * Gets the user profile from the server
+ */
+ getProfile<T extends UserProfile>(): Promise<T>
+ /**
+ * Resets the password for the current user
+ * @param current the user's current password
+ * @param newPass the user's new password
+ * @param args any additional arguments to send to the server
+ */
+ resetPassword(current: string, newPass: string, args: object): Promise<WebMessage>
+ /**
+ * Sends a heartbeat to the server to keep the session alive
+ * and regenerate credentials as designated by the server.
+ */
+ heartbeat(): Promise<AxiosResponse>
+}
+
+export type IUser = User;
+
+export interface IUserLoginRequest {
+ /**
+ * Finalizes a login process with the given response from the server
+ * @param response The finalized login response from the server
+ */
+ finalize(response: ITokenResponse): Promise<void>
+}
+
+export interface UserConfig {
+ readonly accountBasePath: string;
+}
+
+export interface UserState{
+ /**
+ * A reactive ref to the username of the current user.
+ * Its updated on calls to getProfile
+ */
+ readonly userName: string | undefined
+}
+
+export interface ExtendedLoginResponse<T> extends WebMessage<T> {
+ finalize: (response : ITokenResponse) => Promise<void>
+}
+
+export interface UserProfile {
+ readonly email: string | undefined;
+} \ No newline at end of file
diff --git a/lib/vnlib.browser/src/util.ts b/lib/vnlib.browser/src/util.ts
new file mode 100644
index 0000000..c5df99f
--- /dev/null
+++ b/lib/vnlib.browser/src/util.ts
@@ -0,0 +1,28 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+
+/**
+ * Writes logs to the console if the environment is set to development
+ */
+export const debugLog = function (...args : unknown[]) {
+ if (process.env.NODE_ENV === 'development') {
+ console.log(...args)
+ }
+} \ No newline at end of file
diff --git a/lib/vnlib.browser/src/webcrypto.ts b/lib/vnlib.browser/src/webcrypto.ts
new file mode 100644
index 0000000..d2c7640
--- /dev/null
+++ b/lib/vnlib.browser/src/webcrypto.ts
@@ -0,0 +1,97 @@
+// Copyright (c) 2023 Vaughn Nugent
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy of
+// this software and associated documentation files (the "Software"), to deal in
+// the Software without restriction, including without limitation the rights to
+// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software is furnished to do so,
+// subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { isArrayBuffer, isPlainObject, isString } from 'lodash-es';
+import { ArrayBuffToBase64, Base64ToUint8Array, ArrayToHexString } from './binhelpers';
+
+const crypto = window?.crypto?.subtle || {};
+
+/**
+ * Signs the dataBuffer using the specified key and hmac algorithm by its name eg. 'SHA-256'
+ * @param {ArrayBuffer | String} dataBuffer The data to sign, either as an ArrayBuffer or a base64 string
+ * @param {ArrayBuffer | String} keyBuffer The raw key buffer, or a base64 encoded string
+ * @param {String} alg The name of the hmac algorithm to use eg. 'SHA-256'
+ * @param {String} [toBase64 = false] The output format, the array buffer data, or true for base64 string
+ * @returns {Promise<ArrayBuffer | String>} The signature as an ArrayBuffer or a base64 string
+ */
+export const hmacSignAsync = async (keyBuffer: ArrayBuffer | string, dataBuffer: ArrayBuffer | string, alg : string, toBase64 = false)
+: Promise<ArrayBuffer | string> => {
+ // Check key argument type
+ const rawKeyBuffer = isString(keyBuffer) ? Base64ToUint8Array(keyBuffer as string) : keyBuffer as ArrayBuffer;
+
+ // Check data argument type
+ const rawDataBuffer = isString(dataBuffer) ? Base64ToUint8Array(dataBuffer as string) : dataBuffer as ArrayBuffer;
+
+ // Get the key
+ const hmacKey = await crypto.importKey('raw', rawKeyBuffer, { name: 'HMAC', hash: alg }, false, ['sign']);
+
+ // Sign hmac data
+ const digest = await crypto.sign('HMAC', hmacKey, rawDataBuffer);
+
+ // Encode to base64 if needed
+ return toBase64 ? ArrayBuffToBase64(digest) : digest;
+}
+/**
+ * @function decryptAsync Decrypts syncrhonous or asyncrhonsous en encypted data
+ * asynchronously.
+ * @param {any} data The encrypted data to decrypt. (base64 string or ArrayBuffer)
+ * @param {any} privKey The key to use for decryption (base64 String or ArrayBuffer).
+ * @param {Object} algorithm The algorithm object to use for decryption.
+ * @param {Boolean} toBase64 If true, the decrypted data will be returned as a base64 string.
+ * @returns {Promise} The decrypted data.
+ */
+export const decryptAsync = async (
+ algorithm: AlgorithmIdentifier,
+ privKey: BufferSource | CryptoKey | JsonWebKey,
+ data: string | ArrayBuffer,
+ toBase64 = false): Promise<string | ArrayBuffer> =>
+{
+ // Check data argument type and decode if needed
+ const dataBuffer = isString(data) ? Base64ToUint8Array(data as string) : data as ArrayBuffer;
+
+ let privateKey = privKey
+ // Check key argument type
+ if (privKey instanceof CryptoKey) {
+ privateKey = privKey
+ }
+ // If key is binary data, then import it as raw data
+ else if (isArrayBuffer(privKey)) {
+ privateKey = await crypto.importKey('raw', privKey, algorithm, true, ['decrypt'])
+ }
+ // If the key is an object, then import it as a jwk
+ else if (isPlainObject(privKey)) {
+ privateKey = await crypto.importKey('jwk', privKey as JsonWebKey, algorithm, true, ['decrypt'])
+ }
+
+ // Decrypt the data and return it
+ const decrypted = await crypto.decrypt(algorithm, privateKey as CryptoKey, dataBuffer)
+ return toBase64 ? ArrayBuffToBase64(decrypted) : decrypted
+}
+
+export const getRandomHex = (size: number) : string => {
+ // generate a new random secret and store it
+ const randBuffer = new Uint8Array(size)
+ // generate random id directly on the window.crypto object
+ window.crypto.getRandomValues(randBuffer)
+ // Store the id in the session as hex
+ return ArrayToHexString(randBuffer)
+}
+
+//default export subtle crypto
+export default crypto; \ No newline at end of file