Skip to content

Commit

Permalink
Merge pull request #13 from behzodfaiziev/11-auth-improvement
Browse files Browse the repository at this point in the history
11 auth improvement
  • Loading branch information
behzodfaiziev authored Nov 25, 2024
2 parents 5cb8c24 + 33eb7d5 commit d3d592a
Show file tree
Hide file tree
Showing 13 changed files with 235 additions and 184 deletions.
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
# Changelog

## [0.8.0]

> Note: This version has breaking changes.
- `setAccessToken`, `setRefreshToken`, `clearTokens` methods are removed, since the tokens should be
managed by backend's http only cookies.
- `accessToken` and `refreshToken` properties are removed, since the tokens should be managed by
backend's http only cookies.
- `isClientSideWeb` method is removed, since it is not necessary anymore.
- added `refreshTokenPath` for the path of the refresh token cookie

## [0.7.0]

- added `withCredentials` and `cancelToken` as optional parameters in constructor
- exported

## [0.6.2]

- updated github actions to use strict check on formatting
- updated gitHub actions to use strict check on formatting
- updated README.md

## [0.6.1]
Expand Down
135 changes: 75 additions & 60 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,21 @@ development (TDD) by making network interactions mockable and testable.
## Table of Contents

- [Features](#features)
- [Change-log](#change-log)
- [Changelog](#changelog)
- [Installation](#installation)
- [Usage](#usage)
- [Setting Up the NetworkManager](#setting-up-the-networkmanager)
- [Token Management](#token-management)
- [Making Requests](#making-requests)
- [Request](#request)
- [RequestList](#requestlist)
- [RequestVoid](#requestvoid)
- [Refresh Token](#refresh-token)
- [How to Configure Token Refresh](#how-to-configure-token-refresh)
- [How It Works](#how-it-works)
- [Making Requests according to the Clean Architecture](#making-requests-according-to-the-clean-architecture)
- [Error Handling with ApiException](#error-handling-with-apiexception-according-to-the-clean-architecture)

[//]: # " - [Error Handling with ApiException](#error-handling-with-apiexception-according-to-the-clean-architecture)"

- [Integration with Inversify for Dependency Injection](#integration-with-inversify-for-dependency-injection)
- [Container Module Setup](#container-module-setup)
- [Merging Containers](#merging-containers)
Expand All @@ -31,9 +35,10 @@ development (TDD) by making network interactions mockable and testable.
- **Axios Integration**: Built on top of Axios for flexible HTTP requests.
- **Dependency Injection**: Supports `Inversify` for clean and testable architecture.
- **Error Handling**: Customizable error handling using the `ApiException` class.
- **Token Management**: Handles access and refresh tokens, stored in `localStorage`.
- **Clean Architecture**: Easily integrate with Clean Architecture principles.
- **Refresh Token Support**: Automatically refreshes the access token when it expires.

## Change-log
## Changelog

You can find the changelog [here](CHANGELOG.md).

Expand Down Expand Up @@ -74,23 +79,11 @@ const networkManagerInstance = new NetworkManager({
testMode: isTestMode, // Test mode: false (production), true (development)
baseOptions: {}, // Axios config options
errorParams: networkErrorParams, // Error parameters
isClientSideWeb: typeof window !== "undefined" && typeof localStorage !== "undefined",
withCredentials: true,
refreshTokenPath: "api/auth/refresh-token",
});
```

### Token Management

You can manage access tokens and refresh tokens using `setAccessToken` and `setRefreshToken`.
These tokens are automatically stored in `localStorage`and are automatically used in headers for
future requests.

```typescript
// Set access token
networkManager.setAccessToken("your-access-token");
// Set refresh token
networkManager.setRefreshToken("your-refresh-token");
```

## Making Requests:

### Request:
Expand Down Expand Up @@ -142,6 +135,30 @@ await networkManager.requestVoid({
});
```

## Refresh Token

The NetworkManager automatically handles token refresh when an access token expires. You only need
to provide the API endpoint where the refresh token request is made. Once the access token expires,
the manager will automatically request a new one and retry the failed request with the new token.

### How to Configure Token Refresh

```typescript
const networkManagerInstance = new NetworkManager({
// Other options (e.g., baseUrl, etc.)
refreshTokenPath: "api/auth/refresh-token", // Path to the backend refresh token API
});
```

### How It Works

- **Token Expiry Detection**: When a request returns a 401 Unauthorized error due to an expired
token, NetworkManager detects this and triggers the refresh process.
- **Token Refresh Request**: It sends a request to the provided refreshTokenPath to obtain a new
access token.
- **Retrying Failed Requests**: Once the token is refreshed, it automatically retries the original
failed request with the new token.

## Making Requests according to the Clean Architecture

Using the Clean Architecture, you can create a `RemoteDataSource` class that implements an
Expand All @@ -159,14 +176,11 @@ export class AuthRemoteDataSource implements IAuthRemoteDataSource {
constructor(@inject("INetworkManager") private networkManager: INetworkManager) {}

async signIn(dto: SignInDto): Promise<SignInResponseDto> {
const result = await this.networkManager.request<SignInResponseDto>({
return await this.networkManager.request<SignInResponseDto>({
method: RequestMethod.POST,
url: `/api/auth/sign-in`,
data: dto,
});

this.networkManager.setAccessToken(result.accesToken);
return result;
}
}
```
Expand Down Expand Up @@ -194,43 +208,44 @@ export class AuthRepository implements IAuthRepository {
}
```

## Error Handling with ApiException according to the Clean Architecture

All errors returned by the network manager will be transformed into `ApiException` instances,
providing consistent error-handling across your app. Which are caught with a try-catch block.

```typescript
/// AuthController.ts
@injectable()
export class AuthController {
constructor(@inject(SignIn) private signInUseCase: SignIn) {}

async handleSignIn(dto: SignInDto): Promise<void> {
try {
return await this.signInUseCase.execute(dto);
} catch (error) {
throw error;
}
}
}

/// sign-in.tsx
/// ... other codes
const signInController = container.get<AuthController>(AuthController);

const handleSignIn = async () => {
try {
const dto: SignInDto = { email, password };
setLoading(true);
await signInController.handleSignIn(dto);
router.push("/");
} catch (err) {
setLoading(false);
setError((err as ApiException).message);
}
};
/// ... other codes
```
[//]: #
[//]: # "## Error Handling with ApiException according to the Clean Architecture"
[//]: #
[//]: # "All errors returned by the network manager will be transformed into `ApiException` instances,"
[//]: # "providing consistent error-handling across your app. Which are caught with a try-catch block."
[//]: #
[//]: # "```typescript"
[//]: # "/// AuthController.ts"
[//]: # "@injectable()"
[//]: # "export class AuthController {"
[//]: # " constructor(@inject(SignIn) private signInUseCase: SignIn) {}"
[//]: #
[//]: # " async handleSignIn(dto: SignInDto): Promise<void> {"
[//]: # " try {"
[//]: # " return await this.signInUseCase.execute(dto);"
[//]: # " } catch (error) {"
[//]: # " throw error;"
[//]: # " }"
[//]: # " }"
[//]: # "}"
[//]: #
[//]: # "/// sign-in.tsx"
[//]: # "/// ... other codes"
[//]: # "const signInController = container.get<AuthController>(AuthController);"
[//]: #
[//]: # "const handleSignIn = async () => {"
[//]: # " try {"
[//]: # " const dto: SignInDto = { email, password };"
[//]: # " setLoading(true);"
[//]: # " await signInController.handleSignIn(dto);"
[//]: # ' router.push("/");'
[//]: # " } catch (err) {"
[//]: # " setLoading(false);"
[//]: # " setError((err as ApiException).message);"
[//]: # " }"
[//]: # "};"
[//]: # "/// ... other codes"
[//]: # "```"

## Integration with Inversify for Dependency Injection

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "next-netkit",
"version": "0.7.0",
"version": "0.8.0",
"description": "Network manager",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
Expand All @@ -11,7 +11,7 @@
"build:esm": "tsc --project tsconfig.esm.json",
"format": "prettier --write \"src/**/*.ts\" \"tests/**/*.ts\"",
"lint": "eslint \"/**/*.ts\" --fix",
"test": "rm -rf dist && npm install && npm run format && npm run lint && npm run build && jest",
"test": "rm -rf dist && npm install && npm run format && npm run lint && npx prettier --check --write . && npm run build && jest",
"distCheck": "[ -d dist ] || { echo 'dist folder does not exist'; exit 1; }",
"testPublish": "npm run test && npm run distCheck && npm publish",
"testPublish:dev": "npm run test && npm run distCheck && npm publish --tag dev"
Expand Down
4 changes: 2 additions & 2 deletions scripts/scripts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
npm run format && npm run lint && npx prettier --check --write .

# Publish to npm with test
npm run publishTest
npm run testPublish

# Publish dev version to npm
npm run publish:dev
npm run testPublish:dev
65 changes: 65 additions & 0 deletions src/interceptors/error-handling.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import axios, { AxiosError, AxiosResponse, HttpStatusCode } from "axios";
import { RequestQueue } from "../services/request-queue.service";

export class ErrorHandlingInterceptor {
private isRefreshing = false;

constructor(
private requestQueue: RequestQueue,
private baseUrl: string,
private refreshTokenPath?: string
) {}

getInterceptor() {
return {
onResponseError: async (error: AxiosError): Promise<AxiosResponse | void> => {
/// Throw error if there is no refresh token path
if (!this.refreshTokenPath) {
throw error;
}

/// Reject all queued requests if the error is a 401 sent from the refresh token path
if (error.response?.status === 401 && error.config?.url === this.refreshTokenPath) {
// Reject all queued requests
this.requestQueue.cancelAll("Token refresh failed");
throw error;
}
/// Handle 401 errors by refreshing the token
if (error.response?.status === 401) {
if (this.isRefreshing && error.config) {
// Queue request while token is being refreshed
return this.requestQueue.enqueue(error.config);
}

this.isRefreshing = true;

try {
// Send token refresh request
const result = await axios.post(`${this.baseUrl}/${this.refreshTokenPath}`, {}, { withCredentials: true });
if (result.status >= HttpStatusCode.MultipleChoices) {
throw new Error("Token refresh failed");
}

this.isRefreshing = false;
// Retry all queued requests after successful refresh
await this.requestQueue.processQueue();
if (error.config) {
return axios.request(error.config); // Retry the original request
}
} catch (refreshError) {
this.isRefreshing = false;
// Reject all queued requests due to failed refresh
if (refreshError instanceof Error) {
this.requestQueue.cancelAll(refreshError.message);
} else {
this.requestQueue.cancelAll("Token refresh failed");
}
throw refreshError;
}
}
// If it's not a 401 error or refresh token path, pass it along
throw error;
},
};
}
}
8 changes: 0 additions & 8 deletions src/interfaces/network.config.ts

This file was deleted.

6 changes: 0 additions & 6 deletions src/network-manager.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,4 @@ export interface INetworkManager {
method: RequestMethod;
data?: any;
}): Promise<void>;

setAccessToken(token: string): void;

setRefreshToken(token: string): void;

clearTokens(): void;
}
Loading

0 comments on commit d3d592a

Please sign in to comment.