feat: Complete PL24 architecture overhaul with email & auth improvements

## PL24 Integration Rewrite
- Add dual architecture support: P5 Modern (JSON API) + Legacy (HTML scraping)
- New services: factory, session, fetch, html-parser, legacy-scraper
- Add NHTSA WMI data for accurate brand detection
- Brand-specific parsers for VW, Mercedes, BMW, Audi, Porsche, Ford, etc.
- Add 254 schema images for parts diagrams

## Authentication & Security
- Add PasswordResetToken database model with proper expiration
- Implement dual persistence (Redis + DB) for password reset tokens
- Add email enumeration protection

## Email Module
- New EmailService for password reset emails
- Configurable SMTP settings

## Database Schema
- Add Vehicle fields: equipment, interiorCode, productionDate, salesType
- Add Part.hotspotId for schema hotspot linking
- Add SchemaPic.hotspots JSON field
- Add VehicleCategory.subGroupsJson for subgroup data

## Frontend
- Improve auth pages (login, register, forgot-password)
- Enhance vehicle detail and categories pages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Semih
2026-01-29 08:51:11 +01:00
parent 384aea0a01
commit 757905f41c
277 changed files with 13012 additions and 322 deletions

View File

@@ -44,6 +44,11 @@ pnpm test # Tests
- Parçaları on-demand çek (kullanıcı kategoriye tıklayınca)
- Credentials: `.env` dosyasında
## Test Kullanıcı
- **Email**: admin@sase.tr
- **Password**: SaseAdmin2024
## Kod Stili
- TypeScript strict mode

View File

@@ -21,7 +21,12 @@
"db:migrate:dev": "prisma migrate dev",
"db:seed": "ts-node prisma/seed.ts",
"db:studio": "prisma studio",
"clean": "rm -rf dist"
"clean": "rm -rf dist",
"pl24:analyze": "ts-node scripts/pl24_analysis/catalog_explorer.ts",
"pl24:test-legacy": "ts-node scripts/pl24_analysis/test_legacy_parsers.ts",
"pl24:capture-html": "ts-node scripts/pl24_analysis/legacy_vin_tester.ts",
"pl24:direct-test": "ts-node scripts/pl24_analysis/direct_parser_test.ts",
"pl24:offline-test": "ts-node scripts/pl24_analysis/offline_parser_test.ts"
},
"dependencies": {
"@nestjs/common": "^10.4.15",
@@ -30,6 +35,7 @@
"@nestjs/jwt": "^10.2.0",
"@nestjs/passport": "^10.0.3",
"@nestjs/platform-express": "^10.4.15",
"@nestjs/swagger": "^8.0.0",
"@nestjs/throttler": "^6.3.0",
"@prisma/client": "^6.1.0",
"@sase/shared": "workspace:*",
@@ -38,12 +44,17 @@
"bullmq": "^5.30.1",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"css-select": "^5.1.0",
"domhandler": "^5.0.3",
"domutils": "^3.1.0",
"htmlparser2": "^9.1.0",
"ioredis": "^5.4.2",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"puppeteer": "^24.0.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",
"undici": "^7.2.0",
"uuid": "^11.0.3"
},
"devDependencies": {

View File

@@ -8,27 +8,44 @@ datasource db {
}
model User {
id String @id @default(cuid())
email String @unique
name String?
avatar String?
provider AuthProvider @default(EMAIL)
providerId String?
passwordHash String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
role Role @default(USER)
queryLogs QueryLog[]
selectedBrands UserBrand[]
subscription UserSubscription?
vehicles Vehicle[] @relation("QueriedBy")
id String @id @default(cuid())
email String @unique
name String?
avatar String?
provider AuthProvider @default(EMAIL)
providerId String?
passwordHash String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
role Role @default(USER)
passwordResetTokens PasswordResetToken[]
queryLogs QueryLog[]
selectedBrands UserBrand[]
subscription UserSubscription?
vehicles Vehicle[] @relation("QueriedBy")
@@index([email])
@@index([role])
@@map("users")
}
model PasswordResetToken {
id String @id @default(cuid())
tokenHash String @unique
userId String
expiresAt DateTime
isUsed Boolean @default(false)
usedAt DateTime?
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([tokenHash])
@@index([userId])
@@index([expiresAt])
@@map("password_reset_tokens")
}
model Brand {
id String @id @default(cuid())
code String @unique
@@ -101,28 +118,32 @@ model UserBrand {
}
model Vehicle {
id String @id @default(cuid())
vin String @unique @db.VarChar(17)
brandId String
model String
year Int
series String?
bodyType String?
engineCode String?
engineType String?
engineVolume String?
transmission String?
driveType String?
colorCode String?
rawResponse Json
queriedById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
parts Part[]
queryLogs QueryLog[]
categories VehicleCategory[]
brand Brand @relation(fields: [brandId], references: [id])
queriedBy User? @relation("QueriedBy", fields: [queriedById], references: [id])
id String @id @default(cuid())
vin String @unique @db.VarChar(17)
brandId String
model String
year Int
series String?
bodyType String?
engineCode String?
engineType String?
engineVolume String?
transmission String?
driveType String?
colorCode String?
rawResponse Json
queriedById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
equipment Json?
interiorCode String?
productionDate String?
salesType String?
parts Part[]
queryLogs QueryLog[]
categories VehicleCategory[]
brand Brand @relation(fields: [brandId], references: [id])
queriedBy User? @relation("QueriedBy", fields: [queriedById], references: [id])
@@index([vin])
@@index([brandId])
@@ -158,12 +179,13 @@ model VehicleCategory {
vehicleId String
categoryId String
partCount Int @default(0)
schemaImageUrl String? @db.Text // Original URL from PL24
schemaPicId String? // Reference to deduplicated schema image
createdAt DateTime @default(now())
schemaImageUrl String? @db.Text
schemaPicId String?
subGroupsJson Json?
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
schemaPic SchemaPic? @relation(fields: [schemaPicId], references: [id])
vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
@@unique([vehicleId, categoryId])
@@index([vehicleId])
@@ -173,44 +195,56 @@ model VehicleCategory {
}
model SchemaPic {
id String @id @default(cuid())
imageId String @unique // PL24 image ID (e.g., "194500200") for deduplication
localPath String // Local path: /images/schemas/194500200.png
originalUrl String? @db.Text // Original PL24 URL for reference
fileSize Int? // File size in bytes
width Int? // Image width
height Int? // Image height
createdAt DateTime @default(now())
vehicleCategories VehicleCategory[]
id String @id @default(cuid())
imageId String @unique
localPath String
originalUrl String? @db.Text
fileSize Int?
width Int?
height Int?
createdAt DateTime @default(now())
hotspots Json?
vehicleCategories VehicleCategory[]
@@index([imageId])
@@map("schema_pics")
}
model Part {
id String @id @default(cuid())
vehicleId String
categoryId String
oemCode String
oemCodes Json?
nameEn String
nameTr String
description String? @db.Text
positionCode String?
positionX Float?
positionY Float?
brandPrices Json
imageUrl String?
notes String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
id String @id @default(cuid())
vehicleId String
categoryId String
oemCode String
oemCodes Json?
nameEn String
nameTr String
description String? @db.Text
positionCode String?
positionX Float?
positionY Float?
brandPrices Json
imageUrl String?
notes String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
hotspotId String?
modelCodes String? @db.Text
quantity Int?
remark String?
subGroupId String?
formattedPartNo String?
isPreselected Boolean @default(false)
restrictions Json?
supersededBy String?
supersedes String?
category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade)
vehicle Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
@@index([oemCode])
@@index([vehicleId])
@@index([categoryId])
@@index([vehicleId, categoryId])
@@index([subGroupId])
@@map("parts")
}
@@ -483,6 +517,251 @@ model EmexCategoryTranslation {
@@map("emex_category_translations")
}
model PL24Catalog {
id String @id @default(cuid())
serviceName String @unique
name String
brand String
catalogBase String
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
partGroups PL24PartGroup[]
parts PL24Part[]
vehicles PL24Vehicle[]
@@index([brand])
@@map("pl24_catalogs")
}
model PL24Vehicle {
id String @id @default(cuid())
catalogId String
vehicleId String
name String
model String
series String?
bodyType String?
engineCode String?
engineType String?
engineVolume String?
transmission String?
driveType String?
fuelType String?
yearFrom Int?
yearTo Int?
catalogPath String @db.Text
mainGroupsPath String? @db.Text
ssd String? @db.VarChar(500)
rawSegments Json?
uniqueKey String @unique @db.VarChar(255)
dataHash String? @db.Char(32)
scrapeComplete Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
groupLinks PL24VehicleGroup[]
partLinks PL24VehiclePart[]
vinLinks PL24VehicleVin[]
catalog PL24Catalog @relation(fields: [catalogId], references: [id], onDelete: Cascade)
@@index([catalogId])
@@index([vehicleId])
@@index([model])
@@index([engineCode])
@@map("pl24_vehicles")
}
model PL24VehicleVin {
id String @id @default(cuid())
vin String @unique @db.VarChar(17)
vehicleId String
productionDate String?
colorCode String?
interiorCode String?
transmissionCode String?
equipment String? @db.Text
salesType String?
createdAt DateTime @default(now())
vehicle PL24Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
@@index([vehicleId])
@@map("pl24_vehicle_vins")
}
model PL24PartGroup {
id String @id @default(cuid())
catalogId String
parentId String?
groupCode String
name String
nameTr String?
description String? @db.Text
level Int @default(1)
sortOrder Int @default(0)
hasParts Boolean @default(false)
hasChildren Boolean @default(false)
lft Int?
rgt Int?
iconUrl String? @db.Text
uniqueKey String @unique @db.VarChar(255)
dataHash String? @db.Char(32)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
catalog PL24Catalog @relation(fields: [catalogId], references: [id], onDelete: Cascade)
parent PL24PartGroup? @relation("PL24GroupTree", fields: [parentId], references: [id])
children PL24PartGroup[] @relation("PL24GroupTree")
images PL24PartImage[]
parts PL24Part[]
vehicleLinks PL24VehicleGroup[]
vehicleParts PL24VehiclePart[]
@@index([catalogId])
@@index([parentId])
@@index([groupCode])
@@map("pl24_part_groups")
}
model PL24VehicleGroup {
id String @id @default(cuid())
vehicleId String
groupId String
linkPath String? @db.Text
linkWid String?
partCount Int @default(0)
isScraped Boolean @default(false)
schemaImageUrl String? @db.Text
schemaPicId String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
group PL24PartGroup @relation(fields: [groupId], references: [id], onDelete: Cascade)
schemaPic PL24SchemaPic? @relation(fields: [schemaPicId], references: [id])
vehicle PL24Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
@@unique([vehicleId, groupId])
@@index([vehicleId])
@@index([groupId])
@@index([schemaPicId])
@@map("pl24_vehicle_groups")
}
model PL24Part {
id String @id @default(cuid())
catalogId String
groupId String?
partNumber String @db.VarChar(50)
partNumberClean String? @db.VarChar(50)
formattedPartNo String?
name String
nameTr String?
description String? @db.Text
brand String?
quantity Float @default(1)
unit String @default("PC")
positionCode String?
positionX Float?
positionY Float?
remark String? @db.Text
modelCodes String? @db.Text
notes String? @db.Text
uniqueKey String @unique @db.VarChar(255)
dataHash String? @db.Char(32)
isActive Boolean @default(true)
isDiscontinued Boolean @default(false)
supersededById String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
altNumbers PL24PartNumber[]
catalog PL24Catalog @relation(fields: [catalogId], references: [id], onDelete: Cascade)
group PL24PartGroup? @relation(fields: [groupId], references: [id])
vehicleLinks PL24VehiclePart[]
@@index([catalogId])
@@index([groupId])
@@index([partNumber])
@@index([partNumberClean])
@@map("pl24_parts")
}
model PL24PartNumber {
id String @id @default(cuid())
partId String
number String @db.VarChar(50)
numberClean String? @db.VarChar(50)
numberType String @default("ALTERNATE")
brand String?
priority Int @default(0)
createdAt DateTime @default(now())
part PL24Part @relation(fields: [partId], references: [id], onDelete: Cascade)
@@unique([partId, number])
@@index([partId])
@@index([number])
@@index([numberClean])
@@map("pl24_part_numbers")
}
model PL24VehiclePart {
id String @id @default(cuid())
vehicleId String
partId String
groupId String?
quantity Float @default(1)
position String?
notes String? @db.Text
dateFrom DateTime?
dateTo DateTime?
conditions Json?
createdAt DateTime @default(now())
group PL24PartGroup? @relation(fields: [groupId], references: [id])
part PL24Part @relation(fields: [partId], references: [id], onDelete: Cascade)
vehicle PL24Vehicle @relation(fields: [vehicleId], references: [id], onDelete: Cascade)
@@unique([vehicleId, partId, groupId])
@@index([vehicleId])
@@index([partId])
@@index([groupId])
@@map("pl24_vehicle_parts")
}
model PL24SchemaPic {
id String @id @default(cuid())
imageId String @unique
localPath String
originalUrl String? @db.Text
width Int?
height Int?
fileSize Int?
mimeType String? @db.VarChar(50)
hotspots Json?
createdAt DateTime @default(now())
downloadedAt DateTime?
vehicleGroups PL24VehicleGroup[]
@@index([imageId])
@@map("pl24_schema_pics")
}
model PL24PartImage {
id String @id @default(cuid())
groupId String?
imageType String @default("DIAGRAM")
originalUrl String @db.Text
localPath String?
imageId String? @unique
width Int?
height Int?
fileSize Int?
isPrimary Boolean @default(false)
sortOrder Int @default(0)
createdAt DateTime @default(now())
downloadedAt DateTime?
group PL24PartGroup? @relation(fields: [groupId], references: [id])
@@index([groupId])
@@index([imageId])
@@map("pl24_part_images")
}
enum Role {
USER
MODERATOR

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Some files were not shown because too many files have changed in this diff Show More