Files
harheimertc/server/utils/auth.js
2025-12-20 14:49:57 +01:00

327 lines
9.3 KiB
JavaScript

import bcrypt from 'bcryptjs'
import jwt from 'jsonwebtoken'
import { promises as fs } from 'fs'
import path from 'path'
import { encryptObject, decryptObject } from './encryption.js'
// Export migrateUserRoles für Verwendung in anderen Modulen
export function migrateUserRoles(user) {
if (!user) return user
// Wenn bereits roles Array vorhanden, nichts tun
if (Array.isArray(user.roles)) {
return user
}
// Wenn role vorhanden, zu roles Array konvertieren
if (user.role) {
user.roles = [user.role]
delete user.role
} else {
// Fallback: Standard-Rolle
user.roles = ['mitglied']
}
return user
}
const JWT_SECRET = process.env.JWT_SECRET || 'harheimertc-secret-key-change-in-production'
// Handle both dev and production paths
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
// filename is always a hardcoded constant (e.g., 'users.json'), never user input
const getDataPath = (filename) => {
const cwd = process.cwd()
// In production (.output/server), working dir is .output
if (cwd.endsWith('.output')) {
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
return path.join(cwd, '../server/data', filename)
}
// In development, working dir is project root
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
return path.join(cwd, 'server/data', filename)
}
const USERS_FILE = getDataPath('users.json')
const SESSIONS_FILE = getDataPath('sessions.json')
// Get encryption key from environment
function getEncryptionKey() {
return process.env.ENCRYPTION_KEY || 'local_development_encryption_key_change_in_production'
}
// Check if data is encrypted by trying to parse as JSON first
function isEncrypted(data) {
try {
const parsed = JSON.parse(data.trim())
if (Array.isArray(parsed)) {
return false // Unencrypted array
}
if (typeof parsed === 'object' && parsed !== null && !parsed.encryptedData) {
return false
}
return false
} catch (e) {
// JSON parsing failed - likely encrypted base64
return true
}
}
// Read users from file (with encryption support and migration)
export async function readUsers() {
try {
const data = await fs.readFile(USERS_FILE, 'utf-8')
const encrypted = isEncrypted(data)
let users = []
if (encrypted) {
const encryptionKey = getEncryptionKey()
try {
users = decryptObject(data, encryptionKey)
} catch (decryptError) {
console.error('Fehler beim Entschlüsseln der Benutzerdaten:', decryptError)
try {
users = JSON.parse(data)
console.warn('Entschlüsselung fehlgeschlagen, versuche als unverschlüsseltes Format zu lesen')
} catch (parseError) {
console.error('Konnte Benutzerdaten weder entschlüsseln noch als JSON lesen')
return []
}
}
} else {
// Plain JSON - migrate to encrypted format
users = JSON.parse(data)
console.log('Migriere unverschlüsselte Benutzerdaten zu verschlüsselter Speicherung...')
}
// Migriere Rollen von role zu roles Array
let needsMigration = false
users = users.map(user => {
const migrated = migrateUserRoles(user)
if (!Array.isArray(user.roles) && user.role) {
needsMigration = true
}
return migrated
})
// Wenn Migration nötig war, speichere zurück
if (needsMigration) {
console.log('Migriere Rollen von role zu roles Array...')
await writeUsers(users)
} else if (!encrypted) {
// Write back encrypted wenn noch nicht verschlüsselt
await writeUsers(users)
}
return users
} catch (error) {
if (error.code === 'ENOENT') {
return []
}
console.error('Fehler beim Lesen der Benutzerdaten:', error)
return []
}
}
// Write users to file (always encrypted)
export async function writeUsers(users) {
try {
const encryptionKey = getEncryptionKey()
const encryptedData = encryptObject(users, encryptionKey)
await fs.writeFile(USERS_FILE, encryptedData, 'utf-8')
return true
} catch (error) {
console.error('Fehler beim Schreiben der Benutzerdaten:', error)
return false
}
}
// Prüft ob Sessions-Daten verschlüsselt sind
function isSessionsEncrypted(data) {
try {
const parsed = JSON.parse(data.trim())
if (Array.isArray(parsed)) {
return false
}
if (typeof parsed === 'object' && parsed !== null && !parsed.encryptedData) {
return false
}
return false
} catch (e) {
return true
}
}
// Read sessions from file (with encryption support)
export async function readSessions() {
try {
const data = await fs.readFile(SESSIONS_FILE, 'utf-8')
const encrypted = isSessionsEncrypted(data)
if (encrypted) {
const encryptionKey = getEncryptionKey()
try {
return decryptObject(data, encryptionKey)
} catch (decryptError) {
console.error('Fehler beim Entschlüsseln der Sessions:', decryptError)
try {
const plainData = JSON.parse(data)
console.warn('Entschlüsselung fehlgeschlagen, versuche als unverschlüsseltes Format zu lesen')
return plainData
} catch (parseError) {
console.error('Konnte Sessions weder entschlüsseln noch als JSON lesen')
return []
}
}
} else {
// Plain JSON - migriere zu verschlüsselter Speicherung
const sessions = JSON.parse(data)
console.log('Migriere unverschlüsselte Sessions zu verschlüsselter Speicherung...')
await writeSessions(sessions)
return sessions
}
} catch (error) {
if (error.code === 'ENOENT') {
return []
}
console.error('Fehler beim Lesen der Sessions:', error)
return []
}
}
// Write sessions to file (always encrypted)
export async function writeSessions(sessions) {
try {
const encryptionKey = getEncryptionKey()
const encryptedData = encryptObject(sessions, encryptionKey)
await fs.writeFile(SESSIONS_FILE, encryptedData, 'utf-8')
return true
} catch (error) {
console.error('Fehler beim Schreiben der Sessions:', error)
return false
}
}
// Hash password
export async function hashPassword(password) {
const salt = await bcrypt.genSalt(10)
return await bcrypt.hash(password, salt)
}
// Verify password
export async function verifyPassword(password, hash) {
return await bcrypt.compare(password, hash)
}
// Generate JWT token
export function generateToken(user) {
// Stelle sicher, dass Rollen migriert sind
const migratedUser = migrateUserRoles({ ...user })
const roles = Array.isArray(migratedUser.roles) ? migratedUser.roles : (migratedUser.role ? [migratedUser.role] : ['mitglied'])
return jwt.sign(
{
id: user.id,
email: user.email,
roles: roles
},
JWT_SECRET,
{ expiresIn: '7d' }
)
}
// Verify JWT token
export function verifyToken(token) {
try {
return jwt.verify(token, JWT_SECRET)
} catch (error) {
return null
}
}
// Get user by ID
export async function getUserById(id) {
const users = await readUsers()
const user = users.find(u => u.id === id)
return user ? migrateUserRoles(user) : null
}
// Get user by email
export async function getUserByEmail(email) {
const users = await readUsers()
const user = users.find(u => u.email === email)
return user ? migrateUserRoles(user) : null
}
// Prüft ob Benutzer eine bestimmte Rolle hat
export function hasRole(user, role) {
if (!user) return false
const roles = Array.isArray(user.roles) ? user.roles : (user.role ? [user.role] : [])
return roles.includes(role)
}
// Prüft ob Benutzer eine der angegebenen Rollen hat
export function hasAnyRole(user, ...roles) {
if (!user) return false
const userRoles = Array.isArray(user.roles) ? user.roles : (user.role ? [user.role] : [])
return roles.some(role => userRoles.includes(role))
}
// Prüft ob Benutzer alle angegebenen Rollen hat
export function hasAllRoles(user, ...roles) {
if (!user) return false
const userRoles = Array.isArray(user.roles) ? user.roles : (user.role ? [user.role] : [])
return roles.every(role => userRoles.includes(role))
}
// Get user from token
export async function getUserFromToken(token) {
const decoded = verifyToken(token)
if (!decoded) return null
const users = await readUsers()
const user = users.find(u => u.id === decoded.id)
// Migriere Rollen beim Laden
if (user) {
migrateUserRoles(user)
}
return user
}
// Create session
export async function createSession(userId, token) {
const sessions = await readSessions()
const session = {
id: Date.now().toString(),
userId,
token,
createdAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString() // 7 days
}
sessions.push(session)
await writeSessions(sessions)
return session
}
// Delete session
export async function deleteSession(token) {
const sessions = await readSessions()
const filtered = sessions.filter(s => s.token !== token)
await writeSessions(filtered)
}
// Clean expired sessions
export async function cleanExpiredSessions() {
const sessions = await readSessions()
const now = new Date()
const valid = sessions.filter(s => new Date(s.expiresAt) > now)
await writeSessions(valid)
}