Fügt Unterstützung für myTischtennis-Integration hinzu. Aktualisiert die Mitglieder-Controller und -Routen, um die Aktualisierung von TTR/QTTR-Werten zu ermöglichen. Ergänzt die Benutzeroberfläche in MembersView.vue zur Aktualisierung der Bewertungen und fügt neue Routen für die myTischtennis-Daten hinzu. Aktualisiert die Datenmodelle, um die neuen Felder für TTR und QTTR zu integrieren.
This commit is contained in:
@@ -54,7 +54,7 @@ class MemberService {
|
||||
}
|
||||
|
||||
async setClubMember(userToken, clubId, memberId, firstName, lastName, street, city, birthdate, phone, email, active = true, testMembership = false,
|
||||
picsInInternetAllowed = false, gender = 'unknown') {
|
||||
picsInInternetAllowed = false, gender = 'unknown', ttr = null, qttr = null) {
|
||||
try {
|
||||
console.log('[setClubMembers] - Check access');
|
||||
await checkAccess(userToken, clubId);
|
||||
@@ -77,6 +77,8 @@ class MemberService {
|
||||
member.testMembership = testMembership;
|
||||
member.picsInInternetAllowed = picsInInternetAllowed;
|
||||
if (gender) member.gender = gender;
|
||||
if (ttr !== undefined) member.ttr = ttr;
|
||||
if (qttr !== undefined) member.qttr = qttr;
|
||||
await member.save();
|
||||
} else {
|
||||
await Member.create({
|
||||
@@ -92,6 +94,8 @@ class MemberService {
|
||||
testMembership: testMembership,
|
||||
picsInInternetAllowed: picsInInternetAllowed,
|
||||
gender: gender || 'unknown',
|
||||
ttr: ttr,
|
||||
qttr: qttr,
|
||||
});
|
||||
}
|
||||
console.log('[setClubMembers] - return response');
|
||||
@@ -146,6 +150,189 @@ class MemberService {
|
||||
return { status: 500, error: 'Failed to retrieve image' };
|
||||
}
|
||||
}
|
||||
|
||||
async updateRatingsFromMyTischtennis(userToken, clubId) {
|
||||
console.log('[updateRatingsFromMyTischtennis] - Check access');
|
||||
await checkAccess(userToken, clubId);
|
||||
|
||||
const user = await getUserByToken(userToken);
|
||||
console.log('[updateRatingsFromMyTischtennis] - User:', user.id);
|
||||
|
||||
const myTischtennisService = (await import('./myTischtennisService.js')).default;
|
||||
const myTischtennisClient = (await import('../clients/myTischtennisClient.js')).default;
|
||||
|
||||
try {
|
||||
// 1. myTischtennis-Session abrufen
|
||||
console.log('[updateRatingsFromMyTischtennis] - Get session for user', user.id);
|
||||
const session = await myTischtennisService.getSession(user.id);
|
||||
console.log('[updateRatingsFromMyTischtennis] - Session retrieved:', {
|
||||
hasAccessToken: !!session.accessToken,
|
||||
hasCookie: !!session.cookie,
|
||||
expiresAt: session.expiresAt
|
||||
});
|
||||
|
||||
const account = await myTischtennisService.getAccount(user.id);
|
||||
console.log('[updateRatingsFromMyTischtennis] - Account data:', {
|
||||
id: account?.id,
|
||||
email: account?.email,
|
||||
clubId: account?.clubId,
|
||||
clubName: account?.clubName,
|
||||
fedNickname: account?.fedNickname,
|
||||
hasSession: !!(account?.accessToken)
|
||||
});
|
||||
|
||||
if (!account) {
|
||||
console.error('[updateRatingsFromMyTischtennis] - No account found!');
|
||||
return {
|
||||
status: 400,
|
||||
response: {
|
||||
message: 'Kein myTischtennis-Account gefunden.',
|
||||
updated: 0,
|
||||
errors: [],
|
||||
debug: { userId: user.id }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (!account.clubId || !account.fedNickname) {
|
||||
console.error('[updateRatingsFromMyTischtennis] - Missing clubId or fedNickname:', {
|
||||
clubId: account.clubId,
|
||||
fedNickname: account.fedNickname
|
||||
});
|
||||
return {
|
||||
status: 400,
|
||||
response: {
|
||||
message: 'Club-ID oder Verbandskürzel nicht verfügbar. Bitte einmal einloggen.',
|
||||
updated: 0,
|
||||
errors: [],
|
||||
debug: {
|
||||
hasClubId: !!account.clubId,
|
||||
hasFedNickname: !!account.fedNickname,
|
||||
clubId: account.clubId,
|
||||
fedNickname: account.fedNickname
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Rangliste vom Verein abrufen
|
||||
console.log('[updateRatingsFromMyTischtennis] - Get club rankings', {
|
||||
clubId: account.clubId,
|
||||
fedNickname: account.fedNickname,
|
||||
hasCookie: !!session.cookie
|
||||
});
|
||||
|
||||
const rankings = await myTischtennisClient.getClubRankings(
|
||||
session.cookie,
|
||||
account.clubId,
|
||||
account.fedNickname
|
||||
);
|
||||
|
||||
console.log('[updateRatingsFromMyTischtennis] - Rankings result:', {
|
||||
success: rankings.success,
|
||||
entriesCount: rankings.entries?.length || 0,
|
||||
error: rankings.error
|
||||
});
|
||||
|
||||
if (!rankings.success) {
|
||||
return {
|
||||
status: 500,
|
||||
response: {
|
||||
message: rankings.error || 'Fehler beim Abrufen der Rangliste',
|
||||
updated: 0,
|
||||
errors: [],
|
||||
debug: {
|
||||
clubId: account.clubId,
|
||||
fedNickname: account.fedNickname,
|
||||
rankingsError: rankings.error
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 3. Alle Mitglieder des Clubs laden
|
||||
console.log('[updateRatingsFromMyTischtennis] - Load club members for clubId:', clubId);
|
||||
const members = await Member.findAll({ where: { clubId } });
|
||||
console.log('[updateRatingsFromMyTischtennis] - Found members:', members.length);
|
||||
|
||||
let updated = 0;
|
||||
const errors = [];
|
||||
const notFound = [];
|
||||
const matched = [];
|
||||
|
||||
// 4. Für jedes Mitglied TTR aktualisieren
|
||||
for (const member of members) {
|
||||
const firstName = member.firstName;
|
||||
const lastName = member.lastName;
|
||||
|
||||
// Suche nach Match in rankings entries
|
||||
const rankingEntry = rankings.entries.find(entry =>
|
||||
entry.firstname.toLowerCase() === firstName.toLowerCase() &&
|
||||
entry.lastname.toLowerCase() === lastName.toLowerCase()
|
||||
);
|
||||
|
||||
if (rankingEntry) {
|
||||
try {
|
||||
// fedRank ist der TTR-Wert
|
||||
const oldTtr = member.ttr;
|
||||
member.ttr = rankingEntry.fedRank;
|
||||
// TODO: QTTR muss von einem anderen Endpoint geholt werden
|
||||
await member.save();
|
||||
updated++;
|
||||
matched.push({
|
||||
name: `${firstName} ${lastName}`,
|
||||
oldTtr: oldTtr,
|
||||
newTtr: rankingEntry.fedRank
|
||||
});
|
||||
console.log(`[updateRatingsFromMyTischtennis] - Updated ${firstName} ${lastName}: TTR ${oldTtr} → ${rankingEntry.fedRank}`);
|
||||
} catch (error) {
|
||||
console.error(`[updateRatingsFromMyTischtennis] - Error updating ${firstName} ${lastName}:`, error);
|
||||
errors.push({
|
||||
member: `${firstName} ${lastName}`,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
} else {
|
||||
notFound.push(`${firstName} ${lastName}`);
|
||||
console.log(`[updateRatingsFromMyTischtennis] - Not found in rankings: ${firstName} ${lastName}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[updateRatingsFromMyTischtennis] - Update complete');
|
||||
console.log(`Updated: ${updated}, Not found: ${notFound.length}, Errors: ${errors.length}`);
|
||||
|
||||
let message = `${updated} Mitglied(er) aktualisiert.`;
|
||||
if (notFound.length > 0) {
|
||||
message += ` ${notFound.length} nicht in myTischtennis-Rangliste gefunden.`;
|
||||
}
|
||||
if (errors.length > 0) {
|
||||
message += ` ${errors.length} Fehler beim Speichern.`;
|
||||
}
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
response: {
|
||||
message: message,
|
||||
updated: updated,
|
||||
matched: matched,
|
||||
notFound: notFound,
|
||||
errors: errors,
|
||||
totalEntries: rankings.entries.length,
|
||||
totalMembers: members.length
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[updateRatingsFromMyTischtennis] - Error:', error);
|
||||
return {
|
||||
status: 500,
|
||||
response: {
|
||||
message: error.message || 'Fehler beim Aktualisieren',
|
||||
updated: 0,
|
||||
errors: [error.message]
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new MemberService();
|
||||
256
backend/services/myTischtennisService.js
Normal file
256
backend/services/myTischtennisService.js
Normal file
@@ -0,0 +1,256 @@
|
||||
import MyTischtennis from '../models/MyTischtennis.js';
|
||||
import User from '../models/User.js';
|
||||
import myTischtennisClient from '../clients/myTischtennisClient.js';
|
||||
import HttpError from '../exceptions/HttpError.js';
|
||||
|
||||
class MyTischtennisService {
|
||||
/**
|
||||
* Get myTischtennis account for user
|
||||
*/
|
||||
async getAccount(userId) {
|
||||
const account = await MyTischtennis.findOne({
|
||||
where: { userId },
|
||||
attributes: ['id', 'email', 'savePassword', 'lastLoginAttempt', 'lastLoginSuccess', 'expiresAt', 'userData', 'clubId', 'clubName', 'fedNickname', 'createdAt', 'updatedAt']
|
||||
});
|
||||
return account;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update myTischtennis account
|
||||
*/
|
||||
async upsertAccount(userId, email, password, savePassword, userPassword) {
|
||||
// Verify user's app password
|
||||
const user = await User.findByPk(userId);
|
||||
if (!user) {
|
||||
throw new HttpError(404, 'Benutzer nicht gefunden');
|
||||
}
|
||||
|
||||
let loginResult = null;
|
||||
|
||||
// Wenn ein Passwort gesetzt/geändert wird, App-Passwort verifizieren
|
||||
if (password) {
|
||||
const isValidPassword = await user.validatePassword(userPassword);
|
||||
if (!isValidPassword) {
|
||||
throw new HttpError(401, 'Ungültiges Passwort');
|
||||
}
|
||||
|
||||
// Login-Versuch bei myTischtennis
|
||||
loginResult = await myTischtennisClient.login(email, password);
|
||||
if (!loginResult.success) {
|
||||
throw new HttpError(401, loginResult.error || 'myTischtennis-Login fehlgeschlagen. Bitte überprüfen Sie Ihre Zugangsdaten.');
|
||||
}
|
||||
}
|
||||
|
||||
// Find or create account
|
||||
let account = await MyTischtennis.findOne({ where: { userId } });
|
||||
|
||||
const now = new Date();
|
||||
|
||||
if (account) {
|
||||
// Update existing
|
||||
account.email = email;
|
||||
account.savePassword = savePassword;
|
||||
|
||||
if (password && savePassword) {
|
||||
account.setPassword(password);
|
||||
} else if (!savePassword) {
|
||||
account.encryptedPassword = null;
|
||||
}
|
||||
|
||||
if (loginResult && loginResult.success) {
|
||||
account.lastLoginAttempt = now;
|
||||
account.lastLoginSuccess = now;
|
||||
account.accessToken = loginResult.accessToken;
|
||||
account.refreshToken = loginResult.refreshToken;
|
||||
account.expiresAt = loginResult.expiresAt;
|
||||
account.cookie = loginResult.cookie;
|
||||
account.userData = loginResult.user;
|
||||
|
||||
// Hole Club-ID und Federation
|
||||
console.log('[myTischtennisService] - Getting user profile...');
|
||||
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
|
||||
console.log('[myTischtennisService] - Profile result:', {
|
||||
success: profileResult.success,
|
||||
clubId: profileResult.clubId,
|
||||
clubName: profileResult.clubName,
|
||||
fedNickname: profileResult.fedNickname
|
||||
});
|
||||
|
||||
if (profileResult.success) {
|
||||
account.clubId = profileResult.clubId;
|
||||
account.clubName = profileResult.clubName;
|
||||
account.fedNickname = profileResult.fedNickname;
|
||||
console.log('[myTischtennisService] - Updated account with club data');
|
||||
} else {
|
||||
console.error('[myTischtennisService] - Failed to get profile:', profileResult.error);
|
||||
}
|
||||
} else if (password) {
|
||||
account.lastLoginAttempt = now;
|
||||
}
|
||||
|
||||
await account.save();
|
||||
} else {
|
||||
// Create new
|
||||
const accountData = {
|
||||
userId,
|
||||
email,
|
||||
savePassword,
|
||||
lastLoginAttempt: password ? now : null,
|
||||
lastLoginSuccess: loginResult?.success ? now : null
|
||||
};
|
||||
|
||||
if (loginResult && loginResult.success) {
|
||||
accountData.accessToken = loginResult.accessToken;
|
||||
accountData.refreshToken = loginResult.refreshToken;
|
||||
accountData.expiresAt = loginResult.expiresAt;
|
||||
accountData.cookie = loginResult.cookie;
|
||||
accountData.userData = loginResult.user;
|
||||
|
||||
// Hole Club-ID
|
||||
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
|
||||
if (profileResult.success) {
|
||||
accountData.clubId = profileResult.clubId;
|
||||
accountData.clubName = profileResult.clubName;
|
||||
}
|
||||
}
|
||||
|
||||
account = await MyTischtennis.create(accountData);
|
||||
|
||||
if (password && savePassword) {
|
||||
account.setPassword(password);
|
||||
await account.save();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: account.id,
|
||||
email: account.email,
|
||||
savePassword: account.savePassword,
|
||||
lastLoginAttempt: account.lastLoginAttempt,
|
||||
lastLoginSuccess: account.lastLoginSuccess,
|
||||
expiresAt: account.expiresAt
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete myTischtennis account
|
||||
*/
|
||||
async deleteAccount(userId) {
|
||||
const deleted = await MyTischtennis.destroy({
|
||||
where: { userId }
|
||||
});
|
||||
return deleted > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify login with stored or provided credentials
|
||||
*/
|
||||
async verifyLogin(userId, providedPassword = null) {
|
||||
const account = await MyTischtennis.findOne({ where: { userId } });
|
||||
|
||||
if (!account) {
|
||||
throw new HttpError(404, 'Kein myTischtennis-Account verknüpft');
|
||||
}
|
||||
|
||||
let password = providedPassword;
|
||||
|
||||
// Wenn kein Passwort übergeben wurde, versuche gespeichertes Passwort zu verwenden
|
||||
if (!password) {
|
||||
if (!account.savePassword || !account.encryptedPassword) {
|
||||
throw new HttpError(400, 'Kein Passwort gespeichert. Bitte geben Sie Ihr Passwort ein.');
|
||||
}
|
||||
password = account.getPassword();
|
||||
}
|
||||
|
||||
// Login-Versuch
|
||||
const now = new Date();
|
||||
account.lastLoginAttempt = now;
|
||||
const loginResult = await myTischtennisClient.login(account.email, password);
|
||||
|
||||
if (loginResult.success) {
|
||||
account.lastLoginSuccess = now;
|
||||
account.accessToken = loginResult.accessToken;
|
||||
account.refreshToken = loginResult.refreshToken;
|
||||
account.expiresAt = loginResult.expiresAt;
|
||||
account.cookie = loginResult.cookie;
|
||||
account.userData = loginResult.user;
|
||||
|
||||
// Hole Club-ID und Federation
|
||||
console.log('[myTischtennisService] - Getting user profile...');
|
||||
const profileResult = await myTischtennisClient.getUserProfile(loginResult.cookie);
|
||||
console.log('[myTischtennisService] - Profile result:', {
|
||||
success: profileResult.success,
|
||||
clubId: profileResult.clubId,
|
||||
clubName: profileResult.clubName,
|
||||
fedNickname: profileResult.fedNickname
|
||||
});
|
||||
|
||||
if (profileResult.success) {
|
||||
account.clubId = profileResult.clubId;
|
||||
account.clubName = profileResult.clubName;
|
||||
account.fedNickname = profileResult.fedNickname;
|
||||
console.log('[myTischtennisService] - Updated account with club data');
|
||||
} else {
|
||||
console.error('[myTischtennisService] - Failed to get profile:', profileResult.error);
|
||||
}
|
||||
|
||||
await account.save();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
accessToken: loginResult.accessToken,
|
||||
refreshToken: loginResult.refreshToken,
|
||||
expiresAt: loginResult.expiresAt,
|
||||
user: loginResult.user,
|
||||
clubId: account.clubId,
|
||||
clubName: account.clubName
|
||||
};
|
||||
} else {
|
||||
await account.save(); // Save lastLoginAttempt
|
||||
throw new HttpError(401, loginResult.error || 'myTischtennis-Login fehlgeschlagen');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if account is configured and ready
|
||||
*/
|
||||
async checkAccountStatus(userId) {
|
||||
const account = await MyTischtennis.findOne({ where: { userId } });
|
||||
|
||||
return {
|
||||
exists: !!account,
|
||||
hasEmail: !!account?.email,
|
||||
hasPassword: !!(account?.savePassword && account?.encryptedPassword),
|
||||
hasValidSession: !!account?.accessToken && account?.expiresAt > Date.now() / 1000,
|
||||
needsConfiguration: !account || !account.email,
|
||||
needsPassword: !!account && (!account.savePassword || !account.encryptedPassword)
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored session for user (for authenticated API requests)
|
||||
*/
|
||||
async getSession(userId) {
|
||||
const account = await MyTischtennis.findOne({ where: { userId } });
|
||||
|
||||
if (!account) {
|
||||
throw new HttpError(404, 'Kein myTischtennis-Account verknüpft');
|
||||
}
|
||||
|
||||
// Check if session is valid
|
||||
if (!account.accessToken || !account.expiresAt || account.expiresAt < Date.now() / 1000) {
|
||||
throw new HttpError(401, 'Session abgelaufen. Bitte erneut einloggen.');
|
||||
}
|
||||
|
||||
return {
|
||||
accessToken: account.accessToken,
|
||||
refreshToken: account.refreshToken,
|
||||
cookie: account.cookie,
|
||||
expiresAt: account.expiresAt,
|
||||
userData: account.userData
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default new MyTischtennisService();
|
||||
|
||||
@@ -88,7 +88,7 @@ class TournamentService {
|
||||
include: [{
|
||||
model: Member,
|
||||
as: 'member',
|
||||
attributes: ['id', 'firstName', 'lastName'],
|
||||
attributes: ['id', 'firstName', 'lastName', 'ttr', 'qttr'],
|
||||
}],
|
||||
order: [[{ model: Member, as: 'member' }, 'firstName', 'ASC']]
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user