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
All checks were successful
Deploy to production / deploy (push) Successful in 2m56s
This commit is contained in:
0
backend/services/BaseService.js
Normal file → Executable file
0
backend/services/BaseService.js
Normal file → Executable file
0
backend/services/ContactService.js
Normal file → Executable file
0
backend/services/ContactService.js
Normal file → Executable file
0
backend/services/adminService.js
Normal file → Executable file
0
backend/services/adminService.js
Normal file → Executable file
0
backend/services/authService.js
Normal file → Executable file
0
backend/services/authService.js
Normal file → Executable file
0
backend/services/blogService.js
Normal file → Executable file
0
backend/services/blogService.js
Normal file → Executable file
0
backend/services/calendarService.js
Normal file → Executable file
0
backend/services/calendarService.js
Normal file → Executable file
2
backend/services/chatService.js
Normal file → Executable file
2
backend/services/chatService.js
Normal file → Executable 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
0
backend/services/chatTcpBridge.js
Normal file → Executable file
0
backend/services/dashboardService.js
Normal file → Executable file
0
backend/services/dashboardService.js
Normal file → Executable file
0
backend/services/emailService.js
Normal file → Executable file
0
backend/services/emailService.js
Normal file → Executable file
0
backend/services/falukantPoliticalPowersService.js
Normal file → Executable file
0
backend/services/falukantPoliticalPowersService.js
Normal file → Executable file
6
backend/services/falukantService.js
Normal file → Executable file
6
backend/services/falukantService.js
Normal file → Executable 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
0
backend/services/forumService.js
Normal file → Executable file
0
backend/services/friendshipService.js
Normal file → Executable file
0
backend/services/friendshipService.js
Normal file → Executable file
0
backend/services/match3Service.js
Normal file → Executable file
0
backend/services/match3Service.js
Normal file → Executable file
0
backend/services/minigamesService.js
Normal file → Executable file
0
backend/services/minigamesService.js
Normal file → Executable file
0
backend/services/modelsProxyService.js
Normal file → Executable file
0
backend/services/modelsProxyService.js
Normal file → Executable file
0
backend/services/moderationService.js
Normal file → Executable file
0
backend/services/moderationService.js
Normal file → Executable file
0
backend/services/newsService.js
Normal file → Executable file
0
backend/services/newsService.js
Normal file → Executable file
0
backend/services/npcCreationJobService.js
Normal file → Executable file
0
backend/services/npcCreationJobService.js
Normal file → Executable file
16
backend/services/oauthService.js
Normal file → Executable file
16
backend/services/oauthService.js
Normal file → Executable 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 };
|
||||
};
|
||||
};
|
||||
|
||||
108
backend/services/pushNotificationService.js
Normal file
108
backend/services/pushNotificationService.js
Normal 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
124
backend/services/settingsService.js
Normal file → Executable 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
14
backend/services/socialnetworkService.js
Normal file → Executable 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
0
backend/services/taxiHighscoreService.js
Normal file → Executable file
0
backend/services/taxiMapService.js
Normal file → Executable file
0
backend/services/taxiMapService.js
Normal file → Executable file
0
backend/services/taxiService.js
Normal file → Executable file
0
backend/services/taxiService.js
Normal file → Executable file
0
backend/services/vocabService.js
Normal file → Executable file
0
backend/services/vocabService.js
Normal file → Executable file
0
backend/services/webSocketService.js
Normal file → Executable file
0
backend/services/webSocketService.js
Normal file → Executable file
Reference in New Issue
Block a user