-
+
+
-
-
{{ toastTitle }}
-
-
{{ message }}
+
+
+ {{ notificationTitle }}
+
+
+
+ {{ message }}
+
-
+
diff --git a/src/components/SettingsPrompt/SettingsPrompt.vue b/src/components/SettingsPrompt/SettingsPrompt.vue
new file mode 100644
index 0000000..53e0b58
--- /dev/null
+++ b/src/components/SettingsPrompt/SettingsPrompt.vue
@@ -0,0 +1,312 @@
+
+
+
+
+ Session Settings
+
+
+
+
+
+ mdi-close
+
+
+ Session Settings
+
+
+
+ Start Session
+
+ Stop Session
+
+
+
+
+ WebAPI Access Token
+
+
+
+
+
+
+
+
+ Copy to clipboard
+
+
+
+
+
+
+
+ Generate new token
+
+
+
+
+
+
+
+
+
+
+
+ Share
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/composables/useClipboard/useClipboard.test.ts b/src/composables/useClipboard/useClipboard.test.ts
new file mode 100644
index 0000000..727d3aa
--- /dev/null
+++ b/src/composables/useClipboard/useClipboard.test.ts
@@ -0,0 +1,50 @@
+import { describe, it, expect } from 'vitest';
+import useClipboard from '@/composables/useClipboard/useClipboard';
+import clipboard from 'clipboardy';
+
+describe('useClipboard Composable', () => {
+ const { clipboardText, writeClipboardText, readClipboardText } =
+ useClipboard();
+
+ it('clipboardText is empty on initialization', () => {
+ expect(clipboardText.value).toBe('');
+ });
+
+ it('returns false if text is empty', async () => {
+ const result = await writeClipboardText('');
+
+ expect(result).toBe(false);
+ });
+
+ it('writes text to clipboard', async () => {
+ const text = 'Hello, world!';
+ const result = await writeClipboardText(text);
+
+ /**
+ * It is currently not possible to write into the clipboard when run in a GitHub Action therefore always true
+ */
+ if (result) {
+ expect(result).toBe(true);
+ expect(clipboardText.value).toBe(text);
+ } else {
+ expect(result).toBe(false);
+ expect(clipboardText.value).toBe('');
+ }
+ });
+
+ it('reads text from clipboard', async () => {
+ const text = 'Hello, world!';
+ await writeClipboardText(text);
+
+ const result = await readClipboardText();
+
+ /**
+ * It is currently not possible to read the clipboard when run in a GitHub Action therefore always true
+ */
+ if (result) {
+ expect(result).toBe(text);
+ } else {
+ expect(result).toBe('');
+ }
+ });
+});
diff --git a/src/composables/useClipboard/useClipboard.ts b/src/composables/useClipboard/useClipboard.ts
new file mode 100644
index 0000000..0064a80
--- /dev/null
+++ b/src/composables/useClipboard/useClipboard.ts
@@ -0,0 +1,86 @@
+import { ref } from 'vue';
+import clipboard from 'clipboardy';
+
+/**
+ * Clipboard composable
+ */
+export default function useClipboard() {
+ /**
+ * Text from the clipboard
+ */
+ const clipboardText = ref('');
+
+ /**
+ * Check if the clipboard is supported by the browser
+ */
+ const supported = navigator && navigator.clipboard;
+
+ /**
+ * Save the given text to the clipboard
+ * @param text
+ * @returns
+ */
+ const writeClipboardText = async (text: string): Promise
=> {
+ if (!text) {
+ return false;
+ }
+
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
+ try {
+ await navigator.clipboard.writeText(text);
+ clipboardText.value = text;
+ return true;
+ } catch (error) {
+ console.error(error);
+ return false;
+ }
+ } else if (typeof clipboard !== 'undefined') {
+ try {
+ clipboard.writeSync(text);
+ clipboardText.value = text;
+ return true;
+ } catch (error) {
+ console.error(error);
+ return false;
+ }
+ } else {
+ console.error('Clipboard not supported');
+ return false;
+ }
+ };
+
+ /**
+ * Read the text from the clipboard
+ */
+ const readClipboardText = async (): Promise => {
+ if (typeof navigator !== 'undefined' && navigator.clipboard) {
+ try {
+ const text = await navigator.clipboard.readText();
+ clipboardText.value = text;
+ return text;
+ } catch (error) {
+ console.error(error);
+ return '';
+ }
+ } else if (typeof clipboard !== 'undefined') {
+ try {
+ const text = clipboard.readSync();
+ clipboardText.value = text;
+ return text;
+ } catch (error) {
+ console.error(error);
+ return '';
+ }
+ } else {
+ console.error('Clipboard not supported');
+ return '';
+ }
+ };
+
+ return {
+ clipboardText,
+ supported,
+ writeClipboardText,
+ readClipboardText,
+ };
+}
diff --git a/src/composables/useForceRerender.test.ts b/src/composables/useForceRerender/useForceRerender.test.ts
similarity index 93%
rename from src/composables/useForceRerender.test.ts
rename to src/composables/useForceRerender/useForceRerender.test.ts
index 23dc5e0..989682e 100644
--- a/src/composables/useForceRerender.test.ts
+++ b/src/composables/useForceRerender/useForceRerender.test.ts
@@ -3,7 +3,7 @@ import { mount } from '@vue/test-utils';
import {
useForceRerender,
isRerendering,
-} from '@/composables/useForceRerender';
+} from '@/composables/useForceRerender/useForceRerender';
describe('useForceRerender Composable', () => {
it('should trigger a rerender when called', async () => {
diff --git a/src/composables/useForceRerender.ts b/src/composables/useForceRerender/useForceRerender.ts
similarity index 100%
rename from src/composables/useForceRerender.ts
rename to src/composables/useForceRerender/useForceRerender.ts
diff --git a/src/composables/useNotificationSystem.test.ts b/src/composables/useNotificationSystem/useNotificationSystem.test.ts
similarity index 89%
rename from src/composables/useNotificationSystem.test.ts
rename to src/composables/useNotificationSystem/useNotificationSystem.test.ts
index effac22..675e5cf 100644
--- a/src/composables/useNotificationSystem.test.ts
+++ b/src/composables/useNotificationSystem/useNotificationSystem.test.ts
@@ -1,12 +1,12 @@
import { describe, it, expect, beforeEach } from 'vitest';
-import useNotifications from '@/composables/useNotificationSystem';
+import useNotificationSystem from '@/composables/useNotificationSystem/useNotificationSystem';
// When the Notification is outsourced from the App.vue properly in a proper own component, this test should be updated to reflect that and also check the component if the Notification is actually rendered.
-describe('useNotifications Composable', () => {
+describe('useNotificationSystem Composable', () => {
let notifications: any;
beforeEach(() => {
- notifications = useNotifications();
+ notifications = useNotificationSystem();
});
it('should add a general notification and delete it afterwards', () => {
@@ -78,7 +78,7 @@ describe('useNotifications Composable', () => {
});
it('stopBodyOverflow adds hide-overflow class to document body', () => {
- const { stopBodyOverflow } = useNotifications();
+ const { stopBodyOverflow } = useNotificationSystem();
stopBodyOverflow();
@@ -86,7 +86,7 @@ describe('useNotifications Composable', () => {
});
it('allowBodyOverflow removes hide-overflow class from document body', () => {
- const { allowBodyOverflow } = useNotifications();
+ const { allowBodyOverflow } = useNotificationSystem();
allowBodyOverflow();
diff --git a/src/composables/useNotificationSystem.ts b/src/composables/useNotificationSystem/useNotificationSystem.ts
similarity index 82%
rename from src/composables/useNotificationSystem.ts
rename to src/composables/useNotificationSystem/useNotificationSystem.ts
index d0bc2de..d0ad7d4 100644
--- a/src/composables/useNotificationSystem.ts
+++ b/src/composables/useNotificationSystem/useNotificationSystem.ts
@@ -1,11 +1,17 @@
import { ref } from 'vue';
+import useTokenGenerator from '@/composables/useTokenGenerator/useTokenGenerator';
/**
* notifications list
*/
const notifications = ref([]);
-export default function useNotifications() {
+/**
+ * Notification System Composable
+ */
+export default function useNotificationSystem() {
+ const { generateValidToken } = useTokenGenerator();
+
/**
* Create a notification
* @param options
@@ -19,7 +25,7 @@ export default function useNotifications() {
notifications.value.push(
...[
{
- id: createUUID(),
+ id: generateValidToken(),
..._options,
},
]
@@ -95,24 +101,6 @@ export default function useNotifications() {
};
}
-/**
- * Create a unique id
- * @returns a unique id as a string
- */
-//TODO this is redundant (we already have a function creating a random string. Reuse that after refactoring.
-function createUUID(): string {
- let dt = new Date().getTime();
- var uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(
- /[xy]/g,
- function (c) {
- var r = (dt + Math.random() * 16) % 16 | 0;
- dt = Math.floor(dt / 16);
- return (c == 'x' ? r : (r & 0x3) | 0x8).toString(16);
- }
- );
- return uuid;
-}
-
/**
* Notification interface
*/
diff --git a/src/composables/useRunning.ts b/src/composables/useRunning/useRunning.ts
similarity index 92%
rename from src/composables/useRunning.ts
rename to src/composables/useRunning/useRunning.ts
index c3bf0e9..24019dc 100644
--- a/src/composables/useRunning.ts
+++ b/src/composables/useRunning/useRunning.ts
@@ -1,5 +1,5 @@
import { ref } from 'vue';
-import { Vigad } from '../proc/Vigad';
+import { Vigad } from '../../proc/Vigad';
/**
* Reactive boolean that can be used to check the capture status.
diff --git a/src/composables/useTokenGenerator/useTokenGenerator.test.ts b/src/composables/useTokenGenerator/useTokenGenerator.test.ts
new file mode 100644
index 0000000..fc92566
--- /dev/null
+++ b/src/composables/useTokenGenerator/useTokenGenerator.test.ts
@@ -0,0 +1,30 @@
+import { describe, it, expect } from 'vitest';
+import useTokenGenerator from '@/composables/useTokenGenerator/useTokenGenerator';
+
+describe('useTokenGenerator Composable', () => {
+ const { generateValidToken, defaultRules, minTokenLenght } =
+ useTokenGenerator();
+
+ it('generateToken returns a string', () => {
+ const token = generateValidToken();
+ expect(typeof token).toBe('string');
+ });
+
+ it('generateToken generates a token of minimum length', () => {
+ const token = generateValidToken();
+ expect(token.length).toBeGreaterThanOrEqual(minTokenLenght.value);
+ });
+
+ it('generateToken generates a token that meets all rules', () => {
+ const runs = 100;
+ for (let index = 0; index < runs; index++) {
+ const token = generateValidToken();
+ expect(defaultRules.required(token)).toBe(true);
+ expect(defaultRules.min(token)).toBe(true);
+ expect(defaultRules.uppercase(token)).toBe(true);
+ expect(defaultRules.lowercase(token)).toBe(true);
+ expect(defaultRules.special(token)).toBe(true);
+ expect(defaultRules.number(token)).toBe(true);
+ }
+ });
+});
diff --git a/src/composables/useTokenGenerator/useTokenGenerator.ts b/src/composables/useTokenGenerator/useTokenGenerator.ts
new file mode 100644
index 0000000..b4f9a09
--- /dev/null
+++ b/src/composables/useTokenGenerator/useTokenGenerator.ts
@@ -0,0 +1,90 @@
+import { ref } from 'vue';
+
+/**
+ * Token generator composable
+ */
+export default function useTokenGenerator() {
+ /**
+ * Character set for the access token
+ */
+ const characterSet =
+ 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*_+-=?';
+
+ /**
+ * Minimum token length
+ */
+ const minTokenLenght = ref(8);
+
+ /**
+ * Default Rules for the access token
+ */
+ const defaultRules = {
+ required: (value: string) =>
+ !!value || 'An access token is required to start a session',
+ min: (v: string) =>
+ v.length >= minTokenLenght.value ||
+ `Min ${minTokenLenght.value} characters`,
+ uppercase: (v: string) =>
+ /[A-Z]/.test(v) || 'Must include at least one uppercase letter',
+ lowercase: (v: string) =>
+ /[a-z]/.test(v) || 'Must include at least one lowercase letter',
+ special: (v: string) =>
+ /[\W_]/.test(v) || 'Must include at least one special character',
+ number: (v: string) =>
+ /[0-9]+/.test(v) || 'Must include at least one number',
+ };
+
+ /**
+ * Generate a random token using crypto module in Node.js or browser depending on the environment (Node.js or browser) - browser for application and Node.js for tests and GitHub Actions
+ */
+ const generateToken = (
+ lenght: number = 32,
+ alphabet: string = characterSet
+ ): string => {
+ let generatedToken = '';
+
+ // Checks if the environment is Node.js
+ if (typeof process !== 'undefined' && process?.versions?.node) {
+ // generate random bytes using crypto module in Node.js
+ const { randomBytes } = require('crypto');
+ const sourceBytes = randomBytes(lenght);
+ generatedToken = Array.from(sourceBytes)
+ .map((x: any) => alphabet[x % alphabet.length])
+ .join('');
+ } else {
+ // generate random bytes using crypto module in browser
+ const sourceBytes = new Uint8Array(lenght);
+ window.crypto.getRandomValues(sourceBytes);
+ generatedToken = Array.from(sourceBytes)
+ .map((x) => alphabet[x % alphabet.length])
+ .join('');
+ }
+ return generatedToken;
+ };
+
+ /**
+ * Generate a valid token using the rules defined in the rules object
+ * @returns a valid token
+ */
+ const generateValidToken = (): string => {
+ const token = generateToken();
+ if (
+ defaultRules.lowercase(token) === true &&
+ defaultRules.uppercase(token) === true &&
+ defaultRules.special(token) === true &&
+ defaultRules.number(token) === true &&
+ defaultRules.min(token) === true
+ ) {
+ return token;
+ }
+ return generateValidToken();
+ };
+
+ return {
+ characterSet,
+ minTokenLenght,
+ defaultRules,
+ generateToken,
+ generateValidToken,
+ };
+}
diff --git a/src/views/CapturePage.vue b/src/views/CapturePage.vue
index 05ec0cc..18bc024 100644
--- a/src/views/CapturePage.vue
+++ b/src/views/CapturePage.vue
@@ -41,7 +41,7 @@