feat: setup account page for customer and centeralize the otps and phone usages to use the iam user

This commit is contained in:
Nathnael
2026-07-16 12:08:45 +00:00
parent 318af79962
commit f71bbbf782
26 changed files with 1057 additions and 62 deletions

View File

@@ -81,6 +81,11 @@ import type {
ForgotPasswordRequestPayload,
ForgotPasswordVerifyPayload,
ResetTicket,
SendContactOtpPayload,
SendContactOtpResponse,
UpdateAccountNamePayload,
UpdateContactPayload,
UpdateContactResponse,
} from "@/types/auth";
// ---------------------------------------------------------------------------
@@ -147,6 +152,26 @@ export const api = {
logout: endpoint<void, void>("auth", "logout", authService.logout),
},
// The signed-in user's own IAM account — the phone/email that OTPs and SMS
// actually go to. Separate from `companies`, which is business profile data.
account: {
sendContactOtp: endpoint<SendContactOtpPayload, SendContactOtpResponse>(
"account",
"sendContactOtp",
authService.sendContactOtp,
),
updateContact: endpoint<UpdateContactPayload, UpdateContactResponse>(
"account",
"updateContact",
authService.updateContact,
),
updateName: endpoint<UpdateAccountNamePayload, { success: true }>(
"account",
"updateName",
authService.updateAccountName,
),
},
companies: {
getInfo: endpoint<void, CompanyInfoResponse | null>(
"companies",

View File

@@ -11,11 +11,17 @@ import type {
OtpPayload,
OtpResponse,
ResetTicket,
SendContactOtpPayload,
SendContactOtpResponse,
SetPasswordPayload,
SignupPayload,
SignupResponse,
UpdateAccountNamePayload,
UpdateContactPayload,
UpdateContactResponse,
} from "@/types/auth";
import { client } from "@/utils/api";
import { unwrap } from "@/utils/endpoint";
import { ApiResponse } from "@edr/types";
export const authService = {
@@ -105,6 +111,35 @@ export const authService = {
return res.data.data;
},
// The three calls below manage the signed-in user's own account record
// (`/api/me`), which is what OTPs and SMS notifications are delivered to.
// Changing phone/email is OTP-gated server-side: the code goes to the NEW
// value, and the write only lands once it is verified.
sendContactOtp: async (body: SendContactOtpPayload) => {
const res = await client.post<ApiResponse<SendContactOtpResponse>>(
URL_CONSTANTS.ACCOUNT.CONTACT_OTP,
body,
);
return unwrap(res.data);
},
updateContact: async (body: UpdateContactPayload) => {
const res = await client.patch<ApiResponse<UpdateContactResponse>>(
URL_CONSTANTS.ACCOUNT.CONTACT,
body,
);
return unwrap(res.data);
},
updateAccountName: async (body: UpdateAccountNamePayload) => {
const res = await client.patch<ApiResponse<{ success: true }>>(
URL_CONSTANTS.ACCOUNT.NAME,
body,
);
return unwrap(res.data);
},
refreshToken: async () => {
const refreshTokenCookie = document.cookie
.split("; ")

View File

@@ -128,8 +128,6 @@ export interface SignContractPayload {
consentText?: string;
/** Sudo-mode OTP challenge; required when role=CUSTOMER. */
otp?: string;
/** Phone the OTP was sent to; required when role=CUSTOMER. */
otpPhone?: string;
}
export interface ApproveDeliveryResponse {

View File

@@ -268,9 +268,10 @@ export const contractsService = {
return data.data ?? data;
},
// Ask the server to send the signing OTP to the CONTRACT COMPANY's registered
// phone. The client never picks the number (the server verifies against the
// same one), so send and verify can't disagree. Returns a masked hint.
// Ask the server to send the signing OTP to the signer's own registered phone.
// The client never picks the number (the server resolves it from the
// authenticated user and verifies against the same one), so send and verify
// can't disagree. Returns a masked hint.
sendSigningOtp: async (id: string): Promise<{ sentTo: string }> => {
const { data } = await client.post(C.CONTRACT_SEND_SIGNING_OTP(id));
return data.data ?? data;