alpine-components.js 268 lignes · 7472 octets
/* Alpine components for the CSP build (@alpinejs/csp).
   Loaded before alpine-csp.min.js so these listeners are registered
   before Alpine.start() dispatches alpine:init.
   Method bodies here are real JavaScript: fetch, window and document
   are allowed. They must not appear in HTML Alpine expressions. */
document.addEventListener("alpine:init", () => {
  Alpine.data("twoFactorPage", () => ({
    code: "",
    error: "",
    verifying: false,
    useBackupCode: false,
    challengeToken: "",
    errorInvalid: "",
    errorNetwork: "",

    init() {
      const params = new URLSearchParams(window.location.search);
      this.challengeToken = params.get("challenge") || "";
      this.errorInvalid = this.$el.dataset.errorInvalid || "";
      this.errorNetwork = this.$el.dataset.errorNetwork || "";
    },

    toggleBackup() {
      this.useBackupCode = !this.useBackupCode;
      this.code = "";
    },

    async verify() {
      if (!this.code || !this.challengeToken) return;
      this.verifying = true;
      this.error = "";
      try {
        const res = await fetch("/api/v1/auth/2fa/verify", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            challenge_token: this.challengeToken,
            code: this.code,
          }),
        });
        const data = await res.json();
        if (res.ok) {
          if (data.refresh_token) {
            localStorage.setItem("refresh_token", data.refresh_token);
          }
          if (data.user && data.user.must_change_password === true) {
            window.location.href = "/settings/password?must_change=true";
          } else {
            window.location.href = "/home";
          }
        } else {
          this.error = data.error || this.errorInvalid;
          this.code = "";
        }
      } catch (e) {
        this.error = this.errorNetwork;
      }
      this.verifying = false;
    },
  }));

  Alpine.data("securityPage", () => ({
    setupStarted: false,
    setupComplete: false,
    setupError: "",
    qrCode: "",
    secretBase32: "",
    verifyCode: "",
    verifying: false,
    showBackupCodes: false,
    backupCodes: [],
    regenerating: false,
    showDisableModal: false,
    disablePassword: "",
    disableError: "",
    disabling: false,
    csrf: "",
    regenerateConfirm: "",

    init() {
      this.csrf = this.$el.dataset.csrf || "";
      this.regenerateConfirm = this.$el.dataset.regenerateConfirm || "";
    },

    async startSetup() {
      this.setupStarted = true;
      this.setupError = "";
      try {
        const res = await fetch("/settings/security/totp/enable", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "X-CSRF-Token": this.csrf,
          },
        });
        const data = await res.json();
        if (res.ok) {
          this.qrCode = data.qr_code_data_uri;
          this.secretBase32 = data.secret_base32;
        } else {
          this.setupError = data.error || "Error";
          this.setupStarted = false;
        }
      } catch (e) {
        this.setupError = "Network error";
        this.setupStarted = false;
      }
    },

    async verifyAndEnable() {
      if (this.verifyCode.length !== 6) return;
      this.verifying = true;
      this.setupError = "";
      try {
        const res = await fetch("/settings/security/totp/verify", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "X-CSRF-Token": this.csrf,
          },
          body: JSON.stringify({ code: this.verifyCode }),
        });
        const data = await res.json();
        if (res.ok) {
          this.setupComplete = true;
          this.backupCodes = data.backup_codes;
          this.showBackupCodes = true;
        } else {
          this.setupError = data.error || "Invalid code";
          this.verifyCode = "";
        }
      } catch (e) {
        this.setupError = "Network error";
      }
      this.verifying = false;
    },

    async regenerateBackupCodes() {
      if (!confirm(this.regenerateConfirm)) return;
      this.regenerating = true;
      try {
        const res = await fetch("/settings/security/backup-codes", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "X-CSRF-Token": this.csrf,
          },
        });
        const data = await res.json();
        if (res.ok) {
          this.backupCodes = data.backup_codes;
          this.showBackupCodes = true;
        }
      } catch (e) {}
      this.regenerating = false;
    },

    async disableTotp() {
      this.disabling = true;
      this.disableError = "";
      try {
        const res = await fetch("/settings/security/totp/disable", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "X-CSRF-Token": this.csrf,
          },
          body: JSON.stringify({ password: this.disablePassword }),
        });
        if (res.ok) {
          window.location.reload();
        } else {
          const data = await res.json();
          this.disableError = data.error || "Error";
        }
      } catch (e) {
        this.disableError = "Network error";
      }
      this.disabling = false;
    },

    copyBackupCodes() {
      const text = this.backupCodes.join("\n");
      if (navigator.clipboard && navigator.clipboard.writeText) {
        navigator.clipboard.writeText(text);
      } else {
        const ta = document.createElement("textarea");
        ta.value = text;
        ta.style.position = "fixed";
        ta.style.opacity = "0";
        document.body.appendChild(ta);
        ta.select();
        document.execCommand("copy");
        document.body.removeChild(ta);
      }
    },

    acknowledgeBackupCodes() {
      this.showBackupCodes = false;
      window.location.reload();
    },

    closeDisableModal() {
      this.showDisableModal = false;
      this.disablePassword = "";
      this.disableError = "";
    },
  }));

  Alpine.data("analyticsOptOut", () => ({
    storageKey: "",
    optedOut: false,

    init() {
      this.storageKey = this.$el.dataset.storageKey || "";
      try {
        this.optedOut = localStorage.getItem(this.storageKey) !== null;
      } catch (e) {}
    },

    toggle() {
      try {
        if (this.optedOut) {
          localStorage.removeItem(this.storageKey);
          this.optedOut = false;
        } else {
          localStorage.setItem(this.storageKey, "1");
          this.optedOut = true;
        }
      } catch (e) {}
    },
  }));

  Alpine.data("fediversePage", () => ({
    saved: false,
    deleteTarget: null,
    revokeTarget: null,
    killDomain: null,

    init() {
      this.saved = this.$el.dataset.saved === "true";
      this.$watch("saved", (value) => {
        if (value) {
          setTimeout(() => {
            this.saved = false;
          }, 4000);
        }
      });
    },

    revokeHandle() {
      if (!this.revokeTarget) return "";
      const parts = this.revokeTarget.split("|");
      return parts.length > 1 ? parts[1] : "";
    },

    openKillModal() {
      if (!this.killDomain) return;
      const modal = document.getElementById("kill-domain-modal");
      if (modal) modal.showModal();
    },

    closeKillModal() {
      const modal = document.getElementById("kill-domain-modal");
      if (modal) modal.close();
    },
  }));
});