0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Online TOTP Generatorつくってみた【ワンタイムパスワード生成】

Last updated at Posted at 2023-08-26

See the Pen TOTP Generator by John Doe (@04) on CodePen.

キーボードですばやく操作できるようになりました。デバッグで簡単に使用できます。

<!-- Use preprocessors via the lang attribute! e.g. <template lang="pug"> -->
<template>
  <div id="app">
    <div class="bar-bg" v-show="entries.length"></div>
    <div class="bar" ref="bar" v-show="entries.length"></div>
    <div class="entries" v-if="entries.length">
      <div
        class="entry"
        v-for="(entry, index) in entries"
        tabindex="0"
        @keydown.enter="copy(entry.otp)"
        @keydown.prevent.space="copy(entry.otp)"
        @keydown.delete="deleteEntry(index)"
        @keydown.ctrl.c="isCollapsed() && copy(entry.otp)"
        @keydown.stop.up="focusPrev"
        @keydown.stop.left="focusPrev"
        @keydown.stop.down="focusNext"
        @keydown.stop.right="focusNext"
      >
        <div class="entry-left">
          <div v-if="entry.label">{{ entry.label }}</div>
          <div>secret: {{ entry.secret }}</div>
          <div v-if="entry.issuer">issuer: {{ entry.issuer }}</div>
          <div>{{ new Date(entry.created_at) }}</div>
        </div>
        <div @click="copy(entry.otp)">
          <h1>{{ entry.otp }}</h1>
        </div>
      </div>
      <h1 title="Delete a entry by DELETE key">
        Copy a OTP by
        <span class="key">Enter</span>
        ,
        <span class="key">Space</span>
        ,
        <span class="key">Ctrl</span>
        +
        <span class="key">C</span>
        , click&nbsp;a&nbsp;OTP
      </h1>
    </div>
    <div
      v-else
      class="initial"
      title="If you paste QR image, add some white padding around the QR image by Win + Shift + S."
    >
      <h1>
        Paste a secret key, URL, QR image by
        <span class="key">Ctrl</span>
        +
        <span class="key">V</span>
      </h1>
    </div>
    <a
      href="https://tholman.com/github-corners/"
      class="github-corner"
      aria-label="View source on GitHub"
      target="_blank"
    >
      <svg
        width="80"
        height="80"
        viewBox="0 0 250 250"
        style="
          fill: #64ceaa;
          color: #fff;
          position: fixed;
          top: 0;
          border: 0;
          right: 0;
        "
        aria-hidden="true"
      >
        <path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
        <path
          d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2"
          fill="currentColor"
          style="transform-origin: 130px 106px"
          class="octo-arm"
        ></path>
        <path
          d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z"
          fill="currentColor"
          class="octo-body"
        ></path>
      </svg>
    </a>
  </div>
</template>

<script>
import jsQR from "https://cdn.skypack.dev/jsqr@1.4.0";

async function scanQRFromFile(file) {
  const img = new Image();
  img.src = URL.createObjectURL(file);
  await new Promise((resolve) => img.addEventListener("load", resolve));
  const canvas = new OffscreenCanvas(img.naturalWidth, img.naturalHeight);
  const ctx = canvas.getContext("2d");
  ctx.imageSmoothingEnabled = false;
  ctx.drawImage(img, 0, 0);
  const imageData = ctx.getImageData(0, 0, img.naturalWidth, img.naturalHeight);
  return jsQR(imageData.data, img.naturalWidth, img.naturalHeight)?.data;
}

export default {
  data() {
    return {
      entries: JSON.parse(localStorage.getItem("entries")) ?? []
    };
  },
  mounted() {
    this.update();
    addEventListener("keydown", (e) => {
      if (["ArrowDown", "ArrowRight"].includes(e.key)) {
        if (!document.querySelector(".entry:first-of-type")) return;
        document.querySelector(".entry:first-of-type").focus();
      }
      if (["ArrowUp", "ArrowLeft"].includes(e.key)) {
        if (!document.querySelector(".entry:last-of-type")) return;
        document.querySelector(".entry:last-of-type").focus();
      }
    });
    addEventListener("paste", async (e) => {
      const text = e.clipboardData.getData("text");
      this.addText(text);
      if (!e.clipboardData.files.length) return;
      const data = await scanQRFromFile(e.clipboardData.files[0]);
      if (!data) {
        Toastify({
          text: "Could not scan the QR code.",
          duration: 10000,
          style: {
            background: "linear-gradient(to right, #ff5f6d, #ffc371)"
          },
          gravity: "bottom"
        }).showToast();
        Toastify({
          text: "Add some white padding around the QR image by PrintScreen.",
          duration: 10000,
          destination:
            "https://github.com/cozmo/jsQR/issues/142#issuecomment-1364690399",
          newWindow: true,
          style: {
            background: "linear-gradient(to right, #ff5f6d, #ffc371)"
          },
          gravity: "bottom"
        }).showToast();
        return;
      }
      this.addText(data);
    });
    this.$nextTick().then(() => {
      if (!document.querySelector(".entry:first-of-type")) return;
      document.querySelector(".entry:first-of-type").focus();
    });
  },
  methods: {
    update() {
      const ms = 1000 * 30;
      const duration = ms - (Date.now() % ms);
      setTimeout(this.update, duration);
      this.$refs.bar.animate(
        [
          { transform: `translateX(-${((ms - duration) / ms) * 100}%)` },
          { transform: `translateX(-100%)` }
        ],
        {
          duration
        }
      );
      this.updateOtp();
    },
    updateOtp() {
      for (const entry of this.entries) {
        const totp = new OTPAuth.TOTP(entry);
        entry.otp = totp.generate();
      }
    },
    addEntry(entry) {
      entry.created_at = new Date();
      const totp = new OTPAuth.TOTP(entry);
      entry.otp = totp.generate();
      this.entries.unshift(entry);
      localStorage.setItem("entries", JSON.stringify(this.entries));
      this.copy(entry.otp);
      scrollTo({ top: 0, behavior: "smooth" });
    },
    deleteEntry(index) {
      this.$delete(this.entries, index);
      localStorage.setItem("entries", JSON.stringify(this.entries));
    },
    addText(text) {
      if (!text) return;
      if (/^[2-7]{6}$/.test(text)) return;
      if (/^[A-Z2-7]+$/.test(text)) this.addEntry({ secret: text });
      try {
        const url = new URL(text);
        const entry = Object.fromEntries(url.searchParams);
        entry.label = decodeURIComponent(url.pathname.split("/").at(-1));
        this.addEntry(entry);
      } catch (e) {
        console.error(e);
      }
    },
    copy(text) {
      const activeElement = document.activeElement;
      Toastify({ text, gravity: "bottom" }).showToast();
      const input = document.body.appendChild(document.createElement("input"));
      input.value = text;
      input.select();
      document.execCommand("copy");
      input.parentNode.removeChild(input);
      activeElement.focus();
    },
    isCollapsed() {
      return getSelection().isCollapsed;
    },
    focusPrev(e) {
      if (!e.target.previousElementSibling) return;
      e.target.previousElementSibling.focus();
    },
    focusNext(e) {
      if (!e.target.nextElementSibling) return;
      e.target.nextElementSibling.focus();
    }
  }
};
</script>

<!-- Use preprocessors via the lang attribute! e.g. <style lang="scss"> -->
<style>
body {
  margin: 0;
}

#app {
  text-align: center;
  color: #2c3e50;
}

.bar-bg {
  position: sticky;
  top: 0;
  height: 8px;
  background: lightgreen;
}

.bar {
  position: sticky;
  top: 0;
  margin-top: -8px;
  height: 8px;
  background: green;
  will-change: transform;
}

.entries {
  margin: 0 auto;
  max-width: 768px;
}

.entry {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  margin: 16px;
  border: 2px solid #fafafa;
  border-radius: 8px;
  padding: 8px;
  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.26);
  outline-color: green;
}

.entry:focus {
  border: 2px solid green;
}

.entry-left {
  text-align: left;
}

.key {
  border: 1px solid #19191c33;
  border-radius: 4px;
  padding: 0px 12px;
  line-height: 1.8;
  background: #19191c0d;
}

.initial {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
}

.github-corner:hover .octo-arm {
  animation: octocat-wave 560ms ease-in-out;
}
@keyframes octocat-wave {
  0%,
  100% {
    transform: rotate(0);
  }
  20%,
  60% {
    transform: rotate(-25deg);
  }
  40%,
  80% {
    transform: rotate(10deg);
  }
}
@media (max-width: 500px) {
  .github-corner:hover .octo-arm {
    animation: none;
  }
  .github-corner .octo-arm {
    animation: octocat-wave 560ms ease-in-out;
  }
}
</style>

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?