Files
CBS-Mobile-App/src/modules/auth/services/authApi.ts
T

152 lines
4.6 KiB
TypeScript

import { env } from '../../../config/env';
import { apiClient } from '../../../services/api/apiClient';
import { isCbsApiError } from '../../../services/api/cbsErrors';
import { endpoints } from '../../../services/api/endpoints';
import { deviceIdentity } from '../../../services/security/deviceIdentity';
import { mpinCrypto } from '../../../services/security/mpinCrypto';
import type {
CreateMpinResult,
LoginResult,
SessionIdentity,
StartRegistrationResult,
VerifyOtpResult,
} from '../../../types/cbs';
/**
* `MALFORMED_REQUEST` on an MPIN call is what a stale or wrong public key looks
* like from the client — the server will not say more, because saying more
* would leak whether the profile exists. The key is fetched fresh on every
* encryption, so a retry picks up a rotated key; try exactly once more.
* A second failure is a real problem worth surfacing.
*/
const withKeyRetry = async <T>(call: () => Promise<T>): Promise<T> => {
try {
return await call();
} catch (error) {
if (isCbsApiError(error) && error.code === 'MALFORMED_REQUEST') {
return call();
}
throw error;
}
};
export const authService = {
/**
* Looks the customer up in their bank's core system, links every account it
* returns and sends an OTP to the handset.
*/
async startRegistration(mobileNumber: string, orgId: string = env.orgId) {
const response = await apiClient.post<StartRegistrationResult>(
endpoints.registration.start,
{ orgId, mobileNumber },
);
return response.data;
},
/**
* Verifies the OTP, binds the profile to this device and allocates an MMID to
* every linked account. Returns the single-use, five-minute setup token that
* MPIN creation requires.
*/
async verifyRegistrationOtp({
mobileNumber,
otp,
orgId = env.orgId,
}: {
mobileNumber: string;
otp: string;
orgId?: string;
}) {
const deviceId = await deviceIdentity.get();
const response = await apiClient.post<VerifyOtpResult>(endpoints.registration.verifyOtp, {
orgId,
mobileNumber,
otp,
deviceId,
});
return response.data;
},
/**
* Creates the MPIN and completes registration. There is deliberately no
* orgId and no mobile number in the request — both come from the setup token,
* which is what stops a caller setting someone else's MPIN. The deviceId must
* match the one sent at verify-otp.
*/
async createMpin({ setupToken, mpin }: { setupToken: string; mpin: string }) {
return withKeyRetry(async () => {
const deviceId = await deviceIdentity.get();
const encryptedMpin = await mpinCrypto.encryptMpin(mpin);
const response = await apiClient.post<CreateMpinResult>(endpoints.registration.mpin, {
setupToken,
deviceId,
encryptedMpin,
});
return response.data;
});
},
/**
* Verifies the MPIN and opens a session. The MPIN is encrypted exactly as it
* was at setup — same key, same payload shape.
*/
async login({
mobileNumber,
mpin,
orgId = env.orgId,
setupToken,
}: {
mobileNumber: string;
mpin: string;
orgId?: string;
/** See `LoginRequest.setupToken` — a device re-bind, and only that. */
setupToken?: string | null;
}) {
return withKeyRetry(async () => {
const deviceId = await deviceIdentity.get();
const encryptedMpin = await mpinCrypto.encryptMpin(mpin);
const response = await apiClient.post<LoginResult>(endpoints.auth.login, {
orgId,
mobileNumber,
deviceId,
encryptedMpin,
// Spread rather than set: an ordinary sign-in must not carry the key at
// all, and `setupToken: undefined` is a key once this goes through
// JSON.stringify on some clients.
...(setupToken ? { setupToken } : {}),
});
return response.data;
});
},
/** Who the current session belongs to. Doubles as a keep-alive. */
async getIdentity() {
const response = await apiClient.get<SessionIdentity>(endpoints.auth.me);
return response.data;
},
/**
* Revokes the session immediately. The endpoint always answers 200, even for
* a token that was already dead — a different answer would tell a caller
* whether a token they hold is live. Nothing that happens here may block the
* local sign-out, so every failure is swallowed: the customer asked to be
* signed out, and a network error must not leave them signed in.
*/
async logout() {
try {
await apiClient.post(endpoints.auth.logout);
} catch {
// Intentionally ignored. The local session is cleared regardless.
}
},
};