aboutsummaryrefslogtreecommitdiff
path: root/extension/src
diff options
context:
space:
mode:
Diffstat (limited to 'extension/src')
-rw-r--r--extension/src/entries/options/components/Activity.vue14
-rw-r--r--extension/src/entries/options/components/EventHistory.vue164
-rw-r--r--extension/src/entries/options/components/Identities.vue8
-rw-r--r--extension/src/entries/options/components/SiteSettings.vue167
-rw-r--r--extension/src/entries/options/index.html2
-rw-r--r--extension/src/entries/options/main.js4
-rw-r--r--extension/src/entries/popup/Components/OtpLogin.vue6
-rw-r--r--extension/src/entries/popup/Components/PageContent.vue192
-rw-r--r--extension/src/entries/popup/Components/PassLogin.vue13
-rw-r--r--extension/src/entries/store/features.ts12
-rw-r--r--extension/src/entries/store/index.ts53
-rw-r--r--extension/src/entries/store/types.ts10
-rw-r--r--extension/src/features/index.ts2
-rw-r--r--extension/src/features/server-api/index.ts30
-rw-r--r--extension/src/features/settings.ts111
15 files changed, 456 insertions, 332 deletions
diff --git a/extension/src/entries/options/components/Activity.vue b/extension/src/entries/options/components/Activity.vue
index c62fb83..87ef2ad 100644
--- a/extension/src/entries/options/components/Activity.vue
+++ b/extension/src/entries/options/components/Activity.vue
@@ -1,5 +1,5 @@
<template>
- <div id="ev-history" class="flex flex-col w-full mt-4 sm:px-2">
+ <div id="ev-history" class="flex flex-col w-full mt-4 overflow-x-hidden sm:px-2">
<form @submit.prevent="">
<div class="w-full max-w-xl mx-auto">
<h3 class="text-center">
@@ -13,24 +13,26 @@
<div class="flex justify-center">
</div>
</div>
-
+
<div class="my-6 ">
- <EvHistoryTable :readonly="false" :requests="pending" @deny="deny" @approve="approve" />
+ <EvHistoryTable :readonly="false" :requests="pending" @deny="deny" @approve="approve" />
</div>
- <AutoRules />
+ <div class="">
+ <AutoRules />
+ </div>
<div class="flex flex-row justify-between mt-16">
<div class="font-bold">
History
</div>
<div class="flex justify-center">
- <pagination :pages="pages" />
+ <pagination :pages="pages" />
</div>
</div>
<div class="mt-1">
- <EvHistoryTable :readonly="true" :requests="permHistory" @deny="deny" @approve="approve" />
+ <EvHistoryTable :readonly="true" :requests="permHistory" @deny="deny" @approve="approve" />
</div>
<div class="mt-4 ml-auto w-fit">
diff --git a/extension/src/entries/options/components/EventHistory.vue b/extension/src/entries/options/components/EventHistory.vue
index b6cd13e..4fa97d4 100644
--- a/extension/src/entries/options/components/EventHistory.vue
+++ b/extension/src/entries/options/components/EventHistory.vue
@@ -8,13 +8,18 @@
<pagination :pages="pagination" />
</div>
</div>
- <div class="">
+ <div class="mt-2">
<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="pl-2"></th>
<th class="p-2 font-medium whitespace-nowrap dark:text-white">
- Event
+ Account
+ </th>
+ <th class="p-2 font-medium whitespace-nowrap dark:text-white">
+ Kind
+ </th>
+ <th class="p-2 font-medium whitespace-nowrap dark:text-white">
+ Note
</th>
<th class="p-2 font-medium whitespace-nowrap dark:text-white">
Time
@@ -25,50 +30,30 @@
<tbody class="divide-y divide-gray-200 dark:divide-dark-500">
<tr v-for="event in evHistory" :key="event.Id" class="">
- <td class="pl-2 whitespace-nowrap">
- <div class="flex flex-col items-end gap-0.5">
- <div class="">
- ID:
- </div>
- <div class="">
- EventId:
- </div>
- <div class="">
- PubKey:
- </div>
- <div class="">
- Content:
- </div>
- </div>
+ <td class="pl-2 truncate whitespace-nowrap overflow-ellipsis">
+ <a href="#" @click="goToKeyView(event)" class="text-blue-500 hover:underline">
+ {{ lookupKeyFromLoadedKeys(event.pubkey) }}
+ </a>
</td>
<td class="p-2">
- <div class="flex flex-col flex-1 gap-0.5">
- <div class="truncate overflow-ellipsis">
- {{ event.Id }}
- </div>
-
- <div class="truncate overflow-ellipsis">
- {{ event.id }}
- </div>
-
- <div class="truncate overflow-ellipsis">
- <a href="#" @click="goToKeyView(event)" class="text-blue-500 hover:underline">
- {{ event.pubkey }}
- </a>
- </div>
- <div class="truncate overflow-ellipsis">
- {{ event.content }}
- </div>
+ {{ event.kind }}
+ </td>
+ <td class="p-2 max-w-40">
+ <div class="truncate overflow-ellipsis">
+ {{ event.content }}
</div>
</td>
<td class="p-2 whitespace-nowrap">
{{ timeAgo(event, timeStamp) }}
</td>
- <td class="p-2 text-right whitespace-nowrap">
- <div class="button-group">
- <button class="rounded btn xs" @click="deleteEvent(event)">
+ <td class="flex">
+ <div class="my-1 button-group">
+ <button class="btn xs" @click="deleteEvent(event)">
<fa-icon icon="trash" />
</button>
+ <button class="w-8 btn xs" @click="showEvent(event)">
+ <fa-icon icon="ellipsis-v" />
+ </button>
</div>
</td>
</tr>
@@ -76,22 +61,103 @@
</table>
</div>
</div>
+
+ <Dialog :open="openEvent != null" @close="showEvent()">
+
+ <div class="fixed inset-0 bg-black/40" aria-hidden="true" />
+
+ <!-- Full-screen container to center the panel -->
+ <div class="fixed inset-0 flex justify-center w-screen p-4 mt-36">
+ <!-- The actual dialog panel -->
+ <DialogPanel
+ class="w-full max-w-xl p-3 mb-auto bg-white border border-gray-400 dark:bg-dark-800 dark:text-white dark:border-dark-500">
+ <DialogTitle class="text-lg font-bold"> Event Details </DialogTitle>
+ <div class="mt-2">
+
+ </div>
+ <div class="">
+
+ <div class="grid justify-center grid-flow-row grid-cols-3 text-left">
+
+ <div class="">
+ <h5 class="text-sm font-bold">Kind</h5>
+ </div>
+ <div class="">
+ <h5 class="text-sm font-bold">Time</h5>
+ </div>
+ <div class="">
+ <h5 class="text-sm font-bold">User</h5>
+ </div>
+
+ <div class="">
+ <p class="text-sm">{{ openEvent?.kind }}</p>
+ </div>
+ <div class="">
+ <p class="text-sm">{{ createShortDateAndTime(openEvent!) }}</p>
+ </div>
+ <div class="">
+ <p class="text-sm">
+ {{ lookupKeyFromLoadedKeys(openEvent!.pubkey) }}
+ </p>
+ </div>
+ </div>
+ <div class="mt-4 ">
+ <h4 class="text-sm font-bold">Content</h4>
+ <p
+ class="p-2 mt-2 text-sm text-gray-600 whitespace-pre-wrap bg-gray-100 dark:bg-dark-700 dark:text-gray-300 max-h-[16rem] overflow-y-auto">
+ {{ openEvent?.content }}
+ </p>
+ </div>
+ <div class="mt-4">
+ <h4 class="text-sm font-bold">Tags</h4>
+ <p class="px-2 overflow-x-auto max-w-[100%]">
+ <ul class="max-h-[16rem] overflow-y-auto">
+ <li v-for="tag in openEvent?.tags" :key="tag"
+ class="my-1 text-xs text-gray-600 dark:text-gray-400">
+ {{ tag }}
+ </li>
+ </ul>
+ </p>
+ </div>
+ <div class="mt-4 ">
+ <h4 class="text-sm font-bold">Event ID</h4>
+ <p
+ class="p-2 mt-2 text-sm text-gray-600 whitespace-pre-wrap bg-gray-100 dark:bg-dark-700 dark:text-gray-300 max-h-[16rem] overflow-y-auto">
+ {{ openEvent?.id }}
+ </p>
+ </div>
+ <div class="mt-4 ">
+ <h4 class="text-sm font-bold">Signature</h4>
+ <p
+ class="p-2 mt-2 text-sm text-gray-600 whitespace-pre-wrap bg-gray-100 dark:bg-dark-700 dark:text-gray-300 max-h-[16rem] overflow-y-auto">
+ {{ openEvent?.sig }}
+ </p>
+ </div>
+ </div>
+
+ </DialogPanel>
+ </div>
+
+ </Dialog>
</template>
<script setup lang="ts">
-import { apiCall } from '@vnuge/vnlib.browser';
+import { apiCall, useConfirm } from '@vnuge/vnlib.browser';
import { computed } from 'vue';
import { formatTimeAgo, get, useOffsetPagination, useTimestamp } from '@vueuse/core';
-import { } from '@headlessui/vue'
import { useStore } from '../../store';
import { EventEntry, NostrEvent } from '../../../features';
-import { map, slice } from 'lodash';
+import { find, map, slice } from 'lodash';
import { useQuery } from '../../../features/util';
+import { Dialog, DialogPanel, DialogTitle } from '@headlessui/vue'
const store = useStore()
const tabId = useQuery('t');
const keyId = useQuery('kid');
+const openEvId = useQuery('activeEvent');
+
+const { reveal } = useConfirm()
const pagination = useOffsetPagination({
pageSize: 10,
@@ -115,8 +181,18 @@ const evHistory = computed<Array<NostrEvent & EventEntry>>(() => {
})
})
+const openEvent = computed<NostrEvent & EventEntry | undefined>(() => find(evHistory.value, e => e.Id === openEvId.asRef.value))
+const showEvent = (event?: EventEntry) => openEvId.set(event?.Id ?? '')
+
+const deleteEvent = async (event: EventEntry) => {
+
+ const { isCanceled } = await reveal({
+ title: 'Delete Event',
+ text: 'Are you sure you want to delete this event forever?',
+ })
+
+ if(isCanceled) return
-const deleteEvent = (event: EventEntry) => {
//Call delete event function
apiCall(() => store.plugins.history.deleteEvent(event))
}
@@ -132,6 +208,10 @@ const createShortDateAndTime = (request: EventEntry) => {
return `${month}/${day}/${year} ${hours}:${minutes}:${seconds}`
}
+const lookupKeyFromLoadedKeys = (pubkey: string) => {
+ return find(store.allKeys, { PublicKey: pubkey })?.UserName ?? pubkey
+}
+
const timeAgo = (entry: EventEntry, timeStamp: number) => {
return formatTimeAgo(new Date(entry.Created), { }, timeStamp)
}
diff --git a/extension/src/entries/options/components/Identities.vue b/extension/src/entries/options/components/Identities.vue
index 8236b83..02c115d 100644
--- a/extension/src/entries/options/components/Identities.vue
+++ b/extension/src/entries/options/components/Identities.vue
@@ -3,7 +3,7 @@
<div class="flex justify-end gap-2 text-black dark:text-white">
<div class="">
<div class="">
- <button class="rounded btn sm" @click="onNip05Download">
+ <button class="btn sm" @click="onNip05Download">
NIP-05
<fa-icon icon="download" class="ml-1" />
</button>
@@ -11,7 +11,7 @@
</div>
<div class="mb-2">
<Popover class="relative" v-slot="{ open }">
- <PopoverButton class="rounded btn sm">Create</PopoverButton>
+ <PopoverButton class="btn sm">Create</PopoverButton>
<PopoverOverlay v-if="open" class="fixed inset-0 bg-black opacity-50" />
<PopoverPanel class="absolute z-10 mt-2 md:-right-0" v-slot="{ close }">
<div class="p-3 bg-white border border-gray-200 rounded shadow-lg dark:border-dark-600 dark:bg-dark-900">
@@ -30,7 +30,7 @@
</div>
</div>
<div class="flex justify-end mt-2">
- <button class="rounded sm btn" type="submit">Create</button>
+ <button class="sm btn" type="submit">Create</button>
</div>
</form>
</div>
@@ -40,7 +40,7 @@
</div>
<div class="">
<div class="">
- <button class="rounded btn sm" @click="identity.refreshKeys()">
+ <button class="btn sm" @click="identity.refreshKeys()">
<fa-icon icon="refresh" class="" />
</button>
</div>
diff --git a/extension/src/entries/options/components/SiteSettings.vue b/extension/src/entries/options/components/SiteSettings.vue
index a2df205..8deac92 100644
--- a/extension/src/entries/options/components/SiteSettings.vue
+++ b/extension/src/entries/options/components/SiteSettings.vue
@@ -1,8 +1,8 @@
<template>
<div class="flex flex-col w-full mt-4 sm:px-2">
-
+
<form @submit.prevent="">
- <div class="w-full max-w-md mx-auto">
+ <div class="w-full max-w-md mx-auto">
<h3 class="text-center">
Extension settings
</h3>
@@ -11,57 +11,45 @@
<div class="">
<div class="w-full">
<div class="flex flex-row w-full">
- <Switch
- v-model="originProtection"
+ <Switch v-model="originProtection"
:class="originProtection ? 'bg-black dark:bg-gray-50' : 'bg-gray-200 dark:bg-dark-600'"
- class="relative inline-flex items-center h-5 rounded-full w-11"
- >
+ class="relative inline-flex items-center h-5 rounded-full w-11">
<span class="sr-only">Origin protection</span>
- <span
- :class="originProtection ? 'translate-x-6' : 'translate-x-1'"
- class="inline-block w-4 h-4 transition transform bg-white rounded-full dark:bg-dark-900"
- />
+ <span :class="originProtection ? 'translate-x-6' : 'translate-x-1'"
+ class="inline-block w-4 h-4 transition transform bg-white rounded-full dark:bg-dark-900" />
</Switch>
<div class="my-auto ml-2 text-sm dark:text-gray-200">
- Tracking protection
+ Tracking protection
</div>
</div>
</div>
</div>
-
+
<div class="mt-3">
<div class="flex flex-row w-fit">
- <Switch
- v-model="v$.heartbeat.$model"
+ <Switch v-model="v$.heartbeat.$model"
:class="v$.heartbeat.$model ? 'bg-black dark:bg-white' : 'bg-gray-200 dark:bg-dark-600'"
- class="relative inline-flex items-center h-5 mx-auto rounded-full w-11"
- >
+ class="relative inline-flex items-center h-5 mx-auto rounded-full w-11">
<span class="sr-only">Stay logged in</span>
- <span
- :class="v$.heartbeat.$model ? 'translate-x-6' : 'translate-x-1'"
- class="inline-block w-4 h-4 transition transform rounded-full bg-gray-50 dark:bg-dark-900"
- />
+ <span :class="v$.heartbeat.$model ? 'translate-x-6' : 'translate-x-1'"
+ class="inline-block w-4 h-4 transition transform rounded-full bg-gray-50 dark:bg-dark-900" />
</Switch>
<div class="my-auto ml-2 text-sm dark:text-gray-200">
- Stay logged-in
+ Stay logged-in
</div>
</div>
</div>
<div class="mt-3">
<div class="flex flex-row w-fit">
- <Switch
- v-model="v$.authPopup.$model"
+ <Switch v-model="v$.authPopup.$model"
:class="v$.authPopup.$model ? 'bg-black dark:bg-white' : 'bg-gray-200 dark:bg-dark-600'"
- class="relative inline-flex items-center h-5 mx-auto rounded-full w-11"
- >
+ class="relative inline-flex items-center h-5 mx-auto rounded-full w-11">
<span class="sr-only">Permissions Popup</span>
- <span
- :class="v$.authPopup.$model ? 'translate-x-6' : 'translate-x-1'"
- class="inline-block w-4 h-4 transition transform rounded-full bg-gray-50 dark:bg-dark-900"
- />
+ <span :class="v$.authPopup.$model ? 'translate-x-6' : 'translate-x-1'"
+ class="inline-block w-4 h-4 transition transform rounded-full bg-gray-50 dark:bg-dark-900" />
</Switch>
<div class="my-auto ml-2 text-sm dark:text-gray-200">
- Permissions Popup
+ Permissions Popup
</div>
</div>
</div>
@@ -69,6 +57,9 @@
</div>
<h3 class="text-center">
Server settings
+ <span class="my-auto ml-1 text-sm text-green-500" v-show="store.isServerValid">
+ (connected)
+ </span>
</h3>
<p class="text-xs dark:text-gray-400">
You must be careful when editing these settings as you may loose connection to your vault
@@ -77,59 +68,33 @@
<div class="flex justify-end mt-2">
<div class="button-group">
<button class="rounded btn sm" @click="toggleEdit()">
- <fa-icon v-if="editMode" icon="lock-open"/>
- <fa-icon v-else icon="lock"/>
+ <fa-icon v-if="editMode" icon="lock-open" />
+ <fa-icon v-else icon="lock" />
</button>
- <a :href="data.apiUrl" target="_blank">
+ <a :href="store.status.EpConfig.apiBaseUrl" target="_blank">
<button type="button" class="rounded btn sm">
- <fa-icon icon="external-link-alt"/>
+ <fa-icon icon="external-link-alt" />
</button>
</a>
</div>
</div>
<fieldset>
<div class="pl-1 mt-2">
-
- </div>
- <div class="mt-2">
- <label class="pl-1">BaseUrl</label>
- <input
- class="w-full input"
- :class="{'error': v$.apiUrl.$invalid }"
- v-model="v$.apiUrl.$model"
- :readonly="!editMode"
- />
- <p class="pl-1 mt-1 text-xs text-gray-600 dark:text-gray-400">
- * The http path to the vault server (must start with http:// or https://)
- </p>
- </div>
- <div class="mt-2">
- <label class="pl-1">Account endpoint</label>
- <input
- class="w-full input"
- v-model="v$.accountBasePath.$model"
- :class="{ 'error': v$.accountBasePath.$invalid }"
- :readonly="!editMode"
- />
- <p class="pl-1 mt-1 text-xs text-gray-600 dark:text-gray-400">
- * This is the path to the account server endpoint (must start with /)
- </p>
+
</div>
<div class="mt-2">
- <label class="pl-1">Nostr endpoint</label>
- <input
- class="w-full input"
- v-model="v$.nostrEndpoint.$model"
- :class="{ 'error': v$.nostrEndpoint.$invalid }"
- :readonly="!editMode"
- />
+ <label class="pl-1">
+ Server Url
+ </label>
+ <input class="w-full input" :class="{'error': v$.discoveryUrl.$invalid }"
+ v-model="v$.discoveryUrl.$model" :readonly="!editMode" />
<p class="pl-1 mt-1 text-xs text-gray-600 dark:text-gray-400">
- * This is the path to the Nostr plugin endpoint path (must start with /)
+ * The http path to the vault server (must start with https://)
</p>
</div>
</fieldset>
<div class="flex justify-end mt-2">
- <button :disabled="!modified || waiting" class="rounded btn sm" @click="onSave">Save</button>
+ <button :disabled="!modified || waiting" class="btn sm" @click="onSave">Save</button>
</div>
</div>
</form>
@@ -139,25 +104,27 @@
<script setup lang="ts">
import { apiCall, useDataBuffer, useVuelidateWrapper, useWait } from '@vnuge/vnlib.browser';
import { computed, watch } from 'vue';
-import { useToggle, watchDebounced } from '@vueuse/core';
+import { Mutable, useToggle, watchDebounced } from '@vueuse/core';
import { maxLength, helpers, required } from '@vuelidate/validators'
import { Switch } from '@headlessui/vue'
import { useStore } from '../../store';
import { storeToRefs } from 'pinia';
-import useVuelidate from '@vuelidate/core'
+import { useVuelidate } from '@vuelidate/core'
+import { PluginConfig } from '../../../features';
const store = useStore()
const { settings } = storeToRefs(store)
const { waiting } = useWait();
+const { setSiteConfig } = store.plugins.settings
const { apply, data, buffer, modified, update } = useDataBuffer(settings.value, async sb =>{
- const newConfig = await store.saveSiteConfig(sb.buffer)
+ const newConfig = await setSiteConfig(sb.buffer)
apply(newConfig)
return newConfig;
})
//Watch for store settings changes and apply them
-watch(settings, v => apply(v))
+watch(settings, apply)
const originProtection = computed({
get: () => store.isOriginProtectionOn,
@@ -165,24 +132,13 @@ const originProtection = computed({
})
const url = (val : string) => /^https?:\/\/[a-zA-Z0-9\.\:\/-]+$/.test(val);
-const path = (val : string) => /^\/[a-zA-Z0-9-_]+$/.test(val);
const vRules = {
- apiUrl: {
+ discoveryUrl: {
required:helpers.withMessage('Base url is required', required),
maxLength: helpers.withMessage('Base url must be less than 100 characters', maxLength(100)),
url: helpers.withMessage('You must input a valid url', url)
},
- accountBasePath: {
- required:helpers.withMessage('Account path is required', required),
- maxLength: maxLength(50),
- alphaNum: helpers.withMessage('Account path is not a valid endpoint path that begins with /', path)
- },
- nostrEndpoint:{
- required: helpers.withMessage('Nostr path is required', required),
- maxLength: maxLength(50),
- alphaNum: helpers.withMessage('Nostr path is not a valid endpoint path that begins with /', path)
- },
authPopup: {},
heartbeat: {},
}
@@ -217,21 +173,48 @@ const onSave = async () => {
}
const testConnection = async () =>{
- return await apiCall(async ({axios, toaster}) =>{
+ return await apiCall(async ({ toaster }) =>{
try{
- await axios.get(`${buffer.apiUrl}`);
- toaster.general.success({
- title: 'Success',
- text: 'Succcesfully connected to the vault server'
- });
- return true;
+
+ //See if the discovery url ends with a well-known file path (also checks for well-formatted url)
+ const url = new URL(buffer.discoveryUrl);
+
+ if (url.pathname === '/'){
+ //append the well-known path
+ url.pathname = '/.well-known/nvault';
+ }
+
+ const result = await store.plugins.settings.testServerAddress(url.href);
+
+ if (result){
+
+ //Safe to update the buffer incase we changed it
+ (buffer as Mutable<PluginConfig>).discoveryUrl = url.href;
+
+ toaster.form.success({
+ title: 'Success',
+ text: 'Succcesfully discoverted your new Nvault server'
+ });
+
+ return true;
+ }
+ else{
+ toaster.form.error({
+ title: 'Invalid Url',
+ text: 'The address you entered was not a valid discovery url.',
+ duration: 6000
+ });
+ }
}
catch(e){
+ console.error(e);
toaster.form.error({
title: 'Warning',
- text: `Failed to connect to the vault server. Status code: ${(e as any).response?.status}`
+ text: `Failed to connect to the vault server. Check your url.`
});
}
+
+ return false;
})
}
diff --git a/extension/src/entries/options/index.html b/extension/src/entries/options/index.html
index 976e7ea..6c70777 100644
--- a/extension/src/entries/options/index.html
+++ b/extension/src/entries/options/index.html
@@ -14,7 +14,7 @@
}
</style>
</head>
- <body class="w-full">
+ <body class="w-full overflow-x-hidden">
<div id="app"></div>
<script type="module" src="./main.js"></script>
</body>
diff --git a/extension/src/entries/options/main.js b/extension/src/entries/options/main.js
index 901dfdd..78fb4df 100644
--- a/extension/src/entries/options/main.js
+++ b/extension/src/entries/options/main.js
@@ -23,12 +23,12 @@ import Pagination from '../../components/Pagination.vue';
/* FONT AWESOME CONFIG */
import { library } from '@fortawesome/fontawesome-svg-core'
-import { faCheck, 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, faEllipsisV, 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, permissionsPlugin, useBackgroundPiniaPlugin } from "../store";
-library.add(faCopy, faEdit, faChevronLeft, faMoon, faSun, faLock, faLockOpen, faExternalLinkAlt, faTrash, faDownload, faChevronRight, faPlus, faRefresh, faTrashCan, faMinusCircle ,faTrashCan, faCheck)
+library.add(faCopy, faEdit, faChevronLeft, faMoon, faSun, faLock, faLockOpen, faExternalLinkAlt, faTrash, faDownload, faChevronRight, faPlus, faRefresh, faTrashCan, faMinusCircle ,faTrashCan, faCheck, faEllipsisV)
//Create the background feature wiring
const bgPlugins = useBackgroundPiniaPlugin('options')
diff --git a/extension/src/entries/popup/Components/OtpLogin.vue b/extension/src/entries/popup/Components/OtpLogin.vue
index a2b8ac7..2fc95f2 100644
--- a/extension/src/entries/popup/Components/OtpLogin.vue
+++ b/extension/src/entries/popup/Components/OtpLogin.vue
@@ -6,7 +6,7 @@
</fieldset>
<div class="flex justify-end mt-2">
<div class="px-3">
- <button class="w-24 rounded btn sm primary">
+ <button class="w-24 btn sm primary">
<fa-icon v-if="waiting" icon="spinner" class="animate-spin" />
<span v-else>Submit</span>
</button>
@@ -20,7 +20,7 @@ import { apiCall, useWait } from "@vnuge/vnlib.browser";
import { ref } from "vue";
import { useStore } from "../../store";
-const { login } = useStore()
+const store = useStore()
const { waiting } = useWait()
const token = ref('')
@@ -28,7 +28,7 @@ const token = ref('')
const onSubmit = async () => {
await apiCall(async ({ toaster }) => {
try{
- await login(token.value)
+ await store.plugins.user.login(token.value)
toaster.form.success({
'title': 'Login successful',
diff --git a/extension/src/entries/popup/Components/PageContent.vue b/extension/src/entries/popup/Components/PageContent.vue
index 37e119d..9bc9627 100644
--- a/extension/src/entries/popup/Components/PageContent.vue
+++ b/extension/src/entries/popup/Components/PageContent.vue
@@ -1,105 +1,117 @@
<template>
- <div
- id="injected-root"
- class="flex flex-col text-left w-[20rem] min-h-[25rem]"
- >
-
- <div class="flex flex-row w-full gap-2 p-1.5 bg-black text-white dark:bg-dark-600 shadow">
- <div class="flex flex-row flex-auto my-auto">
- <div class="my-auto mr-2">
- <img class="h-6" src="/icons/32.png" />
- </div>
- <h3 class="block my-auto">NVault</h3>
- <div class="px-3 py-.5 m-auto text-sm rounded-full h-fit active-badge" :class="[isTabAllowed ? 'active' : 'inactive']">
- {{ isTabAllowed ? 'Active' : 'Inactive' }}
- </div>
- </div>
- <div class="my-auto" v-if="loggedIn">
- <button class="rounded btn xs" @click.prevent="logout">
- <fa-icon icon="arrow-right-from-bracket" />
- </button>
- </div>
- <div class="my-auto">
- <button class="rounded btn xs" @click="toggleDark" >
- <fa-icon class="w-4" v-if="darkMode" icon="sun"/>
- <fa-icon class="w-4" v-else icon="moon" />
- </button>
+ <div id="injected-root" class="flex flex-col text-left w-[20rem] min-h-[25rem]">
+
+ <div class="flex flex-row w-full gap-2 p-1.5 bg-black text-white dark:bg-dark-600 shadow">
+ <div class="flex flex-row flex-auto my-auto">
+ <div class="my-auto mr-2">
+ <img class="h-6" src="/icons/32.png" />
</div>
- <div class="my-auto">
- <button class="rounded btn xs" @click="openOptions">
- <fa-icon :icon="['fas', 'gear']"/>
- </button>
+ <h3 class="block my-auto">NVault</h3>
+ <div class="px-3 py-.5 m-auto text-sm rounded-full h-fit active-badge"
+ :class="[isTabAllowed ? 'active' : 'inactive']">
+ {{ isTabAllowed ? 'Active' : 'Inactive' }}
</div>
</div>
-
- <div v-if="!loggedIn">
- <Login></Login>
+ <div class="my-auto" v-if="loggedIn">
+ <button class="rounded btn xs" @click.prevent="logout">
+ <fa-icon icon="arrow-right-from-bracket" />
+ </button>
+ </div>
+ <div class="my-auto">
+ <button class="rounded btn xs" @click="toggleDark">
+ <fa-icon class="w-4" v-if="darkMode" icon="sun" />
+ <fa-icon class="w-4" v-else icon="moon" />
+ </button>
+ </div>
+ <div class="my-auto">
+ <button class="rounded btn xs" @click="openOptions">
+ <fa-icon :icon="['fas', 'gear']" />
+ </button>
+ </div>
+ </div>
+
+ <div v-if="!store.isServerValid" class="flex flex-row gap-2 mx-auto mt-2 text-center text-red-500">
+ <div class="text-center">
+ <svg class="w-6 h-6" aria-hidden="true" xmlns="http://www.w3.org/2000/svg"
+ fill="none" viewBox="0 0 24 24">
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
+ d="M12 13V8m0 8h0m9-4a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
+ </svg>
+ </div>
+ <div class="text-center">
+ Not connected to server
</div>
+ </div>
- <div v-else class="flex justify-center">
- <div class="w-full px-3 m-auto">
+ <div v-if="!loggedIn">
+ <Login></Login>
+ </div>
- <div class="text-sm text-center">
- {{ userName }}
- </div>
-
- <div class="">
- <label class="mb-0.5 text-sm">
- Identity
- </label>
- <IdentitySelection></IdentitySelection>
- </div>
-
- <div class="w-full mt-1">
- <div class="flex flex-col">
- <div class="flex flex-row gap-2 p-1.5 bg-gray-100 border border-gray-200 dark:bg-dark-800 dark:border-dark-400">
- <div class="text-sm break-all">
- {{ pubKey ?? 'No key selected' }}
- </div>
- <div class="my-auto ml-auto cursor-pointer" :class="{'text-primary-500': copied }">
- <fa-icon class="mr-1" icon="copy" @click="copy(pubKey!)"/>
- </div>
- </div>
+ <div v-else class="flex justify-center">
+ <div class="w-full px-3 m-auto">
+
+ <div class="text-sm text-center">
+ {{ userName }}
+ </div>
+
+ <div class="">
+ <label class="mb-0.5 text-sm">
+ Identity
+ </label>
+ <IdentitySelection></IdentitySelection>
+ </div>
+
+ <div class="w-full mt-1">
+ <div class="flex flex-col">
+ <div
+ class="flex flex-row gap-2 p-1.5 bg-gray-100 border border-gray-200 dark:bg-dark-800 dark:border-dark-400">
+ <div class="text-sm break-all">
+ {{ pubKey ?? 'No key selected' }}
+ </div>
+ <div class="my-auto ml-auto cursor-pointer" :class="{'text-primary-500': copied }">
+ <fa-icon class="mr-1" icon="copy" @click="copy(pubKey!)" />
</div>
</div>
+ </div>
+ </div>
- <div class="mt-4">
- <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 dark:text-dark-100" readonly/>
-
- <button v-if="isTabAllowed" class="btn xs" @click="store.dissallowOrigin()">
- <fa-icon icon="minus" />
- </button>
- <button v-else class="btn xs" @click="store.allowOrigin()">
- <fa-icon icon="plus" />
- </button>
- </div>
-
- <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 class="mt-4">
+ <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 dark:text-dark-100" readonly />
+
+ <button v-if="isTabAllowed" class="btn xs" @click="store.dissallowOrigin()">
+ <fa-icon icon="minus" />
+ </button>
+ <button v-else class="btn xs" @click="store.allowOrigin()">
+ <fa-icon icon="plus" />
+ </button>
+ </div>
+
+ <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>
- <notifications class="toaster" group="form" position="top-right" />
+ <notifications class="toaster" group="form" position="top-right" />
- </div>
+ </div>
</template>
<script setup lang="ts">
@@ -108,11 +120,11 @@ import { storeToRefs } from "pinia";
import { useStore } from "../../store";
import { apiCall, configureNotifier } from "@vnuge/vnlib.browser";
import { useClipboard } from '@vueuse/core'
+import { map } from "lodash";
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})
@@ -132,7 +144,7 @@ watchEffect(() => darkMode.value ? document.body.classList.add('dark') : documen
const logout = () =>{
apiCall(async ({ toaster }) =>{
- await store.logout()
+ await store.plugins.user.logout()
toaster.general.success({
'title':'Success',
'text': 'You have been logged out'
diff --git a/extension/src/entries/popup/Components/PassLogin.vue b/extension/src/entries/popup/Components/PassLogin.vue
index 29aeeb6..bf32a7f 100644
--- a/extension/src/entries/popup/Components/PassLogin.vue
+++ b/extension/src/entries/popup/Components/PassLogin.vue
@@ -23,17 +23,17 @@
<form class="" @submit.prevent="onSubmit()">
<fieldset class="px-4 input-container">
<div class="">
- <label class="">Username</label>
+ <label class="text-sm">Username</label>
<input type="text" name="username" class="w-full input" v-model="username" />
</div>
<div class="mt-1">
- <label class="">Password</label>
+ <label class="text-sm">Password</label>
<input type="password" name="password" class="w-full input" v-model="password" />
</div>
</fieldset>
<div class="flex justify-end mt-2">
<div class="px-3">
- <button class="w-24 rounded btn sm primary">
+ <button class="w-24 btn sm primary">
<fa-icon v-if="waiting" icon="spinner" class="animate-spin" />
<span v-else>Submit</span>
</button>
@@ -52,6 +52,7 @@ import VOtpInput from "vue3-otp-input";
const { waiting } = useWait()
const store = useStore();
+const { user } = store.plugins
const showTotp = computed(() => store.mfaStatus?.type === 'totp')
@@ -71,19 +72,19 @@ const onSubmit = () => {
return
}
- await store.login(username.value, password.value)
+ await user.login(username.value, password.value)
});
};
const onSubmitTotp = (code: string) => {
//Invoke totp login
- apiCall(() => store.plugins.user.submitMfa({ code: toNumber(code) }));
+ apiCall(() => user.submitMfa({ code: toNumber(code) }));
};
</script>
<style lang="scss">
#totp-login .otp-input input {
- @apply w-10 p-0.5 rounded text-center text-lg mx-1 focus:border-primary-500;
+ @apply w-10 p-0.5 text-center text-lg mx-1 focus:border-primary-500;
}
</style> \ No newline at end of file
diff --git a/extension/src/entries/store/features.ts b/extension/src/entries/store/features.ts
index d714ac6..fd2a3f2 100644
--- a/extension/src/entries/store/features.ts
+++ b/extension/src/entries/store/features.ts
@@ -16,6 +16,7 @@
import 'pinia'
import { } from 'lodash'
import { PiniaPluginContext } from 'pinia'
+import { computed } from 'vue'
import type { IMfaFlowContinuiation } from '@vnuge/vnlib.browser'
import {
@@ -34,7 +35,7 @@ import {
usePermissionApi
} from "../../features"
-import { ChannelContext } from '../../messaging'
+import type { ChannelContext } from '../../messaging'
export type BgPlugins = ReturnType<typeof usePlugins>
export type BgPluginState<T> = { plugins: BgPlugins } & T
@@ -43,6 +44,8 @@ declare module 'pinia' {
export interface PiniaCustomProperties {
plugins: BgPlugins
mfaStatus: Partial<IMfaFlowContinuiation> | null
+ isServerValid: boolean
+ toggleDarkMode: () => void
}
}
@@ -87,16 +90,19 @@ export const useBackgroundPiniaPlugin = (context: ChannelContext) => {
onWatchableChange(settings, async () => {
//Update settings and dark mode on change
store.settings = await settings.getSiteConfig();
- store.darkMode = await settings.getDarkMode();
+ store.status = await settings.getStatus();
}, { immediate: true })
onWatchableChange(history, async () => {
//Load event history
store.eventHistory = await history.getEvents();
}, { immediate: true })
-
+
return{
plugins,
+ isServerValid: computed(() => store.status.isValid),
+ //Main api functions
+ toggleDarkMode: () => settings.setDarkMode(!store.darkMode)
}
}
} \ No newline at end of file
diff --git a/extension/src/entries/store/index.ts b/extension/src/entries/store/index.ts
index 0b4d3cd..abfba75 100644
--- a/extension/src/entries/store/index.ts
+++ b/extension/src/entries/store/index.ts
@@ -16,44 +16,31 @@
import 'pinia'
import { } from 'lodash'
import { defineStore } from 'pinia'
-import { PluginConfig } from '../../features/'
-import { NostrStoreState } from './types'
+import { PluginConfig, EventEntry, ConfigStatus } from '../../features/'
+import { computed, shallowRef } from 'vue'
+import { get } from '@vueuse/core'
-export type * from './types'
export * from './allowedOrigins'
export * from './features'
export * from './identity'
export * from './mfaconfig'
export * from './permissions'
-export const useStore = defineStore({
- id: 'main',
- state: (): NostrStoreState =>({
- loggedIn: false,
- userName: '',
- settings: {} as any,
- darkMode: false,
- eventHistory: [],
- }),
- actions: {
+export const useStore = defineStore('main', () => {
- async login (usernameOrToken: string, password?: string) {
- await this.plugins.user.login(usernameOrToken, password);
- },
-
- async logout () {
- await this.plugins.user.logout();
- },
-
- saveSiteConfig(config: PluginConfig) {
- return this.plugins.settings.setSiteConfig(config)
- },
-
- async toggleDarkMode(){
- await this.plugins.settings.setDarkMode(this.darkMode === false)
- },
- },
- getters:{
-
- },
-}) \ No newline at end of file
+ const loggedIn = shallowRef<boolean>(false)
+ const userName = shallowRef<string>('')
+ const settings = shallowRef<PluginConfig>({} as PluginConfig)
+ const eventHistory = shallowRef<EventEntry[]>([])
+ const status = shallowRef<ConfigStatus>({} as ConfigStatus)
+ const darkMode = computed<boolean>(() => get(status).isDarkMode)
+
+ return{
+ loggedIn,
+ userName,
+ settings,
+ status,
+ darkMode,
+ eventHistory
+ }
+})
diff --git a/extension/src/entries/store/types.ts b/extension/src/entries/store/types.ts
deleted file mode 100644
index 536cf04..0000000
--- a/extension/src/entries/store/types.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { } from "webextension-polyfill";
-import type { PluginConfig, EventEntry } from "../../features";
-
-export interface NostrStoreState {
- loggedIn: boolean;
- userName: string | null;
- settings: PluginConfig;
- darkMode: boolean;
- eventHistory: EventEntry[];
-} \ No newline at end of file
diff --git a/extension/src/features/index.ts b/extension/src/features/index.ts
index d7e1b05..6f235ad 100644
--- a/extension/src/features/index.ts
+++ b/extension/src/features/index.ts
@@ -16,7 +16,7 @@
//Export all shared types
export type { NostrPubKey, LoginMessage, NostrEvent, NostrRelay, EventEntry } from './types'
export type * from './framework'
-export type { PluginConfig } from './settings'
+export type { PluginConfig, ConfigStatus } from './settings'
export type { PkiPubKey, EcKeyParams, LocalPkiApi as PkiApi } from './pki-api'
export type { NostrApi } from './nostr-api'
export type { UserApi } from './auth-api'
diff --git a/extension/src/features/server-api/index.ts b/extension/src/features/server-api/index.ts
index 35bed6f..f6d7123 100644
--- a/extension/src/features/server-api/index.ts
+++ b/extension/src/features/server-api/index.ts
@@ -20,6 +20,7 @@ import { type WebMessage, type UserProfile } from "@vnuge/vnlib.browser"
import { initEndponts } from "./endpoints"
import { cloneDeep } from "lodash"
import type { EncryptionRequest, EventEntry, NostrEvent, NostrPubKey, NostrRelay } from "../types"
+import type { EndpointConfig } from "../settings"
export enum Endpoints {
GetKeys = 'getKeys',
@@ -55,13 +56,16 @@ export interface ServerApi{
execRequest: ExecRequestHandler
}
-export const useServerApi = (nostrUrl: Ref<string>, accUrl: Ref<string>): ServerApi => {
+export const useServerApi = (endpoints: Ref<EndpointConfig>): ServerApi => {
const { registerEndpoint, execRequest } = initEndponts()
+ const nostrUrl = () => get(endpoints).nostrBasePath
+ const accUrl = () => get(endpoints).accountBasePath
+
registerEndpoint({
id: Endpoints.GetKeys,
method: 'GET',
- path: () => `${get(nostrUrl)}?type=getKeys`,
+ path: () => `${nostrUrl()}?type=getKeys`,
onRequest: () => Promise.resolve(),
onResponse: (response) => Promise.resolve(response)
})
@@ -69,7 +73,7 @@ export const useServerApi = (nostrUrl: Ref<string>, accUrl: Ref<string>): Server
registerEndpoint({
id: Endpoints.DeleteKey,
method: 'DELETE',
- path: (key: NostrPubKey) => `${get(nostrUrl)}?type=identity&id=${key.Id}`,
+ path: (key: NostrPubKey) => `${nostrUrl()}?type=identity&id=${key.Id}`,
onRequest: () => Promise.resolve(),
onResponse: async (response: WebMessage) => response.getResultOrThrow()
})
@@ -77,7 +81,7 @@ export const useServerApi = (nostrUrl: Ref<string>, accUrl: Ref<string>): Server
registerEndpoint({
id: Endpoints.SignEvent,
method: 'POST',
- path: () => `${get(nostrUrl)}?type=signEvent`,
+ path: () => `${nostrUrl()}?type=signEvent`,
onRequest: (event) => Promise.resolve(event),
onResponse: async (response: WebMessage<NostrEvent>) => {
const res = response.getResultOrThrow()
@@ -89,7 +93,7 @@ export const useServerApi = (nostrUrl: Ref<string>, accUrl: Ref<string>): Server
registerEndpoint({
id: Endpoints.GetRelays,
method: 'GET',
- path: () => `${get(nostrUrl)}?type=getRelays`,
+ path: () => `${nostrUrl()}?type=getRelays`,
onRequest: () => Promise.resolve(),
onResponse: (response) => Promise.resolve(response)
})
@@ -97,7 +101,7 @@ export const useServerApi = (nostrUrl: Ref<string>, accUrl: Ref<string>): Server
registerEndpoint({
id: Endpoints.SetRelay,
method: 'POST',
- path: () => `${get(nostrUrl)}?type=relay`,
+ path: () => `${nostrUrl()}?type=relay`,
onRequest: (relay:NostrRelay) => Promise.resolve(relay),
onResponse: (response) => Promise.resolve(response)
})
@@ -105,7 +109,7 @@ export const useServerApi = (nostrUrl: Ref<string>, accUrl: Ref<string>): Server
registerEndpoint({
id: Endpoints.CreateId,
method: 'PUT',
- path: () => `${get(nostrUrl)}?type=identity`,
+ path: () => `${nostrUrl()}?type=identity`,
onRequest: (identity: NostrPubKey) => Promise.resolve(identity),
onResponse: async (response: WebMessage<NostrEvent>) => response.getResultOrThrow()
})
@@ -113,7 +117,7 @@ export const useServerApi = (nostrUrl: Ref<string>, accUrl: Ref<string>): Server
registerEndpoint({
id: Endpoints.UpdateId,
method: 'PATCH',
- path: () => `${get(nostrUrl)}?type=identity`,
+ path: () => `${nostrUrl()}?type=identity`,
onRequest: (identity:NostrPubKey) => {
const id = cloneDeep(identity) as any;
delete id.Created;
@@ -126,7 +130,7 @@ export const useServerApi = (nostrUrl: Ref<string>, accUrl: Ref<string>): Server
registerEndpoint({
id: Endpoints.UpdateProfile,
method: 'POST',
- path: () => `${get(accUrl)}`,
+ path: () => `${accUrl()}`,
onRequest: (profile: UserProfile) => Promise.resolve(cloneDeep(profile)),
onResponse: async (response: WebMessage<string>) => response.getResultOrThrow()
})
@@ -135,7 +139,7 @@ export const useServerApi = (nostrUrl: Ref<string>, accUrl: Ref<string>): Server
registerEndpoint({
id:Endpoints.Encrypt,
method:'POST',
- path: () => `${get(nostrUrl)}?type=encrypt`,
+ path: () => `${nostrUrl()}?type=encrypt`,
onRequest: (data: EncryptionRequest) => Promise.resolve(data),
onResponse: async (response: WebMessage<{ ciphertext:string, iv:string }>) =>{
const { ciphertext, iv } = response.getResultOrThrow()
@@ -146,7 +150,7 @@ export const useServerApi = (nostrUrl: Ref<string>, accUrl: Ref<string>): Server
registerEndpoint({
id:Endpoints.Decrypt,
method:'POST',
- path: () => `${get(nostrUrl)}?type=decrypt`,
+ path: () => `${nostrUrl()}?type=decrypt`,
onRequest: (data: EncryptionRequest) => Promise.resolve(data),
onResponse: async (response: WebMessage<string>) => response.getResultOrThrow()
})
@@ -155,7 +159,7 @@ export const useServerApi = (nostrUrl: Ref<string>, accUrl: Ref<string>): Server
registerEndpoint({
id: Endpoints.GetHistory,
method: 'GET',
- path: () => `${get(nostrUrl)}?type=getEvents`,
+ path: () => `${nostrUrl()}?type=getEvents`,
onRequest: () => Promise.resolve(),
onResponse: (response : EventEntry[]) => Promise.resolve(response) //Pass through response, should be an array of events or an error
})
@@ -163,7 +167,7 @@ export const useServerApi = (nostrUrl: Ref<string>, accUrl: Ref<string>): Server
registerEndpoint({
id: Endpoints.DeleteSingleEvent,
method: 'DELETE',
- path: (evnt: EventEntry) => `${get(nostrUrl)}?type=event&id=${evnt.Id}`,
+ path: (evnt: EventEntry) => `${nostrUrl()}?type=event&id=${evnt.Id}`,
onRequest: () => Promise.resolve(),
onResponse: (response) => Promise.resolve(response)
})
diff --git a/extension/src/features/settings.ts b/extension/src/features/settings.ts
index 0bd7101..ad6c2f4 100644
--- a/extension/src/features/settings.ts
+++ b/extension/src/features/settings.ts
@@ -14,41 +14,53 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import { storage } from "webextension-polyfill"
-import { defaultsDeep } from 'lodash'
+import { defaultsDeep, defer, find, isArray, isEmpty } from 'lodash'
import { configureApi, debugLog } from '@vnuge/vnlib.browser'
-import { MaybeRefOrGetter, readonly, Ref, shallowRef, watch } from "vue";
+import { computed, MaybeRefOrGetter, readonly, Ref, shallowRef, watch } from "vue";
import { JsonObject } from "type-fest";
import { Watchable } from "./types";
import { BgRuntime, FeatureApi, optionsOnly, IFeatureExport, exportForegroundApi, popupAndOptionsOnly } from './framework'
-import { get, set, toRefs } from "@vueuse/core";
+import { get, set } from "@vueuse/core";
import { waitForChangeFn, useStorage } from "./util";
import { ServerApi, useServerApi } from "./server-api";
export interface PluginConfig extends JsonObject {
- readonly apiUrl: string;
- readonly accountBasePath: string;
- readonly nostrEndpoint: string;
+ readonly discoveryUrl: string;
readonly heartbeat: boolean;
readonly maxHistory: number;
readonly tagFilter: boolean;
readonly authPopup: boolean;
+ readonly darkMode: boolean;
}
//Default storage config
const defaultConfig : PluginConfig = Object.freeze({
- apiUrl: import.meta.env.VITE_API_URL,
- accountBasePath: import.meta.env.VITE_ACCOUNTS_BASE_PATH,
- nostrEndpoint: import.meta.env.VITE_NOSTR_ENDPOINT,
+ discoveryUrl: import.meta.env.VITE_DISCOVERY_URL,
heartbeat: import.meta.env.VITE_HEARTBEAT_ENABLED === 'true',
maxHistory: 50,
tagFilter: true,
authPopup: true,
+ darkMode: false,
});
+export interface EndpointConfig extends JsonObject {
+ readonly apiBaseUrl: string;
+ readonly accountBasePath: string;
+ readonly nostrBasePath: string;
+}
+
+export interface ConfigStatus {
+ readonly EpConfig: EndpointConfig;
+ readonly isDarkMode: boolean;
+ readonly isValid: boolean;
+}
+
export interface AppSettings{
saveConfig(config: PluginConfig): void;
useStorageSlot<T>(slot: string, defaultValue: MaybeRefOrGetter<T>): Ref<T>;
useServerApi(): ServerApi,
+ setDarkMode(darkMode: boolean): void;
+ readonly status: Readonly<Ref<ConfigStatus>>;
readonly currentConfig: Readonly<Ref<PluginConfig>>;
}
@@ -56,18 +68,55 @@ export interface SettingsApi extends FeatureApi, Watchable {
getSiteConfig: () => Promise<PluginConfig>;
setSiteConfig: (config: PluginConfig) => Promise<PluginConfig>;
setDarkMode: (darkMode: boolean) => Promise<void>;
- getDarkMode: () => Promise<boolean>;
+ getStatus: () => Promise<ConfigStatus>;
+ testServerAddress: (address: string) => Promise<boolean>;
+}
+
+interface ServerDiscoveryResult{
+ readonly endpoints: {
+ readonly name: string;
+ readonly path: string;
+ }[]
+}
+
+const discoverAndSetEndpoints = async (discoveryUrl: string, epConfig: Ref<EndpointConfig | undefined>) => {
+ const res = await fetch(discoveryUrl)
+ const { endpoints } = await res.json() as ServerDiscoveryResult;
+
+ const urls: EndpointConfig = {
+ apiBaseUrl: new URL(discoveryUrl).origin,
+ accountBasePath: find(endpoints, p => p.name == "account")?.path || "/account",
+ nostrBasePath: find(endpoints, p => p.name == "nostr")?.path || "/nostr",
+ };
+
+ //Set once the urls are discovered
+ set(epConfig, urls);
}
export const useAppSettings = (): AppSettings => {
const _storageBackend = storage.local;
+ const _darkMode = shallowRef(false);
const store = useStorage<PluginConfig>(_storageBackend, 'siteConfig', defaultConfig);
+ const endpointConfig = shallowRef<EndpointConfig>({nostrBasePath: '', accountBasePath: '', apiBaseUrl: ''})
+
+ const status = computed<ConfigStatus>(() => {
+ return{
+ EpConfig: get(endpointConfig),
+ isDarkMode: get(_darkMode),
+ isValid: !isEmpty(get(endpointConfig).nostrBasePath)
+ }
+ })
//Merge the default config for nullables with the current config on startyup
defaultsDeep(store.value, defaultConfig);
- watch(store, (config, _) => {
+ //Watch for changes to the discovery url, then cause a discovery
+ watch([store], ([{ discoveryUrl }]) => {
+ defer(() => discoverAndSetEndpoints(discoveryUrl, endpointConfig))
+ }, { immediate: true }) //alaways run on startup
+
+ watch([endpointConfig], ([epconf]) => {
//Configure the vnlib api
configureApi({
session: {
@@ -75,10 +124,10 @@ export const useAppSettings = (): AppSettings => {
browserIdSize: 32,
},
user: {
- accountBasePath: config.accountBasePath,
+ accountBasePath: epconf?.accountBasePath,
},
axios: {
- baseURL: config.apiUrl,
+ baseURL: epconf?.apiBaseUrl,
tokenHeader: import.meta.env.VITE_WEB_TOKEN_HEADER,
},
storage: localStorage
@@ -88,18 +137,19 @@ export const useAppSettings = (): AppSettings => {
//Save the config and update the current config
const saveConfig = (config: PluginConfig) => set(store, config);
-
- //Reactive urls for server api
- const { accountBasePath, nostrEndpoint } = toRefs(store)
- const serverApi = useServerApi(nostrEndpoint, accountBasePath)
+
+ //Local reactive server api
+ const serverApi = useServerApi(endpointConfig)
return {
saveConfig,
+ status,
currentConfig: readonly(store),
useStorageSlot: <T>(slot: string, defaultValue: MaybeRefOrGetter<T>) => {
return useStorage<T>(_storageBackend, slot, defaultValue)
},
- useServerApi: () => serverApi
+ useServerApi: () => serverApi,
+ setDarkMode: (darkMode: boolean) => set(_darkMode, darkMode)
}
}
@@ -108,10 +158,8 @@ export const useSettingsApi = () : IFeatureExport<AppSettings, SettingsApi> =>{
return{
background: ({ state }: BgRuntime<AppSettings>) => {
- const _darkMode = shallowRef(false);
-
return {
- waitForChange: waitForChangeFn([state.currentConfig, _darkMode]),
+ waitForChange: waitForChangeFn([state.currentConfig, state.status]),
getSiteConfig: () => Promise.resolve(state.currentConfig.value),
setSiteConfig: optionsOnly(async (config: PluginConfig): Promise<PluginConfig> => {
@@ -123,18 +171,29 @@ export const useSettingsApi = () : IFeatureExport<AppSettings, SettingsApi> =>{
//Return the config
return get(state.currentConfig)
}),
- setDarkMode: popupAndOptionsOnly(async (darkMode: boolean) => {
- _darkMode.value = darkMode
+ setDarkMode: popupAndOptionsOnly((darkMode: boolean) => {
+ state.setDarkMode(darkMode);
+ return Promise.resolve();
}),
- getDarkMode: async () => get(_darkMode),
+ getStatus: () => {
+ //Since value is computed it needs to be manually unwrapped
+ const { isDarkMode, isValid, EpConfig } = get(state.status);
+ return Promise.resolve({ isDarkMode, isValid, EpConfig })
+ },
+ testServerAddress: optionsOnly(async (url: string) => {
+ const res = await fetch(url);
+ const data = await res.json() as ServerDiscoveryResult;
+ return isArray(data?.endpoints) && !isEmpty(data.endpoints);
+ })
}
},
foreground: exportForegroundApi([
'getSiteConfig',
'setSiteConfig',
'setDarkMode',
- 'getDarkMode',
- 'waitForChange'
+ 'waitForChange',
+ 'getStatus',
+ 'testServerAddress'
])
}
} \ No newline at end of file