feat: add check validity to the user signup

This commit is contained in:
Nathnael
2026-07-04 06:47:20 +00:00
parent 077512fdaf
commit 2f6f06b7c4
8 changed files with 136 additions and 17 deletions

View File

@@ -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 });
}
}

View File

@@ -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<User>,
) {}
async check({
email,
phone,
}: CheckAvailabilityQuery): Promise<CheckAvailabilityResult> {
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,
};
}
}

View File

@@ -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 {}

View File

@@ -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: {

View File

@@ -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 },
);

View File

@@ -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<CheckAvailabilityPayload, CheckAvailabilityResponse>(
"auth",
"checkAvailability",
authService.checkAvailability,
),
sendOTP: endpoint<OtpPayload, OtpResponse>(
"auth",
"sendOTP",

View File

@@ -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<SignupResponse & ApiResponse<void>> (
const res = await client.post<SignupResponse & ApiResponse<void>>(
URL_CONSTANTS.USERS.SIGN_UP,
body,
);
@@ -31,9 +33,7 @@ export const authService = {
},
getMyInfo: async () => {
const res = await client.get<AuthUser>(
URL_CONSTANTS.USERS.ME,
);
const res = await client.get<AuthUser>(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<ApiResponse<CheckAvailabilityResponse>>(
URL_CONSTANTS.USERS.CHECK_AVAILABILITY,
{ params },
);
return res.data;
},
sendOTP: async (body: OtpPayload) => {
const res = await client.post<ApiResponse<OtpResponse>>(
URL_CONSTANTS.OTP.SEND,

View File

@@ -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;