From e14e02e7f2dc4a2512f7ec0ce36d8e4714859a0f Mon Sep 17 00:00:00 2001 From: natib21 Date: Fri, 3 Jul 2026 14:07:43 +0000 Subject: [PATCH] fix --- .../1890000000006-AddDriverFaydaSubUnique.ts | 23 ++++++++++++ .../src/modules/drivers/drivers.service.ts | 37 ++++++++++++++++++- .../modules/drivers/entities/driver.entity.ts | 5 ++- .../src/components/fleet/FleetFormDialog.tsx | 25 +++++++++---- .../src/pages/fleet/config/drivers.ts | 12 +++--- .../src/pages/fleet/config/resources.ts | 5 +++ 6 files changed, 91 insertions(+), 16 deletions(-) create mode 100644 apps/edr-freight-api/src/migrations/1890000000006-AddDriverFaydaSubUnique.ts diff --git a/apps/edr-freight-api/src/migrations/1890000000006-AddDriverFaydaSubUnique.ts b/apps/edr-freight-api/src/migrations/1890000000006-AddDriverFaydaSubUnique.ts new file mode 100644 index 000000000..04c9a7000 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/1890000000006-AddDriverFaydaSubUnique.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +/** + * Enforce one driver record per verified Fayda identity. A unique index on + * fayda_sub blocks a second driver from being created against the same Fayda + * OIDC subject; NULLs stay distinct so legacy/unverified rows are unaffected. + */ +export class AddDriverFaydaSubUnique1890000000006 implements MigrationInterface { + name = "AddDriverFaydaSubUnique1890000000006"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE UNIQUE INDEX IF NOT EXISTS "UQ_DRIVERS_FAYDA_SUB" + ON freight.drivers (fayda_sub) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + DROP INDEX IF EXISTS freight."UQ_DRIVERS_FAYDA_SUB" + `); + } +} diff --git a/apps/edr-freight-api/src/modules/drivers/drivers.service.ts b/apps/edr-freight-api/src/modules/drivers/drivers.service.ts index 2464a26ac..cbfb6787d 100644 --- a/apps/edr-freight-api/src/modules/drivers/drivers.service.ts +++ b/apps/edr-freight-api/src/modules/drivers/drivers.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException, ConflictException } from '@nestjs/common'; +import { Injectable, NotFoundException, ConflictException, BadRequestException } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { CreateDriverDto } from './dto/create-driver.dto'; @@ -13,6 +13,12 @@ export class DriversService { ) {} async create(dto: CreateDriverDto): Promise { + if (dto.faydaVerified !== true) { + throw new BadRequestException( + 'Driver identity must be verified with Fayda before saving', + ); + } + const existing = await this.driverRepo.findOne({ where: [ { licenseNumber: dto.licenseNumber }, @@ -33,6 +39,17 @@ export class DriversService { } } + if (dto.faydaSub) { + const dupe = await this.driverRepo.findOne({ + where: { faydaSub: dto.faydaSub }, + }); + if (dupe) { + throw new ConflictException( + 'A driver is already registered for this Fayda identity', + ); + } + } + const driver = this.driverRepo.create(dto); return this.driverRepo.save(driver); } @@ -106,7 +123,25 @@ export class DriversService { } } + if (dto.faydaSub && dto.faydaSub !== driver.faydaSub) { + const dupe = await this.driverRepo.findOne({ + where: { faydaSub: dto.faydaSub }, + }); + if (dupe) { + throw new ConflictException( + 'A driver is already registered for this Fayda identity', + ); + } + } + Object.assign(driver, dto); + + if (driver.faydaVerified !== true) { + throw new BadRequestException( + 'Driver identity must be verified with Fayda before saving', + ); + } + return this.driverRepo.save(driver); } diff --git a/apps/edr-freight-api/src/modules/drivers/entities/driver.entity.ts b/apps/edr-freight-api/src/modules/drivers/entities/driver.entity.ts index 889b688cb..58a08b849 100644 --- a/apps/edr-freight-api/src/modules/drivers/entities/driver.entity.ts +++ b/apps/edr-freight-api/src/modules/drivers/entities/driver.entity.ts @@ -64,7 +64,8 @@ export class Driver extends BaseEntity { @Column({ name: 'fayda_verified', type: 'boolean', default: false, nullable: true }) faydaVerified?: boolean; - /** Fayda OIDC subject the identity was verified against. */ - @Column({ name: 'fayda_sub', type: 'varchar', nullable: true }) + /** Fayda OIDC subject the identity was verified against. Unique — one driver + * record per verified Fayda identity (NULLs allowed for legacy/unverified). */ + @Column({ name: 'fayda_sub', type: 'varchar', unique: true, nullable: true }) faydaSub?: string | null; } diff --git a/apps/edr-freight-web/backoffice/src/components/fleet/FleetFormDialog.tsx b/apps/edr-freight-web/backoffice/src/components/fleet/FleetFormDialog.tsx index 82a89c33e..f07659dd8 100644 --- a/apps/edr-freight-web/backoffice/src/components/fleet/FleetFormDialog.tsx +++ b/apps/edr-freight-web/backoffice/src/components/fleet/FleetFormDialog.tsx @@ -258,6 +258,12 @@ const FleetFormDialog = ({ }, [fields]); const handleSubmit = () => { + // Hard gate: a driver record cannot be saved until its identity is verified + // with Fayda. Mirrored server-side in DriversService. + if (verifyWithFayda && !faydaVerified) { + setFaydaError("Verify the driver's identity with Fayda before saving."); + return; + } if (!validate()) return; const payload = Object.fromEntries( Object.entries(values) @@ -278,6 +284,9 @@ const FleetFormDialog = ({ const renderField = (field: FleetFormFieldDef) => { const value = values[field.name]; const error = errors[field.name]; + // Fayda-owned identity fields (name/email/phone/DOB/gender) are populated + // only by verification and never hand-edited. + const isDisabled = Boolean(field.disabled || field.faydaLocked); if (field.type === "select") { return ( @@ -297,7 +306,7 @@ const FleetFormDialog = ({ } error={error} searchable - disabled={selectOptionsLoading} + disabled={selectOptionsLoading || isDisabled} rightSection={ selectOptionsLoading ? ( @@ -327,7 +336,7 @@ const FleetFormDialog = ({ error={error} searchable clearable - disabled={selectOptionsLoading} + disabled={selectOptionsLoading || isDisabled} rightSection={ selectOptionsLoading ? ( @@ -351,7 +360,7 @@ const FleetFormDialog = ({ })) } error={error} - disabled={field.disabled} + disabled={isDisabled} /> ); } @@ -371,7 +380,7 @@ const FleetFormDialog = ({ } error={error} minRows={3} - disabled={field.disabled} + disabled={isDisabled} /> ); } @@ -391,7 +400,7 @@ const FleetFormDialog = ({ })) } error={error} - disabled={field.disabled} + disabled={isDisabled} description={field.description || "Select a date"} rightSection={ @@ -429,7 +438,7 @@ const FleetFormDialog = ({ })) } error={error} - disabled={field.disabled} + disabled={isDisabled} /> ); }; @@ -457,7 +466,8 @@ const FleetFormDialog = ({ ) : ( - Verify the driver's identity with Fayda to prefill their details. + Identity must be verified with Fayda before this driver can be + saved. )} diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/config/drivers.ts b/apps/edr-freight-web/backoffice/src/pages/fleet/config/drivers.ts index 2f3fb5aeb..ec242906e 100644 --- a/apps/edr-freight-web/backoffice/src/pages/fleet/config/drivers.ts +++ b/apps/edr-freight-web/backoffice/src/pages/fleet/config/drivers.ts @@ -50,12 +50,12 @@ export const driversConfig: FleetResourceConfig = { ], formFields: [ { name: "licenseNumber", label: "License Number", type: "text", required: true }, - { name: "firstName", label: "First Name", type: "text", required: true }, - { name: "lastName", label: "Last Name", type: "text", required: true }, - { name: "email", label: "Email", type: "email", required: true }, - { name: "phoneNumber", label: "Phone Number", type: "text", required: true }, - { name: "dateOfBirth", label: "Date of Birth", type: "date", required: true }, - { name: "gender", label: "Gender", type: "select", options: DRIVER_GENDER_OPTIONS }, + { name: "firstName", label: "First Name", type: "text", required: true, faydaLocked: true }, + { name: "lastName", label: "Last Name", type: "text", required: true, faydaLocked: true }, + { name: "email", label: "Email", type: "email", required: true, faydaLocked: true }, + { name: "phoneNumber", label: "Phone Number", type: "text", required: true, faydaLocked: true }, + { name: "dateOfBirth", label: "Date of Birth", type: "date", required: true, faydaLocked: true }, + { name: "gender", label: "Gender", type: "select", options: DRIVER_GENDER_OPTIONS, faydaLocked: true }, { name: "licenseExpiryDate", label: "License Expiry Date", type: "date", required: true }, { name: "status", label: "Status", type: "select", required: true, options: DRIVER_STATUS_OPTIONS }, { name: "vehicleTypesAuthorized", label: "Authorized Vehicle Types", type: "multiselect", options: VEHICLE_TYPE_OPTIONS }, diff --git a/apps/edr-freight-web/backoffice/src/pages/fleet/config/resources.ts b/apps/edr-freight-web/backoffice/src/pages/fleet/config/resources.ts index c38c0c118..29a8795e7 100644 --- a/apps/edr-freight-web/backoffice/src/pages/fleet/config/resources.ts +++ b/apps/edr-freight-web/backoffice/src/pages/fleet/config/resources.ts @@ -40,6 +40,11 @@ export interface FleetResourceColumn { export interface FleetFormFieldDef extends FormFieldDef { dynamicOptions?: FleetDynamicOptions; noneOption?: boolean; + /** + * Field is owned by the Fayda identity — populated only by verification and + * never hand-edited. Rendered disabled in the form. + */ + faydaLocked?: boolean; } export interface FleetListFilterDef {