mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
fix
This commit is contained in:
@@ -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<void> {
|
||||
await queryRunner.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "UQ_DRIVERS_FAYDA_SUB"
|
||||
ON freight.drivers (fayda_sub)
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
DROP INDEX IF EXISTS freight."UQ_DRIVERS_FAYDA_SUB"
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -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<Driver> {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 ? (
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
@@ -327,7 +336,7 @@ const FleetFormDialog = ({
|
||||
error={error}
|
||||
searchable
|
||||
clearable
|
||||
disabled={selectOptionsLoading}
|
||||
disabled={selectOptionsLoading || isDisabled}
|
||||
rightSection={
|
||||
selectOptionsLoading ? (
|
||||
<Loader2 size={14} className="animate-spin" />
|
||||
@@ -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={
|
||||
<ActionIcon size="sm" variant="subtle" color="green">
|
||||
@@ -429,7 +438,7 @@ const FleetFormDialog = ({
|
||||
}))
|
||||
}
|
||||
error={error}
|
||||
disabled={field.disabled}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -457,7 +466,8 @@ const FleetFormDialog = ({
|
||||
</Badge>
|
||||
) : (
|
||||
<Text size="sm" c="dimmed">
|
||||
Verify the driver's identity with Fayda to prefill their details.
|
||||
Identity must be verified with Fayda before this driver can be
|
||||
saved.
|
||||
</Text>
|
||||
)}
|
||||
<Button
|
||||
@@ -489,6 +499,7 @@ const FleetFormDialog = ({
|
||||
color="edr-green"
|
||||
loading={isSubmitting}
|
||||
onClick={handleSubmit}
|
||||
disabled={verifyWithFayda && !faydaVerified}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user