web/satellite: unauthorized error (401) interception for http requests

Implemented interception for http requests.
We redirect user to login page on every 401 response.

Issue:
https://github.com/storj/storj/issues/5339

Change-Id: Icba4fc0031cb2b4e682a1be078cdcf95b7fa6bfe
This commit is contained in:
Vitalii 2022-12-15 14:52:28 +02:00 committed by Storj Robot
parent e598c2b3b1
commit 2ebdc5ff2f
13 changed files with 114 additions and 199 deletions

View File

@ -932,7 +932,7 @@ func (a *Auth) serveJSONError(w http.ResponseWriter, err error) {
// getStatusCode returns http.StatusCode depends on console error class. // getStatusCode returns http.StatusCode depends on console error class.
func (a *Auth) getStatusCode(err error) int { func (a *Auth) getStatusCode(err error) int {
switch { switch {
case console.ErrValidation.Has(err), console.ErrCaptcha.Has(err), console.ErrMFAMissing.Has(err): case console.ErrValidation.Has(err), console.ErrCaptcha.Has(err), console.ErrMFAMissing.Has(err), console.ErrMFAPasscode.Has(err), console.ErrMFARecoveryCode.Has(err), console.ErrChangePassword.Has(err):
return http.StatusBadRequest return http.StatusBadRequest
case console.ErrUnauthorized.Has(err), console.ErrTokenExpiration.Has(err), console.ErrRecoveryToken.Has(err), console.ErrLoginCredentials.Has(err): case console.ErrUnauthorized.Has(err), console.ErrTokenExpiration.Has(err), console.ErrRecoveryToken.Has(err), console.ErrLoginCredentials.Has(err):
return http.StatusUnauthorized return http.StatusUnauthorized
@ -940,8 +940,6 @@ func (a *Auth) getStatusCode(err error) int {
return http.StatusConflict return http.StatusConflict
case errors.Is(err, errNotImplemented): case errors.Is(err, errNotImplemented):
return http.StatusNotImplemented return http.StatusNotImplemented
case console.ErrMFAPasscode.Has(err), console.ErrMFARecoveryCode.Has(err):
return http.StatusBadRequest
default: default:
return http.StatusInternalServerError return http.StatusInternalServerError
} }
@ -966,12 +964,12 @@ func (a *Auth) getUserErrorMessage(err error) string {
case console.ErrMFAConflict.Has(err): case console.ErrMFAConflict.Has(err):
return "Expected either passcode or recovery code, but got both" return "Expected either passcode or recovery code, but got both"
case console.ErrMFAPasscode.Has(err): case console.ErrMFAPasscode.Has(err):
return "The MFA passcode is not valid or has expired. You have just used up one of your login attempts" return "The MFA passcode is not valid or has expired"
case console.ErrMFARecoveryCode.Has(err): case console.ErrMFARecoveryCode.Has(err):
return "The MFA recovery code is not valid or has been previously used. You have just used up one of your login attempts" return "The MFA recovery code is not valid or has been previously used"
case console.ErrLoginCredentials.Has(err): case console.ErrLoginCredentials.Has(err):
return "Your login credentials are incorrect, please try again" return "Your login credentials are incorrect, please try again"
case console.ErrValidation.Has(err): case console.ErrValidation.Has(err), console.ErrChangePassword.Has(err):
return err.Error() return err.Error()
case errors.Is(err, errNotImplemented): case errors.Is(err, errNotImplemented):
return "The server is incapable of fulfilling the request" return "The server is incapable of fulfilling the request"

View File

@ -57,7 +57,7 @@ func TestAuth(t *testing.T) {
"newPassword": user.password + "2", "newPassword": user.password + "2",
})) }))
require.Equal(t, http.StatusUnauthorized, resp.StatusCode) require.Equal(t, http.StatusBadRequest, resp.StatusCode)
_ = body _ = body
//TODO: require.Contains(t, body, "password was incorrect") //TODO: require.Contains(t, body, "password was incorrect")
} }

View File

@ -136,7 +136,7 @@ func (s *Service) DisableUserMFA(ctx context.Context, passcode string, t time.Ti
} }
} }
if !found { if !found {
return ErrUnauthorized.Wrap(ErrMFARecoveryCode.New(mfaRecoveryInvalidErrMsg)) return ErrMFARecoveryCode.New(mfaRecoveryInvalidErrMsg)
} }
} else if passcode != "" { } else if passcode != "" {
valid, err := ValidateMFAPasscode(passcode, user.MFASecretKey, t) valid, err := ValidateMFAPasscode(passcode, user.MFASecretKey, t)

View File

@ -57,6 +57,7 @@ const (
emailNotFoundErrMsg = "There are no users with the specified email" emailNotFoundErrMsg = "There are no users with the specified email"
passwordRecoveryTokenIsExpiredErrMsg = "Your password recovery link has expired, please request another one" passwordRecoveryTokenIsExpiredErrMsg = "Your password recovery link has expired, please request another one"
credentialsErrMsg = "Your login credentials are incorrect, please try again" credentialsErrMsg = "Your login credentials are incorrect, please try again"
changePasswordErrMsg = "Your old password is incorrect, please try again"
passwordTooShortErrMsg = "Your password needs to be at least %d characters long" passwordTooShortErrMsg = "Your password needs to be at least %d characters long"
passwordTooLongErrMsg = "Your password must be no longer than %d characters" passwordTooLongErrMsg = "Your password must be no longer than %d characters"
projectOwnerDeletionForbiddenErrMsg = "%s is a project owner and can not be deleted" projectOwnerDeletionForbiddenErrMsg = "%s is a project owner and can not be deleted"
@ -94,6 +95,9 @@ var (
// ErrLoginCredentials occurs when provided invalid login credentials. // ErrLoginCredentials occurs when provided invalid login credentials.
ErrLoginCredentials = errs.Class("login credentials") ErrLoginCredentials = errs.Class("login credentials")
// ErrChangePassword occurs when provided old password is incorrect.
ErrChangePassword = errs.Class("change password")
// ErrEmailUsed is error type that occurs on repeating auth attempts with email. // ErrEmailUsed is error type that occurs on repeating auth attempts with email.
ErrEmailUsed = errs.Class("email used") ErrEmailUsed = errs.Class("email used")
@ -1306,7 +1310,7 @@ func (s *Service) ChangePassword(ctx context.Context, pass, newPass string) (err
err = bcrypt.CompareHashAndPassword(user.PasswordHash, []byte(pass)) err = bcrypt.CompareHashAndPassword(user.PasswordHash, []byte(pass))
if err != nil { if err != nil {
return ErrUnauthorized.New(credentialsErrMsg) return ErrChangePassword.New(changePasswordErrMsg)
} }
if err := ValidatePassword(newPass); err != nil { if err := ValidatePassword(newPass); err != nil {

View File

@ -1,7 +1,6 @@
// Copyright (C) 2022 Storj Labs, Inc. // Copyright (C) 2022 Storj Labs, Inc.
// See LICENSE for copying information. // See LICENSE for copying information.
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
import { HttpClient } from '@/utils/httpClient'; import { HttpClient } from '@/utils/httpClient';
import { ABHitAction, ABTestApi, ABTestValues } from '@/types/abtesting'; import { ABHitAction, ABTestApi, ABTestValues } from '@/types/abtesting';
@ -29,10 +28,6 @@ export class ABHttpApi implements ABTestApi {
); );
} }
if (response.status === 401) {
throw new ErrorUnauthorized();
}
// use default values on error // use default values on error
return new ABTestValues(); return new ABTestValues();
} }

View File

@ -2,7 +2,6 @@
// See LICENSE for copying information. // See LICENSE for copying information.
import { BaseGql } from '@/api/baseGql'; import { BaseGql } from '@/api/baseGql';
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
import { import {
AccessGrant, AccessGrant,
AccessGrantCursor, AccessGrantCursor,
@ -145,10 +144,6 @@ export class AccessGrantsApiGql extends BaseGql implements AccessGrantsApi {
return; return;
} }
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('can not delete access grant'); throw new Error('can not delete access grant');
} }

View File

@ -4,7 +4,6 @@
import { ErrorBadRequest } from '@/api/errors/ErrorBadRequest'; import { ErrorBadRequest } from '@/api/errors/ErrorBadRequest';
import { ErrorMFARequired } from '@/api/errors/ErrorMFARequired'; import { ErrorMFARequired } from '@/api/errors/ErrorMFARequired';
import { ErrorTooManyRequests } from '@/api/errors/ErrorTooManyRequests'; import { ErrorTooManyRequests } from '@/api/errors/ErrorTooManyRequests';
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
import { TokenInfo, UpdatedUser, User, UsersApi } from '@/types/users'; import { TokenInfo, UpdatedUser, User, UsersApi } from '@/types/users';
import { HttpClient } from '@/utils/httpClient'; import { HttpClient } from '@/utils/httpClient';
import { ErrorTokenExpired } from '@/api/errors/ErrorTokenExpired'; import { ErrorTokenExpired } from '@/api/errors/ErrorTokenExpired';
@ -75,8 +74,6 @@ export class AuthHttpApi implements UsersApi {
switch (response.status) { switch (response.status) {
case 400: case 400:
throw new ErrorBadRequest(errMsg); throw new ErrorBadRequest(errMsg);
case 401:
throw new ErrorUnauthorized(errMsg);
case 429: case 429:
throw new ErrorTooManyRequests(errMsg); throw new ErrorTooManyRequests(errMsg);
default: default:
@ -97,10 +94,6 @@ export class AuthHttpApi implements UsersApi {
return; return;
} }
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('Can not logout. Please try again later'); throw new Error('Can not logout. Please try again later');
} }
@ -125,8 +118,6 @@ export class AuthHttpApi implements UsersApi {
const result = await response.json(); const result = await response.json();
const errMsg = result.error || 'Failed to send password reset link'; const errMsg = result.error || 'Failed to send password reset link';
switch (response.status) { switch (response.status) {
case 404:
throw new ErrorUnauthorized(errMsg);
case 429: case 429:
throw new ErrorTooManyRequests(errMsg); throw new ErrorTooManyRequests(errMsg);
default: default:
@ -185,10 +176,6 @@ export class AuthHttpApi implements UsersApi {
); );
} }
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('can not get user data'); throw new Error('can not get user data');
} }
@ -210,14 +197,8 @@ export class AuthHttpApi implements UsersApi {
return; return;
} }
switch (response.status) { const result = await response.json();
case 401: { throw new Error(result.error);
throw new Error('old password is incorrect, please try again');
}
default: {
throw new Error('can not change password');
}
}
} }
/** /**
@ -236,10 +217,6 @@ export class AuthHttpApi implements UsersApi {
return; return;
} }
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('can not delete user'); throw new Error('can not delete user');
} }
@ -279,8 +256,6 @@ export class AuthHttpApi implements UsersApi {
switch (response.status) { switch (response.status) {
case 400: case 400:
throw new ErrorBadRequest(errMsg); throw new ErrorBadRequest(errMsg);
case 401:
throw new ErrorUnauthorized(errMsg);
case 429: case 429:
throw new ErrorTooManyRequests(errMsg); throw new ErrorTooManyRequests(errMsg);
default: default:
@ -302,10 +277,6 @@ export class AuthHttpApi implements UsersApi {
return await response.json(); return await response.json();
} }
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('Can not generate MFA secret. Please try again later'); throw new Error('Can not generate MFA secret. Please try again later');
} }
@ -326,10 +297,6 @@ export class AuthHttpApi implements UsersApi {
return; return;
} }
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('Can not enable MFA. Please try again later'); throw new Error('Can not enable MFA. Please try again later');
} }
@ -352,16 +319,9 @@ export class AuthHttpApi implements UsersApi {
} }
const result = await response.json(); const result = await response.json();
if (!response.ok) {
const errMsg = result.error || 'Cannot disable MFA. Please try again later'; const errMsg = result.error || 'Cannot disable MFA. Please try again later';
switch (response.status) {
case 401:
throw new ErrorUnauthorized(errMsg);
default:
throw new Error(errMsg); throw new Error(errMsg);
} }
}
}
/** /**
* Used to generate user's MFA recovery codes. * Used to generate user's MFA recovery codes.
@ -376,10 +336,6 @@ export class AuthHttpApi implements UsersApi {
return await response.json(); return await response.json();
} }
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('Can not generate MFA recovery codes. Please try again later'); throw new Error('Can not generate MFA recovery codes. Please try again later');
} }
@ -426,8 +382,6 @@ export class AuthHttpApi implements UsersApi {
switch (response.status) { switch (response.status) {
case 400: case 400:
throw new ErrorBadRequest(errMsg); throw new ErrorBadRequest(errMsg);
case 401:
throw new ErrorUnauthorized(errMsg);
default: default:
throw new Error(errMsg); throw new Error(errMsg);
} }
@ -447,10 +401,6 @@ export class AuthHttpApi implements UsersApi {
return new Date(await response.json()); return new Date(await response.json());
} }
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('Unable to refresh session.'); throw new Error('Unable to refresh session.');
} }
} }

View File

@ -2,7 +2,6 @@
// See LICENSE for copying information. // See LICENSE for copying information.
import { BaseGql } from '@/api/baseGql'; import { BaseGql } from '@/api/baseGql';
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
import { Bucket, BucketCursor, BucketPage, BucketsApi } from '@/types/buckets'; import { Bucket, BucketCursor, BucketPage, BucketsApi } from '@/types/buckets';
import { HttpClient } from '@/utils/httpClient'; import { HttpClient } from '@/utils/httpClient';
@ -70,10 +69,6 @@ export class BucketsApiGql extends BaseGql implements BucketsApi {
const response = await this.client.get(path); const response = await this.client.get(path);
if (!response.ok) { if (!response.ok) {
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('Can not get bucket names'); throw new Error('Can not get bucket names');
} }

View File

@ -5,7 +5,7 @@
* ErrorUnauthorized is a custom error type for performing unauthorized operations. * ErrorUnauthorized is a custom error type for performing unauthorized operations.
*/ */
export class ErrorUnauthorized extends Error { export class ErrorUnauthorized extends Error {
public constructor(message = 'authorization required') { public constructor(message = 'Authorization required') {
super(message); super(message);
} }
} }

View File

@ -3,7 +3,6 @@
import { ErrorTooManyRequests } from './errors/ErrorTooManyRequests'; import { ErrorTooManyRequests } from './errors/ErrorTooManyRequests';
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
import { import {
AccountBalance, AccountBalance,
Coupon, Coupon,
@ -37,10 +36,6 @@ export class PaymentsHttpApi implements PaymentsApi {
const response = await this.client.get(path); const response = await this.client.get(path);
if (!response.ok) { if (!response.ok) {
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('Can not get account balance'); throw new Error('Can not get account balance');
} }
@ -66,10 +61,6 @@ export class PaymentsHttpApi implements PaymentsApi {
return couponType; return couponType;
} }
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('can not setup account'); throw new Error('can not setup account');
} }
@ -83,10 +74,6 @@ export class PaymentsHttpApi implements PaymentsApi {
const response = await this.client.get(path); const response = await this.client.get(path);
if (!response.ok) { if (!response.ok) {
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('can not get projects charges'); throw new Error('can not get projects charges');
} }
@ -124,10 +111,6 @@ export class PaymentsHttpApi implements PaymentsApi {
return; return;
} }
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('can not add credit card'); throw new Error('can not add credit card');
} }
@ -145,10 +128,6 @@ export class PaymentsHttpApi implements PaymentsApi {
return; return;
} }
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('can not remove credit card'); throw new Error('can not remove credit card');
} }
@ -163,9 +142,6 @@ export class PaymentsHttpApi implements PaymentsApi {
const response = await this.client.get(path); const response = await this.client.get(path);
if (!response.ok) { if (!response.ok) {
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('can not list credit cards'); throw new Error('can not list credit cards');
} }
@ -192,10 +168,6 @@ export class PaymentsHttpApi implements PaymentsApi {
return; return;
} }
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('can not make credit card default'); throw new Error('can not make credit card default');
} }
@ -210,9 +182,6 @@ export class PaymentsHttpApi implements PaymentsApi {
const response = await this.client.get(path); const response = await this.client.get(path);
if (!response.ok) { if (!response.ok) {
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('can not list billing history'); throw new Error('can not list billing history');
} }
@ -248,9 +217,6 @@ export class PaymentsHttpApi implements PaymentsApi {
const response = await this.client.get(path); const response = await this.client.get(path);
if (!response.ok) { if (!response.ok) {
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('Can not list token payment history'); throw new Error('Can not list token payment history');
} }
@ -285,7 +251,15 @@ export class PaymentsHttpApi implements PaymentsApi {
const response = await this.client.patch(path, couponCode); const response = await this.client.patch(path, couponCode);
const errMsg = `Could not apply coupon code "${couponCode}"`; const errMsg = `Could not apply coupon code "${couponCode}"`;
if (response.ok) { if (!response.ok) {
switch (response.status) {
case 429:
throw new ErrorTooManyRequests('You\'ve exceeded limit of attempts, try again in 5 minutes');
default:
throw new Error(errMsg);
}
}
const coupon = await response.json(); const coupon = await response.json();
if (!coupon) { if (!coupon) {
@ -304,16 +278,6 @@ export class PaymentsHttpApi implements PaymentsApi {
); );
} }
switch (response.status) {
case 429:
throw new ErrorTooManyRequests('You\'ve exceeded limit of attempts, try again in 5 minutes');
case 401:
throw new ErrorUnauthorized(errMsg);
default:
throw new Error(errMsg);
}
}
/** /**
* getCoupon returns the coupon applied to the user. * getCoupon returns the coupon applied to the user.
* *
@ -323,10 +287,6 @@ export class PaymentsHttpApi implements PaymentsApi {
const path = `${this.ROOT_PATH}/coupon`; const path = `${this.ROOT_PATH}/coupon`;
const response = await this.client.get(path); const response = await this.client.get(path);
if (!response.ok) { if (!response.ok) {
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('cannot retrieve coupon'); throw new Error('cannot retrieve coupon');
} }
@ -359,10 +319,6 @@ export class PaymentsHttpApi implements PaymentsApi {
const response = await this.client.get(path); const response = await this.client.get(path);
if (!response.ok) { if (!response.ok) {
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('Can not get wallet'); throw new Error('Can not get wallet');
} }
@ -385,10 +341,6 @@ export class PaymentsHttpApi implements PaymentsApi {
const response = await this.client.post(path, null); const response = await this.client.post(path, null);
if (!response.ok) { if (!response.ok) {
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('Can not claim new wallet'); throw new Error('Can not claim new wallet');
} }

View File

@ -2,7 +2,6 @@
// See LICENSE for copying information. // See LICENSE for copying information.
import { BaseGql } from '@/api/baseGql'; import { BaseGql } from '@/api/baseGql';
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
import { import {
DataStamp, DataStamp,
Project, Project,
@ -144,7 +143,10 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
const path = `${this.ROOT_PATH}/${projectId}/usage-limits`; const path = `${this.ROOT_PATH}/${projectId}/usage-limits`;
const response = await this.http.get(path); const response = await this.http.get(path);
if (response.ok) { if (!response.ok) {
throw new Error('can not get usage limits');
}
const limits = await response.json(); const limits = await response.json();
return new ProjectLimits( return new ProjectLimits(
@ -155,13 +157,7 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
limits.objectCount, limits.objectCount,
limits.segmentCount, limits.segmentCount,
); );
}
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('can not get usage limits');
} }
/** /**
@ -173,7 +169,11 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
const path = `${this.ROOT_PATH}/usage-limits`; const path = `${this.ROOT_PATH}/usage-limits`;
const response = await this.http.get(path); const response = await this.http.get(path);
if (response.ok) { if (!response.ok) {
throw new Error('can not get total usage limits');
}
const limits = await response.json(); const limits = await response.json();
return new ProjectLimits( return new ProjectLimits(
@ -184,13 +184,6 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
); );
} }
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('can not get total usage limits');
}
/** /**
* Get project daily usage for specific date range. * Get project daily usage for specific date range.
* *
@ -205,7 +198,11 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
const path = `${this.ROOT_PATH}/${projectId}/daily-usage?from=${since}&to=${before}`; const path = `${this.ROOT_PATH}/${projectId}/daily-usage?from=${since}&to=${before}`;
const response = await this.http.get(path); const response = await this.http.get(path);
if (response.ok) { if (!response.ok) {
throw new Error('Can not get project daily usage');
}
const usage = await response.json(); const usage = await response.json();
return new ProjectsStorageBandwidthDaily( return new ProjectsStorageBandwidthDaily(
@ -227,19 +224,13 @@ export class ProjectsApiGql extends BaseGql implements ProjectsApi {
); );
} }
if (response.status === 401) {
throw new ErrorUnauthorized();
}
throw new Error('Can not get project daily usage');
}
public async getSalt(projectId: string): Promise<string> { public async getSalt(projectId: string): Promise<string> {
const path = `${this.ROOT_PATH}/${projectId}/salt`; const path = `${this.ROOT_PATH}/${projectId}/salt`;
const response = await this.http.get(path); const response = await this.http.get(path);
if (response.ok) { if (response.ok) {
return await response.json(); return await response.json();
} }
throw new Error('Can not get project salt'); throw new Error('Can not get project salt');
} }

View File

@ -43,7 +43,7 @@ const errorLink = onError(({ graphQLErrors, networkError }) => {
Vue.prototype.$notify.error('Session token expired', AnalyticsErrorEventSource.OVERALL_SESSION_EXPIRED_ERROR); Vue.prototype.$notify.error('Session token expired', AnalyticsErrorEventSource.OVERALL_SESSION_EXPIRED_ERROR);
setTimeout(() => { setTimeout(() => {
window.location.href = window.location.origin + '/login'; window.location.href = window.location.origin + '/login';
}, 3000); }, 2000);
} else { } else {
nError.result && Vue.prototype.$notify.error(nError.result.error, AnalyticsErrorEventSource.OVERALL_GRAPHQL_ERROR); nError.result && Vue.prototype.$notify.error(nError.result.error, AnalyticsErrorEventSource.OVERALL_GRAPHQL_ERROR);
} }

View File

@ -1,6 +1,8 @@
// Copyright (C) 2019 Storj Labs, Inc. // Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information. // See LICENSE for copying information.
import { ErrorUnauthorized } from '@/api/errors/ErrorUnauthorized';
/** /**
* HttpClient is a custom wrapper around fetch api. * HttpClient is a custom wrapper around fetch api.
* Exposes get, post and delete methods for JSON strings. * Exposes get, post and delete methods for JSON strings.
@ -22,7 +24,13 @@ export class HttpClient {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}; };
return await fetch(path, request); const response = await fetch(path, request);
if (response.status === 401) {
await this.handleUnauthorized();
throw new ErrorUnauthorized();
}
return response;
} }
/** /**
@ -67,4 +75,31 @@ export class HttpClient {
public async delete(path: string): Promise<Response> { public async delete(path: string): Promise<Response> {
return this.sendJSON('DELETE', path, null); return this.sendJSON('DELETE', path, null);
} }
/**
* Handles unauthorized actions.
* Call logout and redirect to login.
*/
private async handleUnauthorized(): Promise<void> {
try {
const logoutPath = '/api/v0/auth/logout';
const request: RequestInit = {
method: 'POST',
body: null,
};
request.headers = {
'Content-Type': 'application/json',
};
await fetch(logoutPath, request);
// eslint-disable-next-line no-empty
} catch (error) {}
setTimeout(() => {
if (!window.location.href.includes('/login')) {
window.location.href = window.location.origin + '/login';
}
}, 2000);
}
} }