Browse Source

refactor(webapp): removes code dup in custom domain setup

fixes #491
Nils Wisiol 4 years ago
parent
commit
43e68adfd8

+ 11 - 1
webapp/src/components/ActivateAccountActionHandler.vue

@@ -19,7 +19,17 @@
             let token = this.response.data.token;
             this.$router.push({ name: 'dynSetup', params: { domain: domain.name }, hash: `#${token}` });
           } else {
-            this.$router.push({ name: 'customSetup', params: { domain: domain.name, keys: domain.keys } });
+            let ds = domain.keys.map(key => key.ds);
+            ds = ds.concat.apply([], ds)
+            this.$router.push({
+              name: 'customSetup',
+              params: {
+                domain: domain.name,
+                ds: ds,
+                dnskey: domain.keys.map(key => key.dnskey),
+                isNew: true,
+              },
+            });
           }
         }
       }

+ 1 - 1
webapp/src/main.js

@@ -11,9 +11,9 @@ import VueTimeago from 'vue-timeago'
 
 
 Vue.config.productionTip = false
+VueClipboard.config.autoSetContainer = true
 Vue.use(VueClipboard)
 Vue.use(Vuelidate)
-
 Vue.use(VueTimeago, {})
 
 new Vue({

+ 2 - 1
webapp/src/router/index.js

@@ -24,7 +24,8 @@ const routes = [
   {
     path: '/custom-setup/:domain',
     name: 'customSetup',
-    component: () => import(/* webpackChunkName: "signup" */ '../views/CustomSetup.vue')
+    component: () => import(/* webpackChunkName: "signup" */ '../views/DomainSetupPage'),
+    props: true,
   },
   {
     path: '/dyn-setup/:domain',

+ 0 - 227
webapp/src/views/Console/DomainDetailsDialog.vue

@@ -1,227 +0,0 @@
-<template>
-  <v-dialog
-    v-model="show"
-    max-width="700px"
-    persistent
-    @keydown.esc="close"
-  >
-    <v-card>
-      <v-card-title>
-        <div class="title">
-          Domain details for <b>{{ name }}</b>
-        </div>
-        <v-spacer />
-        <v-icon @click.stop="close">
-          mdi-close
-        </v-icon>
-      </v-card-title>
-      <v-divider />
-      <v-alert
-        :value="isNew"
-        type="success"
-      >
-        Your domain <b>{{ name }}</b> has been successfully created!
-      </v-alert>
-      <v-card-text>
-        <div class="mt-2 subtitle-1"><v-icon>mdi-numeric-1-circle</v-icon> Delegate your domain</div>
-        <p>
-          Forward the following information to the organization/person where you bought the domain
-          <strong>{{name}}</strong> (usually your provider or technical administrator):
-        </p>
-        <v-layout flex align-end>
-          <div class="caption font-weight-medium">NS records</div>
-          <!--v-spacer></v-spacer>
-          <div v-if="copied != 'ns'">
-            <v-icon
-                    small
-                    v-clipboard:copy="ns.join('\n')"
-                    v-clipboard:success="() => (copied = 'ns')"
-                    v-clipboard:error="() => (copied = '')"
-            >mdi-content-copy</v-icon>
-          </div>
-          <div v-else>copied! <v-icon small>mdi-check</v-icon></div-->
-        </v-layout>
-        <pre
-                class="mb-3 pa-3"
-                v-clipboard:copy="ns.join('\n')"
-                v-clipboard:success="() => (copied = 'ns')"
-                v-clipboard:error="() => (copied = '')"
-        >{{ ns.join('\n') }}</pre>
-
-        <p>
-          Once your provider processes this information, the Internet will start directing DNS queries to deSEC.
-        </p>
-
-        <div class="subtitle-1"><v-icon>mdi-numeric-2-circle</v-icon> Enable DNSSEC</div>
-        <p>
-          You also need to forward <strong>either the <span class="code">DS</span> records <em>or</em> the
-          <span class="code">DNSKEY</span> record(s)</strong> to your domain provider (depending on what they accept).
-          This is required to enable DNSSEC security.
-        </p>
-
-        <div v-if="ds.length > 0">
-          <v-layout flex align-end>
-            <div class="caption font-weight-medium">DS records</div>
-            <!--v-spacer></v-spacer>
-            <div v-if="copied != 'ds'">
-              <v-icon
-                      small
-                      v-clipboard:copy="ds.join('\n')"
-                      v-clipboard:success="() => (copied = 'ds')"
-                      v-clipboard:error="() => (copied = '')"
-              >mdi-content-copy</v-icon>
-            </div>
-            <div v-else>copied! <v-icon small>mdi-check</v-icon></div-->
-          </v-layout>
-          <pre
-                  class="mb-3 pa-3"
-                  v-clipboard:copy="ds.join('\n')"
-                  v-clipboard:success="() => (copied = 'ds')"
-                  v-clipboard:error="() => (copied = '')"
-          >{{ ds.join('\n') }}</pre>
-        </div>
-        <div v-else>
-          <div class="caption font-weight-medium">DS records</div>
-          <p>(unavailable, please contact support)</p>
-        </div>
-
-        <div v-if="dnskey.length > 0">
-          <v-layout flex align-end>
-            <div class="caption font-weight-medium">DNSKEY records</div>
-            <!--v-spacer></v-spacer>
-            <div v-if="copied != 'dnskey'">
-              <v-icon
-                      small
-                      v-clipboard:copy="dnskey.join('\n')"
-                      v-clipboard:success="() => (copied = 'dnskey')"
-                      v-clipboard:error="() => (copied = '')"
-              >mdi-content-copy</v-icon>
-            </div>
-            <div v-else>copied! <v-icon small>mdi-check</v-icon></div-->
-          </v-layout>
-          <pre
-                  class="mb-3 pa-3"
-                  v-clipboard:copy="dnskey.join('\n')"
-                  v-clipboard:success="() => (copied = 'dnskey')"
-                  v-clipboard:error="() => (copied = '')"
-          >{{ dnskey.join('\n') }}</pre>
-        </div>
-        <div v-else>
-          <div class="caption font-weight-medium">DNSKEY records</div>
-          <p>(unavailable, please contact support)</p>
-        </div>
-
-        <div v-if="this.LOCAL_PUBLIC_SUFFIXES.some((suffix) => name.endsWith(`.${suffix}`))">
-          <v-divider class="pb-3"></v-divider>
-          <p>
-            The IP <span v-if="ips.length == 1">address</span><span v-else>addresses</span> associated with
-            this domain <span v-if="ips.length == 1">is:</span><span v-else>are:</span>
-          </p>
-          <ul class="mb-4">
-            <li v-for="ip in ips" :key="ip"><span class="fixed-width">{{ip}}</span></li>
-            <li v-if="!ips.length">(none)</li>
-          </ul>
-        </div>
-        <p>
-          All set up correctly? <a :href="`https://dnssec-analyzer.verisignlabs.com/${name}`" target="_blank">Take a
-          look at DNSSEC Analyzer to check the status of your domain.</a>
-        </p>
-
-        <v-divider></v-divider>
-
-        <p class="mt-4">
-          The DNS information of this domain was last changed {{ published ? timeAgo.format(new Date(published)) : 'never' }}.
-        </p>
-      </v-card-text>
-      <v-card-actions class="pa-3">
-        <v-spacer />
-        <v-btn depressed :to="{name: 'donate'}">Donate</v-btn>
-        <v-btn
-          color="primary"
-          dark
-          depressed
-          @click.native="close"
-        >
-          Close
-        </v-btn>
-        <v-spacer />
-      </v-card-actions>
-    </v-card>
-  </v-dialog>
-</template>
-
-<script>
-import { timeAgo } from '@/utils';
-
-export default {
-  name: 'DomainDetailsDialog',
-  props: {
-    name: {
-      type: String,
-      required: true,
-    },
-    isNew: {
-      type: Boolean,
-      default: false,
-    },
-    ds: {
-      type: Array,
-      required: true,
-    },
-    dnskey: {
-      type: Array,
-      required: true,
-    },
-    ns: {
-      type: Array,
-      default: () => process.env.VUE_APP_DESECSTACK_NS.split(' '),
-    },
-    ips: {
-      type: Array,
-      default: () => [],
-    },
-    published: {
-      type: String,
-      default: '(unknown)',
-    },
-    value: {
-      type: Boolean,
-      default: true,
-    },
-  },
-  data: () => ({
-    copied: '',
-    LOCAL_PUBLIC_SUFFIXES: process.env.VUE_APP_LOCAL_PUBLIC_SUFFIXES.split(' '),
-    timeAgo: timeAgo,
-  }),
-  computed: {
-    show: {
-      get() {
-        return this.value
-      },
-      set(value) {
-         this.$emit('input', value)
-      }
-    }
-  },
-  methods: {
-    close() {
-      this.show = false;
-      this.copied = '';
-    },
-  },
-};
-</script>
-
-<style scoped>
-  .caption {
-    text-transform: uppercase;
-  }
-  .code {
-    font-family: monospace;
-  }
-  pre {
-    background: lightgray;
-    overflow: auto;
-  }
-</style>

+ 65 - 0
webapp/src/views/Console/DomainSetupDialog.vue

@@ -0,0 +1,65 @@
+<template>
+  <v-dialog
+    v-model="show"
+    max-width="700px"
+    persistent
+    @keydown.esc="close"
+  >
+    <v-card>
+      <v-card-title>
+        <div class="title">
+          Setup Instructions for <b>{{ domain }}</b>
+        </div>
+        <v-spacer/>
+        <v-icon @click.stop="close">
+          mdi-close
+        </v-icon>
+      </v-card-title>
+      <v-divider/>
+
+      <domain-setup v-bind="$attrs"></domain-setup>
+
+      <v-divider/>
+      <v-card-actions>
+        <v-spacer/>
+        <v-btn flat @click.stop="close">Close</v-btn>
+      </v-card-actions>
+    </v-card>
+  </v-dialog>
+</template>
+
+<script>
+import DomainSetup from "@/views/DomainSetup";
+
+export default {
+  name: 'DomainSetupPage',
+  components: { DomainSetup },
+  props: {
+    'domain': {
+      type: String,
+      required: true,
+    }
+  },
+  data: () => ({
+    value: {
+      type: Boolean,
+      default: true,
+    },
+  }),
+  computed: {
+    show: {
+      get() {
+        return this.value
+      },
+      set(value) {
+         this.$emit('input', value)
+      }
+    }
+  },
+  methods: {
+    close() {
+      this.show = false;
+    }
+  },
+};
+</script>

+ 0 - 129
webapp/src/views/CustomSetup.vue

@@ -1,129 +0,0 @@
-<template>
-  <v-container
-          class="fill-height"
-          fluid
-  >
-    <v-row
-            align="center"
-            justify="center"
-    >
-      <v-col
-              cols="12"
-              sm="8"
-              md="6"
-      >
-        <v-card class="elevation-12">
-          <v-toolbar
-                  color="primary"
-                  dark
-                  flat
-          >
-            <v-toolbar-title>Domain Registration Completed</v-toolbar-title>
-          </v-toolbar>
-          <v-card-text>
-            <p>
-              Congratulations, you have now configured <span class="fixed-width">{{ $route.params.domain }}</span>!
-            </p>
-            <h2 class="title">Secure Your Domain</h2>
-            <p>
-              Please forward the following information to the organization/person where you bought the domain
-              <code>{{$route.params.domain}}</code>
-              (usually your provider or technical administrator):
-            </p>
-            <v-layout flex align-end>
-              <div class="caption font-weight-medium">NS records</div>
-              <v-spacer></v-spacer>
-              <div v-if="copied != 'ns'">
-                <v-icon
-                  small
-                  v-clipboard:copy="nsList.join('\n')"
-                  v-clipboard:success="() => (copied = 'ns')"
-                  v-clipboard:error="() => (copied = '')"
-                >mdi-content-copy</v-icon>
-              </div>
-              <div v-else>copied! <v-icon small>mdi-check</v-icon></div>
-            </v-layout>
-            <pre
-              class="mb-3 pa-3"
-              v-clipboard:copy="nsList.join('\n')"
-              v-clipboard:success="() => (copied = 'ns')"
-              v-clipboard:error="() => (copied = '')"
-            >{{ nsList.join('\n') }}</pre>
-
-            <div v-if="dsList.length > 0">
-              <v-layout flex align-end>
-                <div class="caption font-weight-medium">DS records (also available via our API)</div>
-                <v-spacer></v-spacer>
-                <div v-if="copied != 'ds'">
-                  <v-icon
-                    small
-                    v-clipboard:copy="dsList.join('\n')"
-                    v-clipboard:success="() => (copied = 'ds')"
-                    v-clipboard:error="() => (copied = '')"
-                  >mdi-content-copy</v-icon>
-                </div>
-                <div v-else>copied! <v-icon small>mdi-check</v-icon></div>
-              </v-layout>
-              <pre
-                class="mb-3 pa-3"
-                v-clipboard:copy="dsList.join('\n')"
-                v-clipboard:success="() => (copied = 'ds')"
-                v-clipboard:error="() => (copied = '')"
-              >{{ dsList.join('\n') }}</pre>
-            </div>
-            <div v-else>
-              <div class="caption font-weight-medium">DS records (also available via our API)</div>
-              <p>(unavailable)</p>
-            </div>
-
-            <p>Once your domain registrar processes this information, your deSEC DNS setup will be ready to use.</p>
-            <p>
-              Questions? Please check out our forum at <a href="https://talk.desec.io/">talk.desec.io</a>. Chances are
-              that someone had the same question before.
-            </p>
-
-            <h2 class="title">Keep deSEC Going</h2>
-            <p>
-              To offer free DNS hosting for everyone, deSEC relies on donations only.
-              If you like our service, please consider donating.
-            </p>
-            <p>
-              <v-btn block outlined :to="{name: 'donate'}">Donate</v-btn>
-            </p>
-          </v-card-text>
-          <v-card-actions>
-            <v-spacer />
-          </v-card-actions>
-        </v-card>
-      </v-col>
-    </v-row>
-  </v-container>
-</template>
-
-<script>
-  export default {
-    name: 'CustomSetup',
-    data: () => ({
-      copied: '',
-      dsList: [],
-      nsList: process.env.VUE_APP_DESECSTACK_NS.split(' '),
-    }),
-    async mounted() {
-      let keys = this.$route.params.keys;
-      if (keys) {
-        this.dsList = keys.map(key => key.ds);
-        this.dsList = this.dsList.concat.apply([], this.dsList);
-      }
-    },
-  };
-</script>
-
-<style lang="scss" scoped>
-  .fixed-width {
-    font-family: monospace;
-  }
-  pre {
-    background: lightgray;
-    overflow: auto;
-  }
-</style>

+ 4 - 14
webapp/src/views/DomainList.vue

@@ -1,13 +1,13 @@
 <script>
 import { HTTP, withWorking } from '@/utils';
 import CrudList from './CrudList';
-import DomainDetailsDialog from '@/views/Console/DomainDetailsDialog';
+import DomainSetupDialog from '@/views/Console/DomainSetupDialog';
 
 export default {
   name: 'DomainList',
   extends: CrudList,
   components: {
-    DomainDetailsDialog,
+    DomainSetupDialog,
   },
   data() {
     const self = this;
@@ -77,24 +77,14 @@ export default {
                 .get(url)
                 .then(r => {
                   d.keys = r.data.keys;
-                  d.published = r.data.published;
                 })
             );
           }
           let ds = d.keys.map(key => key.ds);
           ds = ds.concat.apply([], ds)
           let dnskey = d.keys.map(key => key.dnskey);
-          this.extraComponentBind = {'name': d.name, 'ds': ds, 'dnskey': dnskey, 'published': d.published, 'is-new': isNew};
-          if (process.env.VUE_APP_LOCAL_PUBLIC_SUFFIXES.split(' ').some((suffix) => d.name.endsWith(`.${suffix}`))) {
-            this.extraComponentBind['ips'] = [];
-            await withWorking(this.error, async (o) => {
-              let urlRRset = `${url}/rrsets/?subname=&type=`;
-              for (let type of ['A', 'AAAA']) {
-                await HTTP.get(`${urlRRset}${type}`).then(r => r.data.length && o['ips'].push(...r.data[0].records));
-              }
-            }, this.extraComponentBind);
-          }
-          this.extraComponentName = 'DomainDetailsDialog';
+          this.extraComponentBind = {'domain': d.name, 'ds': ds, 'dnskey': dnskey, 'is-new': isNew};
+          this.extraComponentName = 'DomainSetupDialog';
         },
         handleRowClick: (value) => {
           this.$router.push({name: 'domain', params: {domain: value.name}});

+ 214 - 0
webapp/src/views/DomainSetup.vue

@@ -0,0 +1,214 @@
+<template>
+  <div>
+    <v-alert
+        :value="isNew"
+        type="success"
+    >
+      Your domain <b>{{ domain }}</b> has been successfully created!
+    </v-alert>
+    <v-card-text>
+      <p>
+        The following steps need to be completed in order to use
+        <span class="fixed-width">{{ domain }}</span> with deSEC.
+      </p>
+
+      <div class="mt-2 subtitle-1">
+        <v-icon>mdi-numeric-1-circle</v-icon>
+        Delegate your domain
+      </div>
+
+      <p>
+        Forward the following information to the organization/person where you bought the domain
+        <strong>{{ domain }}</strong> (usually your provider or technical administrator):
+      </p>
+      <v-card>
+        <v-tabs v-model="tab1" background-color="transparent" grow>
+          <v-tab href="#ns">Nameservers</v-tab>
+        </v-tabs>
+
+        <v-tabs-items v-model="tab1" class="mb-3">
+          <v-tab-item value="ns">
+            <v-card flat v-if="ns.join('\n')">
+              <pre class="pa-3">{{ ns.join('\n') }}</pre>
+              <v-card-actions>
+                <v-btn
+                    v-clipboard:copy="ns.join('\n')"
+                    v-clipboard:success="copySuccess"
+                    v-clipboard:error="copyError"
+                    outlined
+                    text
+                >
+                  <v-icon>mdi-content-copy</v-icon>
+                  copy to clipboard
+                </v-btn>
+                <v-spacer></v-spacer>
+              </v-card-actions>
+            </v-card>
+            <v-card flat v-else>
+              <v-card-text>Nameservers could not be retrieved.</v-card-text>
+            </v-card>
+          </v-tab-item>
+        </v-tabs-items>
+      </v-card>
+
+      <p>
+        Once your provider processes this information, the Internet will start directing DNS queries to deSEC.
+      </p>
+
+      <div class="subtitle-1">
+        <v-icon>mdi-numeric-2-circle</v-icon>
+        Enable DNSSEC
+      </div>
+      <p>
+        To enable DNSSEC security, you also need to forward one or more of the following information to your
+        domain provider. Which information they accept varies from provider to provider; unfortunately there are
+        also providers that do not support DNSSEC altogether.
+      </p>
+
+      <v-card>
+        <v-tabs
+            v-model="tab2"
+            background-color="transparent"
+            grow
+        >
+          <v-tab v-for="t in tabs" :key="t.key" :href="'#' + t.key">{{ t.title }}</v-tab>
+        </v-tabs>
+
+        <v-tabs-items v-model="tab2" class="mb-4">
+          <v-tab-item v-for="t in tabs" :key="t.key" :value="t.key">
+            <v-card flat v-if="t.data">
+              <v-card-text>{{ t.banner }}</v-card-text>
+              <pre class="pa-3">{{ t.data }}</pre>
+              <v-card-actions>
+                <v-btn
+                    v-clipboard:copy="t.data"
+                    v-clipboard:success="copySuccess"
+                    v-clipboard:error="copyError"
+                    outlined
+                    text
+                >
+                  <v-icon>mdi-content-copy</v-icon>
+                  copy to clipboard
+                </v-btn>
+                <v-spacer></v-spacer>
+              </v-card-actions>
+            </v-card>
+            <v-card flat v-else>
+              <v-card-text>
+                Records could not be retrieved, please
+                <router-link :to="{name: 'login'}">log in</router-link>.
+              </v-card-text>
+            </v-card>
+          </v-tab-item>
+        </v-tabs-items>
+      </v-card>
+
+      <div class="subtitle-1">
+        <v-icon>mdi-numeric-3-circle</v-icon>
+        Check Setup
+      </div>
+      <p>
+        All set up correctly? <a :href="`https://dnssec-analyzer.verisignlabs.com/${domain}`" target="_blank">Take
+        a
+        look at DNSSEC Analyzer</a> to check the status of your domain.
+      </p>
+
+    </v-card-text>
+
+    <!-- copy snackbar -->
+    <v-snackbar v-model="snackbar">
+      {{ snackbar_text }}
+
+      <template v-slot:action="{ attrs }">
+        <v-btn
+            color="pink"
+            text
+            v-bind="attrs"
+            @click="snackbar = false"
+        >
+          Close
+        </v-btn>
+      </template>
+    </v-snackbar>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'DomainSetup',
+  props: {
+    domain: {
+      type: String,
+      required: true,
+    },
+    isNew: {
+      type: Boolean,
+      default: false,
+    },
+    ds: {
+      type: Array,
+      default: () => [],
+    },
+    dnskey: {
+      type: Array,
+      default: () => [],
+    },
+    ns: {
+      type: Array,
+      default: () => process.env.VUE_APP_DESECSTACK_NS.split(' '),
+    },
+  },
+  data: () => ({
+    snackbar: false,
+    snackbar_text: '',
+    tab1: 'ns',
+    tab2: 'ds',
+    LOCAL_PUBLIC_SUFFIXES: process.env.VUE_APP_LOCAL_PUBLIC_SUFFIXES.split(' '),
+  }),
+  computed: {
+    tabs: function () {
+      let self = this;
+      return [
+        {
+          key: 'ds', title: 'DS Records', data: self.ds.join('\n'),
+          banner: 'Your provider may require you to input this information as a block or as individual values. ' +
+              'To obtain individual values, split the text below at the spaces to obtain the key tag, algorithm, ' +
+              'digest type, and digest (in this order).'
+        },
+        {
+          key: 'dnskey', title: 'DNSKEY Records', data: self.dnskey.join('\n'),
+          banner: 'Your provider may require you to input this information as a block or as individual values. ' +
+              'To obtain individual values, split the text below at the spaces to obtain the flags, protocol, ' +
+              'algorithm, and public key (in this order).'
+        },
+      ]
+    },
+  },
+  methods: {
+    copySuccess: function () {
+      this.showSnackbar("Copied to clipboard.");
+    },
+    copyError: function () {
+      this.showSnackbar("Copy to clipboard failed. Please try again manually.");
+    },
+    showSnackbar: function (text) {
+      this.snackbar_text = text;
+      this.snackbar = true;
+    }
+  },
+};
+</script>
+
+<style lang="scss" scoped>
+.caption {
+  text-transform: uppercase;
+}
+
+.code, .fixed-width {
+  font-family: monospace;
+}
+
+pre {
+  overflow: auto;
+}
+</style>

+ 62 - 0
webapp/src/views/DomainSetupPage.vue

@@ -0,0 +1,62 @@
+<template>
+  <v-container class="fill-height" fluid>
+    <v-row align="center" justify="center">
+      <v-col cols="12" sm="8" md="8">
+        <v-card>
+          <v-toolbar
+              color="primary"
+              dark
+              flat
+          >
+            <v-toolbar-title>Setup Instructions for <b>{{ domain }}</b></v-toolbar-title>
+          </v-toolbar>
+
+          <domain-setup v-bind="$attrs" :domain="domain"></domain-setup>
+
+        </v-card>
+      </v-col>
+    </v-row>
+    <v-row align="center" justify="center">
+      <v-col cols="12" sm="8" md="4">
+        <v-card>
+          <v-card-title><div class="title">Find Help</div></v-card-title>
+          <v-card-text>
+            In our forum <router-link :to="{name: 'talk'}">talk.desec.io</router-link>, members of the community
+            discuss ideas and applications of deSEC. If you have a question, chances are that you may be able to find
+            the answer there.
+          </v-card-text>
+          <v-card-actions>
+            <v-btn block outlined :to="{name: 'talk'}">Find Help</v-btn>
+          </v-card-actions>
+        </v-card>
+      </v-col>
+      <v-col cols="12" sm="8" md="4">
+        <v-card>
+          <v-card-title><div class="title">Keep deSEC Going</div></v-card-title>
+          <v-card-text>
+            To offer free DNS hosting for everyone, deSEC relies on donations only.
+            If you like our service, please consider donating.
+          </v-card-text>
+          <v-card-actions>
+            <v-btn block outlined :to="{name: 'donate'}">Donate</v-btn>
+          </v-card-actions>
+        </v-card>
+      </v-col>
+    </v-row>
+  </v-container>
+</template>
+
+<script>
+import DomainSetup from "@/views/DomainSetup";
+
+export default {
+  name: 'DomainSetupPage',
+  components: { DomainSetup },
+  props: {
+    domain: {
+      type: String,
+      required: true,
+    },
+  }
+};
+</script>