Kour.io Safe Verify UI (Yellow Badge)

Safe verification UI for developers/admins — shows yellow badge and replaces user's blue badge with yellow one (best-effort).

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Kour.io Safe Verify UI (Yellow Badge)
// @namespace    LC
// @version      2.2
// @description  Safe verification UI for developers/admins — shows yellow badge and replaces user's blue badge with yellow one (best-effort).
// @author       LC
// @license      CC BY-ND 4.0
// @match        https://kour.io/*
// @grant        GM_addStyle
// ==/UserScript==

(function () {
  'use strict';

  // ---- CONFIG ----
  const config = {
    useServerEndpoint: true, // recommended: server does the verification write
    verifyEndpoint: '/api/admin/verify-user', // server endpoint that accepts POST { uid } and checks ID token
    requiredClaim: 'canVerify', // if doing client-side writes, require this claim or admin
    // Put the data URL of your yellow badge image here (replace placeholder)
    yellowBadgeDataUrl: '__YELLOW_BADGE_DATA_URL__'
  };

  // --- Styles (dark panel) ---
  GM_addStyle(`
    #lcVerifyContainer { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; position: fixed; top: 20px; left: 20px; z-index: 9999; background: #1a1a1a; box-shadow: 0 2px 15px rgba(0,0,0,.3); border-radius: 8px; padding: 15px; width: 320px; border: 1px solid #333; }
    #lcVerifyHeader{ color:#00c853; margin-bottom:12px; font-size:16px; font-weight:600; display:flex; justify-content:space-between; align-items:center;}
    #lcVerifyBtn{ background:linear-gradient(135deg,#008000,#00b04a); color:#fff; border:none; padding:8px 15px; border-radius:6px; font-size:14px; cursor:pointer; width:100%; margin-bottom:8px;}
    #lcVerifyBtn:hover{ opacity:.95; transform:translateY(-1px); }
    #lcHideBtn{ background:#333; color:#00c853; border:none; padding:5px 10px; border-radius:5px; font-size:12px; cursor:pointer; }
    #lcVerifyStatus{ font-size:13px; color:#00c853; margin-top:8px; text-align:center; display:flex; align-items:center; justify-content:center; gap:8px; }
    #lcVerifyStatus img { height: 22px; width: 22px; display:inline-block; vertical-align:middle; }
    .hidden{ display:none; }
  `);

  // --- panel HTML ---
  const panelHTML = `
    <div id="lcVerifyContainer" aria-live="polite">
      <div id="lcVerifyHeader">
        <span>LC Verification</span>
        <button id="lcHideBtn" aria-pressed="false">Hide</button>
      </div>
      <button id="lcVerifyBtn">Verify Account</button>
      <div id="lcVerifyStatus">Ready</div>
    </div>
  `;
  document.body.insertAdjacentHTML('beforeend', panelHTML);

  const container = document.getElementById('lcVerifyContainer');
  const verifyBtn = document.getElementById('lcVerifyBtn');
  const statusText = document.getElementById('lcVerifyStatus');
  const hideBtn = document.getElementById('lcHideBtn');

  hideBtn.addEventListener('click', () => {
    container.classList.toggle('hidden');
    const hidden = container.classList.contains('hidden');
    hideBtn.textContent = hidden ? 'Show' : 'Hide';
    hideBtn.setAttribute('aria-pressed', String(hidden));
  });

  function setStatus(text, ok = true, showBadge = false) {
    // showBadge -> display the yellow badge icon beside status
    statusText.textContent = text;
    statusText.style.color = ok ? '#00c853' : '#ff7043';
    if (showBadge && config.yellowBadgeDataUrl) {
      const img = document.createElement('img');
      img.src = config.yellowBadgeDataUrl;
      img.alt = 'Verified';
      // clear then append
      statusText.innerHTML = '';
      statusText.appendChild(img);
      const span = document.createElement('span');
      span.textContent = ' ' + text;
      statusText.appendChild(span);
    }
  }

  // attempt to replace blue verified badge next to the current user's username
  async function replaceBlueBadgeForCurrentUser(yellowDataUrl, user) {
    if (!yellowDataUrl || !user) return false;
    try {
      const uid = user.uid;
      const displayName = user.displayName || '';
      const email = user.email || '';

      // Strategy:
      // 1) Find elements with data-uid, data-user-id, or data-user attributes matching uid
      // 2) Find elements that contain the user's displayName or email and search for a nearby "verified" badge
      // 3) Replace img/src or svg fill where badge appears with the yellow image (best-effort)

      // helper to replace node (img or svg) with an <img> using data url
      function replaceNodeWithImg(node) {
        if (!node) return false;
        const img = document.createElement('img');
        img.src = yellowDataUrl;
        img.alt = 'Verified';
        img.style.height = (node.clientHeight || 18) + 'px';
        img.style.width = (node.clientWidth || 18) + 'px';
        img.className = (node.className || '') + ' replaced-yellow-verified';
        node.replaceWith(img);
        return true;
      }

      // 1) query by data attributes
      const selectors = [
        `[data-uid="${uid}"]`,
        `[data-user-id="${uid}"]`,
        `[data-user="${uid}"]`,
        `[data-username]`
      ];
      for (const sel of selectors) {
        const els = document.querySelectorAll(sel);
        for (const el of els) {
          // search inside for an <img> or svg that looks like a verified badge
          const candidate = el.querySelector('img[alt*="verify"], img[alt*="Verify"], img[src*="verified"], svg, .verified, .badge, .icon-verified');
          if (candidate) {
            replaceNodeWithImg(candidate);
            return true;
          }
        }
      }

      // 2) Search for username/displayName text nodes, then find sibling badge elements
      if (displayName || email) {
        // collect all textual nodes that match
        const textCandidates = Array.from(document.querySelectorAll('body *')).filter(node => {
          // limit to small elements to avoid huge containers
          const txt = (node.textContent || '').trim();
          if (!txt) return false;
          if (displayName && txt.includes(displayName)) return true;
          if (email && txt.includes(email)) return true;
          return false;
        }).slice(0, 40); // limit

        for (const node of textCandidates) {
          // search nearby for badge
          // check siblings, children, parent
          const nearby = [
            ...Array.from(node.parentElement ? node.parentElement.querySelectorAll('img, svg, .verified, .badge, .icon-verified') : []),
            ...Array.from(node.querySelectorAll ? node.querySelectorAll('img, svg, .verified, .badge, .icon-verified') : [])
          ];
          for (const cand of nearby) {
            // Heuristic: if cand has blue fill or src with 'blue' or 'twitter' or big blue color, replace.
            if (cand.tagName === 'IMG') {
              const src = (cand.getAttribute('src') || '').toLowerCase();
              if (src.includes('blue') || src.includes('verified') || src.includes('twitter') || cand.width > 0) {
                replaceNodeWithImg(cand);
                return true;
              } else {
                // still try replacing if it's likely a small icon
                if ((cand.clientWidth && cand.clientWidth <= 48) || (cand.clientHeight && cand.clientHeight <= 48)) {
                  replaceNodeWithImg(cand);
                  return true;
                }
              }
            } else if (cand.tagName === 'svg') {
              // attempt to replace svg with image
              replaceNodeWithImg(cand);
              return true;
            } else {
              // class-based match
              replaceNodeWithImg(cand);
              return true;
            }
          }
        }
      }

      // 3) global fallback: replace any visible blue circular badge icons (be conservative)
      const imgs = Array.from(document.querySelectorAll('img'));
      for (const im of imgs) {
        const src = (im.getAttribute('src') || '').toLowerCase();
        // heuristics for blue badge images: 'verify', 'check', 'twitter', and blue-ish file names
        if (src.includes('verify') || src.includes('verified') || src.includes('twitter') || src.includes('check')) {
          // check computed color presence via naturalWidth/Height small
          if ((im.clientWidth && im.clientWidth <= 64) || (im.clientHeight && im.clientHeight <= 64)) {
            replaceNodeWithImg(im);
            return true;
          }
        }
      }

      return false;
    } catch (e) {
      console.warn('replaceBlueBadgeForCurrentUser error', e);
      return false;
    }
  }

  verifyBtn.addEventListener('click', async () => {
    setStatus('Checking firebase...');
    if (typeof window.firebase === 'undefined' || !window.firebase.auth) {
      setStatus('firebase not found on page', false);
      return;
    }
    const auth = firebase.auth();
    const user = auth.currentUser;
    if (!user) {
      setStatus('No authenticated user. Please sign in.', false);
      return;
    }

    if (!confirm(`Verify account for ${user.email || user.uid} (uid: ${user.uid})? This action is logged.`)) {
      setStatus('Cancelled by user.', false);
      return;
    }

    setStatus('Obtaining token & claims...');
    try {
      const idTokenResult = await user.getIdTokenResult(true);
      const claims = idTokenResult.claims || {};

      if (config.useServerEndpoint) {
        setStatus('Sending verification request to server...');
        const resp = await fetch(config.verifyEndpoint, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            'Authorization': 'Bearer ' + await user.getIdToken()
          },
          body: JSON.stringify({ uid: user.uid }),
          credentials: 'same-origin'
        });

        if (!resp.ok) {
          const txt = await resp.text().catch(() => resp.statusText);
          setStatus('Server error: ' + txt, false);
          return;
        }
        const json = await resp.json().catch(() => null);
        if (json && json.success) {
          setStatus('Verified', true, true);
          // replace blue badge next to user's name (best-effort)
          await replaceBlueBadgeForCurrentUser(config.yellowBadgeDataUrl, user);
        } else {
          setStatus('Server responded but did not mark verified', false);
        }
        return;
      }

      // CLIENT-SIDE write path (not recommended)
      if (!claims[config.requiredClaim] && !claims.admin) {
        setStatus('Missing required custom claim for client update.', false);
        return;
      }

      setStatus('Updating database (client)...');
      await firebase.database().ref('users/' + user.uid + '/verified').set(true);
      setStatus('Verified', true, true);
      await replaceBlueBadgeForCurrentUser(config.yellowBadgeDataUrl, user);

    } catch (err) {
      console.error(err);
      setStatus('Error: ' + (err && err.message ? err.message : String(err)), false);
    }
  });

  // Attempt to replace badge on page load if we find the user is already marked verified locally
  (async function initReplaceIfAlreadyVerified(){
    if (typeof window.firebase === 'undefined' || !window.firebase.auth) return;
    try {
      const auth = firebase.auth();
      // Wait for auth state
      auth.onAuthStateChanged(async (user) => {
        if (!user) return;
        try {
          // Check local DB or token claims for verification flag
          const idTokenResult = await user.getIdTokenResult();
          const claims = idTokenResult.claims || {};
          const isVerifiedClaim = claims.verified || claims.isVerified || false;

          // Also check realtime DB value (best-effort, do not force offline/online)
          let dbVerified = false;
          try {
            const snap = await firebase.database().ref('users/' + user.uid + '/verified').once('value');
            dbVerified = !!snap.val();
          } catch(e) {
            // ignore DB read failures
          }

          if (isVerifiedClaim || dbVerified) {
            setStatus('Verified', true, true);
            await replaceBlueBadgeForCurrentUser(config.yellowBadgeDataUrl, user);
          }
        } catch (e) {
          // ignore
        }
      });
    } catch(e) {}
  })();

})();