Files
sase.tr/apps/api/src/vehicles/vehicles.controller.ts
Semih Yesilyurt 6154656c3e refactor(decode): keep decode-source details server-side, opaque candidate keys
The multi-candidate decode response leaked provider internals (source name,
pcat car ids, EMEX _ssd/_vid/_quickGroupsUrl/catalogId) and made the client
carry them between requests: the frontend stored candidateSource and echoed
pcatCarId/emexCarIndex back on selection.

Now the candidate list returned to the client carries only display fields
(name, description, parameters) plus an opaque key, and the provider mapping
is stashed in Redis (vin:candidates:*, 30m TTL, resolve-cache fallback). The
pick request sends just { vin, candidate }. Legacy pcatCarId/emexCarIndex
body params still work for already-loaded bundles.

Also drops `source` from the public /vehicles/preview response — no consumer
used it, and provider names must never be public (same policy as
teaser-stats).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
(cherry picked from commit 2077a9724a)
2026-06-10 12:01:40 +03:00

120 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Body, Controller, Delete, Get, Param, Post, Query } from "@nestjs/common";
import { CategoriesService } from "../categories/categories.service";
import { CurrentUser } from "../common/decorators/current-user.decorator";
import { Public } from "../common/decorators/public.decorator";
import { VinValidationPipe } from "../common/pipes/vin-validation.pipe";
import { EmailService } from "../email/email.service";
import { VehiclesService } from "./vehicles.service";
@Controller("vehicles")
export class VehiclesController {
constructor(
private vehiclesService: VehiclesService,
private categoriesService: CategoriesService,
private emailService: EmailService,
) {}
@Public()
@Get("preview/:vin")
async preview(@Param("vin", VinValidationPipe) vin: string) {
return this.vehiclesService.previewVin(vin);
}
/**
* Public catalog-stats teaser for /register?vin= — returns categories /
* parts / schemas counts so the signup page can frame "what opens after
* register". Real data when DB is meaningfully populated; otherwise a
* deterministic VIN-seeded placeholder. Source/provider names are never
* exposed.
*/
@Public()
@Get(":vin/teaser-stats")
async teaserStats(@Param("vin", VinValidationPipe) vin: string) {
return this.vehiclesService.getTeaserStats(vin);
}
@Post("decode")
async decode(
@CurrentUser("id") userId: string,
@Body("vin", VinValidationPipe) vin: string,
// Deprecated: provider-specific picks from pre-stash frontend bundles.
// New clients send only the opaque `candidate` key; the provider mapping
// lives in the server-side stash.
@Body("pcatCarId") pcatCarId?: string,
@Body("emexCarIndex") emexCarIndex?: number,
@Body("candidate") candidate?: string,
) {
return this.vehiclesService.decodeVin(vin, userId, pcatCarId, emexCarIndex, candidate);
}
@Get("history")
async history(
@CurrentUser("id") userId: string,
@Query("page") page?: string,
@Query("limit") limit?: string,
) {
return this.vehiclesService.getHistory(
userId,
page ? Number.parseInt(page, 10) : 1,
limit ? Number.parseInt(limit, 10) : 20,
);
}
@Post("report-vin")
async reportVin(
@CurrentUser("id") userId: string,
@CurrentUser("email") userEmail: string,
@Body("vin", VinValidationPipe) vin: string,
) {
await this.emailService.send({
to: "admin@sase.tr",
subject: `Tanınmayan Şase Bildirimi — ${vin}`,
html: `
<h2>Tanınmayan Şase Bildirimi</h2>
<p>Bir kullanıcı aşağıdaki şase numarasının doğru olduğunu bildirdi:</p>
<p><strong>VIN:</strong> <code>${vin}</code></p>
<p><strong>Kullanıcı:</strong> ${userEmail}</p>
<p><strong>Tarih:</strong> ${new Date().toLocaleString("tr-TR", { timeZone: "Europe/Istanbul" })}</p>
`,
text: `Tanınmayan şase bildirimi: ${vin} — Kullanıcı: ${userEmail}`,
tag: "vin-report",
});
return { sent: true };
}
@Get(":vehicleId/prefetch-status")
async prefetchStatus(@Param("vehicleId") vehicleId: string) {
await this.vehiclesService.getById(vehicleId);
return this.vehiclesService.getPrefetchStatus(vehicleId);
}
@Get(":vehicleId/categories/:categoryId")
async getCategoryParts(
@Param("vehicleId") vehicleId: string,
@Param("categoryId") categoryId: string,
) {
// Decoded vehicles are shared — any authenticated user can read.
await this.vehiclesService.getById(vehicleId);
return this.categoriesService.getCategoryWithParts(categoryId);
}
// On-demand resolution of a "bk. tablo:" cross-reference code whose target
// illustration wasn't seeded at category-load time — drills the relevant
// main group, then returns the resolved category (or null if not found).
@Get(":vehicleId/references/resolve")
async resolveReference(@Param("vehicleId") vehicleId: string, @Query("code") code: string) {
await this.vehiclesService.getById(vehicleId);
return this.categoriesService.resolveReferenceCode(vehicleId, code ?? "");
}
@Get(":id")
async getById(@Param("id") id: string) {
return this.vehiclesService.getById(id);
}
@Delete(":id")
async delete(@Param("id") id: string, @CurrentUser("id") userId: string) {
return this.vehiclesService.deleteVehicle(id, userId);
}
}