@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.
@@ -0,0 +1,86 @@
1
+ import { useEffect, useState, useCallback } from "react";
2
+ import useWebSocket, { ReadyState } from "react-use-websocket";
3
+ import { config } from "../../config";
4
+ import type { ChatMessage, SendMessagePayload } from "../types/live_chat";
5
+ import type { ApiResponse } from "../../api";
6
+
7
+ export function useLiveChatWebSocket(chatId: string | null | undefined) {
8
+ const [token, setToken] = useState<string | null>(null);
9
+
10
+ // We need the token resolved before connecting to WebSocket
11
+ useEffect(() => {
12
+ if (!chatId) return;
13
+
14
+ const fetchToken = async () => {
15
+ const t = await config.getAccessToken();
16
+ setToken(t);
17
+ };
18
+ fetchToken();
19
+ }, [chatId]);
20
+
21
+ // Construct WebSocket URL from the configured baseURL
22
+ const wsUrl = () => {
23
+ if (!config.baseURL || !chatId || !token) return null;
24
+ try {
25
+ const url = new URL(config.baseURL);
26
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
27
+ url.pathname = `${url.pathname.replace(/\/$/, "")}/live_chat/room/${chatId}`;
28
+ return url.toString();
29
+ } catch {
30
+ // If baseURL is relative (e.g. "/api"), use window location
31
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
32
+ const host = window.location.host;
33
+ return `${protocol}//${host}${config.baseURL.replace(/\/$/, "")}/live_chat/room/${chatId}`;
34
+ }
35
+ };
36
+
37
+ const finalUrl = wsUrl();
38
+
39
+ const { sendMessage, lastJsonMessage, readyState } = useWebSocket<
40
+ ApiResponse<ChatMessage>
41
+ >(
42
+ finalUrl,
43
+ {
44
+ // Using token as a subprotocol to pass it due to WebSocket standard not allowing custom headers in browsers.
45
+ // E.g. Sec-WebSocket-Protocol: access_token, <token>
46
+ protocols: token ? ["access_token", token] : [],
47
+ shouldReconnect: (closeEvent) => {
48
+ // Do not reconnect on unauthorized or permission denied errors
49
+ if (
50
+ closeEvent.code === 1008 ||
51
+ closeEvent.code === 403 ||
52
+ closeEvent.code === 1011
53
+ )
54
+ return false;
55
+ return true;
56
+ },
57
+ reconnectAttempts: 10,
58
+ reconnectInterval: 3000,
59
+ },
60
+ // Only connect if we have a valid URL (meaning chat id and token exist)
61
+ !!finalUrl,
62
+ );
63
+
64
+ const connectionStatus = {
65
+ [ReadyState.CONNECTING]: "Connecting",
66
+ [ReadyState.OPEN]: "Open",
67
+ [ReadyState.CLOSING]: "Closing",
68
+ [ReadyState.CLOSED]: "Closed",
69
+ [ReadyState.UNINSTANTIATED]: "Uninstantiated",
70
+ }[readyState];
71
+
72
+ const sendPayload = useCallback(
73
+ (payload: SendMessagePayload) => {
74
+ sendMessage(JSON.stringify(payload));
75
+ },
76
+ [sendMessage],
77
+ );
78
+
79
+ return {
80
+ sendMessage: sendPayload,
81
+ lastMessage: lastJsonMessage?.data || null,
82
+ rawLastMessage: lastJsonMessage,
83
+ connectionStatus,
84
+ readyState,
85
+ };
86
+ }
@@ -0,0 +1,15 @@
1
+ export {
2
+ useGetConversations,
3
+ useGetPaginatedMessages,
4
+ useCreateConversation,
5
+ useSetConversationAgent,
6
+ } from "./hooks/useLiveChat";
7
+
8
+ export { useLiveChatWebSocket } from "./hooks/useLiveChatWebSocket";
9
+
10
+ export type {
11
+ ChatMessage,
12
+ Conversation,
13
+ CreateConversationDTO,
14
+ PaginatedMessages,
15
+ } from "./types/live_chat";
@@ -0,0 +1,48 @@
1
+ export interface SendMessagePayload {
2
+ type: "text" | "file";
3
+ content: string;
4
+ mime_type?: string | null;
5
+ filename?: string | null;
6
+ responding_to?: string | null;
7
+ }
8
+
9
+ export interface ChatMessage {
10
+ id: string;
11
+ conversation_id: string;
12
+ sender_id: string | "System";
13
+ timestamp: string;
14
+ type: "text" | "file";
15
+ content: string;
16
+ mime_type?: string | null;
17
+ filename?: string | null;
18
+ responding_to?: string | null;
19
+ }
20
+
21
+ export interface Conversation {
22
+ _id: string;
23
+ ticket_id: string;
24
+ agent_id?: string | null;
25
+ client_id: string;
26
+ sequential_index: number;
27
+ parent_id?: string | null;
28
+ children_ids: string[];
29
+ started_at: string;
30
+ finished_at?: string | null;
31
+ messages: ChatMessage[];
32
+ }
33
+
34
+ export interface CreateConversationDTO {
35
+ ticket_id: string;
36
+ agent_id?: string | null;
37
+ client_id: string;
38
+ sequential_index?: number;
39
+ parent_id?: string | null;
40
+ }
41
+
42
+ export interface PaginatedMessages {
43
+ messages: ChatMessage[];
44
+ total: number;
45
+ page: number;
46
+ limit: number;
47
+ has_next: boolean;
48
+ }
@@ -0,0 +1,141 @@
1
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
2
+ import { apiClient } from "../../api";
3
+ import type { ApiResponse } from "../../api";
4
+ import type {
5
+ Permission,
6
+ CreatePermissionDTO,
7
+ ReplacePermissionDTO,
8
+ UpdatePermissionDTO,
9
+ AddPermissionRolesDTO,
10
+ } from "../types/permission";
11
+
12
+ export function usePermissions() {
13
+ return useQuery<Permission[]>({
14
+ queryKey: ["permissions"],
15
+ queryFn: async () => {
16
+ const response =
17
+ await apiClient.get<ApiResponse<Permission[]>>("/permissions/");
18
+ return response.data.data;
19
+ },
20
+ });
21
+ }
22
+
23
+ export function usePermission(id: number) {
24
+ return useQuery<Permission>({
25
+ queryKey: ["permissions", id],
26
+ queryFn: async () => {
27
+ const response = await apiClient.get<ApiResponse<Permission>>(
28
+ `/permissions/${id}`,
29
+ );
30
+ return response.data.data;
31
+ },
32
+ enabled: !!id,
33
+ });
34
+ }
35
+
36
+ export function useCreatePermission() {
37
+ const queryClient = useQueryClient();
38
+ return useMutation<Permission, Error, CreatePermissionDTO>({
39
+ mutationFn: async (dto) => {
40
+ const response = await apiClient.post<ApiResponse<Permission>>(
41
+ "/permissions/",
42
+ dto,
43
+ );
44
+ return response.data.data;
45
+ },
46
+ onSuccess: () => {
47
+ queryClient.invalidateQueries({ queryKey: ["permissions"] });
48
+ },
49
+ });
50
+ }
51
+
52
+ export function useReplacePermission() {
53
+ const queryClient = useQueryClient();
54
+ return useMutation<
55
+ Permission,
56
+ Error,
57
+ { id: number; dto: ReplacePermissionDTO }
58
+ >({
59
+ mutationFn: async ({ id, dto }) => {
60
+ const response = await apiClient.put<ApiResponse<Permission>>(
61
+ `/permissions/${id}`,
62
+ dto,
63
+ );
64
+ return response.data.data;
65
+ },
66
+ onSuccess: (_, { id }) => {
67
+ queryClient.invalidateQueries({ queryKey: ["permissions"] });
68
+ queryClient.invalidateQueries({ queryKey: ["permissions", id] });
69
+ },
70
+ });
71
+ }
72
+
73
+ export function useUpdatePermission() {
74
+ const queryClient = useQueryClient();
75
+ return useMutation<
76
+ Permission,
77
+ Error,
78
+ { id: number; dto: UpdatePermissionDTO }
79
+ >({
80
+ mutationFn: async ({ id, dto }) => {
81
+ const response = await apiClient.patch<ApiResponse<Permission>>(
82
+ `/permissions/${id}`,
83
+ dto,
84
+ );
85
+ return response.data.data;
86
+ },
87
+ onSuccess: (_, { id }) => {
88
+ queryClient.invalidateQueries({ queryKey: ["permissions"] });
89
+ queryClient.invalidateQueries({ queryKey: ["permissions", id] });
90
+ },
91
+ });
92
+ }
93
+
94
+ export function useDeletePermission() {
95
+ const queryClient = useQueryClient();
96
+ return useMutation<Permission, Error, number>({
97
+ mutationFn: async (id) => {
98
+ const response = await apiClient.delete<ApiResponse<Permission>>(
99
+ `/permissions/${id}`,
100
+ );
101
+ return response.data.data;
102
+ },
103
+ onSuccess: (_, id) => {
104
+ queryClient.invalidateQueries({ queryKey: ["permissions"] });
105
+ queryClient.invalidateQueries({ queryKey: ["permissions", id] });
106
+ },
107
+ });
108
+ }
109
+
110
+ export function usePermissionRoles(id: number) {
111
+ return useQuery<Permission>({
112
+ queryKey: ["permissions", id, "roles"],
113
+ queryFn: async () => {
114
+ const response = await apiClient.get<ApiResponse<Permission>>(
115
+ `/permissions/${id}/roles`,
116
+ );
117
+ return response.data.data;
118
+ },
119
+ enabled: !!id,
120
+ });
121
+ }
122
+
123
+ export function useAddPermissionRoles() {
124
+ const queryClient = useQueryClient();
125
+ return useMutation<
126
+ Permission,
127
+ Error,
128
+ { id: number; dto: AddPermissionRolesDTO }
129
+ >({
130
+ mutationFn: async ({ id, dto }) => {
131
+ const response = await apiClient.post<ApiResponse<Permission>>(
132
+ `/permissions/${id}/roles`,
133
+ dto,
134
+ );
135
+ return response.data.data;
136
+ },
137
+ onSuccess: (_, { id }) => {
138
+ queryClient.invalidateQueries({ queryKey: ["permissions", id, "roles"] });
139
+ },
140
+ });
141
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./types";
2
+ export * from "./hooks/usePermissions";
@@ -0,0 +1 @@
1
+ export * from "./permission";
@@ -0,0 +1,27 @@
1
+ import type { Role } from "../../roles/types/role";
2
+
3
+ export interface Permission {
4
+ id: number;
5
+ name: string;
6
+ description?: string | null;
7
+ roles?: Role[];
8
+ }
9
+
10
+ export interface CreatePermissionDTO {
11
+ name: string;
12
+ description?: string | null;
13
+ }
14
+
15
+ export interface ReplacePermissionDTO {
16
+ name: string;
17
+ description?: string | null;
18
+ }
19
+
20
+ export interface UpdatePermissionDTO {
21
+ name?: string | null;
22
+ description?: string | null;
23
+ }
24
+
25
+ export interface AddPermissionRolesDTO {
26
+ ids: number[];
27
+ }
@@ -0,0 +1,103 @@
1
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
2
+ import { apiClient, ApiResponse } from "../../api";
3
+ import type { Role, CreateRoleDTO, ReplaceRoleDTO, UpdateRoleDTO, AddRolePermissionsDTO } from "../types/role";
4
+
5
+ export function useRoles() {
6
+ return useQuery<Role[]>({
7
+ queryKey: ["roles"],
8
+ queryFn: async () => {
9
+ const response = await apiClient.get<ApiResponse<Role[]>>("/roles/");
10
+ return response.data.data;
11
+ },
12
+ });
13
+ }
14
+
15
+ export function useRole(id: number) {
16
+ return useQuery<Role>({
17
+ queryKey: ["roles", id],
18
+ queryFn: async () => {
19
+ const response = await apiClient.get<ApiResponse<Role>>(`/roles/${id}`);
20
+ return response.data.data;
21
+ },
22
+ enabled: !!id,
23
+ });
24
+ }
25
+
26
+ export function useCreateRole() {
27
+ const queryClient = useQueryClient();
28
+ return useMutation<Role, Error, CreateRoleDTO>({
29
+ mutationFn: async (dto) => {
30
+ const response = await apiClient.post<ApiResponse<Role>>("/roles/", dto);
31
+ return response.data.data;
32
+ },
33
+ onSuccess: () => {
34
+ queryClient.invalidateQueries({ queryKey: ["roles"] });
35
+ },
36
+ });
37
+ }
38
+
39
+ export function useReplaceRole() {
40
+ const queryClient = useQueryClient();
41
+ return useMutation<Role, Error, { id: number; dto: ReplaceRoleDTO }>({
42
+ mutationFn: async ({ id, dto }) => {
43
+ const response = await apiClient.put<ApiResponse<Role>>(`/roles/${id}`, dto);
44
+ return response.data.data;
45
+ },
46
+ onSuccess: (_, { id }) => {
47
+ queryClient.invalidateQueries({ queryKey: ["roles"] });
48
+ queryClient.invalidateQueries({ queryKey: ["roles", id] });
49
+ },
50
+ });
51
+ }
52
+
53
+ export function useUpdateRole() {
54
+ const queryClient = useQueryClient();
55
+ return useMutation<Role, Error, { id: number; dto: UpdateRoleDTO }>({
56
+ mutationFn: async ({ id, dto }) => {
57
+ const response = await apiClient.patch<ApiResponse<Role>>(`/roles/${id}`, dto);
58
+ return response.data.data;
59
+ },
60
+ onSuccess: (_, { id }) => {
61
+ queryClient.invalidateQueries({ queryKey: ["roles"] });
62
+ queryClient.invalidateQueries({ queryKey: ["roles", id] });
63
+ },
64
+ });
65
+ }
66
+
67
+ export function useDeleteRole() {
68
+ const queryClient = useQueryClient();
69
+ return useMutation<Role, Error, number>({
70
+ mutationFn: async (id) => {
71
+ const response = await apiClient.delete<ApiResponse<Role>>(`/roles/${id}`);
72
+ return response.data.data;
73
+ },
74
+ onSuccess: (_, id) => {
75
+ queryClient.invalidateQueries({ queryKey: ["roles"] });
76
+ queryClient.invalidateQueries({ queryKey: ["roles", id] });
77
+ },
78
+ });
79
+ }
80
+
81
+ export function useRolePermissions(id: number) {
82
+ return useQuery<Role>({
83
+ queryKey: ["roles", id, "permissions"],
84
+ queryFn: async () => {
85
+ const response = await apiClient.get<ApiResponse<Role>>(`/roles/${id}/permissions`);
86
+ return response.data.data;
87
+ },
88
+ enabled: !!id,
89
+ });
90
+ }
91
+
92
+ export function useAddRolePermissions() {
93
+ const queryClient = useQueryClient();
94
+ return useMutation<Role, Error, { id: number; dto: AddRolePermissionsDTO }>({
95
+ mutationFn: async ({ id, dto }) => {
96
+ const response = await apiClient.post<ApiResponse<Role>>(`/roles/${id}/permissions`, dto);
97
+ return response.data.data;
98
+ },
99
+ onSuccess: (_, { id }) => {
100
+ queryClient.invalidateQueries({ queryKey: ["roles", id, "permissions"] });
101
+ },
102
+ });
103
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./types";
2
+ export * from "./hooks/useRoles";
@@ -0,0 +1 @@
1
+ export * from "./role";
@@ -0,0 +1,28 @@
1
+ export interface Role {
2
+ id: number;
3
+ name: string;
4
+ description?: string | null;
5
+ permissions?: Permission[];
6
+ }
7
+
8
+ export interface CreateRoleDTO {
9
+ name: string;
10
+ description?: string | null;
11
+ }
12
+
13
+ export interface ReplaceRoleDTO {
14
+ name: string;
15
+ description?: string | null;
16
+ }
17
+
18
+ export interface UpdateRoleDTO {
19
+ name?: string | null;
20
+ description?: string | null;
21
+ }
22
+
23
+ export interface AddRolePermissionsDTO {
24
+ ids: number[];
25
+ }
26
+
27
+ // Importing Permission here to avoid circular dependency issues, or we can just redefine it as any or import it.
28
+ import type { Permission } from "../../permissions/types/permission";
@@ -0,0 +1,146 @@
1
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
2
+ import { apiClient, ApiResponse } from "../../api";
3
+ import {
4
+ User,
5
+ CreateUserDTO,
6
+ ReplaceUserDTO,
7
+ UpdateUserDTO,
8
+ AddUserRolesDTO,
9
+ } from "../types/user";
10
+
11
+ const PATH = "/users";
12
+
13
+ /**
14
+ * Get all users.
15
+ * @returns
16
+ */
17
+ export const useGetUsers = () => {
18
+ return useQuery({
19
+ queryKey: ["users"],
20
+ queryFn: async (): Promise<User[]> => {
21
+ const response = await apiClient.get<ApiResponse<User[]>>(PATH);
22
+ return response.data.data;
23
+ },
24
+ // staleTime: // ms
25
+ // gcTime: , //
26
+ });
27
+ };
28
+
29
+ /**
30
+ * Get one user.
31
+ * @param id
32
+ * @returns
33
+ */
34
+ export const useGetUser = (id: string) => {
35
+ return useQuery({
36
+ queryKey: ["users", id],
37
+ queryFn: async (): Promise<User> => {
38
+ const response = await apiClient.get<ApiResponse<User>>(`${PATH}/${id}`);
39
+ return response.data.data;
40
+ },
41
+ enabled: !!id, // Prevent the query from running if the ID is missing
42
+ });
43
+ };
44
+
45
+ /**
46
+ * Create a user.
47
+ * @returns
48
+ */
49
+ export const useCreateUser = () => {
50
+ const queryClient = useQueryClient();
51
+
52
+ return useMutation({
53
+ mutationFn: async (user: CreateUserDTO): Promise<User> => {
54
+ const response = await apiClient.post<ApiResponse<User>>(PATH, user);
55
+ return response.data.data;
56
+ },
57
+ // Tell React Query to refresh the 'users' list after a successful creation.
58
+ onSuccess: () => {
59
+ queryClient.invalidateQueries({ queryKey: ["users"] });
60
+ },
61
+ });
62
+ };
63
+
64
+ /**
65
+ * Replace an entire user.
66
+ * @returns
67
+ */
68
+ export const useUpdateUser = () => {
69
+ const queryClient = useQueryClient();
70
+
71
+ return useMutation({
72
+ mutationFn: async ({
73
+ id,
74
+ data,
75
+ }: {
76
+ id: string;
77
+ data: ReplaceUserDTO;
78
+ }): Promise<User> => {
79
+ const response = await apiClient.put<ApiResponse<User>>(
80
+ `${PATH}/${id}`,
81
+ data,
82
+ );
83
+ return response.data.data;
84
+ },
85
+ onSuccess: (_, variables) => {
86
+ queryClient.invalidateQueries({ queryKey: ["users"] });
87
+ queryClient.invalidateQueries({ queryKey: ["users", variables.id] });
88
+ },
89
+ });
90
+ };
91
+
92
+ /**
93
+ * Update specific fields of a user.
94
+ * @returns
95
+ */
96
+ export const usePatchUser = () => {
97
+ const queryClient = useQueryClient();
98
+
99
+ return useMutation({
100
+ mutationFn: async ({
101
+ id,
102
+ data,
103
+ }: {
104
+ id: string;
105
+ data: UpdateUserDTO;
106
+ }): Promise<User> => {
107
+ const response = await apiClient.patch<ApiResponse<User>>(
108
+ `${PATH}/${id}`,
109
+ data,
110
+ );
111
+ return response.data.data;
112
+ },
113
+ onSuccess: (_, variables) => {
114
+ queryClient.invalidateQueries({ queryKey: ["users"] });
115
+ queryClient.invalidateQueries({ queryKey: ["users", variables.id] });
116
+ },
117
+ });
118
+ };
119
+
120
+ /**
121
+ * Add roles to a user.
122
+ * @returns
123
+ */
124
+ export const useAddUserRoles = () => {
125
+ const queryClient = useQueryClient();
126
+
127
+ return useMutation({
128
+ mutationFn: async ({
129
+ id,
130
+ data,
131
+ }: {
132
+ id: string;
133
+ data: AddUserRolesDTO;
134
+ }): Promise<User> => {
135
+ const response = await apiClient.post<ApiResponse<User>>(
136
+ `${PATH}/${id}/roles`,
137
+ data,
138
+ );
139
+ return response.data.data;
140
+ },
141
+ onSuccess: (_, variables) => {
142
+ queryClient.invalidateQueries({ queryKey: ["users"] });
143
+ queryClient.invalidateQueries({ queryKey: ["users", variables.id] });
144
+ },
145
+ });
146
+ };
@@ -0,0 +1,22 @@
1
+ export {
2
+ useGetUsers,
3
+ useGetUser,
4
+ useCreateUser,
5
+ useUpdateUser,
6
+ usePatchUser,
7
+ useAddUserRoles,
8
+ } from "./hooks/useUsers";
9
+
10
+ export type {
11
+ User,
12
+ CreateUserDTO,
13
+ UpdateUserDTO,
14
+ ReplaceUserDTO,
15
+ AddUserRolesDTO,
16
+ } from "./types/user";
17
+
18
+ // # syncdesk-api/app/api/api_router
19
+ // - /auth/ -- /app/domains/auth/routers/auth_router
20
+ // | `/api/users/` | User management (CRUD) |
21
+ // | `/api/roles/` | Role management (CRUD) |
22
+ // | `/api/permissions/` | Permission management (CRUD) |
@@ -0,0 +1,36 @@
1
+ import { OAuthProvider } from "../../auth/types/auth";
2
+
3
+ export interface User {
4
+ id: string;
5
+ email: string;
6
+ username?: string | null;
7
+ name?: string | null;
8
+ oauth_provider?: OAuthProvider | null;
9
+ oauth_provider_id?: string | null;
10
+ is_active: boolean;
11
+ is_verified: boolean;
12
+ roles?: Role[];
13
+ }
14
+
15
+ import type { Role } from "../../roles/types/role";
16
+ import type { Permission } from "../../permissions/types/permission";
17
+
18
+ export interface CreateUserDTO {
19
+ email: string;
20
+ password_hash?: string | null;
21
+ username?: string | null;
22
+ name?: string | null;
23
+ oauth_provider?: OAuthProvider | null;
24
+ oauth_provider_id?: string | null;
25
+ is_active?: boolean;
26
+ is_verified?: boolean;
27
+ role_ids?: number[];
28
+ }
29
+
30
+ export type UpdateUserDTO = Partial<CreateUserDTO>;
31
+
32
+ export type ReplaceUserDTO = CreateUserDTO;
33
+
34
+ export interface AddUserRolesDTO {
35
+ role_ids: number[];
36
+ }