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:
Torsten Schulz (local)
2025-10-17 14:11:28 +02:00
commit e95bb4cb76
86 changed files with 19530 additions and 0 deletions

View File

@@ -0,0 +1,209 @@
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<Object>} Token und Benutzer-Info
*/
async authenticateWithProvider(profile, provider) {
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;
} else {
// Neuer OAuth-Benutzer oder 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) {
// Verknüpfe OAuth mit bestehendem Account
user = authInfo.user;
authIdentity = await AuthIdentity.create({
auth_info_id: authInfo.id,
provider,
identity: providerId,
version: 0
});
} 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({
auth_info_id: authInfo.id,
provider,
identity: providerId,
version: 0
});
}
}
// 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);
await AuthToken.create({
auth_info_id: authInfo.id,
value: crypto.createHash('sha256').update(token).digest('hex'),
expires: expiresAt,
version: 0
});
return {
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<Array>} 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<boolean>} 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();