Refactor code structure for improved readability and maintainability
This commit is contained in:
parent
7b30f17065
commit
c71e4b4cd0
27 changed files with 1738 additions and 324 deletions
|
|
@ -9,6 +9,7 @@ export const config = {
|
|||
frontendUrl: process.env.FACEAI_FRONTEND_URL || 'http://localhost:5173',
|
||||
publicBaseUrl: process.env.FACEAI_PUBLIC_BASE_URL || 'http://localhost:3001',
|
||||
legacyReturnUrl: process.env.FACEAI_LEGACY_RETURN_URL || 'http://localhost:3001/dev/legacy/return',
|
||||
pklRoot: process.env.FACEAI_PKL_ROOT || '/data/pkl',
|
||||
enableLocalLegacyStatic: process.env.FACEAI_ENABLE_LOCAL_LEGACY_STATIC
|
||||
? process.env.FACEAI_ENABLE_LOCAL_LEGACY_STATIC === '1'
|
||||
: process.env.NODE_ENV !== 'production',
|
||||
|
|
|
|||
130
faceai/apps/backend/src/race-storage.js
Normal file
130
faceai/apps/backend/src/race-storage.js
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const ITALIAN_MONTH_NAMES = [
|
||||
'GENNAIO',
|
||||
'FEBBRAIO',
|
||||
'MARZO',
|
||||
'APRILE',
|
||||
'MAGGIO',
|
||||
'GIUGNO',
|
||||
'LUGLIO',
|
||||
'AGOSTO',
|
||||
'SETTEMBRE',
|
||||
'OTTOBRE',
|
||||
'NOVEMBRE',
|
||||
'DICEMBRE'
|
||||
];
|
||||
|
||||
function sanitizePathSegment(value) {
|
||||
const normalized = String(value || '').trim();
|
||||
|
||||
if (!normalized) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (normalized === '.' || normalized === '..' || normalized.includes('..') || /[\\/]/.test(normalized)) {
|
||||
throw new Error('Invalid race storage path segment');
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function normalizeRaceFolderName(value) {
|
||||
return String(value || '')
|
||||
.trim()
|
||||
.replace(/[<>:"/\\|?*]/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
export function buildMonthFolder(year, monthIndex) {
|
||||
const safeYear = sanitizePathSegment(year);
|
||||
const normalizedMonthIndex = Number(monthIndex);
|
||||
if (!safeYear || Number.isNaN(normalizedMonthIndex) || normalizedMonthIndex < 1 || normalizedMonthIndex > 12) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return `${String(normalizedMonthIndex).padStart(2, '0')}.${ITALIAN_MONTH_NAMES[normalizedMonthIndex - 1]}`;
|
||||
}
|
||||
|
||||
export function buildRaceStorage(storageInput = {}) {
|
||||
const year = sanitizePathSegment(storageInput.year);
|
||||
const monthFolder = sanitizePathSegment(storageInput.monthFolder);
|
||||
const raceFolder = sanitizePathSegment(normalizeRaceFolderName(storageInput.raceFolder));
|
||||
|
||||
if (!year || !monthFolder || !raceFolder) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
year,
|
||||
monthFolder,
|
||||
raceFolder,
|
||||
relativeDir: path.posix.join(year, monthFolder, raceFolder)
|
||||
};
|
||||
}
|
||||
|
||||
export async function resolveRacePklAvailability({ pklRoot, race }) {
|
||||
if (!pklRoot) {
|
||||
return {
|
||||
available: false,
|
||||
reasonCode: 'PKL_ROOT_NOT_CONFIGURED',
|
||||
message: 'The PKL root is not configured for this FaceAI environment.',
|
||||
storage: null
|
||||
};
|
||||
}
|
||||
|
||||
const storage = buildRaceStorage(race?.storage || race);
|
||||
if (!storage) {
|
||||
return {
|
||||
available: false,
|
||||
reasonCode: 'MISSING_RACE_STORAGE',
|
||||
message: 'The legacy handoff did not provide the folder metadata required to resolve FaceAI data for this race.',
|
||||
storage: null
|
||||
};
|
||||
}
|
||||
|
||||
const raceDir = path.join(pklRoot, storage.year, storage.monthFolder, storage.raceFolder);
|
||||
|
||||
let entries;
|
||||
try {
|
||||
entries = await fs.readdir(raceDir, { withFileTypes: true });
|
||||
} catch (error) {
|
||||
if (error?.code === 'ENOENT') {
|
||||
return {
|
||||
available: false,
|
||||
reasonCode: 'RACE_DIRECTORY_NOT_FOUND',
|
||||
message: `No FaceAI dataset directory exists for ${storage.relativeDir}.`,
|
||||
storage,
|
||||
raceDir
|
||||
};
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
const pklEntry = entries
|
||||
.filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.pkl'))
|
||||
.sort((left, right) => left.name.localeCompare(right.name, 'en'))[0];
|
||||
|
||||
if (!pklEntry) {
|
||||
return {
|
||||
available: false,
|
||||
reasonCode: 'PKL_FILE_NOT_FOUND',
|
||||
message: `The race directory ${storage.relativeDir} exists, but it does not contain any .pkl file.`,
|
||||
storage,
|
||||
raceDir
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
available: true,
|
||||
reasonCode: null,
|
||||
message: `Using ${storage.relativeDir}/${pklEntry.name}`,
|
||||
storage,
|
||||
raceDir,
|
||||
pklPath: path.join(raceDir, pklEntry.name),
|
||||
pklFileName: pklEntry.name
|
||||
};
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url';
|
|||
import { config } from './config.js';
|
||||
import { signPayload, verifySignedPayload } from './auth.js';
|
||||
import { createSession, getSession, mockCatalog } from './store.js';
|
||||
import { buildRaceStorage, resolveRacePklAvailability } from './race-storage.js';
|
||||
import {
|
||||
acquireActiveSearchLock,
|
||||
createRedisConnection,
|
||||
|
|
@ -94,8 +95,24 @@ async function enforceSearchRateLimit(req, res, next) {
|
|||
next();
|
||||
}
|
||||
|
||||
function issueHandoffToken({ raceId, raceSlug, lang, returnUrl }) {
|
||||
const race = mockCatalog[raceId] || { id: raceId, slug: raceSlug || `race-${raceId}`, name: raceSlug || `Race ${raceId}` };
|
||||
function normalizeRaceForSession(raceInput) {
|
||||
return {
|
||||
...raceInput,
|
||||
storage: buildRaceStorage(raceInput?.storage || {})
|
||||
};
|
||||
}
|
||||
|
||||
async function buildRaceAvailability(race) {
|
||||
return resolveRacePklAvailability({ pklRoot: config.pklRoot, race });
|
||||
}
|
||||
|
||||
function issueHandoffToken({ raceId, raceSlug, raceName, raceStorage, lang, returnUrl }) {
|
||||
const race = mockCatalog[raceId] || {
|
||||
id: raceId,
|
||||
slug: raceSlug || `race-${raceId}`,
|
||||
name: raceName || raceSlug || `Race ${raceId}`,
|
||||
storage: buildRaceStorage(raceStorage || {})
|
||||
};
|
||||
|
||||
return signPayload({
|
||||
type: 'handoff',
|
||||
|
|
@ -108,7 +125,8 @@ function issueHandoffToken({ raceId, raceSlug, lang, returnUrl }) {
|
|||
race: {
|
||||
id: race.id,
|
||||
slug: race.slug,
|
||||
name: race.name
|
||||
name: race.name,
|
||||
storage: buildRaceStorage(raceStorage || race.storage || {})
|
||||
},
|
||||
lang: lang || 'it',
|
||||
returnUrl,
|
||||
|
|
@ -231,9 +249,21 @@ app.get('/dev/legacy/race', (req, res) => {
|
|||
app.get('/dev/legacy/launch', (req, res) => {
|
||||
const raceId = String(req.query.raceId || '101');
|
||||
const raceSlug = String(req.query.raceSlug || mockCatalog[raceId]?.slug || `race-${raceId}`);
|
||||
const raceName = String(req.query.raceName || mockCatalog[raceId]?.name || raceSlug);
|
||||
const lang = String(req.query.lang || 'it');
|
||||
const returnUrl = String(req.query.returnUrl || `${config.publicBaseUrl}/dev/legacy/race?raceId=${encodeURIComponent(raceId)}&lang=${encodeURIComponent(lang)}`);
|
||||
const token = issueHandoffToken({ raceId, raceSlug, lang, returnUrl });
|
||||
const token = issueHandoffToken({
|
||||
raceId,
|
||||
raceSlug,
|
||||
raceName,
|
||||
raceStorage: {
|
||||
year: String(req.query.raceYear || mockCatalog[raceId]?.storage?.year || ''),
|
||||
monthFolder: String(req.query.raceMonthFolder || mockCatalog[raceId]?.storage?.monthFolder || ''),
|
||||
raceFolder: String(req.query.raceFolder || mockCatalog[raceId]?.storage?.raceFolder || '')
|
||||
},
|
||||
lang,
|
||||
returnUrl
|
||||
});
|
||||
res.redirect(`${config.frontendUrl}/auth/callback?token=${encodeURIComponent(token)}`);
|
||||
});
|
||||
|
||||
|
|
@ -261,7 +291,7 @@ app.get('/dev/legacy/return', async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
app.post('/api/auth/exchange', (req, res) => {
|
||||
app.post('/api/auth/exchange', async (req, res) => {
|
||||
try {
|
||||
const { token } = req.body;
|
||||
const payload = verifySignedPayload(token, config.sharedSecret);
|
||||
|
|
@ -269,13 +299,18 @@ app.post('/api/auth/exchange', (req, res) => {
|
|||
throw new Error('Wrong token type');
|
||||
}
|
||||
|
||||
const race = normalizeRaceForSession(payload.race);
|
||||
const availability = await buildRaceAvailability(race);
|
||||
const faceAiAllowed = payload.user.membershipStatus === 'active' && availability.available;
|
||||
|
||||
const sessionId = createSession({
|
||||
user: payload.user,
|
||||
race: payload.race,
|
||||
race,
|
||||
lang: payload.lang,
|
||||
returnUrl: payload.returnUrl,
|
||||
availability,
|
||||
access: {
|
||||
faceAiAllowed: payload.user.membershipStatus === 'active'
|
||||
faceAiAllowed
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -288,11 +323,12 @@ app.post('/api/auth/exchange', (req, res) => {
|
|||
|
||||
res.json({
|
||||
user: payload.user,
|
||||
race: payload.race,
|
||||
race,
|
||||
lang: payload.lang,
|
||||
returnUrl: payload.returnUrl,
|
||||
availability,
|
||||
access: {
|
||||
faceAiAllowed: true
|
||||
faceAiAllowed
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
@ -308,6 +344,19 @@ app.post('/api/searches', requireSession, enforceSearchRateLimit, upload.single(
|
|||
try {
|
||||
const raceId = String(req.body.raceId || req.faceaiSession.race.id);
|
||||
const userId = String(req.faceaiSession.user.id);
|
||||
const race = normalizeRaceForSession(raceId === req.faceaiSession.race.id
|
||||
? req.faceaiSession.race
|
||||
: (mockCatalog[raceId] || req.faceaiSession.race));
|
||||
const availability = await buildRaceAvailability(race);
|
||||
|
||||
if (!availability.available) {
|
||||
res.status(409).json({
|
||||
error: availability.message,
|
||||
code: availability.reasonCode || 'RACE_PKL_UNAVAILABLE'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const activeSearchId = await getActiveSearchId(redis, userId);
|
||||
|
||||
if (activeSearchId) {
|
||||
|
|
@ -327,10 +376,10 @@ app.post('/api/searches', requireSession, enforceSearchRateLimit, upload.single(
|
|||
return;
|
||||
}
|
||||
|
||||
const race = mockCatalog[raceId] || req.faceaiSession.race;
|
||||
const search = await createSearchRecord(redis, {
|
||||
raceId,
|
||||
raceName: race?.name || raceId,
|
||||
raceStorage: race?.storage || availability.storage,
|
||||
userId,
|
||||
returnUrl: req.faceaiSession.returnUrl,
|
||||
lang: req.faceaiSession.lang,
|
||||
|
|
@ -371,6 +420,7 @@ app.post('/api/searches', requireSession, enforceSearchRateLimit, upload.single(
|
|||
id: updatedSearch.id,
|
||||
status: updatedSearch.status,
|
||||
raceId: updatedSearch.raceId,
|
||||
raceStorage: updatedSearch.raceStorage,
|
||||
selfieName: updatedSearch.selfieName,
|
||||
matchCount: updatedSearch.matchCount,
|
||||
errorCode: updatedSearch.errorCode,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@ export const mockCatalog = {
|
|||
id: '101',
|
||||
slug: 'mezza-di-firenze',
|
||||
name: 'Mezza di Firenze',
|
||||
storage: {
|
||||
year: '2026',
|
||||
monthFolder: '04.APRILE',
|
||||
raceFolder: 'PISA'
|
||||
},
|
||||
photos: [
|
||||
{ id: 'f101-001', label: 'Arrivo 001', bib: '245', checkpoint: 'Arrivo', thumb: 'thumb-arrivo-001.jpg' },
|
||||
{ id: 'f101-002', label: 'Arrivo 002', bib: '245', checkpoint: 'Arrivo', thumb: 'thumb-arrivo-002.jpg' },
|
||||
|
|
@ -22,14 +27,33 @@ export const mockCatalog = {
|
|||
},
|
||||
'202': {
|
||||
id: '202',
|
||||
slug: 'trail-del-chianti',
|
||||
name: 'Trail del Chianti',
|
||||
slug: 'mezza-di-pisa',
|
||||
name: 'Mezza di Pisa',
|
||||
storage: {
|
||||
year: '2026',
|
||||
monthFolder: '04.APRILE',
|
||||
raceFolder: 'PISA'
|
||||
},
|
||||
photos: [
|
||||
{ id: 'f202-001', label: 'Bosco 001', bib: '77', checkpoint: 'Bosco', thumb: 'thumb-bosco-001.jpg' },
|
||||
{ id: 'f202-002', label: 'Salita 002', bib: '77', checkpoint: 'Salita', thumb: 'thumb-salita-002.jpg' },
|
||||
{ id: 'f202-003', label: 'Arrivo 003', bib: '77', checkpoint: 'Arrivo', thumb: 'thumb-arrivo-003.jpg' },
|
||||
{ id: 'f202-004', label: 'Bosco 004', bib: '19', checkpoint: 'Bosco', thumb: 'thumb-bosco-004.jpg' }
|
||||
]
|
||||
},
|
||||
'303': {
|
||||
id: '303',
|
||||
slug: 'corsa-di-lucca',
|
||||
name: 'Corsa di Lucca',
|
||||
storage: {
|
||||
year: '2026',
|
||||
monthFolder: '04.APRILE',
|
||||
raceFolder: 'LUCCA'
|
||||
},
|
||||
photos: [
|
||||
{ id: 'f303-001', label: 'Mura 001', bib: '33', checkpoint: 'Mura', thumb: 'thumb-mura-001.jpg' },
|
||||
{ id: 'f303-002', label: 'Centro 002', bib: '33', checkpoint: 'Centro', thumb: 'thumb-centro-002.jpg' }
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue