Browse Source

feat: add credential in storage

Ahmad Kholid 2 years ago
parent
commit
f508041b91

+ 3 - 1
src/background/workflowEngine/blocksHandler/handlerJavascriptCode.js

@@ -17,7 +17,9 @@ export async function javascriptCode({ outputs, data, ...block }, { refData }) {
   }
 
   const payload = { ...block, data, refData: { variables: {} } };
-  if (data.code.includes('automaRefData')) payload.refData = refData;
+  if (data.code.includes('automaRefData')) {
+    payload.refData = { ...refData, secrets: {} };
+  }
 
   if (!data.code.includes('automaNextBlock'))
     payload.data.code += `\nautomaNextBlock()`;

+ 6 - 0
src/background/workflowEngine/engine.js

@@ -60,6 +60,7 @@ class WorkflowEngine {
     this.referenceData = {
       variables,
       table: [],
+      secrets: {},
       loopData: {},
       workflow: {},
       googleSheets: {},
@@ -146,6 +147,11 @@ class WorkflowEngine {
         }
       }
 
+      const credentials = await dbStorage.credentials.toArray();
+      credentials.forEach(({ name, value }) => {
+        this.referenceData.secrets[name] = value;
+      });
+
       const variables = await dbStorage.variables.toArray();
       variables.forEach(({ name, value }) => {
         this.referenceData.variables[`$$${name}`] = value;

+ 151 - 0
src/components/newtab/storage/StorageCredentials.vue

@@ -0,0 +1,151 @@
+<template>
+  <div class="flex mt-6">
+    <ui-input
+      v-model="state.query"
+      :placeholder="t('common.search')"
+      prepend-icon="riSearch2Line"
+    />
+    <div class="flex-grow"></div>
+    <ui-button variant="accent" @click="addState.show = true">
+      {{ t('credential.add') }}
+    </ui-button>
+  </div>
+  <ui-table
+    item-key="id"
+    :headers="tableHeaders"
+    :items="credentials"
+    :search="state.query"
+    class="w-full mt-4"
+  >
+    <template #item-value> ************ </template>
+    <template #item-createdAt="{ item }">
+      {{ dayjs(item.createdAt).format('DD MMMM YYYY, hh:mm A') }}
+    </template>
+    <template #item-actions="{ item }">
+      <v-remixicon
+        name="riDeleteBin7Line"
+        class="cursor-pointer inline-block"
+        @click="deleteCredential(item)"
+      />
+    </template>
+  </ui-table>
+  <ui-modal v-model="addState.show" :title="t('credential.add')">
+    <ui-input v-model="addState.name" placeholder="Name" class="w-full" />
+    <ui-textarea
+      v-model="addState.value"
+      placeholder="value"
+      class="w-full mt-4"
+    />
+    <div class="text-right mt-8">
+      <ui-button class="mr-4" @click="addState.show = false">
+        {{ t('common.cancel') }}
+      </ui-button>
+      <ui-button
+        :disabled="!addState.name"
+        variant="accent"
+        @click="saveCredential"
+      >
+        {{ t('common.save') }}
+      </ui-button>
+    </div>
+  </ui-modal>
+</template>
+<script setup>
+import { shallowReactive, watch } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { useToast } from 'vue-toastification';
+import dayjs from 'dayjs';
+import credentialUtil from '@/utils/credentialUtil';
+import { useLiveQuery } from '@/composable/liveQuery';
+import dbStorage from '@/db/storage';
+
+const { t } = useI18n();
+const toast = useToast();
+const credentials = useLiveQuery(() => dbStorage.credentials.toArray());
+
+const tableHeaders = [
+  {
+    value: 'name',
+    filterable: true,
+    text: t('common.name'),
+    attrs: {
+      class: 'w-3/12 text-overflow',
+    },
+  },
+  {
+    value: 'value',
+    sortable: false,
+    filterable: false,
+    text: 'Value',
+  },
+  {
+    value: 'createdAt',
+    filterable: false,
+    text: 'Created date',
+  },
+  {
+    value: 'actions',
+    filterable: false,
+    sortable: false,
+    text: '',
+    attrs: {
+      class: 'w-24',
+    },
+  },
+];
+
+const state = shallowReactive({
+  id: '',
+  query: '',
+});
+const addState = shallowReactive({
+  type: '',
+  name: '',
+  value: '',
+  show: false,
+});
+
+function deleteCredential({ id }) {
+  dbStorage.credentials.delete(id);
+}
+function saveCredential() {
+  if (!addState.name) return;
+
+  const trimmedName = addState.name.trim();
+  const duplicateName = credentials.value.some(
+    ({ name, id }) => name.trim() === trimmedName && id !== state.id
+  );
+
+  if (duplicateName) {
+    toast.error(`You alread add "${trimmedName}" credential`);
+    return;
+  }
+
+  const encryptedValue = credentialUtil.encrypt(addState.value);
+
+  dbStorage.credentials
+    .add({
+      name: trimmedName,
+      createdAt: Date.now(),
+      value: encryptedValue,
+    })
+    .then(() => {
+      addState.show = false;
+    });
+}
+
+watch(
+  () => addState.show,
+  (value) => {
+    if (value) return;
+
+    state.id = '';
+    Object.assign(addState, {
+      name: '',
+      type: '',
+      value: '',
+      show: false,
+    });
+  }
+);
+</script>

+ 2 - 1
src/db/storage.js

@@ -1,10 +1,11 @@
 import Dexie from 'dexie';
 
 const dbStorage = new Dexie('storage');
-dbStorage.version(1).stores({
+dbStorage.version(2).stores({
   tablesData: '++id, tableId',
   tablesItems: '++id, name, createdAt, modifiedAt',
   variables: '++id, &name',
+  credentials: '++id, &name',
 });
 
 export default dbStorage;

+ 4 - 0
src/locales/en/newtab.json

@@ -32,6 +32,10 @@
       "delete": "Delete table"
     }
   },
+  "credential": {
+    "title": "Credential | Credentials",
+    "add": "Add credential"
+  },
   "workflowPermissions": {
     "title": "Workflow permissions",
     "description": "This workflow requires these permissions to run properly",

+ 33 - 5
src/newtab/pages/Storage.vue

@@ -1,15 +1,28 @@
 <template>
   <div class="container py-8 pb-4">
-    <h1 class="text-2xl font-semibold">
-      {{ t('common.storage') }}
-    </h1>
-    <ui-tabs v-model="state.activeTab" class="mt-5">
+    <div class="flex items-center">
+      <h1 class="text-2xl font-semibold">
+        {{ t('common.storage') }}
+      </h1>
+      <a
+        href="https://docs.automa.site/guide/storage.html"
+        title="Docs"
+        class="text-gray-600 dark:text-gray-200 ml-2"
+        target="_blank"
+      >
+        <v-remixicon name="riInformationLine" size="20" />
+      </a>
+    </div>
+    <ui-tabs v-model="state.activeTab" class="mt-5" @change="onTabChange">
       <ui-tab value="tables">
         {{ t('workflow.table.title', 2) }}
       </ui-tab>
       <ui-tab value="variables">
         {{ t('workflow.variables.title', 2) }}
       </ui-tab>
+      <ui-tab value="credentials">
+        {{ t('credential.title', 2) }}
+      </ui-tab>
     </ui-tabs>
     <ui-tab-panels v-model="state.activeTab">
       <ui-tab-panel value="tables">
@@ -18,18 +31,33 @@
       <ui-tab-panel value="variables">
         <storage-variables />
       </ui-tab-panel>
+      <ui-tab-panel value="credentials">
+        <storage-credentials />
+      </ui-tab-panel>
     </ui-tab-panels>
   </div>
 </template>
 <script setup>
 import { reactive } from 'vue';
 import { useI18n } from 'vue-i18n';
+import { useRoute, useRouter } from 'vue-router';
 import StorageTables from '@/components/newtab/storage/StorageTables.vue';
 import StorageVariables from '@/components/newtab/storage/StorageVariables.vue';
+import StorageCredentials from '@/components/newtab/storage/StorageCredentials.vue';
+
+const tabs = ['tables', 'variables', 'credentials'];
 
 const { t } = useI18n();
+const route = useRoute();
+const router = useRouter();
+
+const { tab } = route.query;
 
 const state = reactive({
-  activeTab: 'tables',
+  activeTab: tabs.includes(tab) ? tab : 'tables',
 });
+
+function onTabChange(value) {
+  router.replace({ query: { tab: value } });
+}
 </script>

+ 32 - 0
src/utils/credentialUtil.js

@@ -0,0 +1,32 @@
+import SHA256 from 'crypto-js/sha256';
+import HmacSHA256 from 'crypto-js/hmac-sha256';
+import AES from 'crypto-js/aes';
+import encUtf8 from 'crypto-js/enc-utf8';
+import getPassKey from './getPassKey';
+import { parseJSON } from './helper';
+
+function encryptValue(value) {
+  const pass = getPassKey('credential');
+  const encryptedValue = AES.encrypt(value, pass).toString();
+  const hmac = HmacSHA256(encryptedValue, SHA256(pass)).toString();
+
+  return hmac + encryptedValue;
+}
+
+function decryptValue(value) {
+  const pass = getPassKey('credential');
+  const hmac = value.substring(0, 64);
+  const encryptedValue = value.substring(64);
+  const decryptedHmac = HmacSHA256(encryptedValue, SHA256(pass)).toString();
+
+  if (hmac !== decryptedHmac) return '';
+
+  const decryptedValue = AES.decrypt(encryptedValue, pass).toString(encUtf8);
+
+  return parseJSON(decryptedValue, decryptedValue);
+}
+
+export default {
+  encrypt: encryptValue,
+  decrypt: decryptValue,
+};

+ 6 - 1
src/utils/referenceData/mustacheReplacer.js

@@ -1,6 +1,7 @@
 import objectPath from 'object-path';
 import dayjs from '@/lib/dayjs';
 import { parseJSON } from '@/utils/helper';
+import credentialUtil from '@/utils/credentialUtil';
 
 const refKeys = {
   table: 'table',
@@ -191,8 +192,12 @@ function replacer(str, { regex, tagLen, modifyPath, data, stringify }) {
       result = funcRef.apply({ refData: data }, funcParams);
     } else {
       const { dataKey, path } = keyParser(key, data);
-
       result = objectPath.get(data[dataKey], path) ?? match;
+
+      if (dataKey === 'secrets') {
+        result =
+          typeof result !== 'string' ? {} : credentialUtil.decrypt(result);
+      }
     }
 
     result =