Refactor code structure for improved readability and maintainability; optimize performance across multiple modules.
All checks were successful
Deploy to production / deploy (push) Successful in 2m56s

This commit is contained in:
Torsten Schulz (local)
2026-07-21 09:08:15 +02:00
parent 75b52de282
commit 1ab9407e79
1400 changed files with 22278 additions and 485 deletions

0
backend/services/BaseService.js Normal file → Executable file
View File

0
backend/services/ContactService.js Normal file → Executable file
View File

0
backend/services/adminService.js Normal file → Executable file
View File

0
backend/services/authService.js Normal file → Executable file
View File

0
backend/services/blogService.js Normal file → Executable file
View File

0
backend/services/calendarService.js Normal file → Executable file
View File

2
backend/services/chatService.js Normal file → Executable file
View File

@@ -4,6 +4,7 @@ import User from '../models/community/user.js';
import Room from '../models/chat/room.js';
import UserParam from '../models/community/user_param.js';
import UserParamType from '../models/type/user_param.js';
import { dispatchRealtimeEvent } from './pushNotificationService.js';
const RABBITMQ_URL = process.env.AMQP_URL || 'amqp://localhost';
const QUEUE = 'oneToOne_messages';
@@ -257,6 +258,7 @@ class ChatService {
this.amqpAvailable = false;
}
}
await dispatchRealtimeEvent(user2HashId, 'chatMessage', { sender: user1HashId });
}
async getOneToOneMessageHistory(user1HashId, user2HashId) {

0
backend/services/chatTcpBridge.js Normal file → Executable file
View File

0
backend/services/dashboardService.js Normal file → Executable file
View File

0
backend/services/emailService.js Normal file → Executable file
View File

0
backend/services/falukantPoliticalPowersService.js Normal file → Executable file
View File

6
backend/services/falukantService.js Normal file → Executable file
View File

@@ -3587,10 +3587,10 @@ class FalukantService extends BaseService {
return { scheduled: false };
}
// Gleicher Default wie adminForceFalukantPregnancy (21 Tage bis Geburt — komprimierte Spielzeit).
const BIRTH_DUE_DAYS = 21;
// Automatische Hochzeits-Schwangerschaften nutzen die kurze Spielzeit von 18 Stunden.
const BIRTH_DUE_HOURS = 18;
const due = new Date();
due.setDate(due.getDate() + BIRTH_DUE_DAYS);
due.setHours(due.getHours() + BIRTH_DUE_HOURS);
await FalukantCharacter.unscoped().update(
{

0
backend/services/forumService.js Normal file → Executable file
View File

0
backend/services/friendshipService.js Normal file → Executable file
View File

0
backend/services/match3Service.js Normal file → Executable file
View File

0
backend/services/minigamesService.js Normal file → Executable file
View File

0
backend/services/modelsProxyService.js Normal file → Executable file
View File

0
backend/services/moderationService.js Normal file → Executable file
View File

0
backend/services/newsService.js Normal file → Executable file
View File

0
backend/services/npcCreationJobService.js Normal file → Executable file
View File

16
backend/services/oauthService.js Normal file → Executable file
View File

@@ -17,6 +17,7 @@ import { encrypt } from '../utils/encryption.js';
const saltRounds = 10;
const OAUTH_STATE_TTL_SECONDS = 15 * 60;
const OAUTH_CALLBACK_PATH = '/auth/oauth/callback';
const NATIVE_OAUTH_CALLBACK_PATH = '/android/oauth/callback';
const STATIC_PROVIDER_DEFS = [
{
@@ -98,6 +99,15 @@ const getFrontendCallbackUrl = () => {
return new URL(OAUTH_CALLBACK_PATH, `${frontendUrl.replace(/\/$/, '')}/`).toString();
};
const getNativeCallbackUrl = () => {
const configuredUrl = process.env.OAUTH_ANDROID_CALLBACK_URL || 'https://www.your-part.de/android/oauth/callback';
const callbackUrl = new URL(configuredUrl);
if (callbackUrl.protocol !== 'https:' || callbackUrl.pathname !== NATIVE_OAUTH_CALLBACK_PATH) {
throw new Error('invalidnativeoauthcallback');
}
return callbackUrl.toString();
};
const normalizeClaims = (claims = {}) => ({
subject: claims.sub || claims.subject || '',
email: typeof claims.email === 'string' ? claims.email.trim().toLowerCase() : null,
@@ -332,7 +342,7 @@ const linkIdentityToUser = async (user, providerSlug, providerConfiguration, cla
export const getOAuthProviders = async () => getProviderDefinitions().filter((provider) => provider.configured);
export const startOAuthLogin = async ({ providerSlug }) => {
export const startOAuthLogin = async ({ providerSlug, client = 'web' }) => {
const provider = getProviderDefinition(providerSlug);
if (!provider || !provider.configured) {
throw new Error('providernotconfigured');
@@ -342,7 +352,7 @@ export const startOAuthLogin = async ({ providerSlug }) => {
const codeVerifier = oidc.randomPKCECodeVerifier();
const codeChallenge = await oidc.calculatePKCECodeChallenge(codeVerifier);
const state = oidc.randomState();
const redirectUri = getFrontendCallbackUrl();
const redirectUri = client === 'android' ? getNativeCallbackUrl() : getFrontendCallbackUrl();
await storeOAuthState(state, {
providerSlug,
@@ -581,4 +591,4 @@ export const removeOAuthIdentity = async ({ userId, identityId }) => {
await identity.destroy();
return { success: true };
};
};

View File

@@ -0,0 +1,108 @@
import fs from 'fs';
import User from '../models/community/user.js';
import PushNotificationDevice from '../models/community/push_notification_device.js';
import PushNotificationSetting from '../models/community/push_notification_setting.js';
const INVALID_TOKEN_CODES = new Set([
'messaging/invalid-registration-token',
'messaging/registration-token-not-registered',
]);
const EVENT_CONFIG = {
chatMessage: { preference: 'chatEnabled', title: 'Neue Nachricht', body: 'Du hast eine neue Chat-Nachricht.', route: 'chat' },
friendloginchanged: { preference: 'friendLoginEnabled', title: 'Freund online', body: 'Ein Freund ist jetzt online.', route: 'social' },
familychanged: { preference: 'falukantEnabled', title: 'Falukant', body: 'Deine Familie hat Neuigkeiten.', route: 'falukant' },
falukantUpdateFamily: { preference: 'falukantEnabled', title: 'Falukant', body: 'Deine Familie wurde aktualisiert.', route: 'falukant' },
falukantUpdateStatus: { preference: 'falukantEnabled', title: 'Falukant', body: 'Es gibt einen neuen Falukant-Status.', route: 'falukant' },
vocabReminder: { preference: 'vocabReminderEnabled', title: 'Vokabeltraining', body: 'Deine Wiederholung wartet auf dich.', route: 'vocab' },
};
let messagingPromise;
async function getMessaging() {
if (messagingPromise !== undefined) return messagingPromise;
messagingPromise = (async () => {
const inlineCredentials = process.env.FIREBASE_SERVICE_ACCOUNT_JSON;
const credentialPath = process.env.FIREBASE_SERVICE_ACCOUNT_PATH;
if (!inlineCredentials && !credentialPath) return null;
try {
const { cert, getApps, initializeApp } = await import('firebase-admin/app');
const { getMessaging: getFirebaseMessaging } = await import('firebase-admin/messaging');
const serviceAccount = JSON.parse(inlineCredentials || fs.readFileSync(credentialPath, 'utf8'));
const app = getApps()[0] || initializeApp({ credential: cert(serviceAccount) });
return getFirebaseMessaging(app);
} catch (error) {
console.error('[push] Firebase Admin konnte nicht initialisiert werden:', error.message);
return null;
}
})();
return messagingPromise;
}
async function getUserByHashedId(hashedUserId) {
const user = await User.findOne({ where: { hashedId: hashedUserId } });
if (!user) throw new Error('usernotfound');
return user;
}
async function getSettings(userId) {
const [settings] = await PushNotificationSetting.findOrCreate({ where: { userId } });
return settings;
}
export async function registerDevice(hashedUserId, { token, enabled = true }) {
const user = await getUserByHashedId(hashedUserId);
const device = await PushNotificationDevice.findOne({ where: { token } });
if (device) {
await device.update({ userId: user.id, platform: 'android', enabled });
} else {
await PushNotificationDevice.create({ userId: user.id, token, platform: 'android', enabled });
}
return { registered: true };
}
export async function disableDevice(hashedUserId, token) {
const user = await getUserByHashedId(hashedUserId);
await PushNotificationDevice.update({ enabled: false }, { where: { userId: user.id, token } });
return { disabled: true };
}
export async function loadSettings(hashedUserId) {
const user = await getUserByHashedId(hashedUserId);
return (await getSettings(user.id)).toJSON();
}
export async function updateSettings(hashedUserId, values) {
const user = await getUserByHashedId(hashedUserId);
const settings = await getSettings(user.id);
await settings.update(values);
return settings.toJSON();
}
export async function dispatchRealtimeEvent(hashedUserId, event, data = {}) {
const config = EVENT_CONFIG[event];
if (!config) return;
const user = await getUserByHashedId(hashedUserId);
const settings = await getSettings(user.id);
if (!settings.enabled || !settings[config.preference]) return;
const messaging = await getMessaging();
if (!messaging) return;
const devices = await PushNotificationDevice.findAll({ where: { userId: user.id, enabled: true, platform: 'android' } });
await Promise.all(devices.map(async (device) => {
try {
await messaging.send({
token: device.token,
notification: { title: config.title, body: config.body },
data: { route: config.route, event, ...Object.fromEntries(Object.entries(data || {}).map(([key, value]) => [key, String(value)])) },
android: { priority: 'high', notification: { channelId: 'yourpart_updates' } },
});
} catch (error) {
if (INVALID_TOKEN_CODES.has(error.code)) {
await device.destroy();
} else {
console.error('[push] Zustellung fehlgeschlagen:', error.message);
}
}
}));
}

124
backend/services/settingsService.js Normal file → Executable file
View File

@@ -173,6 +173,130 @@ class SettingsService extends BaseService{
return [];
}
async getUserAgeAndLanguage(hashedUserId) {
const user = await this.getUserByHashedId(hashedUserId);
const userParams = await this.getUserParams(user.id, ['birthdate', 'language']);
let birthdate = null;
let language = 'en';
for (const param of userParams) {
if (param.paramType.description === 'birthdate') {
birthdate = param.value;
}
if (param.paramType.description === 'language') {
const languageValue = await UserParamValue.findOne({
where: { id: param.value }
});
language = languageValue ? languageValue.value : 'en';
}
}
return {
user,
age: birthdate ? this.calculateAge(birthdate) : null,
language,
};
}
async buildInterestPayload(interest, languageIdByCode) {
const translations = await InterestTranslation.findAll({
where: { interestsId: interest.id },
order: [[ 'id', 'asc' ]]
});
return {
id: interest.id,
name: interest.name,
allowed: interest.allowed,
adultOnly: interest.adultOnly,
interest_translations: translations.map((translation) => ({
id: translation.id,
translation: translation.translation,
language: translation.language,
user_param_value: {
value: languageIdByCode.get(Number(translation.language)) || 'en',
},
})),
};
}
async getPossibleInterests(hashedUserId) {
const { user, age, language } = await this.getUserAgeAndLanguage(hashedUserId);
const languageType = await UserParamType.findOne({ where: { description: 'language' } });
const languageValues = languageType
? await UserParamValue.findAll({ where: { userParamTypeId: languageType.id } })
: [];
const languageIdByCode = new Map(languageValues.map((entry) => [Number(entry.id), entry.value]));
const where = { allowed: true };
if (age !== null && age < 18) {
where[Op.or] = [
{ adultOnly: false },
{ adultOnly: { [Op.eq]: null } }
];
}
const interests = await InterestType.findAll({
where,
order: [[ 'name', 'asc' ]],
});
return Promise.all(interests.map(async (interest) => {
const payload = await this.buildInterestPayload(interest, languageIdByCode);
if (language && payload.interest_translations.length > 0) {
payload.interest_translations.sort((a, b) => {
const aPreferred = a.user_param_value?.value === language ? 0 : 1;
const bPreferred = b.user_param_value?.value === language ? 0 : 1;
return aPreferred - bPreferred;
});
}
return payload;
}));
}
async getInterests(hashedUserId) {
const { user, language } = await this.getUserAgeAndLanguage(hashedUserId);
const languageType = await UserParamType.findOne({ where: { description: 'language' } });
const languageValues = languageType
? await UserParamValue.findAll({ where: { userParamTypeId: languageType.id } })
: [];
const languageIdByCode = new Map(languageValues.map((entry) => [Number(entry.id), entry.value]));
const selectedInterests = await UserInterest.findAll({
where: { userId: user.id },
include: [
{
model: InterestType,
as: 'interest_type',
}
],
order: [[ 'id', 'asc' ]],
});
const payloads = [];
for (const userInterest of selectedInterests) {
const interestType = userInterest.interest_type;
if (!interestType) {
continue;
}
const payload = await this.buildInterestPayload(interestType, languageIdByCode);
if (language && payload.interest_translations.length > 0) {
payload.interest_translations.sort((a, b) => {
const aPreferred = a.user_param_value?.value === language ? 0 : 1;
const bPreferred = b.user_param_value?.value === language ? 0 : 1;
return aPreferred - bPreferred;
});
}
payloads.push({
id: interestType.id,
user_interest_type: payload,
});
}
return payloads;
}
async filterSettings(hashedUserId, type) {
const user = await this.getUserByHashedId(hashedUserId);
const userParams = await this.getUserParams(user.id, ['birthdate', 'gender']);

14
backend/services/socialnetworkService.js Normal file → Executable file
View File

@@ -28,12 +28,26 @@ import DOMPurify from 'dompurify';
import sharp from 'sharp';
import Diary from '../models/community/diary.js';
import Friendship from '../models/community/friendship.js';
import UserBlock from '../models/community/user_block.js';
import { getUserSession } from '../utils/redis.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
class SocialNetworkService extends BaseService {
async blockUser(hashedBlockerId, blockedHashedId) {
const blockerId = await this.checkUserAccess(hashedBlockerId);
const blocked = await User.findOne({ where: { hashedId: blockedHashedId } });
if (!blocked || blocked.id === blockerId) throw new Error('Invalid block target');
await UserBlock.findOrCreate({ where: { blockerId, blockedId: blocked.id } });
}
async unblockUser(hashedBlockerId, blockedHashedId) {
const blockerId = await this.checkUserAccess(hashedBlockerId);
const blocked = await User.findOne({ where: { hashedId: blockedHashedId } });
if (!blocked) return;
await UserBlock.destroy({ where: { blockerId, blockedId: blocked.id } });
}
normalizeAdultVerificationStatus(value) {
if (!value) return 'none';
const normalized = String(value).trim().toLowerCase();

0
backend/services/taxiHighscoreService.js Normal file → Executable file
View File

0
backend/services/taxiMapService.js Normal file → Executable file
View File

0
backend/services/taxiService.js Normal file → Executable file
View File

0
backend/services/vocabService.js Normal file → Executable file
View File

0
backend/services/webSocketService.js Normal file → Executable file
View File