Skip to content

Commit

Permalink
Merge branch 'master' into fix/AG-27144
Browse files Browse the repository at this point in the history
  • Loading branch information
slavaleleka committed Jan 25, 2024
2 parents b997b71 + 7dfce2b commit 8a5e5c1
Show file tree
Hide file tree
Showing 42 changed files with 724 additions and 261 deletions.
2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "adguard-vpn",
"version": "2.1.11",
"version": "2.2.1",
"description": "AdGuard VPN",
"author": "adguard@adguard.com",
"license": "LGPL-3.0",
Expand Down
27 changes: 21 additions & 6 deletions src/_locales/en/messages.json
Expand Up @@ -51,7 +51,7 @@
"message": "An error occurred, try again later"
},
"global_error_message": {
"message": "An error occurred, please contact us via <a>support@adguard-vpn.com</a>"
"message": "An error occurred, please contact us via <a>%support_email%</a>"
},
"about_title": {
"message": "About"
Expand Down Expand Up @@ -939,19 +939,34 @@
"message": "Confirm your email"
},
"confirm_email_info": {
"message": "To get free VPN traffic, please confirm your email address. We’ve sent a confirmation link to"
"message": "We’ve sent a confirmation code to <span>%email%</span>"
},
"confirm_email_done_title": {
"message": "Email address confirmed"
},
"confirm_email_done_info": {
"message": "Thanks for verifying it!"
},
"confirm_email_close_button": {
"message": "Close"
"confirm_email_code_label": {
"message": "Confirmation code"
},
"confirm_email_resend_link_button": {
"message": "Resend link"
"confirm_email_code_placeholder": {
"message": "Contains numbers and letters"
},
"confirm_email_code_invalid": {
"message": "Please check the code — it looks incorrect"
},
"confirm_email_resend_code_button_disabled": {
"message": "Send another code (%count%)"
},
"confirm_email_resend_code_button": {
"message": "Send another code"
},
"confirm_email_no_auth_id_error_on_auth": {
"message": "Failed to log you in. Please try again or contact us via <a>%support_email%</a>"
},
"confirm_email_no_auth_id_error_on_register": {
"message": "Failed to sign you up. Please try again or contact us via <a>%support_email%</a>"
},
"resend_confirm_registration_link_notification": {
"message": "Confirmation link resent"
Expand Down
14 changes: 13 additions & 1 deletion src/background/api/Api.ts
Expand Up @@ -4,6 +4,8 @@ import { notifier } from '../../lib/notifier';

const REQUEST_TIMEOUT_MS = 1000 * 6; // 6 seconds

const HTTP_RESPONSE_STATUS_OK = 200;

interface ConfigInterface {
params?: {
[key: string]: string;
Expand Down Expand Up @@ -92,7 +94,17 @@ export class Api implements ApiInterface {
return response;
}

const responseData = await response.json();
let responseData;
try {
responseData = await response.json();
} catch (e) {
// server response may be empty,
// e.g. 'api/2.0/resend_confirmation_code' response is 200 but may be empty,
// that's why response.json() can throw an error
if (response.status !== HTTP_RESPONSE_STATUS_OK) {
throw new CustomError(response.status, JSON.stringify(e));
}
}
return responseData;
} catch (e) {
if (e instanceof CustomError) {
Expand Down
5 changes: 5 additions & 0 deletions src/background/api/apiTypes.ts
Expand Up @@ -8,6 +8,11 @@ export type AuthCredentials = {
locale: string;
clientId: string;
appId: string;

/**
* Optional email confirmation code.
*/
code?: string;
};

export type RequestProps = {
Expand Down
27 changes: 26 additions & 1 deletion src/background/api/authApi.ts
@@ -1,4 +1,5 @@
import { AUTH_CLIENT_ID } from '../config';
import { appStatus } from '../appStatus';

import { Api } from './Api';
import { fallbackApi } from './fallbackApi';
Expand All @@ -11,7 +12,12 @@ class AuthApi extends Api {
GET_TOKEN: RequestProps = { path: 'oauth/token', method: 'POST' };

getAccessToken(credentials: AuthCredentials) {
const { username, password, twoFactor } = credentials;
const {
username,
password,
twoFactor,
code,
} = credentials;
const { path, method } = this.GET_TOKEN;

type Data = {
Expand All @@ -20,7 +26,9 @@ class AuthApi extends Api {
scope: string;
grant_type: string;
client_id: string;
app_version: string;
'2fa_token'?: string;
code?: string;
};

const params: Data = {
Expand All @@ -29,12 +37,17 @@ class AuthApi extends Api {
scope: 'trust',
grant_type: 'password_2fa',
client_id: AUTH_CLIENT_ID,
app_version: appStatus.version,
};

if (twoFactor) {
params['2fa_token'] = twoFactor;
}

if (code) {
params.code = code;
}

return this.makeRequest(path, { params }, method);
}

Expand Down Expand Up @@ -75,6 +88,18 @@ class AuthApi extends Api {
};
return this.makeRequest(path, { params }, method);
}

RESEND_CONFIRMATION_CODE: RequestProps = { path: 'api/2.0/resend_confirmation_code', method: 'POST' };

resendCode(authId: string) {
const { path, method } = this.RESEND_CONFIRMATION_CODE;

const params = {
auth_id: authId,
};

return this.makeRequest(path, { params }, method);
}
}

export const authApi = new AuthApi(async () => {
Expand Down
33 changes: 28 additions & 5 deletions src/background/auth/auth.ts
Expand Up @@ -8,6 +8,7 @@ import { notifications } from '../notifications';
import { AUTH_CLIENT_ID } from '../config';
import { log } from '../../lib/logger';
import { notifier } from '../../lib/notifier';
import { SUPPORT_EMAIL } from '../../common/constants';
import { translator } from '../../common/translator';
import { fallbackApi } from '../api/fallbackApi';
// eslint-disable-next-line import/no-cycle
Expand Down Expand Up @@ -38,6 +39,7 @@ export interface AuthInterface {
email: string,
appId: string,
): Promise<{ canRegister: string } | { error: string }>;
resendEmailConfirmationCode(authId: string): Promise<void>;
getAccessToken(turnOffProxy?: boolean): Promise<string>;
init(): Promise<void>;
}
Expand Down Expand Up @@ -254,7 +256,7 @@ class Auth implements AuthInterface {

async register(
credentials: AuthCredentials,
): Promise<{ status: string } | { error: string, field?: string }> {
): Promise<{ status: string } | { error: string, field?: string, status?: string, authId?: string }> {
const locale = navigator.language;
let accessToken;
try {
Expand All @@ -264,8 +266,18 @@ class Auth implements AuthInterface {
clientId: AUTH_CLIENT_ID,
});
} catch (e) {
const { error, field } = JSON.parse(e.message);
return { error, field };
const {
error,
field,
status,
authId,
} = JSON.parse(e.message);
return {
error,
field,
status,
authId,
};
}

if (accessToken) {
Expand All @@ -276,7 +288,8 @@ class Auth implements AuthInterface {

return {
error: translator.getMessage('global_error_message', {
a: (chunks: string) => `<a href="mailto:support@adguard-vpn.com" target="_blank">${chunks}</a>`,
support_email: SUPPORT_EMAIL,
a: (chunks: string) => `<a href="mailto:${SUPPORT_EMAIL}" target="_blank">${chunks}</a>`,
}),
};
}
Expand All @@ -297,13 +310,23 @@ class Auth implements AuthInterface {
log.error(e.message);
return {
error: translator.getMessage('global_error_message', {
a: (chunks: string) => `<a href="mailto:support@adguard-vpn.com" target="_blank">${chunks}</a>`,
support_email: SUPPORT_EMAIL,
a: (chunks: string) => `<a href="mailto:${SUPPORT_EMAIL}" target="_blank">${chunks}</a>`,
}),
};
}
return response;
}

/**
* Uses {@link authProvider} to request a new email confirmation code.
*
* @param authId Auth id received from the server previously.
*/
async resendEmailConfirmationCode(authId: string): Promise<void> {
await authProvider.resendEmailConfirmationCode(authId);
}

async setAccessToken(accessToken: AuthAccessToken): Promise<void> {
this.accessTokenData = accessToken;
await authService.saveAccessTokenData(accessToken);
Expand Down
1 change: 1 addition & 0 deletions src/background/authentication/authCache.ts
Expand Up @@ -21,6 +21,7 @@ const AuthCache = (): AuthCacheInterface => {
helpUsImprove: null,
marketingConsent: null,
authError: null,
code: '',
};

let authCache = { ...DEFAULTS };
Expand Down
122 changes: 122 additions & 0 deletions src/background/emailConfirmationService/emailConfirmationService.ts
@@ -0,0 +1,122 @@
import { BrowserApi, browserApi } from '../browserApi';
import { RESEND_EMAIL_CONFIRMATION_CODE_DELAY_SEC } from '../../common/constants';

/**
* Service for email confirmation.
*
* It is used to store confirmation authId and resend code count down.
*/
class EmailConfirmationService {
browserApi: BrowserApi;

/**
* Email for which count down is started.
*
* Needed to check if resend code count down should be restarted on popup navigation,
* i.e. if user's email should be confirmed but user navigates back and changes the password —
* count down should not be restarted in this case.
*/
private email: string | null;

/**
* Value of 'auth_id' field from server response of registration or authorization request
* if user should confirm email.
*/
private confirmationAuthId: string | null;

private COUNTDOWN_START_KEY = 'resend.code.countdown.start.time';

/**
* Timestamp in milliseconds on which count down was started last time.
*/
private countdownStartMs: number | null;

constructor(providedBrowserApi: BrowserApi) {
this.browserApi = providedBrowserApi;
this.confirmationAuthId = null;
}

/**
* Returns resend code count down start timestamp in MILLISECONDS.
*
* If there is no value for {@link countdownStartMs}, it will be fetched from storage.
*
* @returns Timestamp in milliseconds on which count down was started last time
* or null if count down was not started.
*/
getResentCodeCountdownStartFromStorage = async (): Promise<number | null> => {
if (this.countdownStartMs) {
return this.countdownStartMs;
}
this.countdownStartMs = await this.browserApi.storage.get(this.COUNTDOWN_START_KEY) || null;
return this.countdownStartMs;
};

/**
* Sets resend code count down start timestamp in MILLISECONDS in storage.
*
* @param countdownStart Timestamp in milliseconds on which count down was started last time.
*/
setResentCodeCountdownStartToStorage = async (countdownStart: number | null): Promise<void> => {
this.countdownStartMs = countdownStart;
await this.browserApi.storage.set(this.COUNTDOWN_START_KEY, countdownStart);
};

/**
* Resets count down and starts it again if the timer has not been started yet.
*
* @param email User email for which count down should be restarted.
* If provided and is the same as previous, count down is not restarted.
* If not provided, count down is restarted anyway.
*/
public restartCountdown(email?: string): void {
// if email is provided, check if it is the same as previous
if (typeof email !== 'undefined') {
if (this.email === email) {
// do not restart count down if email is the same
return;
}
// update email value
this.email = email;
}

this.setResentCodeCountdownStartToStorage(Date.now());
}

/**
* Returns number of SECONDS left until user can request another code.
*
* @returns Number of seconds left or null if count down was not started or already finished.
*/
public async getCodeCountdown(): Promise<number | null> {
const countdownStartMs = await this.getResentCodeCountdownStartFromStorage();
if (!countdownStartMs) {
return null;
}

// eslint-disable-next-line max-len
const countdownSec = RESEND_EMAIL_CONFIRMATION_CODE_DELAY_SEC - Math.round((Date.now() - countdownStartMs) / 1000);
return countdownSec > 0
? countdownSec
: null;
}

/**
* Sets authId which is needed for email confirmation,
* i.e. for requesting another code.
*
* @param authId AuthId.
*/
public setAuthId(authId: string) {
this.confirmationAuthId = authId;
}

/**
* Returns authId for confirmation.
*/
public get authId(): string | null {
return this.confirmationAuthId;
}
}

export const emailConfirmationService = new EmailConfirmationService(browserApi);
1 change: 1 addition & 0 deletions src/background/emailConfirmationService/index.ts
@@ -0,0 +1 @@
export { emailConfirmationService } from './emailConfirmationService';

0 comments on commit 8a5e5c1

Please sign in to comment.