Өөрийн UI бүтээх

Үйлчилгээний нөхцөлөөр шаардлагатай 150₮ үнийн мэдэгдэл ба SMS товчийг HTML эсвэл React-аар хуулж ашиглана.

Виджет энэ бүгдийг өөрөө хийнэ

Үнэ, код, SMS товч, QR харуулах болон баталгаажмагц товчийг нуухыг Виджет өөрөө хийдэг. Өөрийн дизайн, өөрийн урсгал хэрэгтэй үед л энэ хуудсыг ашиглана уу.

Хэрэглэгч SMS илгээхдээ оператортоо 150₮ (НӨАТ орсон) төлдөг. Тиймээс товч дарахаасаа өмнө үнийг мэдэж байх ёстой.

Жишээ. Энд SMS илгээгдэхгүй.

Үйлчилгээний нөхцөлөөр заавал шаардагдана

  1. SMS товч эсвэл sms: холбоосыг харуулахаас өмнө эсвэл хамт «150₮» үнийг тод харуулна.
  2. Session баталгаажмагц товчийг нууна. Хэрэглэгчийг дахин илгээхэд хүргэж болохгүй.

Жишээ

Тест горимын дуураймал. Товчийг дарахад баталгаажсан мэт харагдаж, товч алга болно.

Утасны дугаар баталгаажуулах

Дугаараа баталгаажуулахын тулд доорх товчийг дарж 150073 руу нэг SMS илгээнэ үү.

150₮ · Төлбөрийг таны оператор авна.

Жишээ. Энд бодит SMS илгээгдэхгүй.

Зөвлөмж

Дугаарыг мэддэг бол (нэвтрэх, төлбөр батлах) phone-ийг заавал дамжуулна уу. Дугаартай холбосон session-ийг өөр дугаараас баталгаажуулах боломжгүй. Баталгаажсаны дараа дугаарыг хэрэглэгчид буцааж харуулна уу: «Таны дугаар 9911••33 мөн үү?»

Хэрхэн ажилладаг вэ

Session үүсгэх

Хуудас таны өөрийн сервер рүү хандаж session үүсгэнэ. Сервер нь 2fa.mn-ийн нууц API түлхүүрээр (sk_) POST /v1/sessions дуудна. API түлхүүр хөтөч рүү хэзээ ч очихгүй.

Үнэ ба товч харуулах

Хуудас үнэ, код, sms_uri-тай «SMS илгээх» товчийг харуулна. Компьютер дээр товчны оронд qr_svg-г харуулж болно.

Төлөв асуух

Хуудас ойролцоогоор 2 секунд тутамд таны серверээр дамжуулан GET /v1/sessions/{id} асууна.

Үр дүнг харуулах

status нь verified болмогц товчийг нууж, амжилтын мессеж харуулна. expired болбол шинэ код авах боломж олгоно.

Webhook бол баталгаажуулалтын жинхэнэ эх сурвалж. Polling нь зөвхөн дэлгэцийг шинэчлэхэд зориулагдсан. Хэрэглэгчийн дугаарыг өөрийн системд баталгаажсан гэж тэмдэглэхдээ webhook-д тулгуурлана уу.

Серверийн прокси (Node.js / Express)

Хөтчийн жишээнүүд энэ хоёр хаягийг дуудна. Өөр хэл дээр ч адил энгийн.

server.js
import express from "express";

const app = express();
app.use(express.json());

const API = "https://api.2fa.mn";
const KEY = process.env.TWOFA_KEY; // sk_test_... эсвэл sk_live_...

async function twofa(path, init = {}) {
  const res = await fetch(API + path, {
    ...init,
    headers: {
      Authorization: `Bearer ${KEY}`,
      "Content-Type": "application/json",
      ...init.headers,
    },
  });
  return { status: res.status, body: await res.json() };
}

// Session үүсгэх
app.post("/api/2fa/session", async (req, res) => {
  const { status, body } = await twofa("/v1/sessions", {
    method: "POST",
    body: JSON.stringify({
      phone: req.body.phone, // заавал биш
      metadata: { user_id: req.user?.id }, // өөрийн хэрэглэгчийн ID
    }),
  });
  if (status !== 201) return res.status(status).json(body);
  const { id, code, short_code, sms_uri, qr_svg, status: s, expires_at } = body;
  res.json({ id, code, short_code, sms_uri, qr_svg, status: s, expires_at });
});

// Төлөв шалгах
app.get("/api/2fa/session/:id", async (req, res) => {
  const { status, body } = await twofa(`/v1/sessions/${encodeURIComponent(req.params.id)}`);
  if (status !== 200) return res.status(status).json(body);
  res.json({ id: body.id, status: body.status });
});

app.listen(3000);

Бодит системд session-ийг тухайн хэрэглэгчтэй холбож хадгалаад, хэрэглэгч зөвхөн өөрийнхөө session-ийг асуух эрхтэй байхаар тохируулна уу.

Хэрэглэгчийн хэсэг

Framework шаардлагагүй. Компьютер дээрх хуудсанд товчны оронд qr_svg харуулна: эхлээд үнэ, дараа нь QR, доор нь заавар. Серверийн прокси qr_svg-г буцаадаг.

verify.html
<div id="twofa" class="twofa">
  <p class="twofa-price">Үнэ: <strong>150₮</strong></p>
  <p class="twofa-hint">
    Доорх товчийг дарж, нээгдсэн SMS-ийг илгээнэ үү.
    Компьютер дээр бол <strong id="twofa-code">······</strong> кодыг
    <strong>150073</strong> руу илгээнэ үү.
  </p>
  <a id="twofa-btn" class="twofa-btn" href="#" hidden>SMS илгээх</a>
  <p id="twofa-status" class="twofa-status" role="status"></p>
</div>

<style>
  .twofa { max-width: 360px; font: 16px/1.5 system-ui, sans-serif; }
  .twofa-price { font-size: 18px; margin: 0 0 4px; }
  .twofa-hint { color: #555; margin: 0 0 16px; }
  .twofa-btn {
    display: block; text-align: center; padding: 14px 16px;
    border-radius: 10px; background: #1e3fae; color: #fff;
    text-decoration: none; font-weight: 600;
  }
  .twofa-status { min-height: 1.5em; margin-top: 12px; }
</style>

<script>
  (function () {
    const btn = document.getElementById("twofa-btn");
    const codeEl = document.getElementById("twofa-code");
    const statusEl = document.getElementById("twofa-status");
    let timer;

    async function start() {
      statusEl.textContent = "";
      const res = await fetch("/api/2fa/session", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({}), // { phone: "99112233" } гэж заавал биш
      });
      if (!res.ok) {
        statusEl.textContent = "Код авч чадсангүй. Дахин оролдоно уу.";
        return;
      }
      const s = await res.json();
      codeEl.textContent = s.code;
      btn.href = s.sms_uri;
      btn.hidden = false;
      poll(s.id);
    }

    function poll(id) {
      clearTimeout(timer);
      timer = setTimeout(async () => {
        try {
          const res = await fetch("/api/2fa/session/" + encodeURIComponent(id));
          const s = await res.json();
          if (s.status === "verified") {
            btn.hidden = true; // баталгаажмагц товчийг нууна
            statusEl.textContent = "Дугаар баталгаажлаа.";
            return;
          }
          if (s.status === "expired") {
            btn.hidden = true;
            statusEl.textContent = "Кодын хугацаа дууслаа. ";
            const retry = document.createElement("a");
            retry.href = "#";
            retry.textContent = "Шинэ код авах";
            retry.onclick = (e) => {
              e.preventDefault();
              start();
            };
            statusEl.appendChild(retry);
            return;
          }
        } catch (e) {
          // сүлжээний түр алдаа: дахин оролдоно
        }
        poll(id);
      }, 2000);
    }

    start();
  })();
</script>
verify-qr.html
<div class="twofa">
  <p class="twofa-price">Үнэ: <strong>150₮</strong></p>
  <div id="twofa-qr-box" hidden>
    <div id="twofa-qr" class="twofa-qr" role="img" aria-label="SMS илгээх QR код"></div>
    <p class="twofa-hint">Утасныхаа камераар уншуулна уу</p>
    <p class="twofa-hint">Эсвэл <strong id="twofa-qr-code"></strong> кодыг <strong>150073</strong> руу илгээнэ үү.</p>
  </div>
  <p id="twofa-qr-status" class="twofa-status" role="status" aria-live="polite"></p>
</div>

<script>
  (async function () {
    const box = document.getElementById("twofa-qr-box");
    const statusEl = document.getElementById("twofa-qr-status");

    const r = await fetch("/api/2fa/session", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({}),
    });
    if (!r.ok) {
      statusEl.textContent = "Код авч чадсангүй. Дахин оролдоно уу.";
      return;
    }
    const s = await r.json();
    // qr_svg-г 2fa.mn тогтмол тэмдэгтүүдээс үүсгэдэг тул шууд оруулж болно.
    document.getElementById("twofa-qr").innerHTML = s.qr_svg;
    document.getElementById("twofa-qr-code").textContent = s.code;
    box.hidden = false;
    statusEl.textContent = "SMS-ийг хүлээж байна…";

    const timer = setInterval(async () => {
      try {
        const res = await fetch("/api/2fa/session/" + encodeURIComponent(s.id));
        if (!res.ok) return;
        const { status } = await res.json();
        if (status === "pending") return;
        clearInterval(timer);
        box.hidden = true; // баталгаажмагц эсвэл хугацаа дуусмагц QR-ыг нууна
        statusEl.textContent =
          status === "verified" ? "Дугаар баталгаажлаа." : "Кодын хугацаа дууслаа. Хуудсаа дахин ачаална уу.";
      } catch {
        // сүлжээний түр алдаа: дахин оролдоно
      }
    }, 2000);
  })();
</script>

<style>
  .twofa-qr { width: 200px; height: 200px; padding: 12px; background: #fff; border-radius: 12px; }
  .twofa-qr svg { display: block; width: 100%; height: 100%; }
  #twofa-qr-box[hidden] { display: none; }
</style>
TwofaVerify.tsx
"use client";

import { useCallback, useEffect, useRef, useState } from "react";

type Session = {
  id: string;
  code: string;
  short_code: string;
  sms_uri: string;
  status: "pending" | "verified" | "expired";
};

type Props = {
  phone?: string;
  onVerified?: (sessionId: string) => void;
};

export function TwofaVerify({ phone, onVerified }: Props) {
  const [session, setSession] = useState<Session | null>(null);
  const [error, setError] = useState<string | null>(null);
  const timer = useRef<ReturnType<typeof setTimeout>>();

  const start = useCallback(async () => {
    setError(null);
    const res = await fetch("/api/2fa/session", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(phone ? { phone } : {}),
    });
    if (!res.ok) {
      setError("Код авч чадсангүй. Дахин оролдоно уу.");
      return;
    }
    setSession(await res.json());
  }, [phone]);

  useEffect(() => {
    start();
  }, [start]);

  useEffect(() => {
    if (!session || session.status !== "pending") return;
    const tick = async () => {
      try {
        const res = await fetch(`/api/2fa/session/${encodeURIComponent(session.id)}`);
        const { status } = await res.json();
        if (status !== "pending") {
          setSession((s) => (s ? { ...s, status } : s));
          if (status === "verified") onVerified?.(session.id);
          return;
        }
      } catch {
        // сүлжээний түр алдаа: дахин оролдоно
      }
      timer.current = setTimeout(tick, 2000);
    };
    timer.current = setTimeout(tick, 2000);
    return () => clearTimeout(timer.current);
  }, [session?.id, session?.status, onVerified]);

  if (error) return <p role="alert">{error}</p>;
  if (!session) return <p>Ачааллаж байна…</p>;

  if (session.status === "verified") {
    // Баталгаажмагц товчийг харуулахгүй
    return <p role="status">Дугаар баталгаажлаа.</p>;
  }

  if (session.status === "expired") {
    return (
      <p role="status">
        Кодын хугацаа дууслаа.{" "}
        <button type="button" onClick={start}>Шинэ код авах</button>
      </p>
    );
  }

  return (
    <div style={{ maxWidth: 360 }}>
      <p style={{ fontSize: 18, margin: "0 0 4px" }}>
        Үнэ: <strong>150₮</strong>
      </p>
      <p style={{ color: "#555", margin: "0 0 16px" }}>
        Товчийг дарж, нээгдсэн SMS-ийг илгээнэ үү. Компьютер дээр бол{" "}
        <strong>{session.code}</strong> кодыг <strong>{session.short_code}</strong> руу илгээнэ үү.
      </p>
      <a
        href={session.sms_uri}
        style={{
          display: "block",
          textAlign: "center",
          padding: "14px 16px",
          borderRadius: 10,
          background: "#1e3fae",
          color: "#fff",
          fontWeight: 600,
          textDecoration: "none",
        }}
      >
        SMS илгээх
      </a>
    </div>
  );
}

Ашиглах:

<TwofaVerify phone="99112233" onVerified={(id) => router.push("/welcome")} />

Текстийн загвар

Өөрийн дизайнаар хийх бол дараах текстийг ашиглаж болно.

БайрлалТекст
ҮнэҮнэ: 150₮
ТовчSMS илгээх
QR зааварУтасныхаа камераар уншуулна уу
ЗааварТовчийг дарж, нээгдсэн SMS-ийг илгээнэ үү.
Компьютер дээр482913 кодыг 150073 руу илгээнэ үү.
АмжилттайДугаар баталгаажлаа.
Хугацаа дууссанКодын хугацаа дууслаа. Шинэ код авах

Шалгах жагсаалт

  • Үнэ товчийн өмнө эсвэл хамт, тод харагдаж байна.
  • Баталгаажмагц товч алга болж байна.
  • Хугацаа дуусахад товч алга болж, шинэ код авах боломжтой.
  • Нууц API түлхүүр зөвхөн сервер дээр байна.
  • Хэрэглэгчийн дугаарыг webhook-д тулгуурлаж баталгаажсан гэж тэмдэглэж байна.

Дараагийн алхам

Энэ хуудас тус болсон уу?