const jwt = require('jsonwebtoken'); const crypto = require('crypto'); const database = require('../config/database'); /** * OAuth Service * Verwaltet OAuth/SSO-Logins (Google, GitHub, etc.) */ class OAuthService { constructor() { this.jwtSecret = process.env.JWT_SECRET || 'your-secret-key-change-in-production'; this.jwtExpiration = process.env.JWT_EXPIRATION || '24h'; } /** * OAuth-Login/Registrierung * @param {Object} profile - OAuth-Provider-Profil * @param {string} provider - Provider-Name (google, github, etc.) * @returns {Promise} Token und Benutzer-Info */ async authenticateWithProvider(profile, provider) { return this.completeOAuthLogin(profile, provider); } async completeOAuthLogin(profile, provider, options = {}) { const { User, AuthInfo, AuthIdentity, AuthToken } = database.getModels(); const providerId = profile.id; const email = profile.emails && profile.emails[0] ? profile.emails[0].value : null; const displayName = profile.displayName || profile.name || email; if (!providerId) { throw new Error('OAuth Provider-ID fehlt'); } // Prüfen ob OAuth-Identity bereits existiert let authIdentity = await AuthIdentity.findOne({ where: { provider, identity: providerId }, include: [{ model: database.getModels().AuthInfo, as: 'authInfo', include: [{ model: User, as: 'user' }] }] }); let user, authInfo; if (authIdentity && authIdentity.authInfo) { // Bestehender OAuth-Benutzer authInfo = authIdentity.authInfo; user = authInfo.user; if (options.linkUserId && Number(user.id) !== Number(options.linkUserId)) { throw new Error('Dieser Google-Account ist bereits mit einem anderen Benutzer verknüpft'); } } else { if (options.linkUserId) { authInfo = await AuthInfo.findOne({ where: { user_id: options.linkUserId }, include: [{ model: User, as: 'user' }] }); if (!authInfo) { throw new Error('Benutzer für Verknüpfung nicht gefunden'); } authIdentity = await AuthIdentity.create({ id: await this.getNextAuthIdentityId(AuthIdentity), auth_info_id: authInfo.id, provider, identity: providerId, version: 0 }); user = authInfo.user; } else { // Neuer OAuth-Benutzer oder explizite Verknüpfung mit bestehendem Account // Prüfen ob Benutzer mit dieser E-Mail bereits existiert if (email) { authInfo = await AuthInfo.findOne({ where: { email }, include: [{ model: User, as: 'user' }] }); } if (authInfo) { return { requiresLink: true, pendingToken: this.createPendingToken(profile, provider), email, displayName }; } else { // Neuen Benutzer erstellen user = await User.create({ full_name: displayName, role: 0, daily_hours: 8, week_hours: 40, week_workdays: 5, preferred_title_type: 0, version: 0, last_change: new Date() }); // Auth-Info erstellen (ohne Passwort für OAuth-only Accounts) authInfo = await AuthInfo.create({ user_id: user.id, email: email || `${provider}_${providerId}@oauth.local`, password_hash: '', // Kein Passwort für OAuth-only password_method: 'oauth', password_salt: '', status: 1, failed_login_attempts: 0, email_token: '', email_token_role: 0, unverified_email: '', version: 0 }); // OAuth-Identity erstellen authIdentity = await AuthIdentity.create({ id: await this.getNextAuthIdentityId(AuthIdentity), auth_info_id: authInfo.id, provider, identity: providerId, version: 0 }); } } } return this.createLoginResult(user, authInfo, provider); } async getNextAuthIdentityId(AuthIdentity) { const maxId = await AuthIdentity.max('id'); return Number(maxId || 0) + 1; } createPendingToken(profile, provider) { return jwt.sign( { provider, providerId: profile.id, email: profile.emails && profile.emails[0] ? profile.emails[0].value : null, displayName: profile.displayName || profile.name || null, type: 'oauth-pending' }, this.jwtSecret, { expiresIn: '15m' } ); } verifyPendingToken(pendingToken) { const data = jwt.verify(pendingToken, this.jwtSecret); if (data.type !== 'oauth-pending' || !data.provider || !data.providerId) { throw new Error('Ungültiger OAuth-Verknüpfungstoken'); } return data; } createStateToken(data) { return jwt.sign({ ...data, type: 'oauth-state' }, this.jwtSecret, { expiresIn: '15m' }); } verifyStateToken(stateToken) { if (!stateToken) return {}; const data = jwt.verify(stateToken, this.jwtSecret); if (data.type !== 'oauth-state') { throw new Error('Ungültiger OAuth-State'); } return data; } async linkPendingToPasswordAccount(pendingToken, email, password) { const authService = require('./AuthService'); const pending = this.verifyPendingToken(pendingToken); const loginResult = await authService.login(email, password, '0'); const profile = { id: pending.providerId, displayName: pending.displayName || loginResult.user.full_name, emails: pending.email ? [{ value: pending.email }] : [] }; return this.completeOAuthLogin(profile, pending.provider, { linkUserId: loginResult.user.id }); } async linkPendingToAuthenticatedUser(pendingToken, userId) { const pending = this.verifyPendingToken(pendingToken); const profile = { id: pending.providerId, displayName: pending.displayName || null, emails: pending.email ? [{ value: pending.email }] : [] }; return this.completeOAuthLogin(profile, pending.provider, { linkUserId: userId }); } createLoginResult(user, authInfo, provider) { const { AuthToken } = database.getModels(); // JWT Token generieren const token = jwt.sign( { userId: user.id, email: authInfo.email, role: user.role, provider }, this.jwtSecret, { expiresIn: this.jwtExpiration } ); // Token in Datenbank speichern const expiresAt = new Date(); expiresAt.setHours(expiresAt.getHours() + 24); return AuthToken.create({ auth_info_id: authInfo.id, value: crypto.createHash('sha256').update(token).digest('hex'), expires: expiresAt, version: 0 }).then(() => ({ token, user: { id: user.id, full_name: user.full_name, email: authInfo.email, role: user.role, provider } })); } /** * OAuth-Identity für Benutzer abrufen * @param {number} userId - Benutzer-ID * @returns {Promise} Liste verknüpfter OAuth-Provider */ async getUserIdentities(userId) { const { AuthInfo, AuthIdentity } = database.getModels(); const authInfo = await AuthInfo.findOne({ where: { user_id: userId }, include: [{ model: AuthIdentity, as: 'identities' }] }); if (!authInfo || !authInfo.identities) { return []; } return authInfo.identities.map(identity => ({ provider: identity.provider, identity: identity.identity, id: identity.id })); } /** * OAuth-Identity entfernen * @param {number} userId - Benutzer-ID * @param {string} provider - Provider-Name * @returns {Promise} Erfolg */ async unlinkProvider(userId, provider) { const { AuthInfo, AuthIdentity } = database.getModels(); const authInfo = await AuthInfo.findOne({ where: { user_id: userId } }); if (!authInfo) { throw new Error('Benutzer nicht gefunden'); } const deleted = await AuthIdentity.destroy({ where: { auth_info_id: authInfo.id, provider } }); return deleted > 0; } } module.exports = new OAuthService();