diff --git a/src/dashboard/scoreko-dev/views/Players.vue b/src/dashboard/scoreko-dev/views/Players.vue
index f6c6614..deec974 100644
--- a/src/dashboard/scoreko-dev/views/Players.vue
+++ b/src/dashboard/scoreko-dev/views/Players.vue
@@ -275,13 +275,13 @@ const checkOAuthStatus = async () => {
oauthLoading.value = false;
clearOAuthPolling();
oauthSessionId.value = '';
- tournamentsError.value = status.error || 'No se pudo completar el login OAuth con start.gg.';
+ tournamentsError.value = status.error || 'Could not complete OAuth login with start.gg.';
}
} catch (error) {
oauthLoading.value = false;
clearOAuthPolling();
oauthSessionId.value = '';
- tournamentsError.value = error instanceof Error ? error.message : 'No se pudo verificar el estado OAuth.';
+ tournamentsError.value = error instanceof Error ? error.message : 'Could not verify OAuth status.';
}
};
@@ -300,7 +300,7 @@ const connectWithStartGGOAuth = async () => {
}, 1500);
} catch (error) {
oauthLoading.value = false;
- tournamentsError.value = error instanceof Error ? error.message : 'No se pudo iniciar OAuth con start.gg.';
+ tournamentsError.value = error instanceof Error ? error.message : 'Could not start OAuth with start.gg.';
}
};
@@ -325,7 +325,7 @@ const saveManualToken = () => {
const loadRecentTournaments = async () => {
const token = startGGToken.value.trim();
if (!token) {
- tournamentsError.value = 'Añade tu token de start.gg para cargar torneos.';
+ tournamentsError.value = 'Add your start.gg token to load tournaments.';
recentTournaments.value = [];
return;
}
@@ -338,10 +338,10 @@ const loadRecentTournaments = async () => {
});
recentTournaments.value = tournaments;
if (!tournaments.length) {
- tournamentsError.value = 'No hay torneos recientes para esta cuenta.';
+ tournamentsError.value = 'There are no recent tournaments for this account.';
}
} catch (error) {
- tournamentsError.value = error instanceof Error ? error.message : 'No se pudieron cargar torneos.';
+ tournamentsError.value = error instanceof Error ? error.message : 'Could not load tournaments.';
recentTournaments.value = [];
} finally {
loadingTournaments.value = false;
@@ -365,7 +365,7 @@ const openStartGGImportDialog = async (tournament: StartGGTournament) => {
startGGPlayers.value = importedPlayers;
selectedStartGGPlayerIds.value = importedPlayers.map((player) => player.id);
} catch (error) {
- const message = error instanceof Error ? error.message : 'No se pudieron cargar jugadores';
+ const message = error instanceof Error ? error.message : 'Could not load players';
window.alert(message);
isImportDialogOpen.value = false;
} finally {
@@ -599,7 +599,7 @@ onBeforeUnmount(() => {
StartGG
- Conecta por OAuth (recomendado) o pega tu token personal para cargar tus torneos creados o donde eres admin. Si aparece "Client authentication failed", revisa que en config uses el Client ID/Secret de un OAuth App de start.gg.
+ Connect via OAuth (recommended) or paste your personal token to load tournaments you created or administrate. If you see "Client authentication failed", verify your config uses the Client ID/Secret from a start.gg OAuth App.
@@ -607,7 +607,7 @@ onBeforeUnmount(() => {
v-if="!hasStartGGTokenConfigured"
color="primary"
icon="login"
- label="Conectar con start.gg"
+ label="Connect with start.gg"
:loading="oauthLoading"
@click="connectWithStartGGOAuth"
/>
@@ -616,7 +616,7 @@ onBeforeUnmount(() => {
outline
color="positive"
icon="check_circle"
- label="Conectado"
+ label="Connected"
class="startgg-connected-btn"
@click="openManualTokenDialog"
/>
@@ -626,7 +626,7 @@ onBeforeUnmount(() => {
outline
color="white"
icon="vpn_key"
- label="Usar API personal"
+ label="Use personal API"
@click="openManualTokenDialog"
/>
@@ -663,7 +663,7 @@ onBeforeUnmount(() => {
input-debounce="0"
clearable
dense
- label="Torneo"
+ label="Tournament"
class="players-underlined-field"
@filter="filterTournaments"
>
@@ -686,9 +686,13 @@ onBeforeUnmount(() => {
+ >
+
Import players
+
@@ -700,24 +704,24 @@ onBeforeUnmount(() => {
- API personal de start.gg
+ Personal start.gg API
- Si OAuth falla, puedes crear tu token personal manualmente con estos pasos:
+ If OAuth fails, you can create your personal token manually with these steps:
- - Ir a https://start.gg/admin/profile/developer
- - Iniciar sesión con tu cuenta
- - De los 3 access tokens, clicar en Third Party
- - Crear uno nuevo y cubrir la descripción con el nombre que quieras
- - Copiar el token generado y pegarlo en Scoreko
+ - Go to https://start.gg/admin/profile/developer
+ - Sign in with your account
+ - From the 3 access tokens, click Third Party
+ - Create a new one and fill the description with any name you want
+ - Copy the generated token and paste it into Scoreko
{
@@ -750,7 +754,7 @@ onBeforeUnmount(() => {
- Importar desde {{ importingTournament?.name || 'start.gg' }}
+ Import from {{ importingTournament?.name || 'start.gg' }}
@@ -760,7 +764,7 @@ onBeforeUnmount(() => {
class="row items-center q-gutter-sm"
>
- Cargando inscritos...
+ Loading participants...
{
diff --git a/src/extension/startgg.ts b/src/extension/startgg.ts
index 1a06786..6c3282d 100644
--- a/src/extension/startgg.ts
+++ b/src/extension/startgg.ts
@@ -1,4 +1,4 @@
-import { createServer, type Server } from 'node:http';
+import { createServer, type Server, type ServerResponse } from 'node:http';
import { randomUUID } from 'node:crypto';
import { getData, type CountryRecord } from 'country-list';
import { nodecg } from './util/nodecg.js';
@@ -64,6 +64,27 @@ interface OAuthTokenResponse {
const oauthSessions = new Map();
let oauthCallbackServer: Server | null = null;
+const getStringProp = (payload: unknown, key: string): string => {
+ if (typeof payload !== 'object' || payload === null || !(key in payload)) {
+ return '';
+ }
+
+ const value = (payload as Record)[key];
+ return typeof value === 'string' ? value.trim() : String(value || '').trim();
+};
+
+const updateOAuthSession = (sessionId: string, update: Partial) => {
+ const session = oauthSessions.get(sessionId);
+ if (!session) {
+ return;
+ }
+
+ oauthSessions.set(sessionId, {
+ ...session,
+ ...update,
+ });
+};
+
const requestStartGG = async (query: string, variables: Record, token: string): Promise => {
const response = await fetch(STARTGG_ENDPOINT, {
method: 'POST',
@@ -75,10 +96,16 @@ const requestStartGG = async (query: string, variables: Record;
+ try {
+ payload = (await response.json()) as StartGGGraphQLResponse;
+ } catch {
+ throw new Error('Invalid JSON response from start.gg');
}
- const payload = (await response.json()) as StartGGGraphQLResponse;
if (payload.errors?.length) {
throw new Error(payload.errors[0]?.message || 'Unknown start.gg error');
}
@@ -137,16 +164,19 @@ const getCallbackUrl = (callbackPort: number) => `http://127.0.0.1:${callbackPor
const cleanupExpiredOAuthSessions = () => {
const now = Date.now();
- oauthSessions.forEach((session) => {
+ oauthSessions.forEach((session, sessionId) => {
if (session.expiresAt <= now && session.status === 'pending') {
- oauthSessions.set(session.sessionId, {
- ...session,
- status: 'expired',
- });
+ updateOAuthSession(sessionId, { status: 'expired' });
}
});
};
+const respondWithCallbackHtml = (res: ServerResponse, statusCode: number, title: string, message: string) => {
+ res.statusCode = statusCode;
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
+ res.end(renderCallbackHtml(title, message));
+};
+
const renderCallbackHtml = (title: string, message: string) => `
@@ -163,7 +193,7 @@ const renderCallbackHtml = (title: string, message: string) => `
${title}
${message}
-
Puedes cerrar esta pestaña y volver a Scoreko.
+
You can close this tab and return to Scoreko.
`;
@@ -252,68 +282,41 @@ const ensureOAuthCallbackServer = async (oauthConfig: OAuthConfig) => {
const session = Array.from(oauthSessions.values()).find((candidate) => candidate.state === state);
if (!session) {
- res.statusCode = 400;
- res.setHeader('Content-Type', 'text/html; charset=utf-8');
- res.end(renderCallbackHtml('OAuth inválido', 'No se encontró una sesión activa para esta autorización.'));
+ respondWithCallbackHtml(res, 400, 'Invalid OAuth', 'No active session was found for this authorization.');
return;
}
if (session.expiresAt <= Date.now()) {
- oauthSessions.set(session.sessionId, {
- ...session,
- status: 'expired',
- });
- res.statusCode = 400;
- res.setHeader('Content-Type', 'text/html; charset=utf-8');
- res.end(renderCallbackHtml('Sesión expirada', 'La sesión OAuth expiró. Vuelve a iniciar el proceso desde Scoreko.'));
+ updateOAuthSession(session.sessionId, { status: 'expired' });
+ respondWithCallbackHtml(res, 400, 'Session expired', 'The OAuth session expired. Start the process again from Scoreko.');
return;
}
if (error) {
- oauthSessions.set(session.sessionId, {
- ...session,
- status: 'error',
- error,
- });
- res.statusCode = 400;
- res.setHeader('Content-Type', 'text/html; charset=utf-8');
- res.end(renderCallbackHtml('OAuth cancelado', `start.gg devolvió el error: ${error}`));
+ updateOAuthSession(session.sessionId, { status: 'error', error });
+ respondWithCallbackHtml(res, 400, 'OAuth canceled', `start.gg returned this error: ${error}`);
return;
}
if (!code) {
- oauthSessions.set(session.sessionId, {
- ...session,
+ updateOAuthSession(session.sessionId, {
status: 'error',
error: 'Missing authorization code',
});
- res.statusCode = 400;
- res.setHeader('Content-Type', 'text/html; charset=utf-8');
- res.end(renderCallbackHtml('OAuth incompleto', 'No se recibió un código de autorización.'));
+ respondWithCallbackHtml(res, 400, 'Incomplete OAuth', 'No authorization code was received.');
return;
}
void exchangeOAuthCodeForToken(code, callbackUrl, oauthConfig)
.then((token) => {
- oauthSessions.set(session.sessionId, {
- ...session,
- status: 'completed',
- token,
- error: undefined,
- });
+ updateOAuthSession(session.sessionId, { status: 'completed', token, error: undefined });
})
.catch((exchangeError) => {
const message = exchangeError instanceof Error ? exchangeError.message : 'Failed to exchange authorization code';
- oauthSessions.set(session.sessionId, {
- ...session,
- status: 'error',
- error: message,
- });
+ updateOAuthSession(session.sessionId, { status: 'error', error: message });
});
- res.statusCode = 200;
- res.setHeader('Content-Type', 'text/html; charset=utf-8');
- res.end(renderCallbackHtml('Autorización recibida', 'Se recibió tu autorización. Finalizando el login en segundo plano...'));
+ respondWithCallbackHtml(res, 200, 'Authorization received', 'Your authorization was received. Finishing sign-in in the background...');
});
await new Promise((resolve, reject) => {
@@ -330,14 +333,14 @@ const ensureOAuthCallbackServer = async (oauthConfig: OAuthConfig) => {
nodecg.listenFor('startgg:createOAuthSession', async (_payload: unknown, ack) => {
const oauthConfig = getOAuthConfig();
if (!oauthConfig) {
- sendAck(ack, 'OAuth no está configurado en esta instalación (faltan startggClientId/startggClientSecret). Usa el Client ID y Client Secret del OAuth app de start.gg.');
+ sendAck(ack, 'OAuth is not configured in this installation (missing startggClientId/startggClientSecret). Use the Client ID and Client Secret from a start.gg OAuth app.');
return;
}
try {
await ensureOAuthCallbackServer(oauthConfig);
} catch (serverError) {
- const message = serverError instanceof Error ? serverError.message : 'No se pudo iniciar el callback OAuth local';
+ const message = serverError instanceof Error ? serverError.message : 'Could not start the local OAuth callback';
sendAck(ack, message);
return;
}
@@ -371,9 +374,7 @@ nodecg.listenFor('startgg:createOAuthSession', async (_payload: unknown, ack) =>
nodecg.listenFor('startgg:getOAuthSessionStatus', (payload: unknown, ack) => {
cleanupExpiredOAuthSessions();
- const sessionId = typeof payload === 'object' && payload !== null && 'sessionId' in payload
- ? String((payload as { sessionId?: string }).sessionId || '').trim()
- : '';
+ const sessionId = getStringProp(payload, 'sessionId');
if (!sessionId) {
sendAck(ack, 'Missing OAuth session id');
@@ -394,9 +395,7 @@ nodecg.listenFor('startgg:getOAuthSessionStatus', (payload: unknown, ack) => {
});
nodecg.listenFor('startgg:fetchRecentTournaments', async (payload: unknown, ack) => {
- const token = typeof payload === 'object' && payload !== null && 'token' in payload
- ? String((payload as { token?: string }).token || '').trim()
- : '';
+ const token = getStringProp(payload, 'token');
if (!token) {
sendAck(ack, 'Missing start.gg API token');
@@ -443,12 +442,8 @@ nodecg.listenFor('startgg:fetchRecentTournaments', async (payload: unknown, ack)
});
nodecg.listenFor('startgg:fetchTournamentPlayers', async (payload: unknown, ack) => {
- const candidate = typeof payload === 'object' && payload !== null ? payload as {
- token?: string;
- slug?: string;
- } : {};
- const token = String(candidate.token || '').trim();
- const slug = String(candidate.slug || '').trim();
+ const token = getStringProp(payload, 'token');
+ const slug = getStringProp(payload, 'slug');
if (!token) {
sendAck(ack, 'Missing start.gg API token');
@@ -485,7 +480,7 @@ nodecg.listenFor('startgg:fetchTournamentPlayers', async (payload: unknown, ack)
try {
let currentPage = 1;
let totalPages = 1;
- const playersMap: Record = {};
+ const playersMap = new Map();
while (currentPage <= totalPages) {
const data = await requestStartGG<{
@@ -514,7 +509,8 @@ nodecg.listenFor('startgg:fetchTournamentPlayers', async (payload: unknown, ack)
throw new Error('Tournament not found');
}
- totalPages = Math.max(data.tournament.participants.pageInfo.totalPages || 1, 1);
+ const apiTotalPages = Number(data.tournament.participants.pageInfo.totalPages);
+ totalPages = Number.isFinite(apiTotalPages) ? Math.max(apiTotalPages, 1) : 1;
data.tournament.participants.nodes.forEach((participant) => {
const playerId = String(participant.id);
@@ -523,20 +519,20 @@ nodecg.listenFor('startgg:fetchTournamentPlayers', async (payload: unknown, ack)
return;
}
const country = resolveCountryCodeFromStartGG(participant.user?.location?.country);
- playersMap[playerId] = {
+ playersMap.set(playerId, {
id: playerId,
gamertag,
name: gamertag,
team: (participant.prefix || '').trim(),
country,
twitter: '',
- };
+ });
});
currentPage += 1;
}
- sendAck(ack, null, Object.values(playersMap));
+ sendAck(ack, null, Array.from(playersMap.values()));
} catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error while importing players';
sendAck(ack, message);