aboutsummaryrefslogtreecommitdiff
path: root/extension/src/entries/options/components
diff options
context:
space:
mode:
authorLibravatar vnugent <public@vaughnnugent.com>2023-09-06 13:51:13 -0400
committerLibravatar vnugent <public@vaughnnugent.com>2023-09-06 13:51:13 -0400
commitcd8e865dad326f85ff2357ad90bbd6aa65dea68e (patch)
tree0d4a0bb8bafc4f807407e99c5e6bf4e1cb34217a /extension/src/entries/options/components
initial commit
Diffstat (limited to 'extension/src/entries/options/components')
-rw-r--r--extension/src/entries/options/components/Identities.vue188
-rw-r--r--extension/src/entries/options/components/Privacy.vue9
-rw-r--r--extension/src/entries/options/components/SiteSettings.vue235
3 files changed, 432 insertions, 0 deletions
diff --git a/extension/src/entries/options/components/Identities.vue b/extension/src/entries/options/components/Identities.vue
new file mode 100644
index 0000000..c86a6ac
--- /dev/null
+++ b/extension/src/entries/options/components/Identities.vue
@@ -0,0 +1,188 @@
+<template>
+ <div class="sm:px-3">
+ <div class="flex justify-end gap-2">
+ <div class="">
+ <div class="">
+ <button class="rounded btn sm" @click="onNip05Download">
+ NIP-05
+ <fa-icon icon="download" class="ml-1" />
+ </button>
+ </div>
+ </div>
+ <div class="mb-2">
+ <Popover class="relative" v-slot="{ open }">
+ <PopoverButton class="rounded btn primary sm">Create</PopoverButton>
+ <PopoverOverlay v-if="open" class="fixed inset-0 bg-black opacity-30" />
+ <PopoverPanel class="absolute z-10 mt-2 md:-left-12" v-slot="{ close }">
+ <div class="p-4 bg-white border border-gray-200 rounded-md shadow-lg dark:border-dark-300 dark:bg-dark-700">
+ <div class="text-sm w-72">
+ <form @submit.prevent="e => onCreate(e, close)">
+ Create new nostr identity
+ <div class="mt-2">
+ <input class="w-full primary" type="text" name="username" placeholder="User Name"/>
+ </div>
+ <div class="mt-2">
+ <input class="w-full primary" type="text" name="key" placeholder="Existing key?"/>
+ <div class="p-1.5 text-xs text-gray-600 dark:text-gray-300">
+ Optional, hexadecimal private key (64 characters)
+ </div>
+ </div>
+ <div class="flex justify-end mt-2">
+ <button class="rounded btn sm primary" type="submit">Create</button>
+ </div>
+ </form>
+ </div>
+ </div>
+ </PopoverPanel>
+ </Popover>
+ </div>
+ </div>
+ <div v-for="key in allKeys" :key="key" class="mt-2 mb-3">
+ <div class="id-card" :class="{'selected': isSelected(key)}" @click.self="selectKey(key)">
+
+ <div class="flex flex-col min-w-0" @click="selectKey(key)">
+ <div class="py-2">
+
+ <table class="w-full text-sm text-left border-collapse">
+ <thead class="">
+ <tr>
+ <th scope="col" class="p-2 font-medium">Nip 05</th>
+ <th scope="col" class="p-2 font-medium">Modified</th>
+ <th scope="col" class="p-2 font-medium"></th>
+ </tr>
+ </thead>
+ <tbody class="border-t border-gray-100 divide-y divide-gray-100 dark:border-dark-500 dark:divide-dark-500">
+ <tr>
+ <th class="p-2 font-medium">{{ key.UserName }}</th>
+ <td class="p-2">{{ prettyPrintDate(key) }}</td>
+ <td class="flex justify-end p-2 ml-auto text-sm font-medium">
+ <div class="ml-auto button-group">
+ <button class="btn sm borderless" @click="copy(key.PublicKey)">
+ <fa-icon icon="copy"/>
+ </button>
+ <button class="btn sm borderless" @click="editKey(key)">
+ <fa-icon icon="edit"/>
+ </button>
+ <button class="btn sm red borderless" @click="onDeleteKey(key)">
+ <fa-icon icon="trash" />
+ </button>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+
+ </div>
+ <div class="py-2 overflow-hidden border-gray-500 border-y dark:border-dark-500 text-ellipsis">
+ <span class="font-semibold">pub:</span>
+ <span class="ml-1">{{ key.PublicKey }}</span>
+ </div>
+ <div class="py-2">
+ <strong>Id:</strong> {{ key.Id }}
+ </div>
+ </div>
+ </div>
+ </div>
+ <a class="hidden" ref="downloadAnchor"></a>
+ </div>
+</template>
+
+<script setup lang="ts">
+
+import { isEqual, map } from 'lodash'
+import { ref, toRefs } from "vue";
+import {
+ Popover,
+ PopoverButton,
+ PopoverPanel
+} from '@headlessui/vue'
+import { apiCall, configureNotifier } from '@vnuge/vnlib.browser';
+import { useManagment, useStatus } from '~/bg-api/options.ts';
+import { notify } from "@kyvg/vue3-notification";
+import { useClipboard } from '@vueuse/core';
+import { NostrIdentiy } from '~/bg-api/bg-api';
+import { NostrPubKey } from '../../background/types';
+
+const emit = defineEmits(['edit-key', 'update-all'])
+const props = defineProps<{
+ allKeys:NostrIdentiy[]
+}>()
+
+const { allKeys } = toRefs(props)
+
+//Configre the notifier to use the toaster
+configureNotifier({ notify, close: notify.close })
+
+const downloadAnchor = ref<HTMLAnchorElement>()
+const { selectedKey } = useStatus()
+const { selectKey, createIdentity, deleteIdentity, getAllKeys } = useManagment()
+const { copy } = useClipboard()
+
+const isSelected = (me : NostrIdentiy) => isEqual(me, selectedKey.value)
+
+const editKey = (key : NostrIdentiy) => emit('edit-key', key);
+
+const onCreate = async (e: Event, onClose : () => void) => {
+
+ //get username input from event
+ const UserName = e.target['username']?.value as string
+ //try to get existing key field
+ const ExistingKey = e.target['key']?.value as string
+
+ //Create new identity
+ await createIdentity({ UserName, ExistingKey })
+ //Update keys
+ emit('update-all');
+ onClose()
+}
+
+const prettyPrintDate = (key : NostrIdentiy) => {
+ const d = new Date(key.LastModified)
+ return `${d.toLocaleDateString()} ${d.toLocaleTimeString()}`
+}
+
+const onDeleteKey = async (key : NostrIdentiy) => {
+
+ if(!confirm(`Are you sure you want to delete ${key.UserName}?`)){
+ return;
+ }
+
+ //Delete identity
+ await deleteIdentity(key)
+
+ //Update keys
+ emit('update-all');
+}
+
+const onNip05Download = () => {
+ apiCall(async () => {
+ //Get all public keys from the server
+ const keys = await getAllKeys() as NostrPubKey[]
+ const nip05 = {}
+ //Map the keys to the NIP-05 format
+ map(keys, k => nip05[k.UserName] = k.PublicKey)
+ //create file blob
+ const blob = new Blob([JSON.stringify({ names:nip05 })], { type: 'application/json' })
+
+ //Download the file
+ downloadAnchor.value!.href = URL.createObjectURL(blob);
+ downloadAnchor.value?.setAttribute('download', 'nostr.json')
+ downloadAnchor.value?.click();
+
+ })
+}
+
+</script>
+
+<style scoped lang="scss">
+
+.id-card{
+ @apply flex md:flex-row flex-col gap-2 p-3 px-12 text-sm duration-75 ease-in-out border-2 rounded-lg shadow-md cursor-pointer w-fit mx-auto;
+ @apply bg-white dark:bg-dark-800 border-gray-200 hover:border-gray-400 dark:border-dark-500 hover:dark:border-dark-200;
+
+ &.selected{
+ @apply border-primary-500 hover:border-primary-500;
+ }
+}
+
+</style> \ No newline at end of file
diff --git a/extension/src/entries/options/components/Privacy.vue b/extension/src/entries/options/components/Privacy.vue
new file mode 100644
index 0000000..7d2ce4d
--- /dev/null
+++ b/extension/src/entries/options/components/Privacy.vue
@@ -0,0 +1,9 @@
+<template>
+ <div class="flex flex-col w-full mt-4 sm:px-2">
+
+ </div>
+</template>
+
+<script setup lang="ts">
+
+</script> \ No newline at end of file
diff --git a/extension/src/entries/options/components/SiteSettings.vue b/extension/src/entries/options/components/SiteSettings.vue
new file mode 100644
index 0000000..eafe8f3
--- /dev/null
+++ b/extension/src/entries/options/components/SiteSettings.vue
@@ -0,0 +1,235 @@
+<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">
+ <h3 class="text-center">
+ Extension settings
+ </h3>
+ <div class="my-6">
+ <fieldset :disabled="waiting">
+ <div class="w-full">
+ <div class="flex flex-row justify-between">
+ <label class="mr-2">Always on NIP-07</label>
+ <Switch
+ v-model="buffer.autoInject"
+ :class="buffer.autoInject ? 'bg-primary-500 dark:bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'"
+ class="relative inline-flex items-center h-6 ml-auto rounded-full w-11"
+ >
+ <span class="sr-only">NIP-07</span>
+ <span
+ :class="buffer.autoInject ? 'translate-x-6' : 'translate-x-1'"
+ class="inline-block w-4 h-4 transition transform bg-white rounded-full"
+ />
+ </Switch>
+ </div>
+ </div>
+ <p class="mt-1 text-xs">
+ Enable auto injection of <code>window.nostr</code> support to all websites. Sites may be able to
+ track you if you enable this feature.
+ </p>
+ </fieldset>
+ </div>
+ <h3 class="text-center">
+ Server settings
+ </h3>
+ <p class="text-sm">
+ You must be careful when editing these settings as you may loose connection to your vault
+ server if you input the wrong values.
+ </p>
+ <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"/>
+ </button>
+ <a :href="data.apiUrl" target="_blank">
+ <button type="button" class="rounded btn sm">
+ <fa-icon icon="external-link-alt"/>
+ </button>
+ </a>
+ </div>
+ </div>
+ <fieldset :disabled="waiting || !editMode">
+ <div class="pl-1 mt-2">
+ <div class="flex flex-row w-full">
+ <div>
+ <label class="mb-2">Stay logged in</label>
+ <Switch
+ v-model="v$.heartbeat.$model"
+ :class="v$.heartbeat.$model ? 'bg-primary-500 dark:bg-primary-600' : 'bg-gray-200 dark:bg-dark-600'"
+ class="relative inline-flex items-center h-6 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 bg-white rounded-full"
+ />
+ </Switch>
+ </div>
+ <div class="my-auto text-xs">
+ Enables keepalive messages to regenerate credentials when they expire
+ </div>
+ </div>
+ </div>
+ <div class="mt-2">
+ <label class="pl-1">BaseUrl</label>
+ <input class="w-full primary" v-model="v$.apiUrl.$model" :class="{'error': v$.apiUrl.$invalid }" />
+ <p class="pl-1 mt-1 text-xs text-red-500">
+ * 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 primary" v-model="v$.accountBasePath.$model" :class="{ 'error': v$.accountBasePath.$invalid }" />
+ <p class="pl-1 mt-1 text-xs text-red-500">
+ * 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 primary" v-model="v$.nostrEndpoint.$model" :class="{ 'error': v$.nostrEndpoint.$invalid }" />
+ <p class="pl-1 mt-1 text-xs text-red-500">
+ * This is the path to the Nostr plugin endpoint path (must start with /)
+ </p>
+ </div>
+ </fieldset>
+ <div class="flex justify-end mt-2">
+ <button :disabled="!modified || waiting" class="rounded btn sm" :class="{'primary':modified}" @click="onSave">Save</button>
+ </div>
+ </div>
+ </form>
+ </div>
+</template>
+
+<script setup lang="ts">
+import { apiCall, useDataBuffer, useFormToaster, useVuelidateWrapper, useWait } from '@vnuge/vnlib.browser';
+import { computed, ref, watch } from 'vue';
+import { useManagment } from '~/bg-api/options.ts';
+import { useToggle, watchDebounced } from '@vueuse/core';
+import { maxLength, helpers, required } from '@vuelidate/validators'
+import { clone, isNil } from 'lodash';
+import{ Switch } from '@headlessui/vue'
+import useVuelidate from '@vuelidate/core'
+
+const { waiting } = useWait();
+const form = useFormToaster();
+const { getSiteConfig, saveSiteConfig } = useManagment();
+
+const { apply, data, buffer, modified } = useDataBuffer({
+ apiUrl: '',
+ accountBasePath: '',
+ nostrEndpoint:'',
+ heartbeat:false,
+ autoInject:true,
+})
+
+const url = (val : string) => /^https?:\/\/[a-zA-Z0-9\.\:\/-]+$/.test(val);
+const path = (val : string) => /^\/[a-zA-Z0-9-_]+$/.test(val);
+
+const vRules = {
+ apiUrl: {
+ 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)
+ },
+ heartbeat: {},
+ darkMode:{}
+}
+
+//Configure validator and validate function
+const v$ = useVuelidate(vRules, buffer)
+const { validate } = useVuelidateWrapper(v$);
+
+const editMode = ref(false);
+const toggleEdit = useToggle(editMode);
+
+const autoInject = computed(() => buffer.autoInject)
+
+const onSave = async () => {
+
+ //Validate
+ const result = await validate();
+ if(!result){
+ return;
+ }
+
+ //Test connection to the server
+ if(await testConnection() !== true){
+ return;
+ }
+
+ form.info({
+ title: 'Reloading in 4 seconds',
+ text: 'Your configuration will be saved and the extension will reload in 4 seconds'
+ })
+
+ await new Promise(r => setTimeout(r, 4000));
+
+ publishConfig();
+
+ //disable dit
+ toggleEdit();
+}
+
+const publishConfig = async () =>{
+ const c = clone(buffer);
+ await saveSiteConfig(c);
+ await loadConfig();
+}
+
+const testConnection = async () =>{
+ return await apiCall(async ({axios, toaster}) =>{
+ try{
+ await axios.get(`${buffer.apiUrl}`);
+ toaster.general.success({
+ title: 'Success',
+ text: 'Succcesfully connected to the vault server'
+ });
+ return true;
+ }
+ catch(e){
+ if(isNil(e.response?.status)){
+ toaster.form.error({
+ title: 'Network error',
+ text: `Please verify your vault server address`
+ });
+ }
+
+ toaster.form.error({
+ title: 'Warning',
+ text: `Failed to connect to the vault server. Status code: ${e.response.status}`
+ });
+ }
+ })
+}
+
+const loadConfig = async () => {
+ const config = await getSiteConfig();
+ apply(config);
+
+ //Watch for changes to autoinject value and publish changes when it does
+ watchDebounced(autoInject, publishConfig, { debounce: 500 })
+}
+
+//If edit mode is toggled off, reload config
+watch(editMode, v => v ? null : loadConfig());
+
+
+loadConfig();
+
+</script>
+
+<style lang="scss">
+
+</style> \ No newline at end of file