Merge branch 'main' of ssh://tsschulz.de:/home/git/trainingstagebuch
This commit is contained in:
@@ -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' });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -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;
|
||||
|
||||
90
backend/scripts/cleanupUserTokenKeys.js
Normal file
90
backend/scripts/cleanupUserTokenKeys.js
Normal file
@@ -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();
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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 }
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<option v-for="entry in dates" :key="entry.id" :value="entry">{{ getFormattedDate(entry.date) }}
|
||||
</option>
|
||||
</select>
|
||||
<button v-if="date && date !== 'new' && trainingPlan.length === 0" class="btn-secondary" @click="deleteCurrentDate">Datum löschen</button>
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="showForm && date === 'new'">
|
||||
@@ -84,7 +85,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<h3>Trainingsplan</h3>
|
||||
<div style="overflow-x: auto; overflow-y: visible; position: relative;">
|
||||
<div style="overflow: visible; position: relative;">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user