Postfach/Team-Verwaltung/Supporter-Abo als Node.js/Express-Server portiert
Vollständiger 1:1-Port des cloudflare-worker/ (dogfather-universe-postfach) auf server-internal/: Bewerbungs-Postfach mit Rollen/Rechten, Team- Zugangsverwaltung (verschlüsselte Owner-only-Codes), DogiCrew-Supporter-Abo (PayPal Subscriptions, Google-Login, Resend-E-Mail, 30-Monats-Prämienzyklus) und automatische TikTok-Live-Erkennung. D1 -> better-sqlite3, Cloudflare- Cron -> node-cron. Läuft ohne gesetzte Secrets weiter (PayPal/Google/Resend/ Ticketanizer/Discord bleiben "nicht konfiguriert" statt kaputt), bis die echten Zugangsdaten eingetragen werden.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
/* =====================================================================
|
||||
crypto.js — Verschlüsselung & Hashing für das Zugangssystem. 1:1 portiert
|
||||
aus cloudflare-worker/src/lib/crypto.js (Web Crypto API, in Node 20+
|
||||
global verfügbar wie im Cloudflare Worker — keine Änderung nötig).
|
||||
===================================================================== */
|
||||
|
||||
const CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // ohne I,O,0,1
|
||||
|
||||
export function generateFriendlyCode() {
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(12));
|
||||
let raw = "";
|
||||
for (const b of bytes) raw += CODE_ALPHABET[b % CODE_ALPHABET.length];
|
||||
return `${raw.slice(0, 4)}-${raw.slice(4, 8)}-${raw.slice(8, 12)}`;
|
||||
}
|
||||
|
||||
export function generateSessionToken() {
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(32));
|
||||
return [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
export function generateId() {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
export async function sha256Hex(text) {
|
||||
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
|
||||
return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, "0")).join("");
|
||||
}
|
||||
|
||||
async function getAesKey() {
|
||||
const raw = Uint8Array.from(atob(process.env.ENCRYPTION_KEY), (c) => c.charCodeAt(0));
|
||||
return crypto.subtle.importKey("raw", raw, "AES-GCM", false, ["encrypt", "decrypt"]);
|
||||
}
|
||||
|
||||
function bufToBase64(buf) {
|
||||
return btoa(String.fromCharCode(...new Uint8Array(buf)));
|
||||
}
|
||||
function base64ToBuf(b64) {
|
||||
return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
||||
}
|
||||
|
||||
/** Verschlüsselt einen Klartext-Code reversibel. Rückgabe: "iv.ciphertext" (base64). */
|
||||
export async function encryptCode(plaintext) {
|
||||
const key = await getAesKey();
|
||||
const iv = crypto.getRandomValues(new Uint8Array(12));
|
||||
const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, new TextEncoder().encode(plaintext));
|
||||
return `${bufToBase64(iv)}.${bufToBase64(ciphertext)}`;
|
||||
}
|
||||
|
||||
/** Entschlüsselt einen mit encryptCode() erzeugten Wert. */
|
||||
export async function decryptCode(encoded) {
|
||||
const [ivB64, ctB64] = encoded.split(".");
|
||||
const key = await getAesKey();
|
||||
const iv = base64ToBuf(ivB64);
|
||||
const ciphertext = base64ToBuf(ctB64);
|
||||
const plainBuf = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, ciphertext);
|
||||
return new TextDecoder().decode(plainBuf);
|
||||
}
|
||||
|
||||
export function nowIso() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
Reference in New Issue
Block a user