accounts-frontend/packages/app/services/api/authentication.ts

193 lines
4.0 KiB
TypeScript

import { UserResponse } from 'app/services/api/accounts';
import logger from 'app/services/logger';
import request, { InternalServerError } from 'app/services/request';
import { getInfo as getInfoEndpoint } from 'app/services/api/accounts';
export interface OAuthResponse {
access_token: string;
refresh_token?: string;
expires_in: number; // count seconds before expire
success: true;
}
export function login({
login,
password,
totp,
rememberMe = false,
}: {
login: string;
password?: string;
totp?: string;
rememberMe: boolean;
}): Promise<OAuthResponse> {
return request.post(
'/api/authentication/login',
{
login,
password,
totp,
rememberMe,
},
{ token: null },
);
}
/**
* @param {string} token - an optional token to overwrite headers
* in middleware and disable token auto-refresh
*
* @returns {Promise}
*/
export function logout(token?: string): Promise<{ success: boolean }> {
return request.post(
'/api/authentication/logout',
{},
{
token,
},
);
}
export function forgotPassword(
login: string,
captcha: string,
): Promise<{
success: boolean;
data: {
canRepeatIn: number;
emailMask: string | void;
repeatFrequency: number;
};
errors: {
[key: string]: string;
};
}> {
return request.post(
'/api/authentication/forgot-password',
{
login,
captcha,
},
{ token: null },
);
}
export function recoverPassword(
key: string,
newPassword: string,
newRePassword: string,
): Promise<OAuthResponse> {
return request.post(
'/api/authentication/recover-password',
{
key,
newPassword,
newRePassword,
},
{ token: null },
);
}
/**
* Resolves if token is valid
*
* @param {number} id
* @param {string} token
* @param {string} refreshToken
*
* @returns {Promise} - resolves with options.token or with a new token
* if it was refreshed. As a side effect the response
* will have a `user` field with current user data
*
*/
export async function validateToken(
id: number,
token: string,
refreshToken: string | void | null,
): Promise<{
token: string;
refreshToken: string | null;
user: UserResponse;
}> {
if (typeof token !== 'string') {
throw new Error('token must be a string');
}
refreshToken = refreshToken || null;
let user: UserResponse;
try {
user = await getInfoEndpoint(id, token);
} catch (resp) {
token = await handleTokenError(resp, refreshToken);
user = await getInfoEndpoint(id, token); // TODO: replace with recursive call
}
return {
token,
refreshToken,
user,
};
}
const recoverableErrors = [
'Token expired',
'Incorrect token',
'You are requesting with an invalid credential.',
];
function handleTokenError(
resp: Error | { message: string },
refreshToken: string | null,
): Promise<string> {
if (resp instanceof InternalServerError) {
// delegate error recovering to the bsod middleware
return new Promise(() => {});
}
if (refreshToken) {
if (recoverableErrors.includes(resp.message)) {
return requestToken(refreshToken);
}
logger.error('Unexpected error during token validation', { resp });
}
return Promise.reject(resp);
}
/**
* Request new access token using a refreshToken
*
* @param {string} refreshToken
*
* @returns {Promise} - resolves to token
*/
export async function requestToken(refreshToken: string): Promise<string> {
try {
const response: OAuthResponse = await request.post(
'/api/authentication/refresh-token',
{
// eslint-disable-next-line @typescript-eslint/camelcase
refresh_token: refreshToken,
},
{
token: null,
},
);
return response.access_token;
} catch (resp) {
const errors = resp.errors || {};
if (errors.refresh_token !== 'error.refresh_token_not_exist') {
logger.error('Failed refreshing token: unknown error', {
resp,
});
}
throw resp;
}
}