Add watcher routes to backend and frontend; implement routing and UI components for watcher management
This commit is contained in:
28
backend/create-watcher-table.sql
Normal file
28
backend/create-watcher-table.sql
Normal file
@@ -0,0 +1,28 @@
|
||||
-- SQL Script: Watcher-Tabelle erstellen
|
||||
-- Speichert Email-Adressen, die die Arbeitszeiten eines Users sehen dürfen
|
||||
-- Ausführen mit: mysql -u root -p stechuhr < create-watcher-table.sql
|
||||
|
||||
USE stechuhr;
|
||||
|
||||
-- Erstelle Watcher-Tabelle falls nicht vorhanden
|
||||
CREATE TABLE IF NOT EXISTS watcher (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL COMMENT 'User dessen Arbeitszeiten gesehen werden dürfen',
|
||||
watcher_email VARCHAR(255) NOT NULL COMMENT 'Email-Adresse der berechtigten Person',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
|
||||
-- Foreign Keys
|
||||
CONSTRAINT fk_watcher_user FOREIGN KEY (user_id)
|
||||
REFERENCES user(id) ON DELETE CASCADE,
|
||||
|
||||
-- Indices für Performance
|
||||
INDEX idx_watcher_user (user_id),
|
||||
INDEX idx_watcher_email (watcher_email),
|
||||
|
||||
-- Unique constraint: Ein Watcher pro User/Email-Kombination
|
||||
UNIQUE KEY idx_watcher_user_email (user_id, watcher_email)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
SELECT 'Watcher-Tabelle erfolgreich erstellt oder bereits vorhanden.' AS status;
|
||||
|
||||
@@ -83,6 +83,7 @@ class Database {
|
||||
const Timefix = require('../models/Timefix');
|
||||
const Timewish = require('../models/Timewish');
|
||||
const Invitation = require('../models/Invitation');
|
||||
const Watcher = require('../models/Watcher');
|
||||
|
||||
// Models mit Sequelize-Instanz initialisieren
|
||||
User.initialize(this.sequelize);
|
||||
@@ -100,6 +101,7 @@ class Database {
|
||||
Timefix.initialize(this.sequelize);
|
||||
Timewish.initialize(this.sequelize);
|
||||
Invitation.initialize(this.sequelize);
|
||||
Watcher.initialize(this.sequelize);
|
||||
|
||||
// Assoziationen definieren
|
||||
this.defineAssociations();
|
||||
@@ -111,7 +113,7 @@ class Database {
|
||||
* Model-Assoziationen definieren
|
||||
*/
|
||||
defineAssociations() {
|
||||
const { User, Worklog, AuthInfo, AuthToken, AuthIdentity, State, WeeklyWorktime, Holiday, HolidayState, Vacation, Sick, SickType, Timefix, Timewish, Invitation } = this.sequelize.models;
|
||||
const { User, Worklog, AuthInfo, AuthToken, AuthIdentity, State, WeeklyWorktime, Holiday, HolidayState, Vacation, Sick, SickType, Timefix, Timewish, Invitation, Watcher } = this.sequelize.models;
|
||||
|
||||
// User Assoziationen
|
||||
User.hasMany(Worklog, { foreignKey: 'user_id', as: 'worklogs' });
|
||||
@@ -181,6 +183,10 @@ class Database {
|
||||
// Invitation Assoziationen
|
||||
Invitation.belongsTo(User, { foreignKey: 'inviter_user_id', as: 'inviter' });
|
||||
User.hasMany(Invitation, { foreignKey: 'inviter_user_id', as: 'invitations' });
|
||||
|
||||
// Watcher Assoziationen
|
||||
Watcher.belongsTo(User, { foreignKey: 'user_id', as: 'user' });
|
||||
User.hasMany(Watcher, { foreignKey: 'user_id', as: 'watchers' });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
73
backend/src/controllers/WatcherController.js
Normal file
73
backend/src/controllers/WatcherController.js
Normal file
@@ -0,0 +1,73 @@
|
||||
const WatcherService = require('../services/WatcherService');
|
||||
|
||||
/**
|
||||
* Controller für Watcher (Berechtigungen)
|
||||
* Verarbeitet HTTP-Requests und delegiert an WatcherService
|
||||
*/
|
||||
class WatcherController {
|
||||
/**
|
||||
* Holt alle Watcher des aktuellen Users
|
||||
*/
|
||||
async getWatchers(req, res) {
|
||||
try {
|
||||
const userId = req.user?.id || 1;
|
||||
const watchers = await WatcherService.getWatchers(userId);
|
||||
res.json(watchers);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Abrufen der Beobachter:', error);
|
||||
res.status(500).json({
|
||||
message: 'Fehler beim Abrufen der Beobachter',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt einen Watcher hinzu
|
||||
*/
|
||||
async addWatcher(req, res) {
|
||||
try {
|
||||
const userId = req.user?.id || 1;
|
||||
const { email } = req.body;
|
||||
|
||||
if (!email) {
|
||||
return res.status(400).json({
|
||||
message: 'Email-Adresse ist erforderlich'
|
||||
});
|
||||
}
|
||||
|
||||
const watcher = await WatcherService.addWatcher(userId, email);
|
||||
res.status(201).json(watcher);
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Hinzufügen des Beobachters:', error);
|
||||
res.status(error.message.includes('bereits eingetragen') ? 409 : 500).json({
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt einen Watcher
|
||||
*/
|
||||
async removeWatcher(req, res) {
|
||||
try {
|
||||
const userId = req.user?.id || 1;
|
||||
const watcherId = parseInt(req.params.id);
|
||||
|
||||
if (isNaN(watcherId)) {
|
||||
return res.status(400).json({ message: 'Ungültige ID' });
|
||||
}
|
||||
|
||||
await WatcherService.removeWatcher(userId, watcherId);
|
||||
res.json({ message: 'Beobachter entfernt' });
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Entfernen des Beobachters:', error);
|
||||
res.status(error.message.includes('nicht gefunden') ? 404 : 500).json({
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new WatcherController();
|
||||
|
||||
@@ -110,6 +110,10 @@ app.use('/api/roles', authenticateToken, rolesRouter);
|
||||
const inviteRouter = require('./routes/invite');
|
||||
app.use('/api/invite', authenticateToken, inviteRouter);
|
||||
|
||||
// Watcher routes (geschützt) - MIT ID-Hashing
|
||||
const watcherRouter = require('./routes/watcher');
|
||||
app.use('/api/watcher', authenticateToken, watcherRouter);
|
||||
|
||||
// Error handling middleware
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err.stack);
|
||||
|
||||
79
backend/src/models/Watcher.js
Normal file
79
backend/src/models/Watcher.js
Normal file
@@ -0,0 +1,79 @@
|
||||
const { Model, DataTypes } = require('sequelize');
|
||||
|
||||
/**
|
||||
* Watcher Model
|
||||
* Repräsentiert Personen, die die Arbeitszeiten eines Users sehen dürfen
|
||||
*/
|
||||
class Watcher extends Model {
|
||||
static initialize(sequelize) {
|
||||
Watcher.init(
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.BIGINT,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
allowNull: false
|
||||
},
|
||||
user_id: {
|
||||
type: DataTypes.BIGINT,
|
||||
allowNull: false,
|
||||
references: {
|
||||
model: 'user',
|
||||
key: 'id'
|
||||
},
|
||||
comment: 'Der User dessen Arbeitszeiten gesehen werden dürfen'
|
||||
},
|
||||
watcher_email: {
|
||||
type: DataTypes.STRING(255),
|
||||
allowNull: false,
|
||||
comment: 'Email-Adresse der Person, die sehen darf'
|
||||
},
|
||||
created_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW
|
||||
},
|
||||
is_active: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
allowNull: false,
|
||||
defaultValue: true
|
||||
}
|
||||
},
|
||||
{
|
||||
sequelize,
|
||||
tableName: 'watcher',
|
||||
timestamps: false,
|
||||
indexes: [
|
||||
{
|
||||
name: 'idx_watcher_user',
|
||||
fields: ['user_id']
|
||||
},
|
||||
{
|
||||
name: 'idx_watcher_email',
|
||||
fields: ['watcher_email']
|
||||
},
|
||||
{
|
||||
name: 'idx_watcher_user_email',
|
||||
fields: ['user_id', 'watcher_email'],
|
||||
unique: true
|
||||
}
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
return Watcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Definiert Assoziationen mit anderen Models
|
||||
*/
|
||||
static associate(models) {
|
||||
Watcher.belongsTo(models.User, {
|
||||
foreignKey: 'user_id',
|
||||
as: 'user'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Watcher;
|
||||
|
||||
@@ -16,6 +16,7 @@ const SickType = require('./SickType');
|
||||
const Timefix = require('./Timefix');
|
||||
const Timewish = require('./Timewish');
|
||||
const Invitation = require('./Invitation');
|
||||
const Watcher = require('./Watcher');
|
||||
|
||||
module.exports = {
|
||||
User,
|
||||
@@ -30,6 +31,7 @@ module.exports = {
|
||||
SickType,
|
||||
Timefix,
|
||||
Timewish,
|
||||
Invitation
|
||||
Invitation,
|
||||
Watcher
|
||||
};
|
||||
|
||||
|
||||
20
backend/src/routes/watcher.js
Normal file
20
backend/src/routes/watcher.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const WatcherController = require('../controllers/WatcherController');
|
||||
const unhashRequestIds = require('../middleware/unhashRequest');
|
||||
|
||||
/**
|
||||
* Routen für Watcher (Berechtigungen)
|
||||
*/
|
||||
|
||||
// GET /api/watcher - Alle Watcher abrufen
|
||||
router.get('/', WatcherController.getWatchers.bind(WatcherController));
|
||||
|
||||
// POST /api/watcher - Neuen Watcher hinzufügen
|
||||
router.post('/', WatcherController.addWatcher.bind(WatcherController));
|
||||
|
||||
// DELETE /api/watcher/:id - Watcher entfernen
|
||||
router.delete('/:id', unhashRequestIds, WatcherController.removeWatcher.bind(WatcherController));
|
||||
|
||||
module.exports = router;
|
||||
|
||||
139
backend/src/services/WatcherService.js
Normal file
139
backend/src/services/WatcherService.js
Normal file
@@ -0,0 +1,139 @@
|
||||
const database = require('../config/database');
|
||||
|
||||
/**
|
||||
* Service-Klasse für Watcher (Berechtigungen)
|
||||
* Verwaltet, wer die Arbeitszeiten eines Users sehen darf
|
||||
*/
|
||||
class WatcherService {
|
||||
/**
|
||||
* Holt alle Watcher eines Users
|
||||
* @param {number} userId - User-ID
|
||||
* @returns {Promise<Array>} Array von Watchern
|
||||
*/
|
||||
async getWatchers(userId) {
|
||||
const { Watcher } = database.getModels();
|
||||
|
||||
const watchers = await Watcher.findAll({
|
||||
where: {
|
||||
user_id: userId,
|
||||
is_active: true
|
||||
},
|
||||
order: [['created_at', 'DESC']],
|
||||
raw: true
|
||||
});
|
||||
|
||||
return watchers.map(w => ({
|
||||
id: w.id,
|
||||
email: w.watcher_email,
|
||||
createdAt: w.created_at ? new Date(w.created_at).toISOString() : null
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt einen Watcher hinzu
|
||||
* @param {number} userId - User-ID
|
||||
* @param {string} watcherEmail - Email-Adresse des Watchers
|
||||
* @returns {Promise<Object>} Erstellter Watcher
|
||||
*/
|
||||
async addWatcher(userId, watcherEmail) {
|
||||
const { Watcher } = database.getModels();
|
||||
|
||||
// Validiere Email
|
||||
if (!watcherEmail || !this._isValidEmail(watcherEmail)) {
|
||||
throw new Error('Ungültige Email-Adresse');
|
||||
}
|
||||
|
||||
// Prüfe ob bereits existiert
|
||||
const existing = await Watcher.findOne({
|
||||
where: {
|
||||
user_id: userId,
|
||||
watcher_email: watcherEmail
|
||||
}
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
if (existing.is_active) {
|
||||
throw new Error('Diese Email-Adresse ist bereits als Beobachter eingetragen');
|
||||
} else {
|
||||
// Reaktiviere inaktiven Watcher
|
||||
existing.is_active = true;
|
||||
await existing.save();
|
||||
return {
|
||||
id: existing.id,
|
||||
email: existing.watcher_email,
|
||||
createdAt: existing.created_at
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Erstelle neuen Watcher
|
||||
const watcher = await Watcher.create({
|
||||
user_id: userId,
|
||||
watcher_email: watcherEmail,
|
||||
is_active: true,
|
||||
created_at: new Date()
|
||||
});
|
||||
|
||||
return {
|
||||
id: watcher.id,
|
||||
email: watcher.watcher_email,
|
||||
createdAt: watcher.created_at
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt einen Watcher (soft delete)
|
||||
* @param {number} userId - User-ID
|
||||
* @param {number} watcherId - Watcher-ID
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async removeWatcher(userId, watcherId) {
|
||||
const { Watcher } = database.getModels();
|
||||
|
||||
const watcher = await Watcher.findByPk(watcherId);
|
||||
|
||||
if (!watcher) {
|
||||
throw new Error('Beobachter nicht gefunden');
|
||||
}
|
||||
|
||||
// Prüfe Berechtigung
|
||||
if (watcher.user_id !== userId) {
|
||||
throw new Error('Keine Berechtigung für diesen Eintrag');
|
||||
}
|
||||
|
||||
// Soft delete: Setze is_active auf false
|
||||
watcher.is_active = false;
|
||||
await watcher.save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob eine Email die Arbeitszeiten eines Users sehen darf
|
||||
* @param {number} userId - User-ID
|
||||
* @param {string} watcherEmail - Email-Adresse des Watchers
|
||||
* @returns {Promise<boolean>} True wenn berechtigt
|
||||
*/
|
||||
async canWatch(userId, watcherEmail) {
|
||||
const { Watcher } = database.getModels();
|
||||
|
||||
const watcher = await Watcher.findOne({
|
||||
where: {
|
||||
user_id: userId,
|
||||
watcher_email: watcherEmail,
|
||||
is_active: true
|
||||
}
|
||||
});
|
||||
|
||||
return !!watcher;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validiert Email-Format
|
||||
*/
|
||||
_isValidEmail(email) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new WatcherService();
|
||||
|
||||
Reference in New Issue
Block a user