aboutsummaryrefslogtreecommitdiff
path: root/extension/src/entries
diff options
context:
space:
mode:
Diffstat (limited to 'extension/src/entries')
-rw-r--r--extension/src/entries/background/main.ts6
-rw-r--r--extension/src/entries/background/script.js2
-rw-r--r--extension/src/entries/background/serviceWorker.js2
-rw-r--r--extension/src/entries/contentScript/auth-popup.html10
-rw-r--r--extension/src/entries/contentScript/primary/components/PromptPopup.vue75
-rw-r--r--extension/src/entries/contentScript/primary/main.js5
-rw-r--r--extension/src/entries/contentScript/renderContent.js2
-rw-r--r--extension/src/entries/contentScript/util.ts38
-rw-r--r--extension/src/entries/nostr-provider.js2
-rw-r--r--extension/src/entries/options/App.vue26
-rw-r--r--extension/src/entries/options/components/AutoRules.vue119
-rw-r--r--extension/src/entries/options/components/EvHistoryTable.vue84
-rw-r--r--extension/src/entries/options/components/EventHistory.vue132
-rw-r--r--extension/src/entries/options/main.js9
-rw-r--r--extension/src/entries/popup/Components/PageContent.vue21
-rw-r--r--extension/src/entries/popup/main.js5
-rw-r--r--extension/src/entries/store/features.ts8
-rw-r--r--extension/src/entries/store/identity.ts2
-rw-r--r--extension/src/entries/store/index.ts3
-rw-r--r--extension/src/entries/store/mfaconfig.ts2
-rw-r--r--extension/src/entries/store/permissions.ts105
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