feat: init commit w/ callback prototype components

This commit is contained in:
Zack Spear
2023-05-26 17:34:23 -07:00
committed by Zack Spear
parent a205bca6ec
commit c96c0a765c
22 changed files with 11134 additions and 0 deletions

1
.env.example Normal file
View File

@@ -0,0 +1 @@
NUXT_PUBLIC_CALLBACK_KEY=aNotSoSecretKeyUsedToObfuscateQueryParams

86
.gitignore vendored Normal file
View File

@@ -0,0 +1,86 @@
# Logs
./logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
coverage-ts
# nyc test coverage
.nyc_output
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
# Visual Studio Code workspace
.vscode/*
!.vscode/extensions.json
# OSX
.DS_Store
# Temp dir for tests
test/__temp__/*
# Built files
dist
# Typescript
typescript
# Ultra runner
.ultra.cache.json
# Github actions
RELEASE_NOTES.md
# Docker Deploy Folder
deploy/*
!deploy/.gitkeep
# pkg cache
.pkg-cache
*.log*
.nuxt
.nitro
.cache
.output
.env*

2
.npmrc Normal file
View File

@@ -0,0 +1,2 @@
shamefully-hoist=true
strict-peer-dependencies=false

42
README.md Normal file
View File

@@ -0,0 +1,42 @@
# Nuxt 3 Minimal Starter
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
## Setup
Make sure to install the dependencies:
```bash
# yarn
yarn install
# npm
npm install
# pnpm
pnpm install
```
## Development Server
Start the development server on `http://localhost:3000`
```bash
npm run dev
```
## Production
Build the application for production:
```bash
npm run build
```
Locally preview production build:
```bash
npm run preview
```
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.

65
_webGui/callbackTest.page Normal file
View File

@@ -0,0 +1,65 @@
Menu="ManagementAccess:200"
Title="Callback Tests"
Icon="icon-u-globe"
Tag="globe"
---
<?php
$myservers_flash_cfg_path='/boot/config/plugins/dynamix.my.servers/myservers.cfg';
$myservers = file_exists($myservers_flash_cfg_path) ? @parse_ini_file($myservers_flash_cfg_path,true) : [];
// print_r($mystatus);
// extract web component JS file from manifest
$jsonManifest = file_get_contents('/usr/local/emhttp/plugins/dynamix.my.servers/webComponents/manifest.json');
$jsonManifestData = json_decode($jsonManifest, true);
$webComponentFile = $jsonManifestData["connect-components.client.mjs"]["file"];
// web component
$localSource = '/plugins/dynamix.my.servers/webComponents/' . $webComponentFile;
// add the web component source to the DOM
echo '<script id="unraid-webcomponents" defer src="' . $localSource . '"></script>';
$serverData = [
"avatar" => (!empty($myservers['remote']['avatar']) && $plgInstalled) ? $myservers['remote']['avatar'] : '',
"config" => [
'valid' => $var['configValid'] === 'yes',
'error' => $var['configValid'] !== 'yes'
? (array_key_exists($var['configValid'], $configErrorEnum) ? $configErrorEnum[$var['configValid']] : 'UNKNOWN_ERROR')
: null,
],
"deviceCount" => $var['deviceCount'],
"email" => $myservers['remote']['email'] ?? '',
"extraOrigins" => explode(',', $myservers['api']['extraOrigins']??''),
"flashproduct" => $var['flashProduct'],
"flashvendor" => $var['flashVendor'],
"flashBackupActivated" => empty($flashbackup_status['activated']) ? '' : 'true',
"guid" => $var['flashGUID'],
"hasRemoteApikey" => !empty($myservers['remote']['apikey']),
"internalip" => ipaddr(),
"internalport" => $_SERVER['SERVER_PORT'],
"keyfile" => empty($var['regFILE'])? "" : str_replace(['+','/','='], ['-','_',''], trim(base64_encode(@file_get_contents($var['regFILE'])))),
"locale" => 'en',
"osVersion" => $var['version'],
"plgVersion" => $plgversion = file_exists('/var/log/plugins/dynamix.unraid.net.plg')
? trim(@exec('/usr/local/sbin/plugin version /var/log/plugins/dynamix.unraid.net.plg 2>/dev/null'))
: ( file_exists('/var/log/plugins/dynamix.unraid.net.staging.plg')
? trim(@exec('/usr/local/sbin/plugin version /var/log/plugins/dynamix.unraid.net.staging.plg 2>/dev/null'))
: 'base-'.$var['version'] ),
"plgInstalled" => $plgInstalled,
"protocol" => $_SERVER['REQUEST_SCHEME'],
"reggen" => (int)$var['regGen'],
"regGuid" => $var['regGUID'],
"registered" => (!empty($myservers['remote']['username']) && $plgInstalled),
"name" => $var['NAME'],
"site" => $_SERVER['REQUEST_SCHEME']."://".$_SERVER['HTTP_HOST'],
"state" => strtoupper(empty($var['regCheck']) ? $var['regTy'] : $var['regCheck']),
"ts" => time(),
"username" => (!empty($myservers['remote']['username']) && $plgInstalled) ? $myservers['remote']['username'] : '',
"wanFQDN" => $nginx['NGINX_WANFQDN'] ?? '',
];
echo "<connect-user-profile server='" . json_encode($serverData) . "'></connect-user-profile>";
?>
<connect-callback-handler></connect-callback-handler>
<connect-auth></connect-auth>
<connect-key-actions></connect-key-actions>
<connect-launchpad></connect-launchpad>
<connect-plugin-promo></connect-plugin-promo>
<connect-wan-ip-check></connect-wan-ip-check>

52
app.vue Normal file
View File

@@ -0,0 +1,52 @@
<script lang="ts" setup>
import type { Server } from '~/types/server';
const nuxtApp = useNuxtApp();
const server = ref<Server>({
name: 'DevServer9000',
description: 'Fully automated media server and lab rig',
guid: '9292-1111-BITE-1111-444444444444',
deviceCount: 8,
site: 'http://localhost:4321',
state: 'TRIAL',
keyTypeForPurchase: 'Trial',
locale: 'en',
});
onBeforeMount(() => {
nuxtApp.$customElements.registerEntry('ConnectComponents');
});
</script>
<template>
<div class="max-w-5xl mx-auto bg-gray-200">
<client-only>
<div class="flex flex-col gap-6 p-6">
<h2>Vue Components</h2>
<UserProfileCe :server="server" />
<AuthCe />
<KeyActionsCe />
<LaunchpadCe />
<PluginPromoCe />
<WanIpCheckCe />
<CallbackHandlerCe />
</div>
<div class="flex flex-col gap-6 p-6">
<h2>Web Components</h2>
<connect-user-profile :server="JSON.stringify(server)"></connect-user-profile>
<connect-auth></connect-auth>
<connect-key-actions></connect-key-actions>
<connect-launchpad></connect-launchpad>
<connect-plugin-promo></connect-plugin-promo>
<connect-wan-ip-check></connect-wan-ip-check>
<connect-callback-handler></connect-callback-handler>
</div>
</client-only>
</div>
</template>
<style lang="postcss">
h2 {
@apply text-xl font-semibold font-mono;
}
</style>

15
components/Auth.ce.vue Normal file
View File

@@ -0,0 +1,15 @@
<script lang="ts" setup>
import 'tailwindcss/tailwind.css';
</script>
<template>
<div class="text-white font-semibold p-4 bg-orange-400 rounded-lg">
Auth.ce
</div>
</template>
<style lang="postcss">
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>

View File

@@ -0,0 +1,26 @@
<script setup lang="ts">
import { useCallbackStore } from '~/store/callback';
import 'tailwindcss/tailwind.css';
import { storeToRefs } from 'pinia';
const callbackStore = useCallbackStore();
const { decryptedData } = storeToRefs(callbackStore);
onBeforeMount(() => {
callbackStore.watcher();
});
</script>
<template>
<div class="text-white font-semibold p-4 bg-orange-400 rounded-lg">
<h2>CallbackHandler.ce</h2>
<pre>{{ JSON.stringify(decryptedData, null, 2) }}</pre>
</div>
</template>
<style lang="postcss">
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>

View File

@@ -0,0 +1,15 @@
<script lang="ts" setup>
import 'tailwindcss/tailwind.css';
</script>
<template>
<div class="text-white font-semibold p-4 bg-orange-400 rounded-lg">
KeyActions.ce
</div>
</template>
<style lang="postcss">
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>

View File

@@ -0,0 +1,15 @@
<script lang="ts" setup>
import 'tailwindcss/tailwind.css';
</script>
<template>
<div class="text-white font-semibold p-4 bg-orange-400 rounded-lg">
Launchpad.ce
</div>
</template>
<style lang="postcss">
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>

View File

@@ -0,0 +1,15 @@
<script lang="ts" setup>
import 'tailwindcss/tailwind.css';
</script>
<template>
<div class="text-white font-semibold p-4 bg-orange-400 rounded-lg">
PluginPromo.ce
</div>
</template>
<style lang="postcss">
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>

View File

@@ -0,0 +1,49 @@
<script lang="ts" setup>
import { storeToRefs } from 'pinia';
import { useCallbackStore } from '~/store/callback';
import { useServerStore } from '~/store/server';
import type { Server } from '~/types/server';
import 'tailwindcss/tailwind.css';
export interface Props {
server?: Server;
}
const props = defineProps<Props>();
const callbackStore = useCallbackStore();
const serverStore = useServerStore();
const { name, description, guid } = storeToRefs(serverStore);
onBeforeMount(() => {
console.debug('[onBeforeMount]', { props }, typeof props.server);
if (!props.server) return console.error('Server data not present');
// set props from web component in store so the data is available throughout other components
if (typeof props.server === 'object') { // handle the testing dev Vue component
serverStore.setServer(props.server);
} else if (typeof props.server === 'string') { // handle web component
try {
const parsedServerProp = JSON.parse(props.server);
serverStore.setServer(parsedServerProp);
} catch (e) {
console.error(e);
}
}
});
</script>
<template>
<div class="text-white bg-blue-700 flex flex-col gap-y-2 p-4 rounded-lg">
<h3 class="italic">{{ name }}</h3>
<h4 class="text-gray-300">{{ description }}</h4>
<h5>{{ guid }}</h5>
<button class="p-2 text-blue-700 hover:text-white bg-white border border-white hover:bg-transparent rounded-sm" @click="callbackStore.send()" type="button">Test Purchase Callback</button>
</div>
</template>
<style lang="postcss">
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>

View File

@@ -0,0 +1,15 @@
<script lang="ts" setup>
import 'tailwindcss/tailwind.css';
</script>
<template>
<div class="text-white font-semibold p-4 bg-orange-400 rounded-lg">
WanIpCheck.ce
</div>
</template>
<style lang="postcss">
@tailwind base;
@tailwind components;
@tailwind utilities;
</style>

59
nuxt.config.ts Normal file
View File

@@ -0,0 +1,59 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({
ssr: false,
devServer: {
port: 4321,
},
modules: [
'@vueuse/nuxt',
'@pinia/nuxt',
'@nuxtjs/tailwindcss',
'nuxt-custom-elements',
],
components: [
{ path: '~/components/helpers', prefix: 'Helper' },
'~/components',
],
runtimeConfig: {
public: { // will be exposed to the client-side
callbackKey: '', // set in .env https://nuxt.com/docs/guide/going-further/runtime-config#environment-variables
}
},
customElements: {
entries: [
{
name: 'ConnectComponents',
tags: [
{
name: 'ConnectUserProfile',
path: '@/components/UserProfile.ce',
},
{
name: 'ConnectAuth',
path: '@/components/Auth.ce',
},
{
name: 'ConnectKeyActions',
path: '@/components/KeyActions.ce',
},
{
name: 'ConnectLaunchpad',
path: '@/components/Launchpad.ce',
},
{
name: 'ConnectPluginPromo',
path: '@/components/PluginPromo.ce',
},
{
name: 'ConnectWanIpCheck',
path: '@/components/WanIpCheck.ce',
},
{
name: 'ConnectCallbackHandler',
path: '@/components/CallbackHandler.ce',
},
],
},
],
},
});

10492
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

28
package.json Normal file
View File

@@ -0,0 +1,28 @@
{
"name": "connect-web-components",
"private": true,
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare",
"serve": "serve dist/nuxt-custom-elements/connect-components"
},
"devDependencies": {
"@nuxtjs/tailwindcss": "^6.7.0",
"@types/crypto-js": "^4.1.1",
"@types/node": "^18",
"@vueuse/core": "^10.1.2",
"@vueuse/nuxt": "^10.1.2",
"nuxt": "^3.5.1",
"nuxt-custom-elements": "^2.0.0-beta.12"
},
"dependencies": {
"@pinia/nuxt": "^0.4.11",
"crypto-js": "^4.1.1"
},
"overrides": {
"vue": "latest"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

3
server/tsconfig.json Normal file
View File

@@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

67
store/callback.ts Normal file
View File

@@ -0,0 +1,67 @@
import AES from 'crypto-js/aes';
import Utf8 from 'crypto-js/enc-utf8';
import { defineStore, createPinia, setActivePinia } from "pinia";
import { useServerStore } from './server';
/**
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
* @see https://github.com/vuejs/pinia/discussions/1085
*/
setActivePinia(createPinia());
export const useCallbackStore = defineStore('callback', () => {
const serverStore = useServerStore();
// store helpers
// const config = useRuntimeConfig(); // results in a nuxt error after web components are built
// const encryptKey = config.public.callbackKey;
const encryptKey = 'Uyv2o8e*FiQe8VeLekTqyX6Z*8XonB';
console.debug('[useCallbackStore]', { encryptKey });
// state
const encryptedMessage = ref('');
const decryptedData = ref();
// getters
// actions
const send = (url: string = 'https://unraid.ddev.site/init-purchase') => {
console.debug('[send]');
const stringifiedData = JSON.stringify({
...serverStore.server,
sender: window.location.href,
});
// @todo don't save to store
encryptedMessage.value = AES.encrypt(stringifiedData, encryptKey).toString();
// build and go to url
const destinationUrl = new URL(url);
console.debug('[send]', encryptedMessage.value, url);
destinationUrl.searchParams.set('data', encryptedMessage.value);
window.location.href = destinationUrl;
};
const watcher = () => {
console.debug('[watcher]');
const currentUrl = new URL(window.location);
console.debug('[watcher]', currentUrl);
const callbackValue = currentUrl.searchParams.get('data');
if (!callbackValue) {
console.debug('[watcher] no callback to handle');
return;
}
const decryptedMessage = AES.decrypt(callbackValue, encryptKey);
decryptedData.value = JSON.parse(decryptedMessage.toString(Utf8));
console.debug('[watcher]', decryptedMessage, decryptedData.value);
if (!decryptedData.value) return console.error('Callback Watcher: Data not present');
if (!decryptedData.value.action) return console.error('Callback Watcher: Required "action" not present');
switch (decryptedData.value.action) {
case 'install':
console.debug(`Installing key ${decryptedData.value.keyUrl}\n\nOEM: ${decryptedData.value.oem}\n\nSender: ${decryptedData.value.sender}`);
break;
case 'register':
console.debug('[Register action]');
break;
default:
console.error('Callback Watcher: Invalid "action"');
break;
}
};
return { send, watcher, decryptedData };
});

45
store/server.ts Normal file
View File

@@ -0,0 +1,45 @@
import { defineStore, createPinia, setActivePinia } from "pinia";
import type { Server, ServerState } from '~/types/server';
/**
* @see https://stackoverflow.com/questions/73476371/using-pinia-with-vue-js-web-components
* @see https://github.com/vuejs/pinia/discussions/1085
*/
setActivePinia(createPinia());
export const useServerStore = defineStore('server', () => {
const description = ref<string | undefined>();
const deviceCount = ref<number | undefined>();
const guid = ref<string | undefined>();
const keyTypeForPurchase = ref<string | undefined>();
const locale = ref<string | undefined>();
const name = ref<string | undefined>();
const site = ref<string | undefined>();
const state = ref<string | undefined>(); // @todo implement ServerState ENUM
const server = computed<Server>(() => {
return {
description: description.value,
deviceCount: deviceCount.value,
guid: guid.value,
keyTypeForPurchase: keyTypeForPurchase.value,
locale: locale.value,
name: name.value,
site: site.value,
state: state.value,
}
});
const setServer = (server: Server) => {
console.debug('[setServer]', server);
description.value = server?.description;
deviceCount.value = server?.deviceCount;
guid.value = server?.guid;
keyTypeForPurchase.value = server?.keyTypeForPurchase;
locale.value = server?.locale;
name.value = server?.name;
site.value = server?.site;
state.value = server?.state;
};
return { name, description, guid, keyTypeForPurchase, locale, deviceCount, site, server, setServer };
});

4
tsconfig.json Normal file
View File

@@ -0,0 +1,4 @@
{
// https://nuxt.com/docs/guide/concepts/typescript
"extends": "./.nuxt/tsconfig.json"
}

38
types/server.ts Normal file
View File

@@ -0,0 +1,38 @@
export enum ServerState {
BASIC = 'BASIC',
PLUS = 'PLUS',
PRO = 'PRO',
TRIAL = 'TRIAL',
EEXPIRED = 'EEXPIRED',
ENOKEYFILE = 'ENOKEYFILE',
EGUID = 'EGUID',
EGUID1 = 'EGUID1',
ETRIAL = 'ETRIAL',
ENOKEYFILE2 = 'ENOKEYFILE2',
ENOKEYFILE1 = 'ENOKEYFILE1',
ENOFLASH = 'ENOFLASH',
EBLACKLISTED = 'EBLACKLISTED',
EBLACKLISTED1 = 'EBLACKLISTED1',
EBLACKLISTED2 = 'EBLACKLISTED2',
ENOCONN = 'ENOCONN',
}
export interface Server {
// state?: ServerState;
state?: string;
name?: string;
description?: string;
deviceCount?: number;
flashProduct?: string;
flashVendor?: string;
guid?: string;
regGuid?: string;
site?: string;
wanFQDN?: string;
regGen?: number;
expireTime?: number;
license?: string;
keyfile?: string;
keyTypeForPurchase?: string;
locale?: string;
}