فهرست منبع

feat(webapp): improve domain info dialog, fixes #730

Peter Thomassen 1 سال پیش
والد
کامیت
049d1894a4

+ 15 - 2
www/webapp/src/components/Field/RecordDNSKEY.vue

@@ -12,14 +12,27 @@ const int16 = between(0, MAX16);
 
 const equals3 = (value) => !value || value == 3;
 
+const dnskey_flag_mnemonics = {
+  256: 'ZSK',
+  257: 'KSK',
+}
+
+export const dnskey_algorithm_mnemonics = {
+  8: 'RSASHA256',
+  13: 'ECDSAP256-SHA256',
+  14: 'ECDSAP384-SHA384',
+  15: 'ED25519',
+  16: 'ED448',
+}
+
 export default {
   name: 'RecordDNSKEY',
   extends: RecordItem,
   data: () => ({
     fields: [
-      { label: 'Flags', validations: { integer, int16 } },
+      { label: 'Flags', validations: { integer, int16 }, mnemonics: dnskey_flag_mnemonics },
       { label: 'Protocol', validations: { integer, equals3 } },
-      { label: 'Algorithm', validations: { integer, int8 } },
+      { label: 'Algorithm', validations: { integer, int8 }, mnemonics: dnskey_algorithm_mnemonics },
       { label: 'Public Key', validations: { base64 } },
     ],
     errors: {

+ 10 - 2
www/webapp/src/components/Field/RecordDS.vue

@@ -1,6 +1,7 @@
 <script>
 import { and, helpers, integer, between } from 'vuelidate/lib/validators';
 import RecordItem from './RecordItem.vue';
+import { dnskey_algorithm_mnemonics } from './RecordDNSKEY.vue';
 
 const hex = helpers.regex('hex', /^([0-9a-fA-F]\s*)*[0-9a-fA-F]$/);
 const trim = and(helpers.regex('trimBegin', /^[^\s]/), helpers.regex('trimEnd', /[^\s]$/));
@@ -11,14 +12,21 @@ const int8 = between(0, MAX8);
 const MAX16 = 65535;
 const int16 = between(0, MAX16);
 
+const digest_types_mnemonics = {
+  1: 'SHA-1',
+  2: 'SHA-256',
+  3: 'GOST',
+  4: 'SHA-384',
+}
+
 export default {
   name: 'RecordDS',
   extends: RecordItem,
   data: () => ({
     fields: [
       { label: 'Key Tag', validations: { integer, int16 } },
-      { label: 'Algorithm', validations: { integer, int8 } },
-      { label: 'Digest Type', validations: { integer, int8 } },
+      { label: 'Algorithm', validations: { integer, int8 }, mnemonics: dnskey_algorithm_mnemonics },
+      { label: 'Digest Type', validations: { integer, int8 }, mnemonics: digest_types_mnemonics },
       { label: 'Digest', validations: { trim, hex } },
     ],
     errors: {

+ 84 - 53
www/webapp/src/components/Field/RecordItem.vue

@@ -1,22 +1,23 @@
 <template>
-  <v-layout class="flex">
-    <div
+  <tr>
+    <td
       v-for="(field, index) in fields"
       :key="index"
-      :class="index == fields.length - 1 ? 'flex-grow-1' : ''"
+      style="vertical-align: top"
     >
       <v-text-field
         ref="input"
+        :hint="hints[index]"
+        :persistent-hint="'mnemonics' in field"
         v-model="field.value"
         :label="hideLabel ? '' : field.label"
         :class="hideLabel ? 'pt-0' : ''"
         :disabled="disabled"
         :readonly="readonly"
         :placeholder="required && !field.optional ? ' ' : '(optional)'"
-        :hide-details="!content.length || !($v.fields.$each[index].$invalid || $v.fields[index].$invalid)"
+        :hide-details="!('mnemonics' in field) && (!content.length || !($v.fields.$each[index].$invalid || $v.fields[index].$invalid))"
         :error="$v.fields.$each[index].$invalid || $v.fields[index].$invalid"
         :error-messages="fieldErrorMessages(index)"
-        :style="{ width: fieldWidth(index) }"
         :append-icon="index == fields.length-1 && !readonly && !disabled ? appendIcon : ''"
         @click:append="$emit('remove', $event)"
         @input="inputHandler()"
@@ -24,14 +25,9 @@
         @keydown="keydownHandler(index, $event)"
         @keyup="(e) => $emit('keyup', e)"
       />
-      <span
-        ref="mirror"
-        aria-hidden="true"
-        style="opacity: 0; position: absolute; width: auto; white-space: pre; z-index: -1"
-      />
       {{ errorMessages.join(' ') }}
-    </div>
-  </v-layout>
+    </td>
+  </tr>
 </template>
 
 <script>
@@ -39,8 +35,6 @@ import { requiredUnless } from 'vuelidate/lib/validators';
 
 export default {
   name: 'RecordItem',
-  components: {
-  },
   props: {
     content: {
       type: String,
@@ -80,6 +74,11 @@ export default {
     ],
     value: '',
   }),
+  computed: {
+    hints: function () {
+      return this.fields.map(field => ('mnemonics' in field && field.mnemonics[field.value]) || "");
+    },
+  },
   watch: {
     content: function () {
       this.update(this.content);
@@ -87,8 +86,58 @@ export default {
   },
   beforeMount() {
     // Initialize per-field value storage
-    this.fields.forEach((field) => {
+    this.fields.forEach((field, i) => {
       this.$set(field, 'value', '');
+      this.$set(field, 'hint', '');
+    });
+  },
+  mounted() {
+    // Set up mirror system
+    this.fields.forEach((field, i) => {
+      function createMirror(template) {
+        const style = window.getComputedStyle(template);
+        const mirror = document.createElement("div");
+        mirror.style.border = style.getPropertyValue('border');
+        mirror.style.fontSize = style.getPropertyValue('font-size');
+        mirror.style.fontFamily = style.getPropertyValue('font-family');
+        mirror.style.fontWeight = style.getPropertyValue('font-weight');
+        mirror.style.fontStyle = style.getPropertyValue('font-style');
+        mirror.style.fontFeatureSettings = style.getPropertyValue('font-feature-settings');
+        mirror.style.fontKerning = style.getPropertyValue('font-kerning');
+        mirror.style.fontStretch = style.getPropertyValue('font-stretch');
+        mirror.style.fontVariant = style.getPropertyValue('font-variant');
+        mirror.style.fontVariantCaps = style.getPropertyValue('font-variant-caps');
+        mirror.style.fontVariantLigatures = style.getPropertyValue('font-variant-ligatures');
+        mirror.style.fontVariantNumeric = style.getPropertyValue('font-variant-numeric');
+        mirror.style.letterSpacing = style.getPropertyValue('letter-spacing');
+        mirror.style.padding = style.getPropertyValue('padding');
+        mirror.style.transformOrigin = style.getPropertyValue('transform-origin');
+        mirror.style.textTransform = style.getPropertyValue('text-transform');
+        mirror.style.whiteSpace = style.getPropertyValue('white-space');
+        mirror.style.marginRight = '1ch';
+        mirror.style.height = '0';
+        mirror.style.visibility = 'hidden';
+        return mirror;
+      }
+      const el = this.$refs.input[i].$el;
+      let mirror;
+      let hint = el.getElementsByClassName("v-messages__message")[0];
+      if(hint) {
+        mirror = createMirror(hint);
+        mirror.className = 'mirror-hint'
+        el.after(mirror);
+      }
+      mirror = createMirror(el);
+      mirror.style.paddingTop = '0px';
+      mirror.className = 'mirror-input'
+      el.after(mirror);
+      let label = el.getElementsByClassName("v-label")[0];
+      if(label) {
+        mirror = createMirror(label);
+        mirror.style.transform = 'translateY(-18px) scale(0.75)';
+        mirror.className = 'mirror-label'
+        el.after(mirror);
+      }
     });
 
     // Update internal and graphical representation
@@ -126,44 +175,6 @@ export default {
 
       return validationStatus.filter(val => !val.passed).map(val => val.message || 'Invalid input.');
     },
-    fieldWidth(index) {
-      let ret = 'auto';
-      const inputs = this.$refs.input;
-      const mirrors = this.$refs.mirror;
-      if (index < this.fields.length - 1 && inputs && mirrors) {
-        const mirror = mirrors[index];
-        while (mirror.childNodes.length) {
-          mirror.removeChild(mirror.childNodes[0]);
-        }
-
-        const style = window.getComputedStyle(inputs[index].$el);
-        mirror.style.border = style.getPropertyValue('border');
-        mirror.style.fontSize = style.getPropertyValue('font-size');
-        mirror.style.fontFamily = style.getPropertyValue('font-family');
-        mirror.style.fontWeight = style.getPropertyValue('font-weight');
-        mirror.style.fontStyle = style.getPropertyValue('font-style');
-        mirror.style.fontFeatureSettings = style.getPropertyValue('font-feature-settings');
-        mirror.style.fontKerning = style.getPropertyValue('font-kerning');
-        mirror.style.fontStretch = style.getPropertyValue('font-stretch');
-        mirror.style.fontVariant = style.getPropertyValue('font-variant');
-        mirror.style.fontVariantCaps = style.getPropertyValue('font-variant-caps');
-        mirror.style.fontVariantLigatures = style.getPropertyValue('font-variant-ligatures');
-        mirror.style.fontVariantNumeric = style.getPropertyValue('font-variant-numeric');
-        mirror.style.letterSpacing = style.getPropertyValue('letter-spacing');
-        mirror.style.padding = style.getPropertyValue('padding');
-        mirror.style.textTransform = style.getPropertyValue('text-transform');
-
-        mirror.appendChild(document.createTextNode(`${this.fields[index].value} `));
-
-        ret = mirror.getBoundingClientRect().width;
-
-        mirror.removeChild(mirror.childNodes[0]);
-        mirror.appendChild(document.createTextNode(`${this.fields[index].label} `));
-        ret = Math.max(ret, mirror.getBoundingClientRect().width);
-        ret += 'px';
-      }
-      return ret;
-    },
     focus() {
       this.$refs.input[0].focus();
     },
@@ -333,6 +344,26 @@ export default {
       // Make sure to reset trailing fields if value does not have enough spaces
       this.fields.forEach((foo, i) => {
         this.$set(this.fields[i], 'value', values[i] || '');
+        const el = this.$refs.input[i].$el.parentNode;
+        let mirror;
+        mirror = el.getElementsByClassName("mirror-label")[0];
+        if (mirror) {
+          mirror.textContent = el.getElementsByTagName("label")[0].textContent;
+        }
+        mirror = el.getElementsByClassName("mirror-input")[0];
+        if (mirror) {
+          mirror.textContent = this.fields[i].value;
+        }
+        mirror = el.getElementsByClassName("mirror-hint")[0];
+        if (mirror) {
+          this.$nextTick(() => {
+            try {
+              mirror.textContent = el.getElementsByClassName("v-messages__message")[0].textContent;
+            } catch {
+              mirror.textContent = ' ';
+            }
+          });
+        }
       });
     },
   },

+ 24 - 18
www/webapp/src/components/Field/RecordList.vue

@@ -1,22 +1,24 @@
 <template>
   <div>
-    <component
-            :is="getRecordComponentName(type)"
-            v-for="(item, index) in value"
-            :key="index"
-            :content="item"
-            :error-messages="errorMessages[index] ? errorMessages[index].content : []"
-            :hide-label="index > 0"
-            :append-icon="value.length > 1 ? mdiClose : ''"
-            :disabled="disabled"
-            :readonly="readonly"
-            :required="required"
-            ref="inputFields"
-            @update:content="$set(value, index, $event)"
-            @input.native="$emit('dirty', $event)"
-            @remove="(e) => removeHandler(index, e)"
-            @keyup="(e) => $emit('keyup', e)"
-    />
+    <table style="border-spacing: 0; width: 100%">
+      <component
+              :is="getRecordComponentName(type)"
+              v-for="(item, index) in value"
+              :key="index"
+              :content="item"
+              :error-messages="errorMessages[index] ? errorMessages[index].content : []"
+              :hide-label="index > 0"
+              :append-icon="value.length > 1 ? mdiClose : ''"
+              :disabled="disabled"
+              :readonly="readonly"
+              :required="required"
+              ref="inputFields"
+              @update:content="$set(value, index, $event)"
+              @input.native="$emit('dirty', $event)"
+              @remove="(e) => removeHandler(index, e)"
+              @keyup="(e) => $emit('keyup', e)"
+      />
+    </table>
     <v-btn
             @click="addHandler"
             class="px-0 text-none"
@@ -25,7 +27,6 @@
             text
             v-if="!readonly && !disabled"
     ><v-icon>{{ mdiPlus }}</v-icon> add another value</v-btn>
-    <!--div><code style="white-space: normal">{{ value }}</code></div-->
   </div>
 </template>
 
@@ -157,3 +158,8 @@ export default {
   },
 };
 </script>
+<style scoped>
+table >>> td:last-child {
+  width: 100%;
+}
+</style>

+ 1 - 1
www/webapp/src/views/Console/DomainSetupDialog.vue

@@ -1,7 +1,7 @@
 <template>
   <v-dialog
     v-model="show"
-    max-width="700px"
+    max-width="900px"
     persistent
     scrollable
     @keydown.esc="close"

+ 98 - 113
www/webapp/src/views/DomainSetup.vue

@@ -6,117 +6,117 @@
   </div>
   <div v-else>
     <p class="mt-4">
-      The following steps need to be completed in order to use
-      <span class="fixed-width">{{ domain }}</span> with deSEC.
+      The following steps need to be completed in order to use your domain with deSEC.
     </p>
 
     <div v-if="!user.authenticated">
-      <div class="text-subtitle-1">
-        <v-icon>{{ mdiNumeric0Circle }}</v-icon>
-        DNS Configuration
+      <div class="my-2 text-h6">
+        <v-icon class="primary--text">{{ mdiNumeric0Circle }}</v-icon>
+        Configure your DNS records
       </div>
-      <p>
-        To ensure a smooth transition when moving your domain to deSEC, setup a password for your deSEC account,
-        log in and configure the DNS for <span class="fixed-width">{{ domain }}</span>, before you proceed below.
-      </p>
-      <v-btn outlined block :to="{name: 'reset-password'}" target="_blank">
-        Assign Account Password
-      </v-btn>
+      <p>Before delegating your domain, you might want to take the following steps:</p>
+      <ul>
+        <li><router-link :to="{name: 'reset-password'}" target="_blank">Set up a password for your deSEC account</router-link>,</li>
+        <li>Log in and <b>configure the DNS records for your domain</b>,</li>
+        <li>Proceed with the next step below.</li>
+      </ul>
     </div>
 
-    <div class="mt-2 text-subtitle-1">
-      <v-icon>{{ mdiNumeric1Circle }}</v-icon>
+    <div class="my-2 text-h6">
+      <v-icon class="primary--text">{{ mdiNumeric1Circle }}</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
-                  @click="copyToClipboard(ns.join('\n'))"
-                  outlined
-                  text
-              >
-                <v-icon>{{ mdiContentCopy }}</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 class="mb-4" v-if="ns">
+      <v-card-title class="grey lighten-2">
+        <v-row>
+          <v-col cols="auto">Nameservers</v-col>
+          <v-spacer></v-spacer>
+          <v-col class="text-right">
+            <v-btn @click="copyToClipboard(ns.join('\n'))" text small>
+              <v-icon>{{ mdiContentCopy }}</v-icon>
+              Copy
+            </v-btn>
+          </v-col>
+        </v-row>
+      </v-card-title>
+      <v-divider></v-divider>
+      <v-card-text v-if="ns.length">
+        <record-list readonly type="NS" :value="ns"></record-list>
+      </v-card-text>
+      <v-alert type="error" v-else>Nameservers could not be retrieved.</v-alert>
     </v-card>
+    <p>Once your provider processes this information, the Internet will start directing DNS queries to deSEC.</p>
 
-    <p>
-      Once your provider processes this information, the Internet will start directing DNS queries to deSEC.
-    </p>
-
-    <div class="text-subtitle-1">
-      <v-icon>{{ mdiNumeric2Circle }}</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.
-    </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>
+    <div v-if="user.authenticated">
+      <div class="my-2 text-h6">
+        <v-icon class="primary--text">{{ mdiNumeric2Circle }}</v-icon>
+        Enable DNSSEC
+      </div>
+      <p>
+        You also need to forward the following DNSSEC information to your domain provider.
+        The exact steps depend on your provider:
+        You may have to enter the information as a block in either <b>DS format</b> or <b>DNSKEY format</b>, or as
+        <b>individual values</b>.
+      </p>
+      <p class="small">
+        Notes: When using block format, some providers require you to add the domain name in the beginning. (Also,
+        <a class="grey--text text--darken-1" href="https://github.com/oskar456/cds-updates" target="_blank">depending on
+        your domain's suffix</a>, we will perform this step automatically.)
+      </p>
 
-      <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
-                  @click="copyToClipboard(t.data)"
-                  outlined
-                  text
-              >
+      <v-card class="mb-4">
+        <v-card-title class="grey lighten-2">
+          <v-row>
+            <v-col cols="auto">DS Format</v-col>
+            <v-spacer></v-spacer>
+            <v-col class="text-right">
+              <v-btn @click="copyToClipboard(ds.join('\n'))" text small>
                 <v-icon>{{ mdiContentCopy }}</v-icon>
-                copy to clipboard
+                Copy
               </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="text-subtitle-1">
-      <v-icon>{{ mdiNumeric3Circle }}</v-icon>
-      Check Setup
+            </v-col>
+          </v-row>
+        </v-card-title>
+        <v-divider></v-divider>
+        <v-card-text style="overflow-x: scroll; overflow-y: hidden" v-if="ds.length">
+          <record-list readonly type="DS" :value="ds"></record-list>
+        </v-card-text>
+        <v-alert type="error" v-else>Parameters could not be retrieved. (Are you logged in?)</v-alert>
+      </v-card>
+
+      <v-card>
+        <v-card-title class="grey lighten-2">
+          <v-row>
+            <v-col cols="auto">DNSKEY Format</v-col>
+            <v-spacer></v-spacer>
+            <v-col class="text-right">
+              <v-btn @click="copyToClipboard(dnskey.join('\n'))" text small>
+                <v-icon>{{ mdiContentCopy }}</v-icon>
+                Copy
+              </v-btn>
+            </v-col>
+          </v-row>
+        </v-card-title>
+        <v-divider></v-divider>
+        <v-card-text style="overflow-x: scroll; overflow-y: hidden" v-if="dnskey.length">
+          <record-list readonly type="DNSKEY" :value="dnskey"></record-list>
+        </v-card-text>
+        <v-alert type="error" v-else>Parameters could not be retrieved. (Are you logged in?)</v-alert>
+      </v-card>
+
+      <div class="my-2 text-h6">
+        <v-icon class="primary--text">{{ mdiNumeric3Circle }}</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>
     </div>
-    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.
 
     <!-- copy snackbar -->
     <v-snackbar v-model="snackbar">
@@ -138,11 +138,15 @@
 </template>
 
 <script>
+import RecordList from '@/components/Field/RecordList.vue';
 import {useUserStore} from "@/store/user";
 import {mdiContentCopy, mdiAlert, mdiNumeric0Circle, mdiNumeric1Circle, mdiNumeric2Circle, mdiNumeric3Circle, mdiCheck} from "@mdi/js";
 
 export default {
   name: 'DomainSetup',
+  components: {
+    RecordList,
+  },
   props: {
     domain: {
       type: String,
@@ -158,7 +162,7 @@ export default {
     },
     ns: {
       type: Array,
-      default: () => import.meta.env.VITE_APP_DESECSTACK_NS.split(' '),
+      default: () => import.meta.env.VITE_APP_DESECSTACK_NS.split(' ').map(v => `${v}.`),
     },
   },
   data: () => ({
@@ -173,8 +177,6 @@ export default {
     snackbar: false,
     snackbar_icon: '',
     snackbar_text: '',
-    tab1: 'ns',
-    tab2: 'ds',
     LOCAL_PUBLIC_SUFFIXES: import.meta.env.VITE_APP_LOCAL_PUBLIC_SUFFIXES.split(' '),
   }),
   computed: {
@@ -187,23 +189,6 @@ export default {
           )
       )
     },
-    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: {
     copyToClipboard: async function (text) {