diff options
author | vnugent <public@vaughnnugent.com> | 2024-01-07 20:39:18 -0500 |
---|---|---|
committer | vnugent <public@vaughnnugent.com> | 2024-01-07 20:39:18 -0500 |
commit | c438ee90e3be4e5e01ae3d045d6b841a03bd46eb (patch) | |
tree | 41c0b2ee815ce979e2af0e79f8fde9b58f5f4627 /extension/src/entries | |
parent | e87c4b69036e32b4fcf3df89e8158fb52df6a4e0 (diff) |
Diffstat (limited to 'extension/src/entries')
21 files changed, 555 insertions, 103 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 |