diff --git a/apps/edr-freight-api/src/modules/auth/check-availability.controller.ts b/apps/edr-freight-api/src/modules/auth/check-availability.controller.ts new file mode 100644 index 000000000..13084d1c8 --- /dev/null +++ b/apps/edr-freight-api/src/modules/auth/check-availability.controller.ts @@ -0,0 +1,22 @@ +import { Controller, Get, Query } from "@nestjs/common"; +import { ApiOperation, ApiTags } from "@nestjs/swagger"; +import { Public } from "@edr/api-common"; + +import { CheckAvailabilityService } from "./check-availability.service"; + +@ApiTags("auth") +@Controller("auth") +@Public() +export class CheckAvailabilityController { + constructor( + private readonly checkAvailabilityService: CheckAvailabilityService, + ) {} + + @Get("check-availability") + @ApiOperation({ + summary: "Check whether an email and/or phone number is already registered", + }) + check(@Query("email") email?: string, @Query("phone") phone?: string) { + return this.checkAvailabilityService.check({ email, phone }); + } +} diff --git a/apps/edr-freight-api/src/modules/auth/check-availability.service.ts b/apps/edr-freight-api/src/modules/auth/check-availability.service.ts new file mode 100644 index 000000000..c9ce84b72 --- /dev/null +++ b/apps/edr-freight-api/src/modules/auth/check-availability.service.ts @@ -0,0 +1,47 @@ +import { BadRequestException, Injectable } from "@nestjs/common"; +import { InjectRepository } from "@nestjs/typeorm"; +import { Repository } from "typeorm"; + +import { User } from "@tria-plc/iamapi-common/entities/iam/user/user.entity"; + +export interface CheckAvailabilityQuery { + email?: string; + phone?: string; +} + +export interface CheckAvailabilityResult { + emailTaken: boolean; + phoneTaken: boolean; +} + +@Injectable() +export class CheckAvailabilityService { + constructor( + @InjectRepository(User) + private readonly userRepository: Repository, + ) {} + + async check({ + email, + phone, + }: CheckAvailabilityQuery): Promise { + if (!email && !phone) { + throw new BadRequestException("email or phone is required"); + } + + const matches = await this.userRepository.find({ + where: [ + ...(email ? [{ email }] : []), + ...(phone ? [{ phoneNumber: phone }] : []), + ], + select: { id: true, email: true, phoneNumber: true }, + }); + + return { + emailTaken: email ? matches.some((user) => user.email === email) : false, + phoneTaken: phone + ? matches.some((user) => user.phoneNumber === phone) + : false, + }; + } +} diff --git a/apps/edr-freight-api/src/modules/auth/freight-auth.module.ts b/apps/edr-freight-api/src/modules/auth/freight-auth.module.ts index a689ba24e..16fbeffda 100644 --- a/apps/edr-freight-api/src/modules/auth/freight-auth.module.ts +++ b/apps/edr-freight-api/src/modules/auth/freight-auth.module.ts @@ -1,10 +1,16 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { User } from '@tria-plc/iamapi-common/entities/iam/user/user.entity'; + +import { CheckAvailabilityController } from './check-availability.controller'; +import { CheckAvailabilityService } from './check-availability.service'; import { FreightMeController } from './freight-me.controller'; import { FreightMeService } from './freight-me.service'; @Module({ - controllers: [FreightMeController], - providers: [FreightMeService], + imports: [TypeOrmModule.forFeature([User])], + controllers: [FreightMeController, CheckAvailabilityController], + providers: [FreightMeService, CheckAvailabilityService], }) export class FreightAuthModule {} diff --git a/apps/edr-freight-web/portal/src/constants/URLS.ts b/apps/edr-freight-web/portal/src/constants/URLS.ts index a4e20b029..a8cafc0d1 100644 --- a/apps/edr-freight-web/portal/src/constants/URLS.ts +++ b/apps/edr-freight-web/portal/src/constants/URLS.ts @@ -14,6 +14,7 @@ export const URL_CONSTANTS = { SET_PASSWORD: "/api/auth/set-password", ME: "/api/auth/me", GENERATE_VERIFICATION_CODE: "/users/generate-verification-code", + CHECK_AVAILABILITY: "/api/auth/check-availability", }, OTP: { diff --git a/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx b/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx index 50320f699..05fa411b5 100644 --- a/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/accounts/SignupPage.tsx @@ -132,12 +132,30 @@ export default function SignupPage() { const passwordValue = watch("password") ?? ""; - // Step 1 — form is valid: send a fresh code to the chosen channel, then - // move to the OTP challenge. + // Step 1 — form is valid: make sure the email/phone aren't already + // registered, then send a fresh code to the chosen channel and move to + // the OTP challenge. const requestOtp = async (data: FormData) => { setError(null); setSending(true); try { + const availability = await api.auth.checkAvailability.call({ + email: data.email, + phone: data.phone, + }); + if (availability.emailTaken && availability.phoneTaken) { + setError("An account with this email and phone number already exists."); + return; + } + if (availability.emailTaken) { + setError("An account with this email already exists."); + return; + } + if (availability.phoneTaken) { + setError("An account with this phone number already exists."); + return; + } + await api.auth.sendOTP.call( channel === "email" ? { email: data.email } : { phone: data.phone }, ); diff --git a/apps/edr-freight-web/portal/src/services/api.ts b/apps/edr-freight-web/portal/src/services/api.ts index 81a2488a9..932f6fc46 100644 --- a/apps/edr-freight-web/portal/src/services/api.ts +++ b/apps/edr-freight-web/portal/src/services/api.ts @@ -66,6 +66,8 @@ import type { import type { ProfileResponse, UpdateProfilePayload } from "@/types/profile"; import type { AuthUser, + CheckAvailabilityPayload, + CheckAvailabilityResponse, GenerateVerificationCodePayload, LoginPayload, LoginResponse, @@ -107,6 +109,11 @@ export const api = { "setPassword", authService.setPassword, ), + checkAvailability: endpoint( + "auth", + "checkAvailability", + authService.checkAvailability, + ), sendOTP: endpoint( "auth", "sendOTP", diff --git a/apps/edr-freight-web/portal/src/services/auth.service.ts b/apps/edr-freight-web/portal/src/services/auth.service.ts index e1de1889a..58b81c5ba 100644 --- a/apps/edr-freight-web/portal/src/services/auth.service.ts +++ b/apps/edr-freight-web/portal/src/services/auth.service.ts @@ -1,14 +1,16 @@ import { URL_CONSTANTS } from "@/constants/URLS"; import type { - AuthUser, - GenerateVerificationCodePayload, - LoginPayload, - LoginResponse, - OtpPayload, - OtpResponse, - SetPasswordPayload, - SignupPayload, - SignupResponse, + AuthUser, + CheckAvailabilityPayload, + CheckAvailabilityResponse, + GenerateVerificationCodePayload, + LoginPayload, + LoginResponse, + OtpPayload, + OtpResponse, + SetPasswordPayload, + SignupPayload, + SignupResponse, } from "@/types/auth"; import { client } from "@/utils/api"; import { ApiResponse } from "@edr/types"; @@ -23,7 +25,7 @@ export const authService = { }, createUser: async (body: SignupPayload) => { - const res = await client.post> ( + const res = await client.post>( URL_CONSTANTS.USERS.SIGN_UP, body, ); @@ -31,9 +33,7 @@ export const authService = { }, getMyInfo: async () => { - const res = await client.get( - URL_CONSTANTS.USERS.ME, - ); + const res = await client.get(URL_CONSTANTS.USERS.ME); return res.data; }, @@ -53,6 +53,14 @@ export const authService = { return res.data.data; }, + checkAvailability: async (params: CheckAvailabilityPayload) => { + const res = await client.get>( + URL_CONSTANTS.USERS.CHECK_AVAILABILITY, + { params }, + ); + return res.data; + }, + sendOTP: async (body: OtpPayload) => { const res = await client.post>( URL_CONSTANTS.OTP.SEND, diff --git a/apps/edr-freight-web/portal/src/types/auth.ts b/apps/edr-freight-web/portal/src/types/auth.ts index 7b58f573b..04357a9ca 100644 --- a/apps/edr-freight-web/portal/src/types/auth.ts +++ b/apps/edr-freight-web/portal/src/types/auth.ts @@ -45,6 +45,16 @@ export interface OtpResponse { message: string; } +export interface CheckAvailabilityPayload { + email?: string; + phone?: string; +} + +export interface CheckAvailabilityResponse { + emailTaken: boolean; + phoneTaken: boolean; +} + export interface SetPasswordPayload { newPassword: string; confirmPassword: string;