152 lines
4.6 KiB
TypeScript
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.
|
|
}
|
|
},
|
|
};
|