diff options
Diffstat (limited to 'extension/src')
-rw-r--r-- | extension/src/entries/options/components/Activity.vue | 14 | ||||
-rw-r--r-- | extension/src/entries/options/components/EventHistory.vue | 164 | ||||
-rw-r--r-- | extension/src/entries/options/components/Identities.vue | 8 | ||||
-rw-r--r-- | extension/src/entries/options/components/SiteSettings.vue | 167 | ||||
-rw-r--r-- | extension/src/entries/options/index.html | 2 | ||||
-rw-r--r-- | extension/src/entries/options/main.js | 4 | ||||
-rw-r--r-- | extension/src/entries/popup/Components/OtpLogin.vue | 6 | ||||
-rw-r--r-- | extension/src/entries/popup/Components/PageContent.vue | 192 | ||||
-rw-r--r-- | extension/src/entries/popup/Components/PassLogin.vue | 13 | ||||
-rw-r--r-- | extension/src/entries/store/features.ts | 12 | ||||
-rw-r--r-- | extension/src/entries/store/index.ts | 53 | ||||
-rw-r--r-- | extension/src/entries/store/types.ts | 10 | ||||
-rw-r--r-- | extension/src/features/index.ts | 2 | ||||
-rw-r--r-- | extension/src/features/server-api/index.ts | 30 | ||||
-rw-r--r-- | extension/src/features/settings.ts | 111 |
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 |