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:
256
backend/src/controllers/AuthController.js
Normal file
256
backend/src/controllers/AuthController.js
Normal file
@@ -0,0 +1,256 @@
|
||||
const authService = require('../services/AuthService');
|
||||
|
||||
/**
|
||||
* Authentication Controller
|
||||
* Verwaltet Auth-bezogene HTTP-Requests
|
||||
*/
|
||||
class AuthController {
|
||||
/**
|
||||
* Benutzer registrieren
|
||||
* POST /api/auth/register
|
||||
*/
|
||||
async register(req, res) {
|
||||
try {
|
||||
const { email, password, full_name } = req.body;
|
||||
|
||||
const result = await authService.register({
|
||||
email,
|
||||
password,
|
||||
full_name
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
message: 'Registrierung erfolgreich',
|
||||
user: result.user
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Registrierungsfehler:', error);
|
||||
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Benutzer einloggen
|
||||
* POST /api/auth/login
|
||||
*/
|
||||
async login(req, res) {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'E-Mail und Passwort sind erforderlich'
|
||||
});
|
||||
}
|
||||
|
||||
const result = await authService.login(email, password);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Login erfolgreich',
|
||||
token: result.token,
|
||||
user: result.user
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Login-Fehler:', error);
|
||||
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ausloggen
|
||||
* POST /api/auth/logout
|
||||
*/
|
||||
async logout(req, res) {
|
||||
try {
|
||||
const token = req.headers.authorization?.replace('Bearer ', '');
|
||||
|
||||
if (token) {
|
||||
await authService.logout(token);
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Logout erfolgreich'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Logout-Fehler:', error);
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Logout'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktuellen Benutzer abrufen
|
||||
* GET /api/auth/me
|
||||
*/
|
||||
async getCurrentUser(req, res) {
|
||||
try {
|
||||
const userId = req.user.userId;
|
||||
|
||||
const profile = await authService.getUserProfile(userId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
user: profile
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen des Benutzers:', error);
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Abrufen der Benutzerdaten'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Passwort-Reset anfordern
|
||||
* POST /api/auth/request-reset
|
||||
*/
|
||||
async requestPasswordReset(req, res) {
|
||||
try {
|
||||
const { email } = req.body;
|
||||
|
||||
if (!email) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'E-Mail ist erforderlich'
|
||||
});
|
||||
}
|
||||
|
||||
const resetToken = await authService.requestPasswordReset(email);
|
||||
|
||||
// Debug-Logs
|
||||
console.log('Reset-Token:', resetToken);
|
||||
console.log('NODE_ENV:', process.env.NODE_ENV);
|
||||
console.log('isDevelopment:', !process.env.NODE_ENV || process.env.NODE_ENV === 'development');
|
||||
|
||||
// In Produktion: E-Mail mit Reset-Link senden
|
||||
// Für Entwicklung: Token zurückgeben (auch wenn E-Mail nicht existiert)
|
||||
const isDevelopment = !process.env.NODE_ENV || process.env.NODE_ENV === 'development';
|
||||
|
||||
if (isDevelopment) {
|
||||
// Im Development-Modus immer einen Token zurückgeben für Testing
|
||||
const devToken = resetToken || 'dev-test-token-' + Date.now() + '-' + email;
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Reset-Link wurde gesendet (DEV)',
|
||||
resetToken: devToken // NUR FÜR ENTWICKLUNG!
|
||||
});
|
||||
} else {
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Falls ein Account mit dieser E-Mail existiert, wurde ein Reset-Link gesendet'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler bei Passwort-Reset-Anfrage:', error);
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Senden des Reset-Links'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Passwort zurücksetzen
|
||||
* POST /api/auth/reset-password
|
||||
*/
|
||||
async resetPassword(req, res) {
|
||||
try {
|
||||
const { token, password } = req.body;
|
||||
|
||||
if (!token || !password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Token und neues Passwort sind erforderlich'
|
||||
});
|
||||
}
|
||||
|
||||
await authService.resetPassword(token, password);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Passwort wurde erfolgreich zurückgesetzt'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Passwort-Reset:', error);
|
||||
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Passwort ändern (eingeloggter Benutzer)
|
||||
* POST /api/auth/change-password
|
||||
*/
|
||||
async changePassword(req, res) {
|
||||
try {
|
||||
const userId = req.user.userId;
|
||||
const { oldPassword, newPassword } = req.body;
|
||||
|
||||
if (!oldPassword || !newPassword) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Altes und neues Passwort sind erforderlich'
|
||||
});
|
||||
}
|
||||
|
||||
await authService.changePassword(userId, oldPassword, newPassword);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Passwort wurde erfolgreich geändert'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Passwort-Ändern:', error);
|
||||
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Token validieren
|
||||
* GET /api/auth/validate
|
||||
*/
|
||||
async validateToken(req, res) {
|
||||
try {
|
||||
// Wenn Middleware durchgelaufen ist, ist Token valid
|
||||
res.json({
|
||||
success: true,
|
||||
valid: true,
|
||||
user: req.user
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
valid: false,
|
||||
error: 'Ungültiger Token'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new AuthController();
|
||||
|
||||
101
backend/src/controllers/OAuthController.js
Normal file
101
backend/src/controllers/OAuthController.js
Normal file
@@ -0,0 +1,101 @@
|
||||
const passport = require('passport');
|
||||
|
||||
/**
|
||||
* OAuth Controller
|
||||
* Verwaltet OAuth-Logins
|
||||
*/
|
||||
class OAuthController {
|
||||
/**
|
||||
* Google OAuth initiieren
|
||||
* GET /api/auth/google
|
||||
*/
|
||||
googleAuth(req, res, next) {
|
||||
passport.authenticate('google', {
|
||||
scope: ['profile', 'email'],
|
||||
session: false
|
||||
})(req, res, next);
|
||||
}
|
||||
|
||||
/**
|
||||
* Google OAuth Callback
|
||||
* GET /api/auth/google/callback
|
||||
*/
|
||||
googleCallback(req, res, next) {
|
||||
passport.authenticate('google', {
|
||||
session: false,
|
||||
failureRedirect: `${process.env.FRONTEND_URL || 'http://localhost:5010'}/login?error=oauth_failed`
|
||||
}, (err, result) => {
|
||||
if (err || !result) {
|
||||
console.error('Google OAuth Fehler:', err);
|
||||
return res.redirect(`${process.env.FRONTEND_URL || 'http://localhost:5010'}/login?error=oauth_failed`);
|
||||
}
|
||||
|
||||
// Redirect zum Frontend mit Token
|
||||
const frontendUrl = process.env.FRONTEND_URL || 'http://localhost:5010';
|
||||
res.redirect(`${frontendUrl}/oauth-callback?token=${result.token}`);
|
||||
})(req, res, next);
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth-Identities für Benutzer abrufen
|
||||
* GET /api/auth/identities
|
||||
*/
|
||||
async getIdentities(req, res) {
|
||||
try {
|
||||
const userId = req.user.userId;
|
||||
const oauthService = require('../services/OAuthService');
|
||||
|
||||
const identities = await oauthService.getUserIdentities(userId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
identities
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Identities:', error);
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Abrufen der OAuth-Verknüpfungen'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OAuth-Provider entfernen
|
||||
* DELETE /api/auth/identity/:provider
|
||||
*/
|
||||
async unlinkProvider(req, res) {
|
||||
try {
|
||||
const userId = req.user.userId;
|
||||
const { provider } = req.params;
|
||||
const oauthService = require('../services/OAuthService');
|
||||
|
||||
const unlinked = await oauthService.unlinkProvider(userId, provider);
|
||||
|
||||
if (unlinked) {
|
||||
res.json({
|
||||
success: true,
|
||||
message: `${provider} wurde erfolgreich entfernt`
|
||||
});
|
||||
} else {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: 'Verknüpfung nicht gefunden'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Entfernen der Identity:', error);
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new OAuthController();
|
||||
|
||||
|
||||
|
||||
265
backend/src/controllers/TimeEntryController.js
Normal file
265
backend/src/controllers/TimeEntryController.js
Normal file
@@ -0,0 +1,265 @@
|
||||
const timeEntryService = require('../services/TimeEntryService');
|
||||
|
||||
/**
|
||||
* Controller-Klasse für Zeiteinträge
|
||||
* Verantwortlich für HTTP-Request/Response-Handling
|
||||
*/
|
||||
class TimeEntryController {
|
||||
/**
|
||||
* Alle Zeiteinträge abrufen
|
||||
* GET /api/time-entries
|
||||
*/
|
||||
async getAllEntries(req, res) {
|
||||
try {
|
||||
const entries = timeEntryService.getAllEntries();
|
||||
res.json(entries);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Einträge:', error);
|
||||
res.status(500).json({
|
||||
error: 'Fehler beim Abrufen der Einträge',
|
||||
message: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Einzelnen Zeiteintrag anhand der ID abrufen
|
||||
* GET /api/time-entries/:id
|
||||
*/
|
||||
async getEntryById(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const entry = timeEntryService.getEntryById(id);
|
||||
|
||||
if (!entry) {
|
||||
return res.status(404).json({
|
||||
error: 'Eintrag nicht gefunden',
|
||||
id: parseInt(id)
|
||||
});
|
||||
}
|
||||
|
||||
res.json(entry);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen des Eintrags:', error);
|
||||
res.status(500).json({
|
||||
error: 'Fehler beim Abrufen des Eintrags',
|
||||
message: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Neuen Zeiteintrag erstellen (Timer starten)
|
||||
* POST /api/time-entries
|
||||
*/
|
||||
async createEntry(req, res) {
|
||||
try {
|
||||
const entryData = req.body;
|
||||
const newEntry = timeEntryService.createEntry(entryData);
|
||||
|
||||
res.status(201).json(newEntry);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Erstellen des Eintrags:', error);
|
||||
|
||||
// Spezifische Fehlerbehandlung
|
||||
if (error.message.includes('läuft bereits')) {
|
||||
return res.status(400).json({
|
||||
error: 'Validierungsfehler',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
error: 'Fehler beim Erstellen des Eintrags',
|
||||
message: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeiteintrag aktualisieren (Timer stoppen oder Daten ändern)
|
||||
* PUT /api/time-entries/:id
|
||||
*/
|
||||
async updateEntry(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updateData = req.body;
|
||||
|
||||
const updatedEntry = timeEntryService.updateEntry(id, updateData);
|
||||
res.json(updatedEntry);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Aktualisieren des Eintrags:', error);
|
||||
|
||||
// Spezifische Fehlerbehandlung
|
||||
if (error.message.includes('nicht gefunden')) {
|
||||
return res.status(404).json({
|
||||
error: 'Eintrag nicht gefunden',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
|
||||
if (error.message.includes('Endzeit') || error.message.includes('Startzeit')) {
|
||||
return res.status(400).json({
|
||||
error: 'Validierungsfehler',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
error: 'Fehler beim Aktualisieren des Eintrags',
|
||||
message: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeiteintrag löschen
|
||||
* DELETE /api/time-entries/:id
|
||||
*/
|
||||
async deleteEntry(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
timeEntryService.deleteEntry(id);
|
||||
|
||||
res.status(204).send();
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Löschen des Eintrags:', error);
|
||||
|
||||
if (error.message.includes('nicht gefunden')) {
|
||||
return res.status(404).json({
|
||||
error: 'Eintrag nicht gefunden',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
error: 'Fehler beim Löschen des Eintrags',
|
||||
message: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktuellen Status abrufen (letzter Worklog-Eintrag)
|
||||
* GET /api/time-entries/current-state
|
||||
*/
|
||||
async getCurrentState(req, res) {
|
||||
try {
|
||||
const userId = req.user.userId;
|
||||
const state = await timeEntryService.getCurrentState(userId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
state: state
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen des aktuellen Status:', error);
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Abrufen des Status'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stempeln (Clock In/Out, Pause Start/Stop)
|
||||
* POST /api/time-entries/clock
|
||||
*/
|
||||
async clock(req, res) {
|
||||
try {
|
||||
const userId = req.user.userId;
|
||||
const { action } = req.body;
|
||||
|
||||
if (!action) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Aktion ist erforderlich'
|
||||
});
|
||||
}
|
||||
|
||||
const result = await timeEntryService.clock(userId, action);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Erfolgreich gestempelt',
|
||||
entry: result
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Stempeln:', error);
|
||||
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Statistiken abrufen
|
||||
* GET /api/time-entries/stats/summary
|
||||
*/
|
||||
async getStats(req, res) {
|
||||
try {
|
||||
const userId = req.user?.userId;
|
||||
const stats = await timeEntryService.getStatistics(userId);
|
||||
res.json(stats || {});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Statistiken:', error);
|
||||
res.status(500).json({
|
||||
error: 'Fehler beim Abrufen der Statistiken',
|
||||
message: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktuell laufenden Timer abrufen
|
||||
* GET /api/time-entries/running
|
||||
*/
|
||||
async getRunningEntry(req, res) {
|
||||
try {
|
||||
const userId = req.user.userId;
|
||||
const runningEntry = await timeEntryService.getRunningEntry(userId);
|
||||
|
||||
if (!runningEntry) {
|
||||
return res.json({}); // Leeres Objekt wenn nichts läuft
|
||||
}
|
||||
|
||||
res.json(runningEntry);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen des laufenden Timers:', error);
|
||||
res.status(500).json({
|
||||
error: 'Fehler beim Abrufen des laufenden Timers',
|
||||
message: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Einträge nach Projekt filtern
|
||||
* GET /api/time-entries/project/:projectName
|
||||
*/
|
||||
async getEntriesByProject(req, res) {
|
||||
try {
|
||||
const { projectName } = req.params;
|
||||
const entries = timeEntryService.getEntriesByProject(projectName);
|
||||
|
||||
res.json({
|
||||
project: projectName,
|
||||
count: entries.length,
|
||||
entries
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Filtern nach Projekt:', error);
|
||||
res.status(500).json({
|
||||
error: 'Fehler beim Filtern nach Projekt',
|
||||
message: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton-Instanz exportieren
|
||||
module.exports = new TimeEntryController();
|
||||
|
||||
35
backend/src/controllers/WeekOverviewController.js
Normal file
35
backend/src/controllers/WeekOverviewController.js
Normal file
@@ -0,0 +1,35 @@
|
||||
const timeEntryService = require('../services/TimeEntryService');
|
||||
|
||||
/**
|
||||
* WeekOverview Controller
|
||||
* Verwaltet Wochenübersicht-Daten
|
||||
*/
|
||||
class WeekOverviewController {
|
||||
|
||||
/**
|
||||
* Wochenübersicht für einen Benutzer abrufen
|
||||
* GET /api/week-overview
|
||||
*/
|
||||
async getWeekOverview(req, res) {
|
||||
try {
|
||||
const userId = req.user.userId;
|
||||
const { weekOffset = 0 } = req.query; // 0 = aktuelle Woche, -1 = letzte Woche, etc.
|
||||
|
||||
const weekData = await timeEntryService.getWeekOverview(userId, parseInt(weekOffset));
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: weekData
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Wochenübersicht:', error);
|
||||
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Fehler beim Laden der Wochenübersicht'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new WeekOverviewController();
|
||||
Reference in New Issue
Block a user