Safe verification UI for developers/admins — shows yellow badge and replaces user's blue badge with yellow one (best-effort).
// ==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) {}
})();
})();