From f21ad3d8a351936de40643a062a97c3b7469e607 Mon Sep 17 00:00:00 2001 From: "Torsten Schulz (local)" Date: Mon, 1 Sep 2025 09:33:54 +0200 Subject: [PATCH] =?UTF-8?q?F=C3=BCgt=20eine=20neue=20Skriptfunktion=20zum?= =?UTF-8?q?=20Bereinigen=20von=20Benutzertoken=20hinzu=20und=20aktualisier?= =?UTF-8?q?t=20die=20Logik=20zum=20Synchronisieren=20des=20UserToken-Model?= =?UTF-8?q?ls.=20Implementiert=20eine=20neue=20Controller-Methode=20zum=20?= =?UTF-8?q?L=C3=B6schen=20von=20Datumsangaben=20f=C3=BCr=20Clubs=20und=20p?= =?UTF-8?q?asst=20die=20Routen=20entsprechend=20an.=20Erg=C3=A4nzt=20die?= =?UTF-8?q?=20Benutzeroberfl=C3=A4che=20in=20DiaryView.vue=20um=20die=20M?= =?UTF-8?q?=C3=B6glichkeit,=20ein=20Datum=20zu=20l=C3=B6schen,=20und=20akt?= =?UTF-8?q?ualisiert=20die=20Logik=20zur=20=C3=9Cberpr=C3=BCfung=20der=20D?= =?UTF-8?q?atumsaktualit=C3=A4t.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/controllers/diaryController.js | 12 +++ backend/package.json | 3 +- backend/routes/diaryRoutes.js | 4 +- backend/scripts/cleanupUserTokenKeys.js | 90 ++++++++++++++++++++ backend/server.js | 2 +- backend/services/diaryDateActivityService.js | 2 +- backend/services/diaryService.js | 18 ++++ frontend/src/views/DiaryView.vue | 53 +++++++++++- 8 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 backend/scripts/cleanupUserTokenKeys.js diff --git a/backend/controllers/diaryController.js b/backend/controllers/diaryController.js index b2686f6..98c9452 100644 --- a/backend/controllers/diaryController.js +++ b/backend/controllers/diaryController.js @@ -116,3 +116,15 @@ const deleteTagFromDiaryDate = async (req, res) => { export { getDatesForClub, createDateForClub, updateTrainingTimes, addDiaryNote, deleteDiaryNote, addDiaryTag, addTagToDiaryDate, deleteTagFromDiaryDate }; + +export const deleteDateForClub = async (req, res) => { + try { + const { clubId, dateId } = req.params; + const { authcode: userToken } = req.headers; + const result = await diaryService.removeDateForClub(userToken, clubId, dateId); + res.status(200).json(result); + } catch (error) { + console.error('[deleteDateForClub] - Error:', error); + res.status(error.statusCode || 500).json({ error: error.message || 'systemerror' }); + } +}; diff --git a/backend/package.json b/backend/package.json index 51a13f6..2f6e4c4 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,7 +5,8 @@ "type": "module", "scripts": { "postinstall": "cd ../frontend && npm install && npm run build", - "dev": "nodemon server.js" + "dev": "nodemon server.js", + "cleanup:usertoken": "node ./scripts/cleanupUserTokenKeys.js" }, "keywords": [], "author": "", diff --git a/backend/routes/diaryRoutes.js b/backend/routes/diaryRoutes.js index cc17bc9..dacfce2 100644 --- a/backend/routes/diaryRoutes.js +++ b/backend/routes/diaryRoutes.js @@ -8,7 +8,8 @@ import { deleteDiaryNote, addDiaryTag, addTagToDiaryDate, - deleteTagFromDiaryDate + deleteTagFromDiaryDate, + deleteDateForClub, } from '../controllers/diaryController.js'; const router = express.Router(); @@ -21,5 +22,6 @@ router.delete('/:clubId/tag', authenticate, deleteTagFromDiaryDate); router.get('/:clubId', authenticate, getDatesForClub); router.post('/:clubId', authenticate, createDateForClub); router.put('/:clubId', authenticate, updateTrainingTimes); +router.delete('/:clubId/:dateId', authenticate, deleteDateForClub); export default router; diff --git a/backend/scripts/cleanupUserTokenKeys.js b/backend/scripts/cleanupUserTokenKeys.js new file mode 100644 index 0000000..c822c59 --- /dev/null +++ b/backend/scripts/cleanupUserTokenKeys.js @@ -0,0 +1,90 @@ +import mysql from 'mysql2/promise'; +import dotenv from 'dotenv'; + +dotenv.config(); + +const dbConfig = { + host: process.env.DB_HOST || 'localhost', + user: process.env.DB_USER || 'root', + password: process.env.DB_PASSWORD || '', + database: process.env.DB_NAME || 'trainingdiary', +}; + +async function getIndexSummary(connection, table) { + const [rows] = await connection.execute(`SHOW INDEX FROM \`${table}\``); + const summary = rows.reduce((acc, r) => { + const key = r.Key_name; + acc[key] = acc[key] || { unique: r.Non_unique === 0, columns: [] }; + acc[key].columns.push(r.Column_name); + return acc; + }, {}); + return summary; +} + +async function cleanupUserTokenKeys() { + let connection; + const table = 'UserToken'; + + try { + console.log('Connecting to DB:', dbConfig); + connection = await mysql.createConnection(dbConfig); + + console.log(`\nBefore cleanup (indexes on ${table}):`); + let before = await getIndexSummary(connection, table); + Object.entries(before).forEach(([name, info]) => { + console.log(` - ${name} ${info.unique ? '(UNIQUE)' : ''} -> [${info.columns.join(', ')}]`); + }); + + // Drop all non-PRIMARY indexes on UserToken + const [indexes] = await connection.execute(`SHOW INDEX FROM \`${table}\``); + const keyNames = Array.from(new Set(indexes.map(i => i.Key_name))).filter(k => k !== 'PRIMARY'); + + for (const keyName of keyNames) { + try { + await connection.execute(`DROP INDEX \`${keyName}\` ON \`${table}\``); + console.log(`Dropped index: ${keyName}`); + } catch (err) { + console.warn(`Could not drop ${keyName}: ${err.code || err.message}`); + } + } + + // Re-create minimal, deterministic indexes + // Unique on token (column is 'token') + try { + await connection.execute(`CREATE UNIQUE INDEX \`uniq_UserToken_token\` ON \`${table}\` (\`token\`)`); + console.log('Created UNIQUE index: uniq_UserToken_token (token)'); + } catch (err) { + console.warn('Could not create uniq_UserToken_token:', err.code || err.message); + } + + // Helpful index on user_id if column exists + try { + const [cols] = await connection.execute(`SHOW COLUMNS FROM \`${table}\` LIKE 'user_id'`); + if (cols && cols.length > 0) { + await connection.execute(`CREATE INDEX \`idx_UserToken_user_id\` ON \`${table}\` (\`user_id\`)`); + console.log('Created INDEX: idx_UserToken_user_id (user_id)'); + } else { + console.log('Column user_id not found, skip creating idx_UserToken_user_id'); + } + } catch (err) { + console.warn('Could not create idx_UserToken_user_id:', err.code || err.message); + } + + console.log(`\nAfter cleanup (indexes on ${table}):`); + const after = await getIndexSummary(connection, table); + Object.entries(after).forEach(([name, info]) => { + console.log(` - ${name} ${info.unique ? '(UNIQUE)' : ''} -> [${info.columns.join(', ')}]`); + }); + + console.log('\nDone.'); + } catch (err) { + console.error('Cleanup failed:', err); + process.exitCode = 1; + } finally { + if (connection) await connection.end(); + } +} + +cleanupUserTokenKeys(); + + diff --git a/backend/server.js b/backend/server.js index 4dcbdce..03f073b 100644 --- a/backend/server.js +++ b/backend/server.js @@ -151,7 +151,7 @@ app.get('*', (req, res) => { await TournamentMatch.sync({ alter: true }); await TournamentResult.sync({ alter: true }); await Accident.sync({ alter: true }); - await UserToken.sync({ alter: true }); + await UserToken.sync(); app.listen(port, () => { console.log(`Server is running on http://localhost:${port}`); diff --git a/backend/services/diaryDateActivityService.js b/backend/services/diaryDateActivityService.js index 5e66b98..2b1d70d 100644 --- a/backend/services/diaryDateActivityService.js +++ b/backend/services/diaryDateActivityService.js @@ -20,7 +20,7 @@ class DiaryDateActivityService { duration: data.duration }); } - restData.predefinedActivityId = predefinedActivity.id; + restData.predefinedActivityId = predefinedActivity.id; const maxOrderId = await DiaryDateActivity.max('orderId', { where: { diaryDateId: data.diaryDateId } }); diff --git a/backend/services/diaryService.js b/backend/services/diaryService.js index c0f236d..9ab8cc7 100644 --- a/backend/services/diaryService.js +++ b/backend/services/diaryService.js @@ -1,4 +1,5 @@ import DiaryDate from '../models/DiaryDates.js'; +import DiaryDateActivity from '../models/DiaryDateActivity.js'; import Club from '../models/Club.js'; import DiaryNote from '../models/DiaryNote.js'; import { DiaryTag } from '../models/DiaryTag.js'; @@ -151,6 +152,23 @@ class DiaryService { await DiaryDateTag.destroy({ where: { tagId } }); } + async removeDateForClub(userToken, clubId, dateId) { + console.log('[DiaryService::removeDateForClub] - Check user access'); + await checkAccess(userToken, clubId); + console.log('[DiaryService::removeDateForClub] - Validate date'); + const diaryDate = await DiaryDate.findOne({ where: { id: dateId, clubId } }); + if (!diaryDate) { + throw new HttpError('Diary entry not found', 404); + } + console.log('[DiaryService::removeDateForClub] - Check for activities'); + const activityCount = await DiaryDateActivity.count({ where: { diaryDateId: dateId } }); + if (activityCount > 0) { + throw new HttpError('Cannot delete date with activities', 409); + } + console.log('[DiaryService::removeDateForClub] - Delete diary date'); + await diaryDate.destroy(); + return { ok: true }; + } } export default new DiaryService(); diff --git a/frontend/src/views/DiaryView.vue b/frontend/src/views/DiaryView.vue index 4b0bdd1..c286c2a 100644 --- a/frontend/src/views/DiaryView.vue +++ b/frontend/src/views/DiaryView.vue @@ -8,6 +8,7 @@ +
@@ -464,6 +465,17 @@ export default { } }, + async refreshDates(selectId) { + const response = await apiClient.get(`/diary/${this.currentClub}`); + this.dates = response.data.map(entry => ({ id: entry.id, date: entry.date })); + // neueste zuerst + this.dates.sort((a, b) => new Date(b.date) - new Date(a.date)); + if (selectId) { + const match = this.dates.find(d => String(d.id) === String(selectId)); + if (match) this.date = { id: match.id, date: match.date }; + } + }, + setCurrentDate() { const today = new Date().toISOString().split('T')[0]; this.newDate = today; @@ -517,11 +529,14 @@ export default { trainingEnd: this.trainingEnd || null, }); this.dates.push({ id: response.data.id, date: response.data.date }); - this.date = { id: response.data.id, date: response.data.date }; + // Liste nach Datum sortieren (neueste zuerst) + await this.refreshDates(response.data.id); this.showForm = false; this.newDate = ''; this.trainingStart = response.data.trainingStart; this.trainingEnd = response.data.trainingEnd; + // Direkt auf das leere Tagebuch des neuen Datums wechseln + await this.handleDateChange(); } catch (error) { alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.'); } @@ -827,6 +842,13 @@ export default { async addPlanItem() { try { if (this.addNewItem || this.addNewTimeblock) { + // Sicherstellen, dass das ausgewählte Datum existiert + const list = await apiClient.get(`/diary/${this.currentClub}`).then(r => r.data); + if (!list.some(e => String(e.id) === String(this.date?.id))) { + await this.refreshDates(); + alert('Ausgewähltes Datum war nicht mehr aktuell. Bitte erneut versuchen.'); + return; + } await apiClient.post(`/diary-date-activities/${this.currentClub}`, { diaryDateId: this.date.id, activity: this.addNewTimeblock ? '' : this.newPlanItem.activity, @@ -850,7 +872,34 @@ export default { this.trainingPlan = await apiClient.get(`/diary-date-activities/${this.currentClub}/${this.date.id}`).then(response => response.data); this.calculateIntermediateTimes(); } catch (error) { - alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.'); + const msg = (error && error.response && error.response.data && error.response.data.error) || 'Ein Fehler ist aufgetreten.'; + if (msg.toLowerCase().includes('foreign key') || msg.toLowerCase().includes('constraint')) { + await this.refreshDates(); + alert('Datum war nicht (mehr) vorhanden. Die Datums-Auswahl wurde aktualisiert. Bitte erneut versuchen.'); + } else { + alert(msg); + } + } + }, + + async deleteCurrentDate() { + if (!this.date || this.date === 'new') return; + if (this.trainingPlan.length > 0) { + alert('Datum kann nicht gelöscht werden: Es sind bereits Aktivitäten vorhanden.'); + return; + } + if (!confirm('Dieses Datum wirklich löschen?')) return; + try { + await apiClient.delete(`/diary/${this.currentClub}/${this.date.id}`); + await this.refreshDates(); + this.date = null; + this.trainingStart = ''; + this.trainingEnd = ''; + this.participants = []; + this.trainingPlan = []; + } catch (e) { + const msg = (e && e.response && e.response.data && e.response.data.error) || 'Fehler beim Löschen.'; + alert(msg); } },