Initial commit: TimeClock v3 - Node.js/Vue.js Zeiterfassung
Features: - Backend: Node.js/Express mit MySQL/MariaDB - Frontend: Vue.js 3 mit Composition API - UTC-Zeithandling für korrekte Zeiterfassung - Timewish-basierte Überstundenberechnung - Wochenübersicht mit Urlaubs-/Krankheits-/Feiertagshandling - Bereinigtes Arbeitsende (Generell/Woche) - Überstunden-Offset für historische Daten - Fixed Layout mit scrollbarem Content - Kompakte UI mit grünem Theme
This commit is contained in:
143
backend/src/utils/hashId.js
Normal file
143
backend/src/utils/hashId.js
Normal file
@@ -0,0 +1,143 @@
|
||||
const crypto = require('crypto');
|
||||
|
||||
/**
|
||||
* Utility für ID-Hashing
|
||||
* Konvertiert numerische IDs in Hashes und zurück
|
||||
*/
|
||||
class HashId {
|
||||
constructor() {
|
||||
// Secret aus Umgebungsvariable oder Fallback
|
||||
this.secret = process.env.HASH_ID_SECRET || 'timeclock-hash-secret-change-in-production';
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert eine numerische ID in einen Hash
|
||||
* @param {number} id - Numerische ID
|
||||
* @returns {string} Hash-String
|
||||
*/
|
||||
encode(id) {
|
||||
if (!id) return null;
|
||||
|
||||
// Erstelle einen deterministischen Hash aus ID + Secret
|
||||
const hmac = crypto.createHmac('sha256', this.secret);
|
||||
hmac.update(id.toString());
|
||||
const hash = hmac.digest('base64url'); // base64url ist URL-sicher
|
||||
|
||||
// Füge die ID in verschlüsselter Form hinzu für Dekodierung
|
||||
const encrypted = this.encryptId(id);
|
||||
|
||||
// Format: {verschlüsselte-id}.{hash-prefix}
|
||||
return `${encrypted}.${hash.substring(0, 12)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert einen Hash zurück in eine numerische ID
|
||||
* @param {string} hash - Hash-String
|
||||
* @returns {number|null} Numerische ID oder null bei Fehler
|
||||
*/
|
||||
decode(hash) {
|
||||
if (!hash || typeof hash !== 'string') return null;
|
||||
|
||||
try {
|
||||
// Extrahiere verschlüsselte ID
|
||||
const parts = hash.split('.');
|
||||
if (parts.length !== 2) return null;
|
||||
|
||||
const encrypted = parts[0];
|
||||
const hashPart = parts[1];
|
||||
|
||||
// Entschlüssele ID
|
||||
const id = this.decryptId(encrypted);
|
||||
if (!id) return null;
|
||||
|
||||
// Verifiziere Hash
|
||||
const expectedHash = this.encode(id);
|
||||
if (!expectedHash || !expectedHash.endsWith(hashPart)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return id;
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Dekodieren der Hash-ID:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verschlüsselt eine ID
|
||||
* @private
|
||||
*/
|
||||
encryptId(id) {
|
||||
const cipher = crypto.createCipheriv(
|
||||
'aes-256-cbc',
|
||||
crypto.scryptSync(this.secret, 'salt', 32),
|
||||
Buffer.alloc(16, 0) // IV
|
||||
);
|
||||
|
||||
let encrypted = cipher.update(id.toString(), 'utf8', 'base64url');
|
||||
encrypted += cipher.final('base64url');
|
||||
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entschlüsselt eine ID
|
||||
* @private
|
||||
*/
|
||||
decryptId(encrypted) {
|
||||
try {
|
||||
const decipher = crypto.createDecipheriv(
|
||||
'aes-256-cbc',
|
||||
crypto.scryptSync(this.secret, 'salt', 32),
|
||||
Buffer.alloc(16, 0) // IV
|
||||
);
|
||||
|
||||
let decrypted = decipher.update(encrypted, 'base64url', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
|
||||
return parseInt(decrypted, 10);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert ein Objekt mit IDs in eines mit Hashes
|
||||
* @param {Object} obj - Objekt mit ID-Feldern
|
||||
* @param {Array<string>} idFields - Array von Feldnamen, die IDs enthalten
|
||||
* @returns {Object} Objekt mit gehashten IDs
|
||||
*/
|
||||
encodeObject(obj, idFields = ['id', 'user_id', 'auth_info_id']) {
|
||||
if (!obj) return obj;
|
||||
|
||||
const result = { ...obj };
|
||||
|
||||
for (const field of idFields) {
|
||||
if (result[field] !== null && result[field] !== undefined) {
|
||||
result[field] = this.encode(result[field]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Konvertiert ein Array von Objekten
|
||||
* @param {Array} array - Array von Objekten
|
||||
* @param {Array<string>} idFields - Array von Feldnamen, die IDs enthalten
|
||||
* @returns {Array} Array mit gehashten IDs
|
||||
*/
|
||||
encodeArray(array, idFields = ['id', 'user_id', 'auth_info_id']) {
|
||||
if (!Array.isArray(array)) return array;
|
||||
|
||||
return array.map(obj => this.encodeObject(obj, idFields));
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton-Instanz
|
||||
const hashId = new HashId();
|
||||
|
||||
module.exports = hashId;
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user