Last active
May 11, 2026 16:50
-
-
Save Windowsfreak/0f4b0b727120d6a408a68a832e5cc7b6 to your computer and use it in GitHub Desktop.
Aurum-Partnerstatistik-Export
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| (async function () { | |
| class AurumCryptor { | |
| #password; | |
| #iterations; | |
| #encoder; | |
| #decoder; | |
| constructor(password, iterations = 150000) { | |
| if (!password) throw new Error("Password is required for key derivation"); | |
| this.#password = password; | |
| this.#iterations = iterations; | |
| this.#encoder = new TextEncoder(); | |
| this.#decoder = new TextDecoder(); | |
| } | |
| async deriveKey(salt) { | |
| const baseKey = await window.crypto.subtle.importKey( | |
| "raw", this.#encoder.encode(this.#password), "PBKDF2", false, ["deriveKey"] | |
| ); | |
| return window.crypto.subtle.deriveKey( | |
| { name: "PBKDF2", salt: salt, iterations: this.#iterations, hash: "SHA-256" }, | |
| baseKey, { name: "AES-GCM", length: 256 }, false, ["encrypt", "decrypt"] | |
| ); | |
| } | |
| async decode(encryptedBase64) { | |
| const encryptedBytes = Uint8Array.from(atob(encryptedBase64), c => c.charCodeAt(0)); | |
| const salt = encryptedBytes.slice(0, 16), iv = encryptedBytes.slice(16, 28), ciphertext = encryptedBytes.slice(28); | |
| const aesKey = await this.deriveKey(salt); | |
| const decryptedBytes = await window.crypto.subtle.decrypt({ name: "AES-GCM", iv }, aesKey, ciphertext); | |
| return this.#decoder.decode(decryptedBytes); | |
| } | |
| } | |
| const cryptor = new AurumCryptor("default-password"); | |
| const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); | |
| const backoffs = [10, 20, 40, 80, 120, 240, 360, 480, 600]; | |
| const section = document.getElementsByTagName("section")[0] || document.body; | |
| const menu = document.getElementsByTagName('aside'); | |
| if (menu.length > 0) menu[0].parentNode.remove(); | |
| section.innerHTML = ` | |
| <div class="max-w-[600px] mx-auto text-center font-sans p-8"> | |
| <h2 class="font-medium md:text-xl mb-[9px] text-[18px] text-text-color-primary">Partner-Struktur Export</h2> | |
| <p class="text-text-color-secondary text-[14px] leading-none font-light mb-8">Extrahiert die komplette Partner-Hierarchie als JSON und CSV.</p> | |
| <button id="startExportBtn" class="text-sm flex gap-2 items-center font-medium font-geologica justify-center rounded-[12px] px-4 py-2.5 transition duration-300 ease-in-out focus:outline-hidden bg-bg-color-main-theme-deep text-white w-full md:hover:bg-bg-color-main-theme-dark"> | |
| Export starten | |
| </button> | |
| <div id="progressContainer" style="margin-top: 1.5rem; display: none;"> | |
| <div class="bg-gray-200 rounded-[8px] w-full h-[20px] overflow-hidden"> | |
| <div id="progressBar" class="bg-green-400 w-0 h-full transition-all duration-300"></div> | |
| </div> | |
| <p id="progressText" class="text-text-color-placeholder mt-1 text-[12px] leading-4 font-light text-left">Bereit...</p> | |
| </div> | |
| <div id="resultContainer" style="margin-top: 1.5rem; display: none; text-align: left;"> | |
| <h3 class="text-[14px] font-medium mb-2">Ergebnisse:</h3> | |
| <div style="display: flex; flex-direction: column; gap: 20px;"> | |
| <div> | |
| <label class="text-[12px] text-text-color-secondary block mb-1">JSON Format:</label> | |
| <textarea id="result-json" style="width: 100%; height: 200px; font-family: monospace; font-size: 12px; padding: 10px; border: 1px solid #ccc; border-radius: 8px;"></textarea> | |
| </div> | |
| <div> | |
| <label class="text-[12px] text-text-color-secondary block mb-1">CSV Format:</label> | |
| <textarea id="result-csv" style="width: 100%; height: 200px; font-family: monospace; font-size: 12px; padding: 10px; border: 1px solid #ccc; border-radius: 8px;"></textarea> | |
| </div> | |
| </div> | |
| </div> | |
| </div>`; | |
| const getEl = id => document.getElementById(id); | |
| const [startBtn, progressContainer, progressBar, progressText, resultContainer, resultJson, resultCsv] = | |
| ['startExportBtn', 'progressContainer', 'progressBar', 'progressText', 'resultContainer', 'result-json', 'result-csv'].map(getEl); | |
| startBtn.addEventListener('click', async () => { | |
| let token = localStorage.getItem("token"); | |
| if (!token) return alert("Fehler: Kein Token im localStorage gefunden."); | |
| const headers = { "Accept": "application/json", "Authorization": `Bearer ${token}`, "X-Requested-With": "aurum-with" }; | |
| async function doRefreshToken() { | |
| const rfToken = localStorage.getItem("tokenRefresh"); | |
| if (!rfToken) throw new Error("Kein Refresh Token gefunden."); | |
| const oldAuth = headers["Authorization"]; | |
| headers["Authorization"] = `Bearer ${rfToken}`; | |
| try { | |
| const res = await fetch("https://api.aurum.foundation/refresh", { method: "POST", headers }); | |
| if (!res.ok) throw new Error(`Refresh failed: ${res.status}`); | |
| const raw = await res.json(); | |
| const data = raw.encrypted ? JSON.parse(await cryptor.decode(raw.encrypted)) : raw; | |
| if (data?.data?.accessToken) { | |
| token = data.data.accessToken; | |
| localStorage.setItem("token", token); | |
| localStorage.setItem("tokenRefresh", data.data.refreshToken); | |
| headers["Authorization"] = `Bearer ${token}`; | |
| return true; | |
| } | |
| } catch (e) { | |
| headers["Authorization"] = oldAuth; | |
| throw new Error("Sitzung abgelaufen und Refresh fehlgeschlagen."); | |
| } | |
| throw new Error("Token refresh failed."); | |
| } | |
| async function fetchWithRetry(url, options, isRefresh = false) { | |
| let attempt = 0; | |
| while (true) { | |
| try { | |
| const res = await fetch(url, options); | |
| if (!res.ok) { | |
| if (res.status === 401 && !isRefresh) { | |
| progressText.textContent = `Sitzung abgelaufen, erneuere Token...`; | |
| await doRefreshToken(); | |
| options.headers["Authorization"] = headers["Authorization"]; | |
| attempt = 0; | |
| continue; | |
| } | |
| if (res.status === 401 || res.status === 403) throw new Error("Sitzung abgelaufen"); | |
| throw new Error(`HTTP ${res.status}`); | |
| } | |
| const raw = await res.json(); | |
| return raw.encrypted ? JSON.parse(await cryptor.decode(raw.encrypted)) : raw; | |
| } catch (e) { | |
| if (e.message.includes("Sitzung abgelaufen")) throw e; | |
| if (attempt < backoffs.length) { | |
| progressText.textContent = `Fehler (${e.message}), warte ${backoffs[attempt]}s...`; | |
| await delay(backoffs[attempt] * 1000); | |
| attempt++; | |
| } else { | |
| throw new Error(`Verbindungsfehler nach ${backoffs.length} Versuchen: ${e.message}`); | |
| } | |
| } | |
| } | |
| } | |
| async function fetchAPI(url) { | |
| return await fetchWithRetry(url, { method: "GET", headers: { ...headers } }); | |
| } | |
| function parseInvest(val) { | |
| if (!val) return 0; | |
| const cleaned = String(val).replace(/\s/g, ''); | |
| return parseFloat(cleaned) || 0; | |
| } | |
| let totalPartnersToFetch = 0; | |
| let currentPartnersFetched = 0; | |
| async function getReferralsRecursive(prettyId) { | |
| const partners = []; | |
| const limit = 15; | |
| let page = 1; | |
| let total = 0; | |
| do { | |
| const url = `https://api.aurum.foundation/partners/statistics?limit=${limit}&page=${page}&prettyId=${prettyId}&search=`; | |
| const data = await fetchAPI(url); | |
| total = data.total || 0; | |
| if (data.referrals && Array.isArray(data.referrals)) { | |
| for (const p of data.referrals) { | |
| currentPartnersFetched++; | |
| const pct = totalPartnersToFetch > 0 ? Math.round((currentPartnersFetched / totalPartnersToFetch) * 100) : 0; | |
| progressBar.style.width = `${Math.min(pct, 99)}%`; | |
| progressText.textContent = `Lade Partner: ${p.name} (${currentPartnersFetched} von ca. ${totalPartnersToFetch})...`; | |
| const node = { | |
| id: p.prettyId, | |
| name: p.name, | |
| rank: (p.rankId || 1) - 1, | |
| invest: parseInvest(p.investmentsAmount), | |
| partners: [] | |
| }; | |
| if (p.referralsCount > 0) { | |
| node.partners = await getReferralsRecursive(p.prettyId); | |
| } | |
| partners.push(node); | |
| } | |
| } | |
| page++; | |
| } while (partners.length < total); | |
| return partners; | |
| } | |
| const rankNames = ["Nova", "Voyager", "Vanguard", "Vanguard Pro", "Nexus", "Oracle", "Prime", "Elite", "Magnat", "Mythos", "Legend"]; | |
| function getCsvTree(root) { | |
| const lines = ["id,name,rank,invest,parent,depth"]; | |
| function recurse(node, parentId = "", depth = 0) { | |
| const rankName = rankNames[node.rank] || rankNames[0]; | |
| lines.push(`${node.id},${node.name},${rankName},${node.invest},"${parentId}",${depth}`); | |
| if (node.partners) { | |
| for (const p of node.partners) recurse(p, node.id, depth + 1); | |
| } | |
| } | |
| recurse(root); | |
| return lines.join("\n"); | |
| } | |
| try { | |
| startBtn.disabled = true; | |
| startBtn.textContent = "Initialisiere..."; | |
| progressContainer.style.display = "block"; | |
| progressText.textContent = "Lade Basisdaten..."; | |
| const rootData = await fetchAPI('https://api.aurum.foundation/'); | |
| const user = rootData.user; | |
| const affStats = rootData.affiliateStats || {}; | |
| // Heuristic for progress bar | |
| totalPartnersToFetch = (affStats.referralsCount || 0) + (affStats.referralsCountSecondLevel || 0); | |
| const resultTree = { | |
| id: user.prettyId, | |
| name: `${user.personal.name} ${user.personal.surname}`.trim(), | |
| rank: (affStats.referralRankId || 1) - 1, | |
| invest: parseInvest(affStats.investAmount), | |
| partners: [] | |
| }; | |
| resultTree.partners = await getReferralsRecursive(""); | |
| progressBar.style.width = "100%"; | |
| progressText.textContent = "Export erfolgreich abgeschlossen!"; | |
| startBtn.textContent = "Abgeschlossen"; | |
| resultJson.value = JSON.stringify(resultTree, null, 2); | |
| resultCsv.value = getCsvTree(resultTree); | |
| resultContainer.style.display = "block"; | |
| } catch (e) { | |
| console.error('Export failed:', e); | |
| progressText.classList.add('text-red-500'); | |
| progressText.textContent = `Fehler: ${e.message}`; | |
| startBtn.textContent = "Export fehlgeschlagen"; | |
| startBtn.disabled = false; | |
| } | |
| }); | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment