diff options
Diffstat (limited to 'extension')
38 files changed, 911 insertions, 164 deletions
diff --git a/extension/src/entries/background/main.ts b/extension/src/entries/background/main.ts index 85e358a..a38eeff 100644 --- a/extension/src/entries/background/main.ts +++ b/extension/src/entries/background/main.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 @@ -25,6 +25,7 @@ import { useEventTagFilterApi, useInjectAllowList, useMfaConfigApi, + usePermissionApi } from "../../features"; import { useBackgroundFeatures } from "../../features/framework"; @@ -42,5 +43,6 @@ register([ usePkiApi, useEventTagFilterApi, useInjectAllowList, - useMfaConfigApi + useMfaConfigApi, + usePermissionApi ])
\ No newline at end of file diff --git a/extension/src/entries/background/script.js b/extension/src/entries/background/script.js index b0211d1..2e3167b 100644 --- a/extension/src/entries/background/script.js +++ b/extension/src/entries/background/script.js @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 diff --git a/extension/src/entries/background/serviceWorker.js b/extension/src/entries/background/serviceWorker.js index b0211d1..2e3167b 100644 --- a/extension/src/entries/background/serviceWorker.js +++ b/extension/src/entries/background/serviceWorker.js @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 diff --git a/extension/src/entries/contentScript/auth-popup.html b/extension/src/entries/contentScript/auth-popup.html new file mode 100644 index 0000000..42a2ce3 --- /dev/null +++ b/extension/src/entries/contentScript/auth-popup.html @@ -0,0 +1,10 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <title>Authorize</title> + <link rel="stylesheet" href="auth-popup.css"> + <script src="primary/main.js"></script> +</head> + <div id="app"></div> +</html>
\ 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 index 156dfb8..1f62877 100644 --- a/extension/src/entries/contentScript/primary/components/PromptPopup.vue +++ b/extension/src/entries/contentScript/primary/components/PromptPopup.vue @@ -1,12 +1,12 @@ <template> - <div v-show="isOpen" id="nvault-ext-prompt" :class="{'dark': darkMode }"> + <div v-show="event" id="nvault-ext-prompt" :class="{'dark': darkMode }"> <div class="fixed top-0 bottom-0 left-0 right-0 text-white" style="z-index:9147483647 !important" > <div class="fixed inset-0 left-0 w-full h-full bg-black/50" @click.self="close" /> - <div class="relative w-full max-w-[28rem] mx-auto mt-36 mb-auto" ref="prompt"> - <div class="w-full p-5 text-gray-800 bg-white border rounded-lg shadow-lg dark:bg-dark-900 dark:border-dark-500 dark:text-gray-200"> + <div v-if="store.permissions.isPopup" class="relative w-full md:max-w-[28rem] mx-auto md:mt-36 mb-auto" ref="prompt"> + <div class="w-full h-screen p-5 text-gray-800 bg-white border shadow-lg md:h-auto md:rounded dark:bg-dark-900 dark:border-dark-500 dark:text-gray-200"> <div v-if="loggedIn" class=""> <div class="flex flex-row justify-between"> @@ -44,7 +44,7 @@ <div class="py-3 text-sm text-center"> <span class="font-bold">{{ site }}</span> - would like to access to + would like access to <span class="font-bold">{{ event?.msg }}</span> </div> @@ -53,7 +53,12 @@ <button class="rounded btn sm" @click="close">Close</button> </div> <div> - <button :disabled="selectedKey?.Id == undefined" class="rounded btn sm" @click="allow">Allow</button> + <button :disabled="selectedKey?.Id == undefined" class="rounded amber btn sm" @click="allow(true)"> + Always Allow + </button> + </div> + <div> + <button :disabled="selectedKey?.Id == undefined" class="rounded btn sm" @click="allow(false)">Allow</button> </div> </div> </div> @@ -85,13 +90,16 @@ <script setup lang="ts"> import { ref } from 'vue' -import { debugLog } from '@vnuge/vnlib.browser'; import { storeToRefs } from 'pinia'; import { computed } from 'vue'; import { Popover, PopoverButton, PopoverPanel } from '@headlessui/vue' import { clone, first } from 'lodash'; -import { usePrompt, type UserPermissionRequest } from '../../util' import { useStore } from '../../../store'; +import { type PermissionRequest } from '../../../../features' + +interface PropmtMessage extends PermissionRequest{ + msg: string; +} const store = useStore() const { loggedIn, selectedKey, darkMode } = storeToRefs(store) @@ -99,38 +107,38 @@ const keyName = computed(() => selectedKey.value?.UserName) const prompt = ref(null) -interface PopupEvent extends UserPermissionRequest { - allow: () => void - close: () => void -} - -const evStack = ref<PopupEvent[]>([]) -const isOpen = computed(() => evStack.value.length > 0) -const event = computed<PopupEvent | undefined>(() => first(evStack.value)); +const event = computed<PropmtMessage | undefined>(() => { + //Use a the current windowpending if set + if(store.permissions.windowPending){ + return getPromptMessage(store.permissions.windowPending) + } + //Otherwise use the first pending event + const pending = first(store.permissions.pending) + return getPromptMessage(pending) +}); 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() + if(event.value){ + store.plugins.permission.deny(event.value.uuid); + } } -const allow = () => { - //Pop the first event off - const res = evStack.value.shift() - res?.allow() + +const allow = (addRule: boolean) => { + if (event.value) { + store.plugins.permission.allow(event.value.uuid, addRule); + } } //Listen for events -usePrompt((ev: UserPermissionRequest):Promise<boolean> => { +const getPromptMessage = (perms: PermissionRequest | undefined): PropmtMessage | undefined => { - ev = clone(ev) + if(!perms) return undefined - debugLog('[usePrompt] =>', ev) - - switch(ev.type){ + const ev = clone(perms) as PropmtMessage + switch(ev.requestType){ case 'getPublicKey': ev.msg = "your public key" break; @@ -150,14 +158,7 @@ usePrompt((ev: UserPermissionRequest):Promise<boolean> => { ev.msg = "unknown event" break; } - - return new Promise((resolve) => { - evStack.value.push({ - ...ev, - allow: () => resolve(true), - close: () => resolve(false), - }) - }) -}) + return ev +} </script> diff --git a/extension/src/entries/contentScript/primary/main.js b/extension/src/entries/contentScript/primary/main.js index bbe5edf..15fc2ec 100644 --- a/extension/src/entries/contentScript/primary/main.js +++ b/extension/src/entries/contentScript/primary/main.js @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 @@ -17,7 +17,7 @@ import { runtime } from "webextension-polyfill"; import { createApp } from "vue"; import { defer } from "lodash"; import { createPinia } from 'pinia'; -import { useBackgroundPiniaPlugin, identityPlugin, originPlugin } from '../../store' +import { useBackgroundPiniaPlugin, identityPlugin, originPlugin, permissionsPlugin } from '../../store' import { onLoad } from "../util"; import renderContent from "../renderContent"; import App from "./App.vue"; @@ -48,6 +48,7 @@ renderContent([], (appRoot, shadowRoot) => { .use(bgPlugins) .use(identityPlugin) .use(originPlugin) + .use(permissionsPlugin) //Add tailwind styles just to the shadow dom element const style = document.createElement('style') diff --git a/extension/src/entries/contentScript/renderContent.js b/extension/src/entries/contentScript/renderContent.js index ca45c4f..1a72d45 100644 --- a/extension/src/entries/contentScript/renderContent.js +++ b/extension/src/entries/contentScript/renderContent.js @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 diff --git a/extension/src/entries/contentScript/util.ts b/extension/src/entries/contentScript/util.ts index 09b515a..aecb7b2 100644 --- a/extension/src/entries/contentScript/util.ts +++ b/extension/src/entries/contentScript/util.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 @@ -18,36 +18,12 @@ import { apiCall } from '@vnuge/vnlib.browser' import { Store, storeToRefs } from 'pinia' import { useScriptTag, watchOnce } from "@vueuse/core" import { useStore } from '../store' - -export type PromptHandler = (request: UserPermissionRequest) => Promise<boolean> - -export interface UserPermissionRequest { - type: string - msg: string - origin: string - data: any -} - -const _promptHandler = (() => { - let _handler: PromptHandler | undefined = undefined; - return { - invoke(event:UserPermissionRequest){ - if (!_handler) { - throw new Error('No prompt handler set') - } - return _handler(event) - }, - set(handler: PromptHandler) { - _handler = handler - } - } -})() - +import { PrStatus } from '../../features' const registerWindowHandler = (store: Store, extName: string) => { const { selectedKey } = storeToRefs(store) - const { nostr } = store.plugins; + const { nostr, permission } = store.plugins; const onAsyncCall = async ({ source, data, origin } : MessageEvent<any>) => { @@ -56,9 +32,9 @@ const registerWindowHandler = (store: Store, extName: string) => { const requestPermission = async (cb: (...args: any) => Promise<any>) => { //await propmt for user to allow the request - const allow = await _promptHandler.invoke({ ...data, origin }) + const allow = await permission.requestAndWaitResult({ ...data, requestType: data.type, origin }) //send request to background - return allow ? await cb() : { error: 'User denied permission' } + return allow == PrStatus.Approved ? await cb() : { error: 'User denied permission' } } //Confirm the message format is correct @@ -133,9 +109,7 @@ const registerWindowHandler = (store: Store, extName: string) => { }); } -export const usePrompt = (callback: PromptHandler) => _promptHandler.set(callback); - -export const onLoad = async (extName: string, scriptUrl: string) => { +export const onLoad = (extName: string, scriptUrl: string) => { const store = useStore() const { isTabAllowed } = storeToRefs(store) diff --git a/extension/src/entries/nostr-provider.js b/extension/src/entries/nostr-provider.js index 9280b4d..1ec821d 100644 --- a/extension/src/entries/nostr-provider.js +++ b/extension/src/entries/nostr-provider.js @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 diff --git a/extension/src/entries/options/App.vue b/extension/src/entries/options/App.vue index d1da48e..3dbe94d 100644 --- a/extension/src/entries/options/App.vue +++ b/extension/src/entries/options/App.vue @@ -26,6 +26,11 @@ </Tab> <Tab v-slot="{ selected }"> <button class="tab-title" :class="{ selected }"> + Activity + </button> + </Tab> + <Tab v-slot="{ selected }"> + <button class="tab-title" :class="{ selected }"> Privacy </button> </Tab> @@ -64,15 +69,15 @@ <TabPanel class="mt-4"> <Identities :all-keys="allKeys" @edit-key="editKey"/> </TabPanel> - <TabPanel> - <Account/> - </TabPanel> - <TabPanel> - <Privacy/> - </TabPanel> - <TabPanel> - <SiteSettings/> - </TabPanel> + + <TabPanel> <Account/> </TabPanel> + + <TabPanel> <EventHistory/> </TabPanel> + + <TabPanel> <Privacy/> </TabPanel> + + <TabPanel> <SiteSettings/> </TabPanel> + <TabPanel> <div class="flex flex-col px-2 mt-4"> <div class="absolute mx-auto"> @@ -130,6 +135,7 @@ import { useStore } from "../store"; import Account from "./components/Account.vue"; import ConfirmPrompt from "../../components/ConfirmPrompt.vue"; import PasswordPrompt from "../../components/PasswordPrompt.vue"; +import EventHistory from "./components/EventHistory.vue"; //Configure the notifier to use the notification library @@ -143,7 +149,7 @@ const keyBuffer = ref<NostrPubKey>({} as NostrPubKey) const editKey = (key: NostrPubKey) =>{ //Goto hidden tab - selectedTab.value = 4 + selectedTab.value = 5 //Set selected key keyBuffer.value = { ...key } } diff --git a/extension/src/entries/options/components/AutoRules.vue b/extension/src/entries/options/components/AutoRules.vue new file mode 100644 index 0000000..16fddd3 --- /dev/null +++ b/extension/src/entries/options/components/AutoRules.vue @@ -0,0 +1,119 @@ +<template> + <div class=""> + <div class="flex flex-row justify-between mt-16"> + <div class="font-bold"> + Approval Rules + </div> + <div class="flex justify-center"> + <nav aria-label="Pagination"> + <ul class="inline-flex items-center space-x-1 text-sm rounded-md"> + <li> + <button @click="prev" class="page-btn"> + <fa-icon icon="chevron-left" class="w-4" /> + </button> + </li> + <li> + <span class="inline-flex items-center px-4 py-2 space-x-1 rounded-md"> + Page + <b class="mx-1">{{ currentPage }}</b> + of + <b class="ml-1">{{ pageCount }}</b> + </span> + </li> + <li> + <button @click="next" class="page-btn"> + <fa-icon icon="chevron-right" class="w-4" /> + </button> + </li> + </ul> + </nav> + </div> + </div> + <div class=""> + <table class="min-w-full text-sm divide-y-2 divide-gray-200 dark:divide-dark-500"> + <thead class="text-left bg-gray-50 dark:bg-dark-700"> + <tr> + <th class="p-2 font-medium whitespace-nowrap dark:text-white"> + Rule + </th> + <th class="p-2 font-medium whitespace-nowrap dark:text-white"> + Origin + </th> + <th class="p-2 font-medium whitespace-nowrap dark:text-white"> + Time + </th> + <th class="p-2"></th> + </tr> + </thead> + + <tbody class="divide-y divide-gray-200 dark:divide-dark-500"> + <tr v-for="rule in currentRulePage" :key="rule.timestamp" class=""> + <td class="p-2 t font-medium truncate max-w-[8rem] whitespace-nowrap "> + {{ rule.type }} + </td> + <td class="p-2 whitespace-nowrap"> + {{ rule.origin }} + </td> + <td class="p-2 whitespace-nowrap"> + {{ createShortDateAndTime(rule) }} + </td> + <td class="p-2 text-right whitespace-nowrap"> + <div class="button-group"> + <button class="rounded btn xs" @click="deleteRule(rule)"> + Revoke + </button> + </div> + </td> + </tr> + </tbody> + </table> + </div> + </div> +</template> + +<script setup lang="ts"> +import { computed } from 'vue'; +import { get, useOffsetPagination } from '@vueuse/core'; +import { } from '@headlessui/vue' +import { useStore } from '../../store'; +import { storeToRefs } from 'pinia'; +import { slice } from 'lodash'; +import { type AutoAllowRule } from '../../../features' + +const store = useStore() +const { } = storeToRefs(store) + +const rules = computed(() => store.permissions.rules) + +const { next, prev, currentPage, currentPageSize, pageCount } = useOffsetPagination({ + pageSize: 10, + total: computed(() => rules.value.length) +}) + +const currentRulePage = computed(() => { + const start = (get(currentPage) - 1) * get(currentPageSize) + const end = start + 10 + return slice(rules.value, start, end) +}) + +const deleteRule = (rule: AutoAllowRule) => { + store.plugins.permission.deleteRule(rule) +} + +const createShortDateAndTime = (request: { timestamp: number}) => { + const date = new Date(request.timestamp) + const hours = date.getHours() + const minutes = date.getMinutes() + const seconds = date.getSeconds() + const day = date.getDate() + const month = date.getMonth() + 1 + const year = date.getFullYear() + return `${month}/${day}/${year} ${hours}:${minutes}:${seconds}` +} + + +</script> + +<style lang="scss"> + +</style>
\ No newline at end of file diff --git a/extension/src/entries/options/components/EvHistoryTable.vue b/extension/src/entries/options/components/EvHistoryTable.vue new file mode 100644 index 0000000..6ea6cac --- /dev/null +++ b/extension/src/entries/options/components/EvHistoryTable.vue @@ -0,0 +1,84 @@ +<template> + <table class="min-w-full divide-y-2 divide-gray-200 dark:divide-dark-500"> + <thead class="text-left bg-gray-50 dark:bg-dark-700"> + <tr> + <th class="p-2 font-medium whitespace-nowrap dark:text-white"> + Type + </th> + <th class="p-2 font-medium whitespace-nowrap dark:text-white"> + Origin + </th> + <th class="p-2 font-medium whitespace-nowrap dark:text-white"> + Time + </th> + <th class="p-2"></th> + </tr> + </thead> + + <tbody class="divide-y divide-gray-200 dark:divide-dark-500"> + <tr v-for="req in requests" :key="req.uuid" class=""> + <td class="p-2 t font-medium truncate max-w-[8rem] whitespace-nowrap "> + {{ req.requestType }} + </td> + <td class="p-2 whitespace-nowrap"> + {{ req.origin }} + </td> + <td class="p-2 whitespace-nowrap"> + {{ createShortDateAndTime(req) }} + </td> + <td class="p-2 text-right whitespace-nowrap"> + <div v-if="!readonly" class="button-group"> + <button class="rounded btn xs" @click="approve(req)"> + <fa-icon icon="check" class="inline" /> + </button> + <button class="rounded btn red xs" @click="deny(req)"> + <fa-icon icon="trash-can" class="inline" /> + </button> + </div> + <div v-else class="text-sm font-bold"> + {{ statusToString(req.status) }} + </div> + </td> + </tr> + </tbody> + </table> +</template> + +<script setup lang="ts"> +import { toRefs } from 'vue'; +import { PermissionRequest, PrStatus } from '../../../features'; + +const emit = defineEmits(['deny', 'approve']) +const props = defineProps<{ + requests: PermissionRequest[], + readonly: boolean +}>() + +const { requests, readonly } = toRefs(props) + +const createShortDateAndTime = (request: PermissionRequest) => { + const date = new Date(request.timestamp) + const hours = date.getHours() + const minutes = date.getMinutes() + const seconds = date.getSeconds() + const day = date.getDate() + const month = date.getMonth() + 1 + const year = date.getFullYear() + return `${month}/${day}/${year} ${hours}:${minutes}:${seconds}` +} + +const deny = (request: PermissionRequest) => emit('deny', request) +const approve = (request: PermissionRequest) => emit('approve', request) + +const statusToString = (status: PrStatus) => { + switch(status) { + case PrStatus.Approved: + return 'Approved' + case PrStatus.Denied: + return 'Denied' + case PrStatus.Pending: + return 'Pending' + } +} + +</script>
\ No newline at end of file diff --git a/extension/src/entries/options/components/EventHistory.vue b/extension/src/entries/options/components/EventHistory.vue new file mode 100644 index 0000000..0711ae6 --- /dev/null +++ b/extension/src/entries/options/components/EventHistory.vue @@ -0,0 +1,132 @@ +<template> + <div id="ev-history" class="flex flex-col w-full mt-4 sm:px-2"> + <form @submit.prevent=""> + <div class="w-full max-w-xl mx-auto"> + <h3 class="text-center"> + Permissions + </h3> + + <div class="flex flex-row justify-between mt-4"> + <div class="font-bold"> + Pending + </div> + <div class="flex justify-center"> + </div> + </div> + + <div class="my-6 "> + <EvHistoryTable :readonly="false" :requests="pending" @deny="deny" @approve="approve" /> + </div> + + <AutoRules /> + + <div class="flex flex-row justify-between mt-16"> + <div class="font-bold"> + History + </div> + <div class="flex justify-center"> + <nav aria-label="Pagination"> + <ul class="inline-flex items-center space-x-1 text-sm rounded-md"> + <li> + <button @click="prev" class="page-btn"> + <fa-icon icon="chevron-left" class="w-4" /> + </button> + </li> + <li> + <span class="inline-flex items-center px-4 py-2 space-x-1 rounded-md"> + Page + <b class="mx-1">{{ currentPage }}</b> + of + <b class="ml-1">{{ pageCount }}</b> + </span> + </li> + <li> + <button @click="next" class="page-btn"> + <fa-icon icon="chevron-right" class="w-4" /> + </button> + </li> + </ul> + </nav> + </div> + </div> + + <div class="mt-1"> + <EvHistoryTable :readonly="true" :requests="evHistoryCurrentPage" @deny="deny" @approve="approve" /> + </div> + + <div class="mt-4 ml-auto w-fit"> + <button class="rounded btn sm red" @click="clearHistory"> + Delete History + </button> + </div> + </div> + </form> + </div> +</template> + +<script setup lang="ts"> +import { useConfirm } from '@vnuge/vnlib.browser'; +import { computed } from 'vue'; +import { get, useOffsetPagination } from '@vueuse/core'; +import { } from '@headlessui/vue' +import { useStore } from '../../store'; +import { PermissionRequest, PrStatus } from '../../../features'; +import EvHistoryTable from './EvHistoryTable.vue'; +import { filter, slice } from 'lodash'; +import AutoRules from './AutoRules.vue'; + +const store = useStore() +const { reveal } = useConfirm() + +const pending = computed(() => store.permissions.pending) +const notPending = computed(() => filter(store.permissions.all, r => r.status !== PrStatus.Pending)) + +const deny = (request: PermissionRequest) => { + if(request.status !== PrStatus.Pending) return + //push deny to store + store.plugins.permission.deny(request.uuid) +} + +const approve = (request: PermissionRequest) => { + if(request.status !== PrStatus.Pending) return + //push allow to store + store.plugins.permission.allow(request.uuid, false) +} + +const { next, prev, currentPage, currentPageSize, pageCount } = useOffsetPagination({ + pageSize: 10, + total: computed(() => notPending.value.length) +}) + +const evHistoryCurrentPage = computed(() => { + const start = (get(currentPage) - 1) * get(currentPageSize) + const end = start + 10 + return slice(notPending.value, start, end) +}) + +const clearHistory = async () => { + const { isCanceled } = await reveal({ + title: 'Clear History', + text: 'Are you sure you want to clear your event history?', + }) + + if(isCanceled) return + + //Clear all history + store.plugins.permission.clearRequests() +} + +</script> + +<style lang="scss"> +#ev-history{ + button.page-btn{ + @apply inline-flex items-center px-2 py-2 space-x-2 font-medium rounded-full; + @apply bg-white border border-gray-300 rounded-full hover:bg-gray-50 dark:bg-dark-600 dark:hover:bg-dark-500 dark:border-dark-300; + } + + form tr { + @apply sm:text-sm text-xs dark:text-gray-400 text-gray-600; + } +} +</style>
\ No newline at end of file diff --git a/extension/src/entries/options/main.js b/extension/src/entries/options/main.js index 7747735..3dd01cb 100644 --- a/extension/src/entries/options/main.js +++ b/extension/src/entries/options/main.js @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 @@ -22,12 +22,12 @@ import Notifications from "@kyvg/vue3-notification"; /* FONT AWESOME CONFIG */ import { library } from '@fortawesome/fontawesome-svg-core' -import { faChevronLeft, faChevronRight, faCopy, faDownload, faEdit, faExternalLinkAlt, faLock, faLockOpen, faMinusCircle, faMoon, faPlus, faRefresh, faSun, faTrash, faTrashCan } from '@fortawesome/free-solid-svg-icons' +import { faCheck, faChevronLeft, faChevronRight, faCopy, faDownload, faEdit, faExternalLinkAlt, faLock, faLockOpen, faMinusCircle, faMoon, faPlus, faRefresh, faSun, faTrash, faTrashCan } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' import { createPinia } from "pinia"; -import { identityPlugin, mfaConfigPlugin, originPlugin, useBackgroundPiniaPlugin } from "../store"; +import { identityPlugin, mfaConfigPlugin, originPlugin, permissionsPlugin, useBackgroundPiniaPlugin } from "../store"; -library.add(faCopy, faEdit, faChevronLeft, faMoon, faSun, faLock, faLockOpen, faExternalLinkAlt, faTrash, faDownload, faChevronRight, faPlus, faRefresh, faTrashCan, faMinusCircle) +library.add(faCopy, faEdit, faChevronLeft, faMoon, faSun, faLock, faLockOpen, faExternalLinkAlt, faTrash, faDownload, faChevronRight, faPlus, faRefresh, faTrashCan, faMinusCircle ,faTrashCan, faCheck) //Create the background feature wiring const bgPlugins = useBackgroundPiniaPlugin('options') @@ -37,6 +37,7 @@ const pinia = createPinia() .use(identityPlugin) //Add the identity plugin .use(originPlugin) //Add the origin plugin .use(mfaConfigPlugin) //Add the mfa config plugin + .use(permissionsPlugin) createApp(App) .use(Notifications) diff --git a/extension/src/entries/popup/Components/PageContent.vue b/extension/src/entries/popup/Components/PageContent.vue index 8a48840..37e119d 100644 --- a/extension/src/entries/popup/Components/PageContent.vue +++ b/extension/src/entries/popup/Components/PageContent.vue @@ -44,7 +44,7 @@ </div> <div class=""> - <label class="mb-0.5 text-sm dark:text-dark-100"> + <label class="mb-0.5 text-sm"> Identity </label> <IdentitySelection></IdentitySelection> @@ -64,12 +64,12 @@ </div> <div class="mt-4"> - <label class="block mb-1 text-xs text-left dark:text-dark-100" > + <label class="block mb-1 text-xs text-left " > Current origin </label> <div v-if="isOriginProtectionOn" class="flex flex-row w-full gap-2"> - <input :value="currentOrigin" class="flex-1 p-1 mx-0 text-sm input" readonly/> + <input :value="currentOrigin" class="flex-1 p-1 mx-0 text-sm input dark:text-dark-100" readonly/> <button v-if="isTabAllowed" class="btn xs" @click="store.dissallowOrigin()"> <fa-icon icon="minus" /> @@ -79,10 +79,20 @@ </button> </div> - <div v-else class="text-xs text-center"> + <div v-else class="text-xs text-center dark:text-dark-100"> <span class="">Tracking protection disabled</span> </div> </div> + <div class="mt-4"> + <label class="block mb-1 text-xs text-left " > + Permissions + </label> + <ul class="flex flex-row flex-wrap gap-2 dark:text-dark-100"> + <li v-for="rule in ruleTypes" :key="rule" class="text-xs"> + {{ rule }} + </li> + </ul> + </div> </div> </div> @@ -102,6 +112,7 @@ import { notify } from "@kyvg/vue3-notification"; import { runtime } from "webextension-polyfill"; import Login from "./Login.vue"; import IdentitySelection from "./IdentitySelection.vue"; +import { map } from "lodash"; configureNotifier({notify, close:notify.close}) @@ -111,6 +122,8 @@ const { copy, copied } = useClipboard() const pubKey = computed(() => selectedKey!.value?.PublicKey) +const ruleTypes = computed<string[]>(() => map(store.permissions.rulesForCurrentOrigin, 'type')) + const openOptions = () => runtime.openOptionsPage(); const toggleDark = () => store.toggleDarkMode() diff --git a/extension/src/entries/popup/main.js b/extension/src/entries/popup/main.js index c8e9ef8..6642262 100644 --- a/extension/src/entries/popup/main.js +++ b/extension/src/entries/popup/main.js @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 @@ -15,7 +15,7 @@ import { createApp } from "vue"; import { createPinia } from "pinia"; -import { identityPlugin, originPlugin, useBackgroundPiniaPlugin } from '../store' +import { identityPlugin, originPlugin, permissionsPlugin, useBackgroundPiniaPlugin } from '../store' import App from "./App.vue"; import Notifications from "@kyvg/vue3-notification"; import '@fontsource/noto-sans-masaram-gondi' @@ -35,6 +35,7 @@ const pinia = createPinia() .use(bgPlugin) //Add the background pinia plugin .use(identityPlugin) .use(originPlugin) + .use(permissionsPlugin) createApp(App) .use(Notifications) diff --git a/extension/src/entries/store/features.ts b/extension/src/entries/store/features.ts index 9f9a4db..ad83e16 100644 --- a/extension/src/entries/store/features.ts +++ b/extension/src/entries/store/features.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 @@ -30,7 +30,8 @@ import { useEventTagFilterApi, useInjectAllowList, onWatchableChange, - useMfaConfigApi + useMfaConfigApi, + usePermissionApi } from "../../features" import { ChannelContext } from '../../messaging' @@ -59,7 +60,8 @@ const usePlugins = (context: ChannelContext) => { pki: use(usePkiApi), tagFilter: use(useEventTagFilterApi), allowedOrigins: use(useInjectAllowList), - mfaConfig: use(useMfaConfigApi) + mfaConfig: use(useMfaConfigApi), + permission: use(usePermissionApi) } } diff --git a/extension/src/entries/store/identity.ts b/extension/src/entries/store/identity.ts index 5bbc67a..ade7c94 100644 --- a/extension/src/entries/store/identity.ts +++ b/extension/src/entries/store/identity.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 diff --git a/extension/src/entries/store/index.ts b/extension/src/entries/store/index.ts index e3eef2f..8be57ff 100644 --- a/extension/src/entries/store/index.ts +++ b/extension/src/entries/store/index.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 @@ -24,6 +24,7 @@ export * from './allowedOrigins' export * from './features' export * from './identity' export * from './mfaconfig' +export * from './permissions' export const useStore = defineStore({ id: 'main', diff --git a/extension/src/entries/store/mfaconfig.ts b/extension/src/entries/store/mfaconfig.ts index 6a5116d..bd8ef83 100644 --- a/extension/src/entries/store/mfaconfig.ts +++ b/extension/src/entries/store/mfaconfig.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 diff --git a/extension/src/entries/store/permissions.ts b/extension/src/entries/store/permissions.ts new file mode 100644 index 0000000..6b011de --- /dev/null +++ b/extension/src/entries/store/permissions.ts @@ -0,0 +1,105 @@ +// Copyright (C) 2024 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 'pinia' +import { filter, find } from 'lodash' +import { PiniaPluginContext, storeToRefs } from 'pinia' +import { computed, shallowRef } from 'vue' + +import { + PrStatus, + onWatchableChange, + PermissionRequest +} from "../../features" +import { get } from '@vueuse/core' +import { AutoAllowRule } from '../../features/permissions' + +export interface PermissionApi { + readonly pending: PermissionRequest[] + readonly all: PermissionRequest[] + readonly windowPending: PermissionRequest | undefined + readonly isPopup: boolean + readonly rules: AutoAllowRule[], + readonly rulesForCurrentOrigin: AutoAllowRule[] +} + +declare module 'pinia' { + export interface PiniaCustomProperties { + permissions: PermissionApi + } +} + +export const permissionsPlugin = ({ store }: PiniaPluginContext) => { + + const { permission } = store.plugins + + const { currentOrigin } = storeToRefs(store) + + const all = shallowRef<PermissionRequest[]>([]) + const activeRequests = computed(() => filter(all.value, r => r.status == PrStatus.Pending)) + const windowPending = shallowRef<PermissionRequest | undefined>() + const rules = shallowRef<AutoAllowRule[]>([]) + + const rulesForCurrentOrigin = computed(() => filter(rules.value, r => r.origin == get(currentOrigin))) + + const closeIfPopup = () => { + const windowQueryArgs = new URLSearchParams(window.location.search) + if (windowQueryArgs.has("closeable")) { + window.close() + } + } + + const getPendingWindowRequest = () => { + const uuid = getWindowUuid() + const req = get(activeRequests) + return find(req, r => r.uuid == uuid) + } + + const getWindowUuid = () => { + const queryArgs = new URLSearchParams(window.location.search) + return queryArgs.get("uuid") + } + + //watch for status changes + onWatchableChange(permission, async () => { + //get latest requests and current ruleset + all.value = await permission.getRequests() + rules.value = await permission.getRules() + + //update window pending request + windowPending.value = getPendingWindowRequest() + + //if there are no more pending requests, close the popup + if (activeRequests.value.length == 0) { + closeIfPopup() + } + + //If the window's request is no longer pending, close the popup + if (getWindowUuid() && !get(windowPending)){ + closeIfPopup() + } + + }, { immediate: true }) + + return { + permissions:{ + all, + rules, + rulesForCurrentOrigin, + pending: activeRequests, + isPopup: getWindowUuid() !== null, + } + } +}
\ No newline at end of file diff --git a/extension/src/features/framework/index.ts b/extension/src/features/framework/index.ts index 2d9cad5..b545335 100644 --- a/extension/src/features/framework/index.ts +++ b/extension/src/features/framework/index.ts @@ -1,5 +1,5 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 diff --git a/extension/src/features/history.ts b/extension/src/features/history.ts index ff7c267..82f31c4 100644 --- a/extension/src/features/history.ts +++ b/extension/src/features/history.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 diff --git a/extension/src/features/identity-api.ts b/extension/src/features/identity-api.ts index d909162..0b8973d 100644 --- a/extension/src/features/identity-api.ts +++ b/extension/src/features/identity-api.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 diff --git a/extension/src/features/index.ts b/extension/src/features/index.ts index f05de9b..0a8e182 100644 --- a/extension/src/features/index.ts +++ b/extension/src/features/index.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 @@ -21,6 +21,8 @@ export type { PkiPubKey, EcKeyParams, LocalPkiApi as PkiApi } from './pki-api' export type { NostrApi } from './nostr-api' export type { UserApi } from './auth-api' export type { IdentityApi } from './identity-api' +export type { MfaUpdateResult } from './mfa-api' +export type { PermissionRequest, PrType, AutoAllowRule } from './permissions' export { useBackgroundFeatures, useForegoundFeatures } from './framework' export { useLocalPki, usePkiApi } from './pki-api' @@ -32,4 +34,5 @@ export { useHistoryApi } from './history' export { useEventTagFilterApi } from './tagfilter-api' export { useInjectAllowList } from './nip07allow-api' export { onWatchableChange } from './util' -export { useMfaConfigApi, type MfaUpdateResult } from './mfa-api'
\ No newline at end of file +export { useMfaConfigApi } from './mfa-api' +export { usePermissionApi, PrStatus } from './permissions'
\ No newline at end of file diff --git a/extension/src/features/mfa-api.ts b/extension/src/features/mfa-api.ts index fc6d51a..85ff49e 100644 --- a/extension/src/features/mfa-api.ts +++ b/extension/src/features/mfa-api.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 diff --git a/extension/src/features/nip07allow-api.ts b/extension/src/features/nip07allow-api.ts index 8c08d5e..4787437 100644 --- a/extension/src/features/nip07allow-api.ts +++ b/extension/src/features/nip07allow-api.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 diff --git a/extension/src/features/nostr-api.ts b/extension/src/features/nostr-api.ts index 046ccea..4aee660 100644 --- a/extension/src/features/nostr-api.ts +++ b/extension/src/features/nostr-api.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 diff --git a/extension/src/features/permissions.ts b/extension/src/features/permissions.ts index c06257b..5473962 100644 --- a/extension/src/features/permissions.ts +++ b/extension/src/features/permissions.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 @@ -13,68 +13,353 @@ // 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 { useStorageAsync } from "@vueuse/core"; -import { find, isEmpty, merge, remove } from "lodash"; -import { storage } from "webextension-polyfill"; -import { useAuthApi } from "./auth-api"; -import { useSettingsApi } from "./settings"; +import { Mutable, get, set, toRefs } from "@vueuse/core"; +import { Ref } from "vue"; +import { defaultTo, defaults, defer, filter, find, forEach, isEqual, isNil } from "lodash"; +import { nanoid } from "nanoid"; +import { useSession } from "@vnuge/vnlib.browser"; +import { type FeatureApi, type BgRuntime, type IFeatureExport, exportForegroundApi, optionsOnly } from "./framework"; +import { waitForChangeFn, waitOne } from "./util"; +import { windows, runtime, Windows, tabs } from "webextension-polyfill"; +import type { TotpUpdateMessage, Watchable } from "./types"; +import type { AppSettings } from "./settings"; -const permissions = useStorageAsync("permissions", [], storage.local); +export interface AutoAllowRule{ + origin: string + type: string + readonly timestamp: number +} -export const setAutoAllow = async (origin, mKind, keyId) => { - permissions.value.push({ origin, mKind, keyId, }) +export type PrType = string +export enum PrStatus{ + Pending, + Approved, + Denied } -/** - * Determines if the user has previously allowed the origin to use the key to sign events - * of the desired kind - * @param {*} origin The site origin requesting the permission - * @param {*} mKind The kind of message being signed - * @param {*} keyId The keyId of the key being used to sign the message - */ -export const isAutoAllow = async (origin, mKind, keyId) => { - return find(permissions.value, p => p.origin === origin && p.mKind === mKind && p.keyId === keyId) !== undefined +export interface PermissionRequest{ + readonly uuid: string + readonly origin: string + readonly requestType: PrType + readonly timestamp: number + readonly status: PrStatus } -/** - * Removes the auto allow permission from the list - * @param {*} origin The site origin requesting the permission - * @param {*} mKind The message kind being signed - * @param {*} keyId The keyId of the key being used to sign the message - */ -export const removeAutoAllow = async (origin, mKind, keyId) => { - //Remove the permission from the list - remove(permissions.value, p => p.origin === origin && p.mKind === mKind && p.keyId === keyId); +export type MfaUpdateResult = TotpUpdateMessage + +export interface PermissionApi extends FeatureApi, Watchable { + getRequests(): Promise<PermissionRequest[]> + allow(requestId: string, addRule: boolean): Promise<void> + deny(requestId: string): Promise<void> + clearRequests(): Promise<void> + requestAndWaitResult(request: Partial<PermissionRequest>): Promise<PrStatus> + + getRules(): Promise<AutoAllowRule[]> + deleteRule(rule: AutoAllowRule): Promise<void> + addRule(rule: AutoAllowRule): Promise<void> } +interface PermissionSlot { + requests: PermissionRequest[] +} -export const useSitePermissions = (() => { +interface RuleSlot{ + rules: AutoAllowRule[] +} - const { apiCall, handleProtectedMessage } = useAuthApi(); - const { currentConfig } = useSettingsApi(); +const useRuleSet = (slot: Ref<RuleSlot>) => { + defaults(slot.value, { rules: [] }) + const { rules } = toRefs(slot) + + return{ + isAllowed: (request: PermissionRequest): boolean => { + //find existing rule + const rule = find(get(rules), r => isEqual(r.origin, request.origin) && isEqual(r.type, request.requestType)) + return !isNil(rule) + }, + addRule: (rule: Partial<AutoAllowRule>) => { + const current = defaultTo(get(rules), []) + + //see if rule aready exists + if (find(current, r => isEqual(r.origin, rule.origin) && isEqual(r.type, rule.type))) { + return; + } + + //add rule to head of store + current.unshift({ + ...rule, + timestamp: Date.now() + } as AutoAllowRule) + + set(rules, current) + }, + deleteRule (rule: AutoAllowRule) { + //Filter all non matching rules + const wo = filter(get(rules), r => !(isEqual(r.origin, rule.origin) && isEqual(r.type, rule.type))) + set(rules, wo) + }, + getRules:(): AutoAllowRule[] =>get(rules) + } +} + +const usePermissions = (slot: Ref<PermissionSlot>, rules: ReturnType<typeof useRuleSet>) => { + + const permPopupUrl = runtime.getURL("src/entries/contentScript/auth-popup.html") + + defaults(slot.value, { rules: [] }) + const { requests } = toRefs(slot) + + const drawWindow = async ({ uuid }: Partial<PermissionRequest>): Promise<Windows.CreateCreateDataType> => { + const current = await windows.getCurrent() + + const minWidth = 350 + const minHeight = 180 + + const maxWidth = 500 + const maxHeight = 250 - const getCurrentPerms = async () => { - const { permissions } = await storage.local.get('permissions'); + const width = Math.min(Math.max(current.width! - 100, minWidth), maxWidth) + const height = Math.min(Math.max(current.height! - 100, minHeight), maxHeight) - //Store a default config if none exists - if (isEmpty(permissions)) { - await storage.local.set({ siteConfig: defaultConfig }); + //draw half way across screen minus half its width + const left = current.left! + (current.width! / 2) - (width / 2) + + return { + url: `${permPopupUrl}?uuid=${uuid}&closeable`, + type: "popup", + height: height, + width: width, + focused: true, + allowScriptsToClose: true, + top: 100, + //try to center popup + left: left, } + } + + const activePopups = new Map<number, PermissionRequest>() - //Merge the default config with the site config - return merge(defaultConfig, siteConfig) + const getRequest = (requestId: string): PermissionRequest | undefined => { + return find(get(requests), r => r.uuid === requestId) } - const onIsSiteEnabled = handleProtectedMessage(async (data) => { + const updateRequest = (request: PermissionRequest, addRule: boolean) => { + const current = get(requests) - }) + const index = current.findIndex(r => r.uuid === request.uuid) + if (index === -1) { + throw new Error("Request not found") + } + + //Set request state + current[index] = request + + //Update storage + set(requests, current) - return () => { + //Add rule if needed + if (addRule) { + rules.addRule({ origin: request.origin, type: request.requestType }) + } + } + + const initNewRequest = (request: Partial<PermissionRequest>): PermissionRequest => { return { - onCreateIdentity, - onUpdateIdentity + ...request, + uuid: nanoid(), + status: PrStatus.Pending, + timestamp: Date.now() + } as PermissionRequest + } + + //Listen for popup close to cleanup request + windows.onRemoved.addListener(async (id) => { + const req = activePopups.get(id) + if (req && req.status === PrStatus.Pending) { + //set denied + (req as Mutable<PermissionRequest>).status = PrStatus.Denied + //popup closed, set to denied + updateRequest(req, false) } + }) + + //Watch for changes to the current tab + tabs.onRemoved.addListener(async (tabId) => { + const tab = await tabs.get(tabId) + const { origin } = new URL(tab.url!) + + //Find ally pending requests for the origin + const pending = filter(requests.value, r => r.status == PrStatus.Pending && r.origin == origin) + + //update all pending requests to denied + forEach(pending, r => updateRequest(r, false)) + }) + + return{ + getRequest, + + async showPermsWindow (request: PermissionRequest): Promise<void> { + const windowsArgs = await drawWindow(request) + const { id } = await windows.create(windowsArgs) + activePopups.set(id!, request) + }, + + pushRequest (request: Partial<PermissionRequest>, showPopup: boolean): PermissionRequest { + //Create new request + const req = initNewRequest(request) + + //See if allowed + if(rules.isAllowed(req)){ + //Set to approved + (req as Mutable<PermissionRequest>).status = PrStatus.Approved + //No need to show popup + showPopup = false + } + + const current = get(requests) + current.unshift(req) + set(requests, current) + + //Show popup if needed + if (showPopup) { + this.showPermsWindow(req) + } + + return req + }, + + allow (requestId: string, addRule: boolean): void { + const request = getRequest(requestId) + if(!request){ + throw new Error("Request not found") + } + //set approved + (request as Mutable<PermissionRequest>).status = PrStatus.Approved + //update request + updateRequest(request, addRule) + }, + + deny(requestId: string): void { + const request = getRequest(requestId) + if (!request) { + throw new Error("Request not found") + } + //set denied + (request as Mutable<PermissionRequest>).status = PrStatus.Denied + //update request + updateRequest(request, false) + }, + + clearAll: () => { + //notify pending requests + forEach(filter(requests.value, r => r.status == PrStatus.Pending), r => { + //set denied + (r as Mutable<PermissionRequest>).status = PrStatus.Denied + //update request + updateRequest(r, false) + }) + + //Then defer clear + defer(() => set(requests, [])) + }, + getAll: () => get(requests) } +} + +export const usePermissionApi = (): IFeatureExport<AppSettings, PermissionApi> => { + + return { + background: ({ state }: BgRuntime<AppSettings>): PermissionApi => { + const { loggedIn } = useSession(); + const { currentConfig } = state + + //Open storage slot for permissions + const reqStore = state.useStorageSlot<PermissionSlot>("permissions", { requests: [] }) + const ruleStore = state.useStorageSlot<RuleSlot>("rules", { rules: [] }) + + //init rules api + const ruleSet = useRuleSet(ruleStore) + const permissions = usePermissions(reqStore, ruleSet) -})()
\ No newline at end of file + return { + waitForChange: waitForChangeFn([currentConfig, loggedIn, reqStore, ruleStore]), + + getRequests: () => Promise.resolve(permissions.getAll()), + + deny(requestId: string) { + permissions.deny(requestId) + return Promise.resolve() + }, + + allow(requestId: string, addRule: boolean) { + permissions.allow(requestId, addRule) + return Promise.resolve() + }, + + clearRequests: optionsOnly(() => { + //clear stored requests + permissions.clearAll() + return Promise.resolve() + }), + + async requestAndWaitResult(request: Partial<PermissionRequest>) { + //push request + const req = permissions.pushRequest(request, true) + + //See if pending + if(req.status !== PrStatus.Pending){ + //completed already, return status + return req.status + } + + do { + + //wait for a change + await waitOne([reqStore]) + + //check if request was approved + const status = permissions.getRequest(req.uuid); + + switch(status?.status){ + case PrStatus.Approved: + return PrStatus.Approved; + case PrStatus.Denied: + return PrStatus.Denied; + case PrStatus.Pending: + //continue waiting + break; + default: + throw new Error("Request was rejected or deleted") + } + + //continue to wait for pending status + } while(true) + }, + + getRules: () => Promise.resolve(ruleSet.getRules()), + + deleteRule: optionsOnly((rule: AutoAllowRule) => { + ruleSet.deleteRule(rule) + return Promise.resolve() + }), + + addRule: optionsOnly((rule: AutoAllowRule) => { + ruleSet.addRule(rule) + return Promise.resolve() + }), + } + }, + foreground: exportForegroundApi<PermissionApi>([ + 'waitForChange', + 'getRequests', + 'clearRequests', + 'requestAndWaitResult', + 'getRules', + 'deleteRule', + 'addRule', + 'allow', + 'deny' + ]), + } +}
\ No newline at end of file diff --git a/extension/src/features/pki-api.ts b/extension/src/features/pki-api.ts index 41fbd48..07fb3df 100644 --- a/extension/src/features/pki-api.ts +++ b/extension/src/features/pki-api.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 diff --git a/extension/src/features/server-api/endpoints.ts b/extension/src/features/server-api/endpoints.ts index 9c73866..5fa1bf4 100644 --- a/extension/src/features/server-api/endpoints.ts +++ b/extension/src/features/server-api/endpoints.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 diff --git a/extension/src/features/server-api/index.ts b/extension/src/features/server-api/index.ts index cd67242..b9524ed 100644 --- a/extension/src/features/server-api/index.ts +++ b/extension/src/features/server-api/index.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 diff --git a/extension/src/features/settings.ts b/extension/src/features/settings.ts index 9a3c32d..ca714a5 100644 --- a/extension/src/features/settings.ts +++ b/extension/src/features/settings.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 diff --git a/extension/src/features/tagfilter-api.ts b/extension/src/features/tagfilter-api.ts index f5f1b6c..22369d0 100644 --- a/extension/src/features/tagfilter-api.ts +++ b/extension/src/features/tagfilter-api.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 diff --git a/extension/src/features/types.ts b/extension/src/features/types.ts index 92cf6cf..fe59011 100644 --- a/extension/src/features/types.ts +++ b/extension/src/features/types.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 diff --git a/extension/src/features/util.ts b/extension/src/features/util.ts index e9147bc..6ec8f15 100644 --- a/extension/src/features/util.ts +++ b/extension/src/features/util.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 @@ -20,15 +20,19 @@ import { type MaybeRefOrGetter, type WatchSource, isProxy, toRaw } from "vue"; import type { Watchable } from "./types"; export const waitForChange = <T extends Readonly<WatchSource<unknown>[]>>(source: [...T]):Promise<void> => { - return new Promise((resolve) => watchOnce(source, () => resolve())) + return new Promise((resolve) => watchOnce<any>(source, () => resolve(), { deep: true })) } export const waitForChangeFn = <T extends Readonly<WatchSource<unknown>[]>>(source: [...T]) => { return (): Promise<void> => { - return new Promise((resolve) => watchOnce(source, () => resolve())) + return new Promise((resolve) => watchOnce<any>(source, () => resolve(), {deep: true})) } } +export const waitOne = <T extends Readonly<WatchSource<unknown>[]>>(source: [...T]): Promise<void> => { + return new Promise((resolve) => watchOnce<any>(source, () => resolve(), { deep: true })) +} + export const useStorage = <T>(storage: any & chrome.storage.StorageArea, key: string, initialValue: MaybeRefOrGetter<T>): RemovableRef<T> => { const wrapper: StorageLikeAsync = { diff --git a/extension/src/manifest.js b/extension/src/manifest.js index f4c38fc..21c56d0 100644 --- a/extension/src/manifest.js +++ b/extension/src/manifest.js @@ -1,4 +1,4 @@ -// Copyright (C) 2023 Vaughn Nugent +// Copyright (C) 2024 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 diff --git a/extension/vite.config.js b/extension/vite.config.js index d28c0f7..35beb26 100644 --- a/extension/vite.config.js +++ b/extension/vite.config.js @@ -43,6 +43,9 @@ export default defineConfig(({ mode }) => { scripts: [ 'src/entries/nostr-provider.js', // defaults to webAccessible: true ], + html: [ + 'src/entries/contentScript/auth-popup.html', + ] }, }), ], |