@titus-system/syncdesk 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.editorconfig +53 -0
- package/.github/workflows/notify-main.yml +17 -0
- package/README.md +41 -0
- package/package.json +42 -0
- package/src/api/client.ts +135 -0
- package/src/api/index.ts +2 -0
- package/src/api/typings.ts +3 -0
- package/src/auth/hooks/useAuth.ts +95 -0
- package/src/auth/index.ts +12 -0
- package/src/auth/types/auth.ts +35 -0
- package/src/auth/types/session.ts +12 -0
- package/src/config.ts +31 -0
- package/src/health/hooks/useHealth.ts +48 -0
- package/src/health/index.ts +2 -0
- package/src/health/types/health.ts +10 -0
- package/src/index.ts +10 -0
- package/src/live_chat/hooks/useLiveChat.ts +103 -0
- package/src/live_chat/hooks/useLiveChatWebSocket.ts +86 -0
- package/src/live_chat/index.ts +15 -0
- package/src/live_chat/types/live_chat.ts +48 -0
- package/src/permissions/hooks/usePermissions.ts +141 -0
- package/src/permissions/index.ts +2 -0
- package/src/permissions/types/index.ts +1 -0
- package/src/permissions/types/permission.ts +27 -0
- package/src/roles/hooks/useRoles.ts +103 -0
- package/src/roles/index.ts +2 -0
- package/src/roles/types/index.ts +1 -0
- package/src/roles/types/role.ts +28 -0
- package/src/users/hooks/useUsers.ts +146 -0
- package/src/users/index.ts +22 -0
- package/src/users/types/user.ts +36 -0
- package/tsconfig.json +29 -0
- package/tsup.config.ts +12 -0
package/.editorconfig
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# EditorConfig is awesome: http://EditorConfig.org
|
|
2
|
+
# 1st version comes from a Gist by @avandrevitor [here](https://gist.github.com/avandrevitor/b917770334af3a43cf8c489f93287a84)
|
|
3
|
+
#
|
|
4
|
+
# Extended version is available in [WesleyGoncalves' GitHub Gist](https://gist.githubusercontent.com/WesleyGoncalves/ae64d95f663a15d8df455d1f0e5f7687)
|
|
5
|
+
#
|
|
6
|
+
# Be aware that:
|
|
7
|
+
# > Comments should go in individual lines, **we are not sure whether appending comments may cause issues**.
|
|
8
|
+
# Emphasis added. [link](https://github.com/editorconfig/editorconfig/wiki/Syntax)
|
|
9
|
+
#
|
|
10
|
+
#
|
|
11
|
+
# You can download this file
|
|
12
|
+
# directly to your project from the command-line
|
|
13
|
+
# curl -O https://gist.githubusercontent.com/WesleyGoncalves/ae64d95f663a15d8df455d1f0e5f7687/raw/.editorconfig
|
|
14
|
+
|
|
15
|
+
# top-most EditorConfig file
|
|
16
|
+
root = true
|
|
17
|
+
|
|
18
|
+
# All PHP files MUST use the Unix LF (linefeed) line ending.
|
|
19
|
+
# Code MUST use an indent of 4 spaces, and MUST NOT use tabs for indenting.
|
|
20
|
+
# All PHP files MUST end with a single blank line.
|
|
21
|
+
# There MUST NOT be trailing whitespace at the end of non-blank lines.
|
|
22
|
+
[*]
|
|
23
|
+
charset = utf-8
|
|
24
|
+
end_of_line = lf
|
|
25
|
+
insert_final_newline = true
|
|
26
|
+
trim_trailing_whitespace = true
|
|
27
|
+
|
|
28
|
+
# PHP-Files | Python | Java
|
|
29
|
+
[*.{php,py,java}]
|
|
30
|
+
indent_style = space
|
|
31
|
+
indent_size = 4
|
|
32
|
+
|
|
33
|
+
[composer.json,docker-compose.yml]
|
|
34
|
+
indent_style = space
|
|
35
|
+
indent_size = 4
|
|
36
|
+
|
|
37
|
+
# **NOT IMPLEMENTED BY editorConfig** PHP-Files. See [this for further information](https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties#ideas-for-domain-specific-properties)
|
|
38
|
+
[*.php]
|
|
39
|
+
curly_bracket_next_line = true
|
|
40
|
+
indent_brace_style = K&R
|
|
41
|
+
spaces_around_operators = true
|
|
42
|
+
# block_comment_start = '/*'
|
|
43
|
+
# block_comment_end = '*/'
|
|
44
|
+
# line_comment = '//'
|
|
45
|
+
|
|
46
|
+
# BLADE-Files | MD | HTML | LESS | SASS | CSS | JS | JSX | TS | TSX | JSON | ReST | YAML | TXT
|
|
47
|
+
[*.{blade.php,md,html,less,sass,scss,css,js{x,},ts{x,},json,rst,y{a,}ml,txt}]
|
|
48
|
+
indent_style = space
|
|
49
|
+
indent_size = 2
|
|
50
|
+
|
|
51
|
+
[{package.json}]
|
|
52
|
+
indent_style = space
|
|
53
|
+
indent_size = 2
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
name: Notify Main Repo
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
dispatch:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- name: Repository Dispatch
|
|
13
|
+
uses: peter-evans/repository-dispatch@v3
|
|
14
|
+
with:
|
|
15
|
+
token: ${{ secrets.PAT_TOKEN }}
|
|
16
|
+
repository: Titus-System/SyncDesk
|
|
17
|
+
event-type: submodule_update
|
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# SyncDesk Library
|
|
2
|
+
|
|
3
|
+
Biblioteca de integração do [SyncDesk](https://github.com/Titus-System/SyncDesk).
|
|
4
|
+
|
|
5
|
+
Essa biblioteca fornece recursos para as aplicações frontend do sistema SyncDesk, permitindo a comunicação eficiente e segura entre os componentes do sistema. Ela inclui funcionalidades de autenticação, gerenciamento de requisições para o backend e manipulação de dados, facilitando a construção de interfaces de usuário responsivas e interativas.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
### Install from npm registry
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install @titus-system/syncdesk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Install locally
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
npm run build
|
|
19
|
+
npm pack
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
It creates a compressed tarball file in your folder. You can install it in your project with:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm install /path/to/your/library-1.0.0.tgz
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Publish new version to npm registry
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
# You may have to login to npm registry first with `npm login`
|
|
32
|
+
npm login
|
|
33
|
+
|
|
34
|
+
npm run build && npm publish
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Auth
|
|
40
|
+
|
|
41
|
+
If the token expires, the auth module will automatically attempt to refresh it using the refresh token. If the refresh token is also expired or invalid, the `config.onUnauthorized` callback will be triggered, allowing you to handle the unauthorized state (e.g., redirecting to the login page).
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@titus-system/syncdesk",
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "",
|
|
5
|
+
"keywords": [],
|
|
6
|
+
"author": "",
|
|
7
|
+
"license": "",
|
|
8
|
+
"bugs": {
|
|
9
|
+
"url": "https://github.com/Titus-System/syncdesk-library/issues"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/Titus-System/syncdesk-library#readme",
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/Titus-System/syncdesk-library.git"
|
|
15
|
+
},
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"require": "./dist/index.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"clean": "rm -rf dist",
|
|
27
|
+
"build": "npm run clean && tsc",
|
|
28
|
+
"dev": "tsc --watch"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@tanstack/react-query": "^5.95.2",
|
|
32
|
+
"react": "^19.2.4"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"axios": "^1.13.6",
|
|
36
|
+
"react-use-websocket": "^4.13.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/react": "^19.2.14",
|
|
40
|
+
"typescript": "^6.0.2"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import axios, { InternalAxiosRequestConfig } from "axios";
|
|
2
|
+
import { config, LibraryConfig } from "../config";
|
|
3
|
+
|
|
4
|
+
export const apiClient = axios.create();
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Configure the library with user-provided settings.
|
|
8
|
+
*/
|
|
9
|
+
export const configureLibrary = (userConfig: LibraryConfig) => {
|
|
10
|
+
// Overwrite the default internal config with the user's settings
|
|
11
|
+
Object.assign(config, userConfig);
|
|
12
|
+
|
|
13
|
+
apiClient.defaults.baseURL = config.baseURL;
|
|
14
|
+
apiClient.defaults.timeout = config.timeout;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// INTERCEPTORS
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Request Interceptor.
|
|
21
|
+
* Automatically adds the access token to the Authorization header of every request if it's available.
|
|
22
|
+
*/
|
|
23
|
+
apiClient.interceptors.request.use(
|
|
24
|
+
async (reqConfig) => {
|
|
25
|
+
const token = await config.getAccessToken();
|
|
26
|
+
if (token && reqConfig.headers) {
|
|
27
|
+
reqConfig.headers.Authorization = `Bearer ${token}`;
|
|
28
|
+
}
|
|
29
|
+
return reqConfig;
|
|
30
|
+
},
|
|
31
|
+
(error) => Promise.reject(error),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Response Interceptor.
|
|
36
|
+
*/
|
|
37
|
+
let isRefreshing = false; // Flag to prevent multiple simultaneous refresh attempts
|
|
38
|
+
let failedQueue: Array<{
|
|
39
|
+
// Queue to hold requests that come in while we're refreshing tokens
|
|
40
|
+
resolve: (token: string) => void;
|
|
41
|
+
reject: (error: any) => void;
|
|
42
|
+
}> = [];
|
|
43
|
+
/** Processes the queue of failed requests after token refresh. */
|
|
44
|
+
const processQueue = (error: any, token: string | null = null) => {
|
|
45
|
+
failedQueue.forEach((promise) => {
|
|
46
|
+
if (error) {
|
|
47
|
+
promise.reject(error);
|
|
48
|
+
} else {
|
|
49
|
+
promise.resolve(token as string);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
failedQueue = [];
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* This interceptor watches for 401 responses. If it sees one, it tries to get a new access token using
|
|
57
|
+
* the refresh token.
|
|
58
|
+
*
|
|
59
|
+
* Meanwhile, it queues up any new requests that also fail with 401, and once it gets a new token,
|
|
60
|
+
* it retries all the failed requests with the new token. If the refresh token is also expired/invalid,
|
|
61
|
+
* it calls the onUnauthorized callback to let the app know they need to log in again.
|
|
62
|
+
*/
|
|
63
|
+
apiClient.interceptors.response.use(
|
|
64
|
+
(response) => response, // success
|
|
65
|
+
async (error) => {
|
|
66
|
+
const originalRequest = error.config as InternalAxiosRequestConfig & {
|
|
67
|
+
_retry?: boolean;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// if 401 and not retried
|
|
71
|
+
if (error.response?.status === 401 && !originalRequest._retry) {
|
|
72
|
+
if (
|
|
73
|
+
originalRequest.url?.includes("/refresh") ||
|
|
74
|
+
originalRequest.url?.includes("/login")
|
|
75
|
+
) {
|
|
76
|
+
config.onUnauthorized();
|
|
77
|
+
return Promise.reject(error);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// If another request is currently refreshing the token, pause this request and add to queue
|
|
81
|
+
if (isRefreshing) {
|
|
82
|
+
return (
|
|
83
|
+
new Promise((resolve, reject) => {
|
|
84
|
+
failedQueue.push({ resolve, reject });
|
|
85
|
+
})
|
|
86
|
+
.then((token) => {
|
|
87
|
+
// on success, add the new token to the request header and return the client
|
|
88
|
+
originalRequest.headers.Authorization = `Bearer ${token}`;
|
|
89
|
+
return apiClient(originalRequest);
|
|
90
|
+
})
|
|
91
|
+
// on error, reject the request
|
|
92
|
+
.catch((err) => Promise.reject(err))
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Mark that we are starting the refresh process
|
|
97
|
+
originalRequest._retry = true;
|
|
98
|
+
isRefreshing = true;
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const refreshToken = await config.getRefreshToken();
|
|
102
|
+
|
|
103
|
+
if (!refreshToken) {
|
|
104
|
+
throw new Error("No refresh token available.");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Do a standard axios call to get the new token, so the interceptors are not called again.
|
|
108
|
+
const refreshResponse = await axios.post(`${config.baseURL}/refresh`, {
|
|
109
|
+
refresh_token: refreshToken,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const newAccessToken = refreshResponse.data.data.access_token;
|
|
113
|
+
const newRefreshToken = refreshResponse.data.data.refresh_token;
|
|
114
|
+
|
|
115
|
+
await config.onTokensRefreshed(newAccessToken, newRefreshToken);
|
|
116
|
+
|
|
117
|
+
// Resume all the other paused requests with the new token
|
|
118
|
+
processQueue(null, newAccessToken);
|
|
119
|
+
|
|
120
|
+
// Retry the original request that failed
|
|
121
|
+
originalRequest.headers.Authorization = `Bearer ${newAccessToken}`;
|
|
122
|
+
return apiClient(originalRequest);
|
|
123
|
+
} catch (refreshError) {
|
|
124
|
+
// If the refresh token is completely expired, kill the queue and log them out
|
|
125
|
+
processQueue(refreshError, null);
|
|
126
|
+
config.onUnauthorized();
|
|
127
|
+
return Promise.reject(refreshError);
|
|
128
|
+
} finally {
|
|
129
|
+
isRefreshing = false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return Promise.reject(error);
|
|
134
|
+
},
|
|
135
|
+
);
|
package/src/api/index.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
import { apiClient, ApiResponse } from "../../api";
|
|
3
|
+
import {
|
|
4
|
+
LoginResponse,
|
|
5
|
+
RegisterUserRequest,
|
|
6
|
+
UserCreatedResponse,
|
|
7
|
+
UserLoginRequest,
|
|
8
|
+
UserWithRoles,
|
|
9
|
+
} from "../types/auth";
|
|
10
|
+
|
|
11
|
+
const PATH = "auth";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get the currently authenticated user's profile.
|
|
15
|
+
*/
|
|
16
|
+
export const useGetMe = () => {
|
|
17
|
+
return useQuery({
|
|
18
|
+
queryKey: ["me"],
|
|
19
|
+
queryFn: async (): Promise<UserWithRoles> => {
|
|
20
|
+
const response = await apiClient.get<ApiResponse<UserWithRoles>>(
|
|
21
|
+
`${PATH}/me`,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return response.data.data;
|
|
25
|
+
},
|
|
26
|
+
// if the user isn't logged in (401)
|
|
27
|
+
retry: false,
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Log in a user.
|
|
33
|
+
*
|
|
34
|
+
* Refresh tokens are handled automatically using Interceptors.
|
|
35
|
+
*/
|
|
36
|
+
export const useLogin = () => {
|
|
37
|
+
const queryClient = useQueryClient();
|
|
38
|
+
|
|
39
|
+
return useMutation({
|
|
40
|
+
mutationFn: async (
|
|
41
|
+
credentials: UserLoginRequest,
|
|
42
|
+
): Promise<LoginResponse> => {
|
|
43
|
+
const response = await apiClient.post<ApiResponse<LoginResponse>>(
|
|
44
|
+
`${PATH}/login`,
|
|
45
|
+
credentials,
|
|
46
|
+
);
|
|
47
|
+
return response.data.data;
|
|
48
|
+
},
|
|
49
|
+
onSuccess: () => {
|
|
50
|
+
// After a successful login, fetch the new user's profile
|
|
51
|
+
queryClient.invalidateQueries({ queryKey: ["me"] });
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Register a new user.
|
|
58
|
+
*/
|
|
59
|
+
export const useRegister = () => {
|
|
60
|
+
const queryClient = useQueryClient();
|
|
61
|
+
|
|
62
|
+
return useMutation({
|
|
63
|
+
mutationFn: async (
|
|
64
|
+
userData: RegisterUserRequest,
|
|
65
|
+
): Promise<UserCreatedResponse> => {
|
|
66
|
+
const response = await apiClient.post<ApiResponse<UserCreatedResponse>>(
|
|
67
|
+
`${PATH}/register`,
|
|
68
|
+
userData,
|
|
69
|
+
);
|
|
70
|
+
return response.data.data;
|
|
71
|
+
},
|
|
72
|
+
onSuccess: () => {
|
|
73
|
+
// Registration also logs them in (returns tokens), so fetch their profile
|
|
74
|
+
queryClient.invalidateQueries({ queryKey: ["me"] });
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Log out the current user.
|
|
81
|
+
*/
|
|
82
|
+
export const useLogout = () => {
|
|
83
|
+
const queryClient = useQueryClient();
|
|
84
|
+
|
|
85
|
+
return useMutation({
|
|
86
|
+
mutationFn: async (): Promise<void> => {
|
|
87
|
+
await apiClient.post(`${PATH}/logout`);
|
|
88
|
+
},
|
|
89
|
+
onSuccess: () => {
|
|
90
|
+
// clear cache
|
|
91
|
+
queryClient.setQueryData(["me"], null);
|
|
92
|
+
queryClient.clear();
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { useGetMe, useLogin, useRegister, useLogout } from "./hooks/useAuth";
|
|
2
|
+
|
|
3
|
+
export type {
|
|
4
|
+
OAuthProvider,
|
|
5
|
+
LoginResponse,
|
|
6
|
+
UserCreatedResponse,
|
|
7
|
+
UserLoginRequest,
|
|
8
|
+
RegisterUserRequest,
|
|
9
|
+
UserWithRoles,
|
|
10
|
+
} from "./types/auth";
|
|
11
|
+
|
|
12
|
+
export type { Status, Session } from "./types/session";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export type OAuthProvider = "local" | "google" | "microsoft";
|
|
2
|
+
|
|
3
|
+
export interface LoginResponse {
|
|
4
|
+
access_token: string;
|
|
5
|
+
refresh_token: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface UserCreatedResponse {
|
|
9
|
+
id: string;
|
|
10
|
+
email: string;
|
|
11
|
+
username: string;
|
|
12
|
+
access_token: string;
|
|
13
|
+
refresh_token: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UserLoginRequest {
|
|
17
|
+
email: string;
|
|
18
|
+
password: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RegisterUserRequest {
|
|
22
|
+
email: string;
|
|
23
|
+
name: string;
|
|
24
|
+
username: string;
|
|
25
|
+
password: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// I am making a safe assumption for your /me route based on "user_with_roles"
|
|
29
|
+
export interface UserWithRoles {
|
|
30
|
+
id: string;
|
|
31
|
+
email: string;
|
|
32
|
+
username: string;
|
|
33
|
+
name?: string | null;
|
|
34
|
+
roles: string[];
|
|
35
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type Status = "active" | "expired" | "invalid" | "revoked";
|
|
2
|
+
|
|
3
|
+
export interface Session {
|
|
4
|
+
id: string;
|
|
5
|
+
user_id: string;
|
|
6
|
+
refresh_token_hash: string;
|
|
7
|
+
status: Status;
|
|
8
|
+
device_info: Record<string, unknown>;
|
|
9
|
+
expires_at: Date | string;
|
|
10
|
+
last_used_at: Date | string;
|
|
11
|
+
revoked_at: Date | string;
|
|
12
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface LibraryConfig {
|
|
2
|
+
/** API URL */
|
|
3
|
+
baseURL: string;
|
|
4
|
+
timeout?: number;
|
|
5
|
+
|
|
6
|
+
/* Get current access token */
|
|
7
|
+
getAccessToken: () => string | null | Promise<string | null>;
|
|
8
|
+
|
|
9
|
+
/** Get the refresh token when it needs a new one */
|
|
10
|
+
getRefreshToken: () => string | null | Promise<string | null>;
|
|
11
|
+
|
|
12
|
+
/** Callback fired when the library successfully gets new tokens from the backend */
|
|
13
|
+
onTokensRefreshed: (
|
|
14
|
+
accessToken: string,
|
|
15
|
+
refreshToken: string,
|
|
16
|
+
) => void | Promise<void>;
|
|
17
|
+
|
|
18
|
+
/** Fired if the user is not authenticated and refresh token fails. */
|
|
19
|
+
onUnauthorized: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const config: Required<LibraryConfig> = {
|
|
23
|
+
baseURL: "",
|
|
24
|
+
timeout: 10000,
|
|
25
|
+
getAccessToken: () => null,
|
|
26
|
+
getRefreshToken: () => null,
|
|
27
|
+
onTokensRefreshed: () => {},
|
|
28
|
+
onUnauthorized: () => {
|
|
29
|
+
console.warn("Unauthorized error caught. No handler provided.");
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { apiClient, ApiResponse } from "../../api";
|
|
3
|
+
import type {
|
|
4
|
+
PingResponse,
|
|
5
|
+
HealthResponse,
|
|
6
|
+
ReadyResponse,
|
|
7
|
+
} from "../types/health";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Ping the server to check for basic connectivity.
|
|
11
|
+
*/
|
|
12
|
+
export const usePing = () => {
|
|
13
|
+
return useQuery({
|
|
14
|
+
queryKey: ["health", "ping"],
|
|
15
|
+
queryFn: async (): Promise<PingResponse> => {
|
|
16
|
+
const response = await apiClient.get<ApiResponse<PingResponse>>("/ping");
|
|
17
|
+
return response.data.data;
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check the detailed health of the server and its dependencies (e.g. databases).
|
|
24
|
+
*/
|
|
25
|
+
export const useHealth = () => {
|
|
26
|
+
return useQuery({
|
|
27
|
+
queryKey: ["health", "status"],
|
|
28
|
+
queryFn: async (): Promise<HealthResponse> => {
|
|
29
|
+
const response =
|
|
30
|
+
await apiClient.get<ApiResponse<HealthResponse>>("/health");
|
|
31
|
+
return response.data.data;
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if the server is ready to accept traffic.
|
|
38
|
+
*/
|
|
39
|
+
export const useReady = () => {
|
|
40
|
+
return useQuery({
|
|
41
|
+
queryKey: ["health", "ready"],
|
|
42
|
+
queryFn: async (): Promise<ReadyResponse> => {
|
|
43
|
+
const response =
|
|
44
|
+
await apiClient.get<ApiResponse<ReadyResponse>>("/ready");
|
|
45
|
+
return response.data.data;
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export * from "./auth";
|
|
2
|
+
export * from "./api";
|
|
3
|
+
export * from "./users";
|
|
4
|
+
export * from "./roles";
|
|
5
|
+
export * from "./permissions";
|
|
6
|
+
export * from "./live_chat";
|
|
7
|
+
export * from "./health";
|
|
8
|
+
|
|
9
|
+
export { config } from "./config";
|
|
10
|
+
export type { LibraryConfig } from "./config";
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
import { apiClient, ApiResponse } from "../../api";
|
|
3
|
+
import {
|
|
4
|
+
Conversation,
|
|
5
|
+
CreateConversationDTO,
|
|
6
|
+
PaginatedMessages,
|
|
7
|
+
} from "../types/live_chat";
|
|
8
|
+
|
|
9
|
+
const PATH = "/conversations";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get all conversations recursively mapping to a ticket.
|
|
13
|
+
* @param ticket_id
|
|
14
|
+
*/
|
|
15
|
+
export const useGetConversations = (ticket_id: string) => {
|
|
16
|
+
return useQuery({
|
|
17
|
+
queryKey: ["conversations", "ticket", ticket_id],
|
|
18
|
+
queryFn: async (): Promise<Conversation[]> => {
|
|
19
|
+
const response = await apiClient.get<ApiResponse<Conversation[]>>(
|
|
20
|
+
`${PATH}/ticket/${ticket_id}`,
|
|
21
|
+
);
|
|
22
|
+
return response.data.data;
|
|
23
|
+
},
|
|
24
|
+
enabled: !!ticket_id,
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get paginated messages for a ticket.
|
|
30
|
+
* @param ticket_id
|
|
31
|
+
* @param page
|
|
32
|
+
* @param limit
|
|
33
|
+
*/
|
|
34
|
+
export const useGetPaginatedMessages = (
|
|
35
|
+
ticket_id: string,
|
|
36
|
+
page = 1,
|
|
37
|
+
limit = 10,
|
|
38
|
+
) => {
|
|
39
|
+
return useQuery({
|
|
40
|
+
queryKey: [
|
|
41
|
+
"conversations",
|
|
42
|
+
"ticket",
|
|
43
|
+
ticket_id,
|
|
44
|
+
"messages",
|
|
45
|
+
{ page, limit },
|
|
46
|
+
],
|
|
47
|
+
queryFn: async (): Promise<PaginatedMessages> => {
|
|
48
|
+
const response = await apiClient.get<ApiResponse<PaginatedMessages>>(
|
|
49
|
+
`${PATH}/ticket/${ticket_id}/messages`,
|
|
50
|
+
{ params: { page, limit } },
|
|
51
|
+
);
|
|
52
|
+
return response.data.data;
|
|
53
|
+
},
|
|
54
|
+
enabled: !!ticket_id,
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Create a new conversation mapping to a ticket.
|
|
60
|
+
*/
|
|
61
|
+
export const useCreateConversation = () => {
|
|
62
|
+
const queryClient = useQueryClient();
|
|
63
|
+
|
|
64
|
+
return useMutation({
|
|
65
|
+
mutationFn: async (dto: CreateConversationDTO): Promise<Conversation> => {
|
|
66
|
+
const response = await apiClient.post<ApiResponse<Conversation>>(
|
|
67
|
+
PATH,
|
|
68
|
+
dto,
|
|
69
|
+
);
|
|
70
|
+
return response.data.data;
|
|
71
|
+
},
|
|
72
|
+
onSuccess: (_, variables) => {
|
|
73
|
+
queryClient.invalidateQueries({
|
|
74
|
+
queryKey: ["conversations", "ticket", variables.ticket_id],
|
|
75
|
+
});
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Assign an agent to a conversation.
|
|
82
|
+
*/
|
|
83
|
+
export const useSetConversationAgent = () => {
|
|
84
|
+
const queryClient = useQueryClient();
|
|
85
|
+
|
|
86
|
+
return useMutation({
|
|
87
|
+
mutationFn: async ({
|
|
88
|
+
chat_id,
|
|
89
|
+
agent_id,
|
|
90
|
+
}: {
|
|
91
|
+
chat_id: string;
|
|
92
|
+
agent_id: string;
|
|
93
|
+
}): Promise<void> => {
|
|
94
|
+
await apiClient.patch(`${PATH}/${chat_id}/set-agent/${agent_id}`);
|
|
95
|
+
},
|
|
96
|
+
// We invalidate the ticket conversations here.
|
|
97
|
+
// To do so effectively, you may need the ticket_id, but the mutation only receives the chat_id.
|
|
98
|
+
// If ticket list invalidation is required, invalidate the whole 'conversations' key.
|
|
99
|
+
onSuccess: () => {
|
|
100
|
+
queryClient.invalidateQueries({ queryKey: ["conversations"] });
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
};
|