diff options
author | vnugent <public@vaughnnugent.com> | 2023-09-06 13:51:13 -0400 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2023-09-06 13:51:13 -0400 |
commit | cd8e865dad326f85ff2357ad90bbd6aa65dea68e (patch) | |
tree | 0d4a0bb8bafc4f807407e99c5e6bf4e1cb34217a /extension/src/entries/contentScript |
initial commit
Diffstat (limited to 'extension/src/entries/contentScript')
6 files changed, 356 insertions, 0 deletions
diff --git a/extension/src/entries/contentScript/nostr-shim.js b/extension/src/entries/contentScript/nostr-shim.js new file mode 100644 index 0000000..26b17a9 --- /dev/null +++ b/extension/src/entries/contentScript/nostr-shim.js @@ -0,0 +1,87 @@ +// Copyright (C) 2023 Vaughn Nugent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + + +import { runtime } from "webextension-polyfill"; +import { isEqual, isNil, isEmpty } from 'lodash' +import { sendMessage } from 'webext-bridge/content-script' +import { apiCall } from '@vnuge/vnlib.browser' +import { useManagment } from './../../bg-api/content-script' + +const { getSiteConfig } = useManagment() +const nip07Enabled = () => getSiteConfig().then(p => p.autoInject); + +//Setup listener for the content script to process nostr messages + +const ext = '@vnuge/nvault-extension' + +let _promptHandler = () => {} + +export const usePrompt = (callback) => { + //Register the callback + _promptHandler = async (event) => { + return new Promise((resolve, reject) => { + callback(event).then(resolve).catch(reject) + }) + } + return {} +} + +//Only inject the script if the site has autoInject enabled +nip07Enabled().then(enabled => { + console.log('Nip07 enabled:', enabled) + if (enabled) { + // inject the script that will provide window.nostr + let script = document.createElement('script'); + script.setAttribute('async', 'false'); + script.setAttribute('type', 'text/javascript'); + script.setAttribute('src', runtime.getURL('src/entries/nostr-provider.js')); + document.head.appendChild(script); + + //Only listen for messages if injection is enabled + window.addEventListener('message', async ({ source, data, origin }) => { + //Confirm the message format is correct + if (!isEqual(source, window) || isEmpty(data) || isNil(data.type)) { + return + } + //Confirm extension is for us + if (!isEqual(data.ext, ext)) { + return + } + + // pass on to background + var response; + await apiCall(async () => { + switch (data.type) { + case 'getPublicKey': + case 'signEvent': + //Check the public key against selected key + case 'getRelays': + case 'nip04.encrypt': + case 'nip04.decrypt': + //await propmt for user to allow the request + const allow = await _promptHandler({ ...data, origin }) + //send request to background + response = allow ? await sendMessage(data.type, { ...data.payload, origin }) : { error: 'User denied permission' } + break; + default: + throw new Error('Unknown nostr message type') + } + }) + // return response message, must have the same id as the request + window.postMessage({ ext, id: data.id, response }, origin); + }); + } +}) diff --git a/extension/src/entries/contentScript/primary/App.vue b/extension/src/entries/contentScript/primary/App.vue new file mode 100644 index 0000000..05dfac0 --- /dev/null +++ b/extension/src/entries/contentScript/primary/App.vue @@ -0,0 +1,17 @@ +<template> + <html> + <body id="injected-root"> + <notifications class="toaster" group="form" position="top-right" /> + <Prompt></Prompt> + </body> + </html> +</template> + +<script setup lang="ts"> +import { configureNotifier } from '@vnuge/vnlib.browser'; +import { notify } from "@kyvg/vue3-notification"; +import Prompt from './components/PromptPopup.vue' + +configureNotifier({ notify, close: notify.close }) + +</script>
\ No newline at end of file diff --git a/extension/src/entries/contentScript/primary/components/PromptPopup.vue b/extension/src/entries/contentScript/primary/components/PromptPopup.vue new file mode 100644 index 0000000..057f66a --- /dev/null +++ b/extension/src/entries/contentScript/primary/components/PromptPopup.vue @@ -0,0 +1,148 @@ +<template> + <div v-show="isOpen" id="nvault-ext-prompt"> + <div class="relative text-white" style="z-index:9147483647 !important" ref="prompt"> + <div class="fixed inset-0 left-0 flex justify-center w-full h-full p-4 bg-black/50"> + <div class="relative w-full max-w-md mx-auto mt-20 mb-auto"> + <div class="w-full p-4 border rounded-lg shadow-lg bg-dark-700 border-dark-400"> + <div v-if="loggedIn" class=""> + <h3 class="">Allow access</h3> + <div class="pl-1 text-sm"> + Identity: + </div> + <div class="p-2 mt-1 text-center border rounded border-dark-400 bg-dark-600"> + <div :class="[selectedKey?.UserName ? '' : 'text-red-500']"> + {{ selectedKey?.UserName ?? 'Select Identity' }} + </div> + </div> + <div class="mt-5 text-center"> + <span class="text-primary-500">{{ site }}</span> + would like to access to + <span class="text-yellow-500">{{ event.msg }}</span> + </div> + <div class="flex gap-2 mt-4"> + <div class=""> + <Popover class="relative"> + <PopoverButton class="rounded btn sm">View Raw</PopoverButton> + <PopoverPanel class="absolute z-10"> + <div class="min-w-[22rem] p-2 border rounded bg-dark-700 border-dark-400 shadow-md text-sm"> + <p class="pl-1"> + Event Data: + </p> + <div class="p-2 mt-1 text-left border rounded border-dark-400 bg-dark-600 overflow-y-auto max-h-[22rem]"> +<pre> +{{ evData }} +</pre> + </div> + </div> + </PopoverPanel> + </Popover> + </div> + <div class="ml-auto"> + <button :disabled="selectedKey?.Id == undefined" class="rounded btn primary sm" @click="allow">Allow</button> + </div> + <div> + <button class="rounded btn sm red" @click="close">Close</button> + </div> + </div> + </div> + <div v-else class=""> + <h3 class="">Log in!</h3> + <div class=""> + You must log in before you can allow access. + </div> + <div class="flex justify-end gap-2 mt-4"> + <div> + <button class="rounded btn sm red" @click="close">Close</button> + </div> + </div> + </div> + </div> + </div> + </div> + </div> + </div> +</template> + +<script setup lang="ts"> +import { ref } from 'vue' +import { usePrompt } from '~/entries/contentScript/nostr-shim' +import { computed } from '@vue/reactivity'; +import { onClickOutside } from '@vueuse/core'; +import { useStatus } from '~/bg-api/content-script.ts'; +import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue' +import { first } from 'lodash'; + +const { loggedIn, selectedKey } = useStatus() + +const prompt = ref(null) + +interface PopupEvent{ + type: string + msg: string + origin: string + data: any + allow: () => void + close: () => void +} + +const evStack = ref<PopupEvent[]>([]) +const isOpen = computed(() => evStack.value.length > 0) +const event = computed<PopupEvent | undefined>(() => first(evStack.value)); + +const site = computed(() => new URL(event.value?.origin || "https://example.com").host) +const evData = computed(() => JSON.stringify(event.value || {}, null, 2)) + + +const close = () => { + //Pop the first event off + const res = evStack.value.shift() + res?.close() +} +const allow = () => { + //Pop the first event off + const res = evStack.value.shift() + res?.allow() +} + +//Setup click outside +//onClickOutside(prompt, () => isOpen.value ? close() : null) + +//Listen for events +usePrompt(async (ev: PopupEvent) => { + + console.log('usePrompt', ev) + + switch(ev.type){ + case 'getPublicKey': + ev.msg = "your public key" + break; + case 'signEvent': + ev.msg = "sign an event" + break; + case 'getRelays': + ev.msg = "get your preferred relays" + break; + case 'nip04.encrypt': + ev.msg = "encrypt data" + break; + case 'nip04.decrypt': + ev.msg = "decrypt data" + break; + } + + return new Promise((resolve, reject) => { + evStack.value.push({ + ...ev, + allow: () => resolve(true), + close: () => resolve(false), + }) + }) +}) + + +</script> + +<style lang="scss"> + + +</style> diff --git a/extension/src/entries/contentScript/primary/main.js b/extension/src/entries/contentScript/primary/main.js new file mode 100644 index 0000000..24ef4ef --- /dev/null +++ b/extension/src/entries/contentScript/primary/main.js @@ -0,0 +1,41 @@ +// Copyright (C) 2023 Vaughn Nugent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + + +import { createApp } from "vue"; +import renderContent from "../renderContent"; +import App from "./App.vue"; +import Notification from '@kyvg/vue3-notification' +import '@fontsource/noto-sans-masaram-gondi' + +//We need inline styles to inject into the shadow dom +import tw from "~/assets/tailwind.scss?inline"; +import localStyle from './style.scss?inline' + +renderContent([], (appRoot, shadowRoot) => { + createApp(App) + .use(Notification) + .mount(appRoot); + + //Add tailwind styles just to the shadow dom element + const style = document.createElement('style') + style.innerHTML = tw.toString() + shadowRoot.appendChild(style) + + //Add local styles + const style2 = document.createElement('style') + style2.innerHTML = localStyle.toString() + shadowRoot.appendChild(style2) +});
\ No newline at end of file diff --git a/extension/src/entries/contentScript/primary/style.scss b/extension/src/entries/contentScript/primary/style.scss new file mode 100644 index 0000000..bcdbbfd --- /dev/null +++ b/extension/src/entries/contentScript/primary/style.scss @@ -0,0 +1,15 @@ + +#injected-root{ + + .toaster{ + @apply fixed top-10 right-2 z-[999999999] max-w-[250px]; + } + + .vue-notification-template.vue-notification.error{ + @apply bg-red-500 text-white px-4 py-2; + + .notification-title{ + + } + } +}
\ No newline at end of file diff --git a/extension/src/entries/contentScript/renderContent.js b/extension/src/entries/contentScript/renderContent.js new file mode 100644 index 0000000..84c5b9f --- /dev/null +++ b/extension/src/entries/contentScript/renderContent.js @@ -0,0 +1,48 @@ +// Copyright (C) 2023 Vaughn Nugent +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + + +import {runtime} from "webextension-polyfill"; + +export default async function renderContent( + cssPaths, + render = (_appRoot) => {} +) { + const appContainer = document.createElement("div"); + const shadowRoot = appContainer.attachShadow({ + mode: import.meta.env.DEV ? "open" : "closed", + }); + const appRoot = document.createElement("div"); + + if (import.meta.hot) { + const { addViteStyleTarget } = await import( + "@samrum/vite-plugin-web-extension/client" + ); + + await addViteStyleTarget(shadowRoot); + } else { + cssPaths.forEach((cssPath) => { + const styleEl = document.createElement("link"); + styleEl.setAttribute("rel", "stylesheet"); + styleEl.setAttribute("href", runtime.getURL(cssPath)); + shadowRoot.appendChild(styleEl); + }); + } + + shadowRoot.appendChild(appRoot); + document.body.appendChild(appContainer); + + render(appRoot, shadowRoot); +}
\ No newline at end of file |