All files / src/routes geo.ts

15.21% Statements 7/46
100% Branches 1/1
100% Functions 1/1
15.21% Lines 7/46

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 661x           1x     1x 26x             26x                                                                                           26x 26x  
import { AppError } from "@ontrack/backend-common";
import type { FastifyPluginAsync } from "fastify";
import type { AppConfig } from "../config.js";
import { requireVerifiedUser, type IdTokenVerifier } from "../lib/firebase-auth.js";
import { throwIfUpstreamFailed } from "../lib/http-client.js";
 
export const geoRoutes: FastifyPluginAsync<{
  config: AppConfig;
  verifyIdToken: IdTokenVerifier;
}> = async (app, opts) => {
  const { config, verifyIdToken } = opts;
 
  /**
   * Анализ GPX: симплификация трека (RDP) + метрики (длина, набор высоты, bbox).
   * Принимает multipart с полем `file` (как /track), проксирует сырой GPX во
   * внутренний geo-service с X-Service-Token. Только Bearer + emailVerified.
   */
  app.post("/analyze", async (request, reply) => {
    await requireVerifiedUser(request, verifyIdToken);
 
    const file = await request.file();
    if (!file) {
      throw new AppError(400, "FILE_REQUIRED", "Expected a multipart 'file' field with a GPX track");
    }
    const buffer = await file.toBuffer();
 
    const tolerance = (request.query as { tolerance?: string }).tolerance;
    const search =
      tolerance && /^\d+(\.\d+)?$/.test(tolerance) ? `?tolerance=${tolerance}` : "";
 
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), config.upstreamTimeoutMs);
 
    try {
      const response = await fetch(`${config.geoServiceUrl}/v1/geo/analyze${search}`, {
        method: "POST",
        headers: {
          "x-service-token": config.serviceToken,
          "content-type": "application/gpx+xml",
        },
        body: new Uint8Array(buffer),
        signal: controller.signal,
      });
 
      const body = Buffer.from(await response.arrayBuffer());
      throwIfUpstreamFailed(response.status, body);
 
      const ct = response.headers.get("content-type");
      if (ct) {
        reply.header("Content-Type", ct);
      }
      return reply.code(response.status).send(body);
    } catch (error) {
      if (error instanceof AppError) {
        throw error;
      }
      if (error instanceof Error && error.name === "AbortError") {
        throw new AppError(504, "UPSTREAM_TIMEOUT", "Upstream request timed out");
      }
      throw new AppError(502, "UPSTREAM_UNAVAILABLE", "Upstream service unavailable");
    } finally {
      clearTimeout(timeout);
    }
  });
};