chore: restrukturierung der microservices in dev/lobby und dev/sector-dev (monorepo)

This commit is contained in:
2026-06-08 22:08:44 +02:00
parent 87f4d935c4
commit c1eb570b43
90 changed files with 10481 additions and 2 deletions
@@ -0,0 +1,606 @@
import argon2 from 'argon2';
import jwt from 'jsonwebtoken';
import pool from '../db.js';
import { sendEmail } from './email.service.js';
/**
* Generiert einen zufälligen 6-stelligen Code als String
*/
const generateOTP = () => {
return Math.floor(100000 + Math.random() * 900000).toString();
};
/**
* Registriert einen neuen Nutzer, speichert das OTP und löst den E-Mail-Versand aus.
*
* @param {string} username - Der gewünschte Benutzername
* @param {string} email - Die E-Mail-Addresse
* @param {string} password - Das Klartext-Passwort
* @returns {object} - Die accountId des neuen Nutzers
*/
export const registerUser = async (username, email, password) => {
// Wir holen uns einen dedizierten Client aus dem pool für unsere Transaktion
const client = await pool.connect();
const lowerCaseEmail = email.toLowerCase();
try {
// Transaktion starten: Entweder alles klappt, oder nichts wird gespeichert
await client.query('BEGIN');
// Prüfen, ob der Nutzer oder die E-Mail schon existiert
const checkResult = await client.query(`
SELECT username, email FROM lobby_accounts
WHERE LOWER(username) = LOWER($1) OR email = $2`,
[username, lowerCaseEmail]
);
if (checkResult.rows.length > 0) {
const existingRecord = checkResult.rows[0];
if (existingRecord.email === lowerCaseEmail) {
const error = new Error('Email already exists');
error.code = 'ERR_EMAIL_TAKEN';
throw error;
} else {
const error = new Error('Username already exists');
error.code = 'ERR_USERNAME_TAKEN';
throw error;
}
}
// Passwort sicher hashen
const passwordHash = await argon2.hash(password);
// Nutzer in de Datenbank eintragen
const insertAccount = await client.query(`
INSERT INTO lobby_accounts (username, email, password_hash)
VALUES ($1, $2, $3) RETURNING id`,
[username, lowerCaseEmail, passwordHash]
);
const accountId = insertAccount.rows[0].id;
// OTP generieren uns in der auth_otps Tabelle speichern (15min Gültigkeit)
const otpCode = generateOTP();
await client.query(`
INSERT INTO auth_otps (account_id, otp_code, type, expires_at)
VALUES ($1, $2, 'register', NOW() + INTERVAL '15 minutes')`,
[accountId, otpCode]
);
// Registierungs E-Mail über unseren E-Mail Service versenden
const mailHtml = `
<h2>Willkommen bei Void-Genesis, ${username}!</h2>
<p>Dein Verifizierungscode lautet:</p>
<h1 style="color: #00f0ff; letter-spacing: 5px;">${otpCode}</h1>
<p>Dieser Code ist 15 Minuten lang gültig.</p>
`;
await sendEmail(lowerCaseEmail, username, 'Void-Genesis - Aktiviere deinen Account', mailHtml);
// Wenn wir hier ankommen, lief alles fehlerfrei, wir committen die Daten.
await client.query('COMMIT');
return { accountId };
} catch (error) {
// bei JEDEM Fehler machen wir einen Rollback. Die Datenbank bleibt sauber.
await client.query('ROLLBACK');
if (error.code === '23505') {
if (error.constraint === 'lobby_accounts_username_key') {
const customErr = new Error('Username already exists');
customErr.code = 'ERR_USERNAME_TAKEN';
throw customErr;
}
if (error.constraint === 'lobby_accounts_email_key') {
const customErr = new Error('Email already exists');
customErr.code = 'ERR_EMAIL_TAKEN';
throw customErr;
}
}
throw error;
} finally {
client.release();
}
}
/**
* Verifiziert die E-Mail-Adresse eines Nutzers
*
* @param {string} email - Die E-Mail-Addresse des Nutzers
* @param {string} otpCode - Der 6-stellige Code aus der E-Mail
* @returns {boolean} - true wenn erfolgreich
*/
export const verifyEmail = async (email, otpCode) => {
const client = await pool.connect();
const lowerCaseEmail = email.toLowerCase();
try {
await client.query('BEGIN');
// Nutzer anhand der E-Mail suchen un die ID sowie Verifizierungsstatus abfragen
const accountResult = await client.query(
'SELECT id, is_email_verified FROM lobby_accounts WHERE email = $1',
[lowerCaseEmail]
);
if (accountResult.rows.length === 0) {
// ERR_USER_NOT_FOUND: Es existiert kein Account mit dieser E-Mail
const error = new Error('User not found');
error.code = 'ERR_USER_NOT_FOUND';
throw error;
}
const account = accountResult.rows[0];
// Prüfen, ob der Nutzer nicht ohnehin schon verifiziert ist
if (account.is_email_verified) {
// ERR_ALREADY_VERIFIED: Diese E-Mail-Addresse wurde bereits bestätigt
const error = new Error('Already verified');
error.code = 'ERR_ALREADY_VERIFIED';
throw error;
}
// Den passenden OTP-Code in der Datenbank suchen
const otpResult = await client.query(`
SELECT id, expires_at FROM auth_otps
WHERE account_id = $1 AND otp_code = $2 AND type = 'register'`,
[account.id, otpCode]
);
if (otpResult.rows.length === 0) {
// ERR_INVALID_OTP: Der eingegebene Code ist falsch.
const error = new Error('Invalid OTP');
error.code = 'ERR_INVALID_OTP';
throw error;
}
const otpRecord = otpResult.rows[0];
// Prüfen, ob der Cide bereits abgelaufen ist (älter als 15 Minuten)
if (new Date() > new Date(otpRecord.expires_at)) {
// ERR_OTP_EXPIRED: Der Code ist abgelaufen. Bitte fordere einen neuen an.
const error = new Error('OTP expired');
error.code = 'ERR_OTP_EXPIRED';
throw error;
}
// Account als verifiziert markieren
await client.query(
'UPDATE lobby_accounts SET is_email_verified = true WHERE id = $1',
[account.id]
);
// Den genutzten OTP-Code aus der Datenbank löschen (Sicherheit und Sauberkeit)
await client.query(
'DELETE FROM auth_otps WHERE id = $1',
[otpRecord.id]
);
// Alles lief perfekt, Änderungen speichern
await client.query('COMMIT');
return true;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
};
/**
* Loggt einen Nutzer ein, verifiziert das Passwort uns erstellt JWT-Tokens
*
* @param {string} identifier - Kann der Benutzername oder die E-Mail-Address sein
* @param {string} password - Das eingegebene Klartext-Passwort
* @returns {object} - Enthält accessToken, refreshToken und grundlegende Nutzerdaten
*/
export const loginUser = async (identifier, password) => {
const client = await pool.connect();
try {
// Nutzer suchen
const accountResult = await client.query(`
SELECT id, username, email, password_hash, is_email_verified, is_admin, res_premium
FROM lobby_accounts
WHERE LOWER(username) = LOWER($1) OR email = LOWER($1)`,
[identifier]
);
// Wenn kein Nutzer gefunden wurde
if (accountResult.rows.length === 0) {
// ERR_INVALID_CREDENTIALS: Benutzername/E-Mail oder Passwort falsch
const error = new Error('Invalid credentials');
error.code = 'ERR_INVALID_CREDENTIALS';
throw error;
}
const account = accountResult.rows[0];
// Prüfen, ob die Email bereits bestätigt wurde
if (!account.is_email_verified) {
// ERR_EMAIL_NOT_VERIFIED: Bitte bestätige zuerst deine E-Mail-Addresse
const error = new Error('Email not verified');
error.code = 'ERR_EMAIL_NOT_VERIFIED';
error.email = account.email;
throw error;
}
// Passwort vergleichen (argon2)
const isPasswordValid = await argon2.verify(account.password_hash, password);
if (!isPasswordValid) {
const error = new Error('Invalied credentials');
error.code = 'ERR_INVALID_CREDENTIALS';
throw error;
}
// JWT-Tokens geneieren
// Das Access-Token speichert die wichtigsten Infos für schnelle Abfragen
const tokenPayload = {
accountId: account.id,
username: account.username,
isAdmin: account.is_admin
};
const accessToken = jwt.sign(
tokenPayload,
process.env.JWT_ACCESS_SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ accountId: account.id },
process.env.JWT_REFRESH_SECRET,
{ expiresIn: '30d' }
);
// Refresh-Token in der Datenbank speichern (Vorbereitung für das Single-Session System)
await client.query(`
INSERT INTO refresh_tokens (account_id, token, expires_at)
VALUES ($1, $2, NOW() + INTERVAL '30 days')`,
[account.id, refreshToken]
);
// Erfolgreiche Rückgabe der Daten
return {
accessToken,
refreshToken,
user: {
id: account.id,
username: account.username,
email: account.email,
res_premium: account.res_premium
}
};
} finally {
client.release();
}
};
/**
* Prüft ein Refresh-Token und generiert ein neues, aktuelles Access-Token
*
* @param {string} refreshToken - Das langlebige Refresh-Token des Nutzers.
* @returns {object} - Enthält das neue accessToken
*/
export const refreshAccessToken = async (refreshToken) => {
const client = await pool.connect();
try {
// JWT-Signatur und Ablaufdatum des Refresh-Tokens kryptografisch verifizieren
// Wenn das Token manipuliert oder abgelaufen ist, wirft jwt.verify sofort einen Fehler.
const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
const { accountId } = payload;
// Sicherheits-Check: Existiert diese Token noch in unserer Datenbank?
// Wenn sich ein zweites Gerät eingeloggt hat, wurden die altenm Tokens gelöscht.
// Dieser Check schlägt dasnn fehl und blockiert den unerlaubten Zugriff!
const tokenResult = await client.query(
'SELECT id FROM refresh_tokens WHERE account_id = $1 AND token = $2',
[accountId, refreshToken]
);
if (tokenResult.rows.length === 0) {
const error = new Error('Refresh token was revoked or does not exist');
error.code = 'ERR_UNAUTHORIZED';
throw error;
}
// Aktuelle Benutzerdaten frosch aus der Datenbank abfragen
// Das behebt das "Stale Data" Problem. Falls der Admin Status geändert wurde,
// steht er im neuen Token sofort korrekt drin, ohne die DB bei jedem API.Call zu belasten
const accountResult = await client.query(
'SELECT id, username, email, is_admin, res_premium FROM lobby_accounts WHERE id = $1',
[accountId]
);
if (accountResult.rows.length === 0) {
const error = new Error('Account no longer exists');
error.code = 'ERR_UNAUTHORIZED';
throw error;
}
const account = accountResult.rows[0];
// Neues kurzlebiges Access-Token erstellen (wieder für 15 min)
const tokenPayload = {
accountId: account.id,
username: account.username,
isAdmin: account.is_admin
};
const accessToken = jwt.sign(
tokenPayload,
process.env.JWT_ACCESS_SECRET,
{ expiresIn: '15m' }
);
return {
accessToken,
user: {
id: account.id,
username: account.username,
email: account.email,
res_premium: account.res_premium
}
};
} catch (error) {
// Abfangen von JWT-spezifischen Fehlern (z.B. Token abgelaufen oder Signatur falsch)
if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError' || error.name === 'SyntaxError') {
const customErr = new Error('Invalid, expired or malformed token');
customErr.code = 'ERR_UNAUTHORIZED';
throw customErr;
}
// Alle anderen Fehhler (zB Datenbank Fehler) weiterwerfen
throw error;
} finally {
client.release();
}
}
/**
* Sendet einen neuen OTP-Code an den Nutzer, falls der alte abgelaufen ist oder nicht ankam.
* Vorherige, ungenutzte Codes werdeb zur Sicherheit invalidiert (gelöscht)
*
* @param {string} email - Die E-Mail Addresse des Nutzers
* @returns {boolean} - true, wenn die E-Mail erfolgreich versendet wurde.
*/
export const resendOTP = async (email) => {
const client = await pool.connect();
const lowerCaseEmail = email.toLowerCase();
try {
await client.query('BEGIN');
// Prüfen, ob der Nutzer überhaupt existiert
const accountResult = await client.query(
'SELECT id, username, is_email_verified FROM lobby_accounts WHERE email = $1',
[lowerCaseEmail]
);
if (accountResult.rows.length === 0) {
// Aus Sicherheitsgründen verraten wir dem Frontend nicht zwingend,
// dass die E-Mail nicht existieren, aber für unser internes Error-Handling
// werfen wir einen spezifischen Fehler.
const error = new Error('User nor found');
error.code = 'ERR:USER_NOT_FOUND';
throw error;
}
const account = accountResult.rows[0];
// Prüfen, ob der Account nicht vielleicht schon verifiziert ist
// Wenn ja, macht ein neuer Code keinen Sinn.
if (account.is_email_verified) {
const error = new Error('Already verified');
error.code = 'ERR_ALREADY_VERIFIED';
throw error;
}
const lastOtpResult = await client.query(`
SELECT created_at FROM auth_otps WHERE account_id = $1 AND type = 'register' ORDER BY created_at DESC LIMIT 1`,
[account.id]
);
if (lastOtpResult.rows.length > 0) {
const lastCreatedTime = new Date(lastOtpResult.rows[0].created_at).getTime();
const currentTime = new Date().getTime();
const timeDifference = currentTime - lastCreatedTime;
if (timeDifference < 120000) {
const error = new Error('Cooldown active');
error.code = 'ERR_COOLDOWN_ACTIVE';
throw error;
}
}
// Zur Sichherheit alle alten, noch aktiven Registierungs-OTPs dieses Nutzers löschen.
// So verhindern wir, dass alte Codes in Postfächern noch funktionieren und minimieren
// Spam-Risiken.
await client.query(
`DELETE FROM auth_otps WHERE account_id = $1 AND type = 'register'`,
[account.id]
);
// Einen frischen, 6-stelligen Cide generieren
const newOtpCode = generateOTP();
// Den neuen Code in die Datenbank eintragen
await client.query(`
INSERT INTO auth_otps (account_id, otp_code, type, expires_at)
VALUES ($1, $2, 'register', NOW() + INTERVAL '15 minutes')`,
[account.id, newOtpCode]
);
// 6. Die neue E-Mail zusammenbauen und versenden
const mailHtml = `
<h2>Hallo ${account.username}, hier ist dein neuer Code!</h2>
<p>Du hast einen neuen Verifizierungscode für Void-Genesis angefordert:</p>
<h1 style="color: #00f0ff; letter-spacing: 5px;">${newOtpCode}</h1>
<p>Dieser Code ist ab jetzt wieder 15 Minuten lang gültig.</p>
`;
await sendEmail(lowerCaseEmail, account.username, 'Void-Genesis - Dein neue Code', mailHtml);
await client.query('COMMIT');
return true;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
};
/**
* Startet den Prozess zum Zurücksetzten des Passworts.
* Generiert einen speziellen OTP-Code und sendet ihn per E-Mail.
*
* @param {string} email - Die Email-Adresse des Nutzers
* @returns {boolean} - true, wenn die E-Mail erfolgreich versendet wurde.
*/
export const requestPasswordReset = async (email) => {
const client = await pool.connect();
const lowerCaseEmail = email.toLowerCase();
try {
await client.query('BEGIN');
const accountResult = await client.query(
'SELECT id, username FROM lobby_accounts WHERE email = $1',
[lowerCaseEmail]
);
if (accountResult.rows.length === 0) {
const error = new Error('User not found');
error.code = 'ERR_USER_NOT_FOUND';
throw error;
}
const lastOtpResult = await client.query(`
SELECT created_at FROM auth_otps WHERE account_id = $1 AND type = 'password_reset' ORDER BY created_at DESC LIMIT 1`,
[accountResult.id]
);
if (lastOtpResult.rows.length > 0) {
const lastCreatedTime = new Date(lastOtpResult.rows[0].created_at).getTime();
const currentTime = new Date().getTime();
const timeDifference = currentTime - lastCreatedTime;
if (timeDifference < 120000) {
const error = new Error('Cooldown active');
error.code = 'ERR_COOLDOWN_ACTIVE';
throw error;
}
}
const account = accountResult.rows[0];
await client.query(
`DELETE FROM auth_otps WHERE account_id = $1 AND type = 'password_reset'`,
[account.id]
);
const resetCode = generateOTP();
await client.query(`
INSERT INTO auth_otps (account_id, otp_code, type, expires_at)
VALUES ($1, $2, 'password_reset', NOW() + INTERVAL '15 minutes')`,
[account.id, resetCode]
);
const mailHtml = `
<h2>Hallo ${account.username},</h2>
<p>Du hast eine Anfrage zum Zurücksetzen deines Passworts gestellt.</p>
<p>Dein Code lautet:</p>
<h1 style="color: #00f0ff; letter-spacing: 5px;">${resetCode}</h1>
<p>Dieser Code ist 15 Minuten lang gültig. Falls du diese Anfrage nicht gestellt hast, kannst du diese E-Mail ignorieren.</p>
`;
await sendEmail(lowerCaseEmail, account.username, 'Void-Genesis - Passwort zurücksetzen', mailHtml);
await client.query('COMMIT');
return true;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
};
/**
* Setzt das neue Passwort, soffern der OTP-Code korrekt und nicht abgelaufen ist
*
* @param {string} email - Die E-Mail Addresse des Nutzers
* @param {string} otpCode - Der 6-stellige Code aus der E-Mail
* @param {string} newPassword - das neue Klartext passwort
* @returns {boolean} - true wenn das Passwort erfolgreich geändert wurde
*/
export const resetPassword = async (email, otpCode, newPassword) => {
const client = await pool.connect();
const lowerCaseEmail = email.toLowerCase();
try {
await client.query('BEGIN');
const accountResult = await client.query(
'SELECT id FROM lobby_accounts WHERE email = $1',
[lowerCaseEmail]
);
if (accountResult.rows.length === 0) {
const error = new Error('User not found');
error.code = 'ERR_USER_NOT_FOUND';
throw error;
}
const account = accountResult.rows[0];
const otpResult = await client.query(`
SELECT id, expires_at FROM auth_otps
WHERE account_id = $1 AND otp_code = $2 AND type = 'password_reset'`,
[account.id, otpCode]
);
if (otpResult.rows.length === 0) {
const error = new Error('Invalid OTP');
error.code = 'ERR_INVALID_OTP';
throw error;
}
const otpRecord = otpResult.rows[0];
if (new Date() > new Date(otpRecord.expires_at)) {
const error = new Error('OTP expired');
error.code = 'ERR_OTP_EXPIRED';
throw error;
}
const passwordHash = await argon2.hash(newPassword);
await client.query(
'UPDATE lobby_accounts SET password_hash = $1 WHERE id = $2',
[passwordHash, account.id]
);
await client.query(
'DELETE FROM refresh_tokens WHERE account_id = $1',
[account.id]
);
await client.query('COMMIT');
return true;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
};
@@ -0,0 +1,55 @@
/**
* Sendet eine E-Mail über die Brevo REST API.
*
* @param {string} toEmail - Die Ziel E-Mail Adresse
* @param {string} toName - Der Benutzername des Empfängers
* @param {string} subject - Der Betreff der E-Mail
* @param {string} htmlContent - Der HTML-Inhalt der E-Mail
* @returns {boolean} - true wenn erfolgreich, wirft einen Fehler wenn nicht.
*/
export const sendEmail = async (toEmail, toName, subject, htmlContent) => {
const mailMode = process.env.MAIL_MODE || 'development';
// Im Development-Modus loggen wir nur, um das API-Limit nicht zu belasten
if (mailMode === 'development') {
console.log(`\n[DEV-MAIL-SIMULATION] An: ${toEmail}`);
console.log(`[DEV-MAIL-SIMULATION] Betreff: ${subject}`);
console.log(`[DEV-MAIL-SIMULATION] Inhalt:\n${htmlContent}\n`);
return true;
}
// Echter Versand über Brevo
try {
const response = await fetch('https://api.brevo.com/v3/smtp/email', {
method: 'POST',
headers: {
'accept': 'application/json',
'api-key': process.env.BREVO_API_KEY,
'content-type': 'application/json'
},
body: JSON.stringify({
sender: {
name: process.env.BREVO_SENDER_NAME,
email: process.env.BREVO_SENDER_EMAIL
},
to: [{
email: toEmail,
name: toName
}],
subject: subject,
htmlContent: htmlContent
})
});
if (!response.ok) {
const errorData = await response.json();
console.error('Brevo API Fehler-Details:', errorData);
throw new Error('Fehler bei der Kommunikation mit dem E-Mail-Provider.');
}
return true;
} catch (error) {
console.error('Fehler im email.service:', error);
throw error; // Den Fehler werfen wir weiter, damit der Aufrufer ihn behandeln kann.
}
}
@@ -0,0 +1,74 @@
// === File: lobby-backend/src/services/heartbeat.service.js ===
import pool from '../db.js';
// ==============================================================================
// Void-Genesis - Heartbeat Service
// ==============================================================================
// Dieser Service pingt alle registrierten Sektoren regelmäßig an.
export const pingSectors = async () => {
const client = await pool.connect();
try {
const result = await client.query('SELECT id, name, api_url FROM sectors WHERE api_url IS NOT NULL');
const sectors = result.rows;
for (const sector of sectors) {
try {
// Wir nutzen einen AbortController, um blockierende Anfragen
// nach 3 Sekunden hart abzubrechen (Timeout).
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
// Nativer, sauberer fetch-Aufruf (Dank NODE_OPTIONS in Docker nun bugfrei)
const response = await fetch(sector.api_url + '/api/health', {
signal: controller.signal
});
clearTimeout(timeoutId);
if (response.ok) {
const data = await response.json();
if (data.status === 'ok') {
// Sektor antwortet korrekt -> Status auf online setzen
await client.query(
"UPDATE sectors SET status = 'online' WHERE id = $1",
[sector.id]
);
// Log-Ausgabe nur für unseren aktiven Entwicklungs-Sektor,
// um die Konsole sauber zu halten.
if (sector.name === 'Development') {
//console.log(`[Heartbeat] ✅ Sektor '${sector.name}' ist online!`);
}
}
} else {
// Server erreichbar, liefert aber keinen 2xx Status
await setSectorOffline(client, sector.id);
}
} catch (error) {
// Sektor gar nicht erreichbar (Container down, Netzwerkfehler, Timeout)
if (sector.name === 'Development') {
console.error(`[Heartbeat] ❌ Sektor '${sector.name}' nicht erreichbar:`, error.message);
}
await setSectorOffline(client, sector.id);
}
}
} catch (error) {
console.error('[Heartbeat] Kritischer Fehler bei der Datenbankabfrage:', error);
} finally {
client.release();
}
};
/**
* Hilfsfunktion, um den Status eines Sektors auf 'offline' zu setzen.
*/
const setSectorOffline = async (client, sectorId) => {
await client.query(
"UPDATE sectors SET status = 'offline' WHERE id = $1",
[sectorId]
);
};
@@ -0,0 +1,106 @@
import pool from '../db.js';
/**
* Ruft die Sektorlisten für einen bestimmten Account ab.
* Unterteilt das Ergebnis in bereits beigetretene Sektoren ("Meine Sektoren")
* und noch verfügbare Sektoren ("Neue Sektoren").
*
* @param {string} accountId - Die UUID des aktuell eingeloggten Spielers
*
* @returns {Promise<Object>} - Ein Objekt mit den Arrays 'mySectors' und 'newSectors'
*/
export const getSectorsList = async (accountId) => {
const client = await pool.connect();
try {
const mySectorsResult = await client.query(`
SELECT
s.id, s.name, s.status, s.speed_fleet, s.speed_economy,
las.local_username, las.gender,
las.created_at as joined_at,
las.updated_at as last_played
FROM sectors s
JOIN lobby_accounts_sectors las ON s.id = las.sector_id
WHERE las.lobby_account_id = $1
ORDER BY las.updated_at DESC
`, [accountId]);
const newSectorsResult = await client.query(`
SELECT
id, name, status, speed_fleet, speed_economy
FROM sectors
WHERE id NOT IN (
SELECT sector_id
FROM lobby_accounts_sectors
WHERE lobby_account_id = $1
)
ORDER BY created_at ASC
`, [accountId]);
const mapSectorData = (sector) => ({
...sector,
online_players: Math.floor(Math.random() * 150) + 10,
planets_count: (Math.floor(Math.random() * 150) + 10) * 12
});
return {
mySectors: mySectorsResult.rows.map(mapSectorData),
newSectors: newSectorsResult.rows.map(mapSectorData)
};
} catch (error) {
console.error('[SectorService] Fehler beim Auslesen der Sektordaten:', error);
throw error;
} finally {
client.release();
}
};
/**
* Trägt einen Spieler in einen neuen Sektor ein.
* Führt die eigentliche Datenbankoperation durch und wirft spezifische
* Fehler-Objekte, falls Constraints (UNIQUE-Regeln) verletzt werden.
*
* @param {string} accountId - Die UUID des Nutzers
* @param {string} sectorId - Die UUID des Sektors
* @param {string} localUsername - Der Commander-Name für diesen Sektor
* @param {string} gender - Das gewählte Geschlecht ('male' oder 'female')
*
* @returns {boolean} - true bei Erfolg
*/
export const joinSector = async (accountId, sectorId, localUsername, gender) => {
const client = await pool.connect();
try {
// Wir versuchen, den Datensatz direkt einzufügen.
await client.query(`
INSERT INTO lobby_accounts_sectors (lobby_account_id, sector_id, local_username, gender)
VALUES ($1, $2, $3, $4)
`, [accountId, sectorId, localUsername, gender]);
return true;
} catch (error) {
// Fehlercode 23505 = Postgres "unique_violation"
if (error.code === '23505') {
if (error.constraint === 'lobby_accounts_sectors_lobby_account_id_sector_id_key') {
const customErr = new Error('Already in sector');
customErr.code = 'ERR_ALREADY_IN_SECTOR';
throw customErr;
}
if (error.constraint === 'lobby_accounts_sectors_sector_id_local_username_key') {
const customErr = new Error('Local username taken');
customErr.code = 'ERR_LOCAL_USERNAME_TAKEN';
throw customErr;
}
}
throw error;
} finally {
client.release();
}
};