@econneq/gql-auth 2.0.1 → 2.0.2
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/README.md +304 -304
- package/dist/authServer.d.ts +1 -1
- package/dist/authServer.d.ts.map +1 -1
- package/dist/authServer.js +1 -1
- package/dist/authServer.js.map +1 -1
- package/dist/utilsGeneral.d.ts +1 -1
- package/dist/utilsGeneral.d.ts.map +1 -1
- package/package.json +7 -8
- package/dist/utils.d.ts +0 -24
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -257
package/README.md
CHANGED
|
@@ -1,304 +1,304 @@
|
|
|
1
|
-
# @econneq/gql-auth
|
|
2
|
-
|
|
3
|
-
GraphQL + Auth utilities for **multi-tenant** and **single-tenant** Next.js apps.
|
|
4
|
-
|
|
5
|
-
Handles:
|
|
6
|
-
- Server-side (internal Docker network) and client-side (browser proxy) GraphQL
|
|
7
|
-
- Multi-tenant header injection (`X-Tenant-ID`, `X-Process-ID`, `X-Widget-ID`)
|
|
8
|
-
- JWT auth on both server and client
|
|
9
|
-
- File uploads (GraphQL multipart spec)
|
|
10
|
-
- Batch mutations with per-item error reporting
|
|
11
|
-
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
## Installation
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
npm install @econneq/gql-auth
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
### Peer dependencies
|
|
21
|
-
|
|
22
|
-
```bash
|
|
23
|
-
npm install @apollo/client graphql next react
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## Environment Variables
|
|
29
|
-
|
|
30
|
-
| Variable | Side | Description |
|
|
31
|
-
|---|---|---|
|
|
32
|
-
| `DOMAIN_URL` | server | Base domain, e.g. `e-conneq.com` |
|
|
33
|
-
| `PROTOCOL` | server | e.g. `https://` |
|
|
34
|
-
| `BACKEND_URL_INT` | server | Internal Docker URL, e.g. `api:8000` |
|
|
35
|
-
| `BACKEND_URL_EXT` | server | External backend host, e.g. `admin.e-conneq.com` |
|
|
36
|
-
| `NEXT_PUBLIC_DOMAIN_URL` | both | Same as DOMAIN_URL (client-accessible) |
|
|
37
|
-
| `NEXT_PUBLIC_PROTOCOL` | both | Same as PROTOCOL (client-accessible) |
|
|
38
|
-
| `NEXT_PUBLIC_BACKEND_URL_EXT` | both | Same as BACKEND_URL_EXT (client-accessible) |
|
|
39
|
-
| `API_KEY` | server | Optional service-to-service key |
|
|
40
|
-
|
|
41
|
-
> The internal URL (`BACKEND_URL_INT`) is **never** exposed to the browser.
|
|
42
|
-
> Client-side mutations route through Next.js `/api/graphql` proxy.
|
|
43
|
-
|
|
44
|
-
---
|
|
45
|
-
|
|
46
|
-
## Config Object
|
|
47
|
-
|
|
48
|
-
Every function accepts a `config` object describing your tenants.
|
|
49
|
-
|
|
50
|
-
```ts
|
|
51
|
-
// config.ts (shared across your project)
|
|
52
|
-
import type { ConfigType } from "@econneq/gql-auth";
|
|
53
|
-
|
|
54
|
-
export const config: ConfigType = {
|
|
55
|
-
Subdomains: [
|
|
56
|
-
{ id: 1, subdomain: "acme", processId: "proc-1", widgetID: "wgt-1" },
|
|
57
|
-
{ id: 2, subdomain: "globex", processId: "proc-2" },
|
|
58
|
-
],
|
|
59
|
-
};
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
**Single-tenant**: put one entry in `Subdomains`. The library will always use it
|
|
63
|
-
without requiring a `domain` parameter.
|
|
64
|
-
|
|
65
|
-
---
|
|
66
|
-
|
|
67
|
-
## Quick start
|
|
68
|
-
|
|
69
|
-
### 1. Register the cookie helper (Next.js app)
|
|
70
|
-
|
|
71
|
-
In your root layout (`app/layout.tsx`) or instrumentation file:
|
|
72
|
-
|
|
73
|
-
```ts
|
|
74
|
-
import { cookies } from "next/headers";
|
|
75
|
-
import { registerCookiesFn } from "@econneq/gql-auth";
|
|
76
|
-
|
|
77
|
-
registerCookiesFn(cookies);
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
This lets the Apollo client forward cookies server-side without a hard
|
|
81
|
-
`next/headers` import inside the package.
|
|
82
|
-
|
|
83
|
-
---
|
|
84
|
-
|
|
85
|
-
### 2. Server-side query (Server Component / Route Handler)
|
|
86
|
-
|
|
87
|
-
```ts
|
|
88
|
-
import { queryServerGraphQL } from "@econneq/gql-auth";
|
|
89
|
-
import { config } from "@/config";
|
|
90
|
-
import { GET_USERS } from "@/graphql/queries";
|
|
91
|
-
|
|
92
|
-
// Multi-tenant
|
|
93
|
-
const data = await queryServerGraphQL({
|
|
94
|
-
query: GET_USERS,
|
|
95
|
-
variables: { active: true },
|
|
96
|
-
domain: "acme", // ← matches Subdomains[].subdomain
|
|
97
|
-
config,
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// Single-tenant (omit domain)
|
|
101
|
-
const data = await queryServerGraphQL({
|
|
102
|
-
query: GET_USERS,
|
|
103
|
-
config,
|
|
104
|
-
});
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
### 3. Server-side mutation (Server Action)
|
|
108
|
-
|
|
109
|
-
```ts
|
|
110
|
-
"use server";
|
|
111
|
-
import { mutateServerGraphQL } from "@econneq/gql-auth";
|
|
112
|
-
|
|
113
|
-
const result = await mutateServerGraphQL({
|
|
114
|
-
query: CREATE_USER_MUTATION,
|
|
115
|
-
variables: { input: formData },
|
|
116
|
-
domain: params.domain,
|
|
117
|
-
config,
|
|
118
|
-
});
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
---
|
|
122
|
-
|
|
123
|
-
### 4. Client-side mutation with ApiFactory
|
|
124
|
-
|
|
125
|
-
```tsx
|
|
126
|
-
"use client";
|
|
127
|
-
import { ApiFactory, getToken } from "@econneq/gql-auth";
|
|
128
|
-
import { useRouter } from "next/navigation";
|
|
129
|
-
import { config } from "@/config";
|
|
130
|
-
|
|
131
|
-
export function MyForm({ params }: { params: { domain: string } }) {
|
|
132
|
-
const router = useRouter();
|
|
133
|
-
|
|
134
|
-
const handleSubmit = async (formData: any) => {
|
|
135
|
-
await ApiFactory({
|
|
136
|
-
newData: formData,
|
|
137
|
-
mutationName: "createUser",
|
|
138
|
-
modelName: "user",
|
|
139
|
-
successField: "username",
|
|
140
|
-
query: CREATE_USER_MUTATION,
|
|
141
|
-
params: { domain: params.domain }, // omit for single-tenant
|
|
142
|
-
config,
|
|
143
|
-
router,
|
|
144
|
-
token: getToken() ?? undefined,
|
|
145
|
-
onAlert: ({ title, status }) => {
|
|
146
|
-
// Connect to your toast/alert system
|
|
147
|
-
console.log(title, status);
|
|
148
|
-
},
|
|
149
|
-
redirect: true,
|
|
150
|
-
redirectPath: "/dashboard",
|
|
151
|
-
});
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
return <form onSubmit={...}>...</form>;
|
|
155
|
-
}
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
#### Batch mutations
|
|
159
|
-
|
|
160
|
-
Pass an array to `newData`:
|
|
161
|
-
|
|
162
|
-
```ts
|
|
163
|
-
await ApiFactory({
|
|
164
|
-
newData: [item1, item2, item3],
|
|
165
|
-
mutationName: "createProduct",
|
|
166
|
-
modelName: "product",
|
|
167
|
-
// ...
|
|
168
|
-
});
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
#### File uploads
|
|
172
|
-
|
|
173
|
-
```ts
|
|
174
|
-
await ApiFactory({
|
|
175
|
-
newData: formData,
|
|
176
|
-
getFileMap: (item) => ({ avatar: item.avatarFile }),
|
|
177
|
-
// ...
|
|
178
|
-
});
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
#### Return data instead of alert
|
|
182
|
-
|
|
183
|
-
```ts
|
|
184
|
-
const user = await ApiFactory<{ id: string; username: string }>({
|
|
185
|
-
returnResponseObject: true,
|
|
186
|
-
// ...
|
|
187
|
-
});
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
---
|
|
191
|
-
|
|
192
|
-
### 5. Direct upload mutation
|
|
193
|
-
|
|
194
|
-
For full control over multipart uploads:
|
|
195
|
-
|
|
196
|
-
```ts
|
|
197
|
-
import { uploadGraphQLMutation } from "@econneq/gql-auth";
|
|
198
|
-
|
|
199
|
-
const response = await uploadGraphQLMutation({
|
|
200
|
-
query: UPLOAD_DOCUMENT_MUTATION.loc!.source.body,
|
|
201
|
-
variables: { title: "Report", file: null },
|
|
202
|
-
fileMap: { file: selectedFile },
|
|
203
|
-
params: { domain: "acme" },
|
|
204
|
-
config,
|
|
205
|
-
token: userToken,
|
|
206
|
-
});
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
---
|
|
210
|
-
|
|
211
|
-
### 6. Auth utilities
|
|
212
|
-
|
|
213
|
-
#### Client-side
|
|
214
|
-
|
|
215
|
-
```ts
|
|
216
|
-
import { getToken, getUser, logout } from "@econneq/gql-auth";
|
|
217
|
-
|
|
218
|
-
const token = getToken(); // cookie → localStorage
|
|
219
|
-
const user = getUser(); // decoded JwtPayload
|
|
220
|
-
logout(); // clears cookies + localStorage
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
#### Server-side
|
|
224
|
-
|
|
225
|
-
```ts
|
|
226
|
-
import {
|
|
227
|
-
getServerToken,
|
|
228
|
-
getServerUser,
|
|
229
|
-
getPermissions,
|
|
230
|
-
} from "@econneq/gql-auth";
|
|
231
|
-
|
|
232
|
-
const token = await getServerToken();
|
|
233
|
-
const user = await getServerUser();
|
|
234
|
-
|
|
235
|
-
// Does the user have any access to module 42?
|
|
236
|
-
const canRead = getPermissions(user?.permissions ?? [], 42);
|
|
237
|
-
|
|
238
|
-
// Does the user have CREATE access to module 42?
|
|
239
|
-
const canCreate = getPermissions(user?.permissions ?? [], 42, "C");
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
---
|
|
243
|
-
|
|
244
|
-
### 7. Apollo client (advanced)
|
|
245
|
-
|
|
246
|
-
Direct access when you need a custom query/mutation outside ApiFactory:
|
|
247
|
-
|
|
248
|
-
```ts
|
|
249
|
-
import { getApolloClient } from "@econneq/gql-auth";
|
|
250
|
-
|
|
251
|
-
const client = await getApolloClient({
|
|
252
|
-
domain: "acme", // omit for single-tenant
|
|
253
|
-
config,
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
const { data } = await client.query({ query: MY_QUERY });
|
|
257
|
-
```
|
|
258
|
-
|
|
259
|
-
---
|
|
260
|
-
|
|
261
|
-
## Architecture
|
|
262
|
-
|
|
263
|
-
```
|
|
264
|
-
Browser Next.js Server Backend (Docker)
|
|
265
|
-
───────── ────────────── ────────────────
|
|
266
|
-
uploadGraphQLMutation ──► /api/graphql (proxy) ──► api:8000/graphql/
|
|
267
|
-
ApiFactory ──► /api/graphql (proxy) ──► api:8000/graphql/
|
|
268
|
-
|
|
269
|
-
queryServerGraphQL ────────────────────────────► http://api:8000/graphql/
|
|
270
|
-
mutateServerGraphQL ────────────────────────────► http://api:8000/graphql/
|
|
271
|
-
getApolloClient ────────────────────────────► http://api:8000/graphql/
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
Server-side calls skip the proxy entirely and hit the backend directly
|
|
275
|
-
through Docker's internal network — faster and no public exposure.
|
|
276
|
-
|
|
277
|
-
---
|
|
278
|
-
|
|
279
|
-
## Permissions format
|
|
280
|
-
|
|
281
|
-
Permission strings encode resource + action:
|
|
282
|
-
|
|
283
|
-
```
|
|
284
|
-
"42" → access resource 42 (any action)
|
|
285
|
-
"42C" → create on resource 42
|
|
286
|
-
"42R" → read on resource 42
|
|
287
|
-
"42U" → update on resource 42
|
|
288
|
-
"42D" → delete on resource 42
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
---
|
|
292
|
-
|
|
293
|
-
## Changelog
|
|
294
|
-
|
|
295
|
-
### 2.0.0
|
|
296
|
-
- Centralised env resolution (`env.ts`)
|
|
297
|
-
- Tenant resolution separated into `tenant.ts` — single-tenant fallback
|
|
298
|
-
- `mutateServerGraphQL` added (server-side mutations)
|
|
299
|
-
- `ApiFactory` typed generics, batch partial-error reporting
|
|
300
|
-
- `registerCookiesFn` removes hard `next/headers` dependency
|
|
301
|
-
- `getServerCookieString` helper exported
|
|
302
|
-
- `parseGraphQLErrors` exported for custom use
|
|
303
|
-
- Subdomains[0] fallback for single-tenant setups
|
|
304
|
-
- `uploadGraphQLMutation` no longer throws on missing tenant in single-tenant mode
|
|
1
|
+
# @econneq/gql-auth
|
|
2
|
+
|
|
3
|
+
GraphQL + Auth utilities for **multi-tenant** and **single-tenant** Next.js apps.
|
|
4
|
+
|
|
5
|
+
Handles:
|
|
6
|
+
- Server-side (internal Docker network) and client-side (browser proxy) GraphQL
|
|
7
|
+
- Multi-tenant header injection (`X-Tenant-ID`, `X-Process-ID`, `X-Widget-ID`)
|
|
8
|
+
- JWT auth on both server and client
|
|
9
|
+
- File uploads (GraphQL multipart spec)
|
|
10
|
+
- Batch mutations with per-item error reporting
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @econneq/gql-auth
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Peer dependencies
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @apollo/client graphql next react
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Environment Variables
|
|
29
|
+
|
|
30
|
+
| Variable | Side | Description |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `DOMAIN_URL` | server | Base domain, e.g. `e-conneq.com` |
|
|
33
|
+
| `PROTOCOL` | server | e.g. `https://` |
|
|
34
|
+
| `BACKEND_URL_INT` | server | Internal Docker URL, e.g. `api:8000` |
|
|
35
|
+
| `BACKEND_URL_EXT` | server | External backend host, e.g. `admin.e-conneq.com` |
|
|
36
|
+
| `NEXT_PUBLIC_DOMAIN_URL` | both | Same as DOMAIN_URL (client-accessible) |
|
|
37
|
+
| `NEXT_PUBLIC_PROTOCOL` | both | Same as PROTOCOL (client-accessible) |
|
|
38
|
+
| `NEXT_PUBLIC_BACKEND_URL_EXT` | both | Same as BACKEND_URL_EXT (client-accessible) |
|
|
39
|
+
| `API_KEY` | server | Optional service-to-service key |
|
|
40
|
+
|
|
41
|
+
> The internal URL (`BACKEND_URL_INT`) is **never** exposed to the browser.
|
|
42
|
+
> Client-side mutations route through Next.js `/api/graphql` proxy.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Config Object
|
|
47
|
+
|
|
48
|
+
Every function accepts a `config` object describing your tenants.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// config.ts (shared across your project)
|
|
52
|
+
import type { ConfigType } from "@econneq/gql-auth";
|
|
53
|
+
|
|
54
|
+
export const config: ConfigType = {
|
|
55
|
+
Subdomains: [
|
|
56
|
+
{ id: 1, subdomain: "acme", processId: "proc-1", widgetID: "wgt-1" },
|
|
57
|
+
{ id: 2, subdomain: "globex", processId: "proc-2" },
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Single-tenant**: put one entry in `Subdomains`. The library will always use it
|
|
63
|
+
without requiring a `domain` parameter.
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Quick start
|
|
68
|
+
|
|
69
|
+
### 1. Register the cookie helper (Next.js app)
|
|
70
|
+
|
|
71
|
+
In your root layout (`app/layout.tsx`) or instrumentation file:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { cookies } from "next/headers";
|
|
75
|
+
import { registerCookiesFn } from "@econneq/gql-auth";
|
|
76
|
+
|
|
77
|
+
registerCookiesFn(cookies);
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
This lets the Apollo client forward cookies server-side without a hard
|
|
81
|
+
`next/headers` import inside the package.
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
### 2. Server-side query (Server Component / Route Handler)
|
|
86
|
+
|
|
87
|
+
```ts
|
|
88
|
+
import { queryServerGraphQL } from "@econneq/gql-auth";
|
|
89
|
+
import { config } from "@/config";
|
|
90
|
+
import { GET_USERS } from "@/graphql/queries";
|
|
91
|
+
|
|
92
|
+
// Multi-tenant
|
|
93
|
+
const data = await queryServerGraphQL({
|
|
94
|
+
query: GET_USERS,
|
|
95
|
+
variables: { active: true },
|
|
96
|
+
domain: "acme", // ← matches Subdomains[].subdomain
|
|
97
|
+
config,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Single-tenant (omit domain)
|
|
101
|
+
const data = await queryServerGraphQL({
|
|
102
|
+
query: GET_USERS,
|
|
103
|
+
config,
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 3. Server-side mutation (Server Action)
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
"use server";
|
|
111
|
+
import { mutateServerGraphQL } from "@econneq/gql-auth";
|
|
112
|
+
|
|
113
|
+
const result = await mutateServerGraphQL({
|
|
114
|
+
query: CREATE_USER_MUTATION,
|
|
115
|
+
variables: { input: formData },
|
|
116
|
+
domain: params.domain,
|
|
117
|
+
config,
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
### 4. Client-side mutation with ApiFactory
|
|
124
|
+
|
|
125
|
+
```tsx
|
|
126
|
+
"use client";
|
|
127
|
+
import { ApiFactory, getToken } from "@econneq/gql-auth";
|
|
128
|
+
import { useRouter } from "next/navigation";
|
|
129
|
+
import { config } from "@/config";
|
|
130
|
+
|
|
131
|
+
export function MyForm({ params }: { params: { domain: string } }) {
|
|
132
|
+
const router = useRouter();
|
|
133
|
+
|
|
134
|
+
const handleSubmit = async (formData: any) => {
|
|
135
|
+
await ApiFactory({
|
|
136
|
+
newData: formData,
|
|
137
|
+
mutationName: "createUser",
|
|
138
|
+
modelName: "user",
|
|
139
|
+
successField: "username",
|
|
140
|
+
query: CREATE_USER_MUTATION,
|
|
141
|
+
params: { domain: params.domain }, // omit for single-tenant
|
|
142
|
+
config,
|
|
143
|
+
router,
|
|
144
|
+
token: getToken() ?? undefined,
|
|
145
|
+
onAlert: ({ title, status }) => {
|
|
146
|
+
// Connect to your toast/alert system
|
|
147
|
+
console.log(title, status);
|
|
148
|
+
},
|
|
149
|
+
redirect: true,
|
|
150
|
+
redirectPath: "/dashboard",
|
|
151
|
+
});
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
return <form onSubmit={...}>...</form>;
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
#### Batch mutations
|
|
159
|
+
|
|
160
|
+
Pass an array to `newData`:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
await ApiFactory({
|
|
164
|
+
newData: [item1, item2, item3],
|
|
165
|
+
mutationName: "createProduct",
|
|
166
|
+
modelName: "product",
|
|
167
|
+
// ...
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### File uploads
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
await ApiFactory({
|
|
175
|
+
newData: formData,
|
|
176
|
+
getFileMap: (item) => ({ avatar: item.avatarFile }),
|
|
177
|
+
// ...
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
#### Return data instead of alert
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
const user = await ApiFactory<{ id: string; username: string }>({
|
|
185
|
+
returnResponseObject: true,
|
|
186
|
+
// ...
|
|
187
|
+
});
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
### 5. Direct upload mutation
|
|
193
|
+
|
|
194
|
+
For full control over multipart uploads:
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
import { uploadGraphQLMutation } from "@econneq/gql-auth";
|
|
198
|
+
|
|
199
|
+
const response = await uploadGraphQLMutation({
|
|
200
|
+
query: UPLOAD_DOCUMENT_MUTATION.loc!.source.body,
|
|
201
|
+
variables: { title: "Report", file: null },
|
|
202
|
+
fileMap: { file: selectedFile },
|
|
203
|
+
params: { domain: "acme" },
|
|
204
|
+
config,
|
|
205
|
+
token: userToken,
|
|
206
|
+
});
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
### 6. Auth utilities
|
|
212
|
+
|
|
213
|
+
#### Client-side
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import { getToken, getUser, logout } from "@econneq/gql-auth";
|
|
217
|
+
|
|
218
|
+
const token = getToken(); // cookie → localStorage
|
|
219
|
+
const user = getUser(); // decoded JwtPayload
|
|
220
|
+
logout(); // clears cookies + localStorage
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
#### Server-side
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
import {
|
|
227
|
+
getServerToken,
|
|
228
|
+
getServerUser,
|
|
229
|
+
getPermissions,
|
|
230
|
+
} from "@econneq/gql-auth";
|
|
231
|
+
|
|
232
|
+
const token = await getServerToken();
|
|
233
|
+
const user = await getServerUser();
|
|
234
|
+
|
|
235
|
+
// Does the user have any access to module 42?
|
|
236
|
+
const canRead = getPermissions(user?.permissions ?? [], 42);
|
|
237
|
+
|
|
238
|
+
// Does the user have CREATE access to module 42?
|
|
239
|
+
const canCreate = getPermissions(user?.permissions ?? [], 42, "C");
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
### 7. Apollo client (advanced)
|
|
245
|
+
|
|
246
|
+
Direct access when you need a custom query/mutation outside ApiFactory:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
import { getApolloClient } from "@econneq/gql-auth";
|
|
250
|
+
|
|
251
|
+
const client = await getApolloClient({
|
|
252
|
+
domain: "acme", // omit for single-tenant
|
|
253
|
+
config,
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const { data } = await client.query({ query: MY_QUERY });
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Architecture
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
Browser Next.js Server Backend (Docker)
|
|
265
|
+
───────── ────────────── ────────────────
|
|
266
|
+
uploadGraphQLMutation ──► /api/graphql (proxy) ──► api:8000/graphql/
|
|
267
|
+
ApiFactory ──► /api/graphql (proxy) ──► api:8000/graphql/
|
|
268
|
+
|
|
269
|
+
queryServerGraphQL ────────────────────────────► http://api:8000/graphql/
|
|
270
|
+
mutateServerGraphQL ────────────────────────────► http://api:8000/graphql/
|
|
271
|
+
getApolloClient ────────────────────────────► http://api:8000/graphql/
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Server-side calls skip the proxy entirely and hit the backend directly
|
|
275
|
+
through Docker's internal network — faster and no public exposure.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Permissions format
|
|
280
|
+
|
|
281
|
+
Permission strings encode resource + action:
|
|
282
|
+
|
|
283
|
+
```
|
|
284
|
+
"42" → access resource 42 (any action)
|
|
285
|
+
"42C" → create on resource 42
|
|
286
|
+
"42R" → read on resource 42
|
|
287
|
+
"42U" → update on resource 42
|
|
288
|
+
"42D" → delete on resource 42
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Changelog
|
|
294
|
+
|
|
295
|
+
### 2.0.0
|
|
296
|
+
- Centralised env resolution (`env.ts`)
|
|
297
|
+
- Tenant resolution separated into `tenant.ts` — single-tenant fallback
|
|
298
|
+
- `mutateServerGraphQL` added (server-side mutations)
|
|
299
|
+
- `ApiFactory` typed generics, batch partial-error reporting
|
|
300
|
+
- `registerCookiesFn` removes hard `next/headers` dependency
|
|
301
|
+
- `getServerCookieString` helper exported
|
|
302
|
+
- `parseGraphQLErrors` exported for custom use
|
|
303
|
+
- Subdomains[0] fallback for single-tenant setups
|
|
304
|
+
- `uploadGraphQLMutation` no longer throws on missing tenant in single-tenant mode
|
package/dist/authServer.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export declare const getServerUser: () => Promise<JwtPayload | null>;
|
|
|
23
23
|
* // Does the user have CREATE access to module 42?
|
|
24
24
|
* getPermissions(user.permissions, 42, "C");
|
|
25
25
|
*/
|
|
26
|
-
export declare const getPermissions: (list: string[], access: number, action?: string) => boolean
|
|
26
|
+
export declare const getPermissions: (list: string[], access: number, action?: string) => Promise<boolean>;
|
|
27
27
|
/**
|
|
28
28
|
* Return the full cookie header string for forwarding to the
|
|
29
29
|
* backend (Django session / CSRF pass-through).
|
package/dist/authServer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authServer.d.ts","sourceRoot":"","sources":["../src/authServer.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAI1C;;;GAGG;AACH,eAAO,MAAM,cAAc,QAAa,OAAO,CAAC,MAAM,GAAG,IAAI,CAS5D,CAAC;AAIF;;;GAGG;AACH,eAAO,MAAM,aAAa,QAAa,OAAO,CAAC,UAAU,GAAG,IAAI,CAU/D,CAAC;AAIF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,GACzB,MAAM,MAAM,EAAE,EACd,QAAQ,MAAM,EACd,SAAS,MAAM,KACd,
|
|
1
|
+
{"version":3,"file":"authServer.d.ts","sourceRoot":"","sources":["../src/authServer.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAI1C;;;GAGG;AACH,eAAO,MAAM,cAAc,QAAa,OAAO,CAAC,MAAM,GAAG,IAAI,CAS5D,CAAC;AAIF;;;GAGG;AACH,eAAO,MAAM,aAAa,QAAa,OAAO,CAAC,UAAU,GAAG,IAAI,CAU/D,CAAC;AAIF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,cAAc,GACzB,MAAM,MAAM,EAAE,EACd,QAAQ,MAAM,EACd,SAAS,MAAM,KACd,OAAO,CAAC,OAAO,CAoBjB,CAAC;AAIF;;;GAGG;AACH,eAAO,MAAM,qBAAqB,QAAa,OAAO,CAAC,MAAM,CAQ5D,CAAC"}
|
package/dist/authServer.js
CHANGED
|
@@ -55,7 +55,7 @@ export const getServerUser = async () => {
|
|
|
55
55
|
* // Does the user have CREATE access to module 42?
|
|
56
56
|
* getPermissions(user.permissions, 42, "C");
|
|
57
57
|
*/
|
|
58
|
-
export const getPermissions = (list, access, action) => {
|
|
58
|
+
export const getPermissions = async (list, access, action) => {
|
|
59
59
|
if (!list?.length)
|
|
60
60
|
return false;
|
|
61
61
|
try {
|
package/dist/authServer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authServer.js","sourceRoot":"","sources":["../src/authServer.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,+CAA+C;AAC/C,+DAA+D;AAC/D,EAAE;AACF,8DAA8D;AAC9D,4DAA4D;AAC5D,kBAAkB;AAClB,gEAAgE;AAEhE,YAAY,CAAC;AAIb,gEAAgE;AAEhE;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,IAA4B,EAAE;IAC/D,IAAI,CAAC;QACH,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;QACjD,MAAM,WAAW,GAAG,MAAM,OAAO,EAAE,CAAC;QACpC,OAAO,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,KAAK,IAAI,IAAI,CAAC;IACjD,CAAC;IAAC,MAAM,CAAC;QACP,0DAA0D;QAC1D,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC,CAAC;AAEF,iEAAiE;AAEjE;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,IAAgC,EAAE;IAClE,MAAM,KAAK,GAAG,MAAM,cAAc,EAAE,CAAC;IACrC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAExB,IAAI,CAAC;QACH,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC;QACjD,OAAO,SAAS,CAAa,KAAK,CAAC,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC,CAAC;AAEF,gEAAgE;AAEhE;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,
|
|
1
|
+
{"version":3,"file":"authServer.js","sourceRoot":"","sources":["../src/authServer.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,+CAA+C;AAC/C,+DAA+D;AAC/D,EAAE;AACF,8DAA8D;AAC9D,4DAA4D;AAC5D,kBAAkB;AAClB,gEAAgE;AAEhE,YAAY,CAAC;AAIb,gEAAgE;AAEhE;;;GAGG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,IAA4B,EAAE;IAC/D,IAAI,CAAC;QACH,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;QACjD,MAAM,WAAW,GAAG,MAAM,OAAO,EAAE,CAAC;QACpC,OAAO,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,KAAK,IAAI,IAAI,CAAC;IACjD,CAAC;IAAC,MAAM,CAAC;QACP,0DAA0D;QAC1D,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC,CAAC;AAEF,iEAAiE;AAEjE;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,IAAgC,EAAE;IAClE,MAAM,KAAK,GAAG,MAAM,cAAc,EAAE,CAAC;IACrC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAExB,IAAI,CAAC;QACH,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,YAAY,CAAC,CAAC;QACjD,OAAO,SAAS,CAAa,KAAK,CAAC,CAAC;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC,CAAC;AAEF,gEAAgE;AAEhE;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EACjC,IAAc,EACd,MAAc,EACd,MAAe,EACG,EAAE;IACpB,IAAI,CAAC,IAAI,EAAE,MAAM;QAAE,OAAO,KAAK,CAAC;IAEhC,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE;YACzB,IAAI,MAAM,IAAI,CAAC,MAAM,EAAE,CAAC;gBACtB,iDAAiD;gBACjD,MAAM,KAAK,GAAG,IAAI,MAAM,CAAC,IAAI,MAAM,SAAS,CAAC,CAAC;gBAC9C,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC3B,CAAC;YAED,IAAI,MAAM,IAAI,MAAM,EAAE,CAAC;gBACrB,OAAO,KAAK,KAAK,GAAG,MAAM,GAAG,MAAM,EAAE,CAAC;YACxC,CAAC;YAED,OAAO,KAAK,CAAC;QACf,CAAC,CAAC,CAAC;IACL,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC,CAAC;AAEF,iEAAiE;AAEjE;;;GAGG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,KAAK,IAAqB,EAAE;IAC/D,IAAI,CAAC;QACH,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;QACjD,MAAM,WAAW,GAAG,MAAM,OAAO,EAAE,CAAC;QACpC,OAAO,WAAW,CAAC,QAAQ,EAAE,CAAC;IAChC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC,CAAC"}
|
package/dist/utilsGeneral.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { InterFieldList, JwtPayload } from ".";
|
|
1
|
+
import { InterFieldList, JwtPayload } from "./index.js";
|
|
2
2
|
export declare const capitalizeEachWord: (str: string) => string;
|
|
3
3
|
export declare const errorLog: (err: any, show?: boolean) => string;
|
|
4
4
|
export declare const logout: () => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utilsGeneral.d.ts","sourceRoot":"","sources":["../src/utilsGeneral.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"utilsGeneral.d.ts","sourceRoot":"","sources":["../src/utilsGeneral.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAMxD,eAAO,MAAM,kBAAkB,GAAI,KAAK,MAAM,WAM7C,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,KAAK,GAAG,EAAE,OAAO,OAAO,WAyBhD,CAAC;AAEF,eAAO,MAAM,MAAM,YAclB,CAAC;AAEF,eAAO,MAAM,QAAQ,qBAKpB,CAAC;AAEF,eAAO,MAAM,OAAO,yBAKnB,CAAA;AAED,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,EAAE,WAO5C,CAAC;AAEF,eAAO,MAAM,UAAU,GACnB,SAAS,MAAM,EACf,MAAM,MAAM,GAAG,QAAQ,KACxB,MA0CF,CAAC;AAEF,eAAO,MAAM,WAAW,mBAOvB,CAAC;AAEF,eAAO,MAAM,WAAW,cAMvB,CAAC;AAEF,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,WAIxC,CAAA;AAED,eAAO,MAAM,iBAAiB,GAAI,KAAK,GAAG,QAUzC,CAAC;AAEF,eAAO,MAAM,KAAK,GACd,6BACI;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,SAU7F,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAAI,UAAU,GAAG,EAAE,mBAAmB,cAAc,EAAE,2BAkDpF,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,QAAQ,MAAM;;;GAKvC,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,WAiDtC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@econneq/gql-auth",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "GraphQL auth utilities for multi-tenant and single-tenant Next.js projects.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"module": "./dist/index.js",
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
|
-
"scripts": {
|
|
10
|
-
"build": "tsc",
|
|
11
|
-
"build:watch": "tsc --watch",
|
|
12
|
-
"prepublishOnly": "npm run build",
|
|
13
|
-
"typecheck": "tsc --noEmit"
|
|
14
|
-
},
|
|
15
9
|
"publishConfig": {
|
|
16
10
|
"access": "public"
|
|
17
11
|
},
|
|
@@ -72,5 +66,10 @@
|
|
|
72
66
|
"@types/node": "^20.0.0",
|
|
73
67
|
"@types/react": "^19.0.0",
|
|
74
68
|
"typescript": "^5.0.0"
|
|
69
|
+
},
|
|
70
|
+
"scripts": {
|
|
71
|
+
"build": "tsc",
|
|
72
|
+
"build:watch": "tsc --watch",
|
|
73
|
+
"typecheck": "tsc --noEmit"
|
|
75
74
|
}
|
|
76
|
-
}
|
|
75
|
+
}
|
package/dist/utils.d.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { InterFieldList, JwtPayload } from ".";
|
|
2
|
-
export declare const capitalizeEachWord: (str: string) => string;
|
|
3
|
-
export declare const errorLog: (err: any, show?: boolean) => string;
|
|
4
|
-
export declare const getToken: () => string | null;
|
|
5
|
-
export declare const getUser: () => JwtPayload | null;
|
|
6
|
-
export declare const getStoredFont: (FONTS: string[]) => string;
|
|
7
|
-
export declare const getTimeAgo: (dateStr: string, type: "past" | "future") => string;
|
|
8
|
-
export declare const getLanguage: () => "en" | "fr";
|
|
9
|
-
export declare const getTemplate: () => number;
|
|
10
|
-
export declare const decodeUrlID: (urlID: string) => string;
|
|
11
|
-
export declare function getAcademicYear(): string;
|
|
12
|
-
export declare const removeEmptyFields: (obj: any) => any;
|
|
13
|
-
export declare const Alert: ({ title, duration, status }: {
|
|
14
|
-
title: string;
|
|
15
|
-
duration: 1000 | 1500 | 2000 | 3000 | 4000 | 5000;
|
|
16
|
-
status?: boolean;
|
|
17
|
-
}) => void;
|
|
18
|
-
export declare const validateFormFields: (formData: any, currentStepFields: InterFieldList[]) => Record<string, string>;
|
|
19
|
-
export declare const lastYears: (number: number) => {
|
|
20
|
-
value: string;
|
|
21
|
-
label: string;
|
|
22
|
-
}[];
|
|
23
|
-
export declare const formatText: (text: string) => string;
|
|
24
|
-
//# sourceMappingURL=utils.d.ts.map
|
package/dist/utils.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,GAAG,CAAC;AAI/C,eAAO,MAAM,kBAAkB,GAAI,KAAK,MAAM,WAM7C,CAAC;AAEF,eAAO,MAAM,QAAQ,GAAI,KAAK,GAAG,EAAE,OAAO,OAAO,WAyBhD,CAAC;AAKF,eAAO,MAAM,QAAQ,qBAKpB,CAAC;AAEF,eAAO,MAAM,OAAO,yBAKnB,CAAA;AAED,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,EAAE,WAO5C,CAAC;AAEF,eAAO,MAAM,UAAU,GACnB,SAAS,MAAM,EACf,MAAM,MAAM,GAAG,QAAQ,KACxB,MA0CF,CAAC;AAGF,eAAO,MAAM,WAAW,mBAOvB,CAAC;AAEF,eAAO,MAAM,WAAW,cAMvB,CAAC;AAEF,eAAO,MAAM,WAAW,GAAI,OAAO,MAAM,WAIxC,CAAA;AAED,wBAAgB,eAAe,IAAI,MAAM,CAUxC;AAED,eAAO,MAAM,iBAAiB,GAAI,KAAK,GAAG,QAUzC,CAAC;AAGF,eAAO,MAAM,KAAK,GACd,6BACI;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,SAU7F,CAAC;AAIF,eAAO,MAAM,kBAAkB,GAAI,UAAU,GAAG,EAAE,mBAAmB,cAAc,EAAE,2BAkDpF,CAAC;AAEF,eAAO,MAAM,SAAS,GAAI,QAAQ,MAAM;;;GAKvC,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,WAiDtC,CAAC"}
|
package/dist/utils.js
DELETED
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
import Swal from "sweetalert2";
|
|
2
|
-
import Cookies from "js-cookie";
|
|
3
|
-
import { jwtDecode } from "jwt-decode";
|
|
4
|
-
export const capitalizeEachWord = (str) => {
|
|
5
|
-
if (!str)
|
|
6
|
-
return ''; // Handle empty or null strings
|
|
7
|
-
return str
|
|
8
|
-
.split(' ')
|
|
9
|
-
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
10
|
-
.join(' ');
|
|
11
|
-
};
|
|
12
|
-
export const errorLog = (err, show) => {
|
|
13
|
-
let mes = "An unknown error occurred";
|
|
14
|
-
if (typeof err === "string")
|
|
15
|
-
mes = err;
|
|
16
|
-
else if (err?.graphQLErrors?.length > 0)
|
|
17
|
-
mes = err.graphQLErrors.map((e) => e.message).join('\n');
|
|
18
|
-
else if (err?.networkError) {
|
|
19
|
-
const netErr = err.networkError;
|
|
20
|
-
if ("result" in netErr && netErr.result?.errors?.length > 0)
|
|
21
|
-
mes = netErr.result.errors.map((e) => e.message).join('\n');
|
|
22
|
-
else if (netErr.message)
|
|
23
|
-
mes = netErr.message;
|
|
24
|
-
}
|
|
25
|
-
else if (err?.extraInfo)
|
|
26
|
-
mes = String(err.extraInfo);
|
|
27
|
-
else if (err?.message)
|
|
28
|
-
mes = err.message;
|
|
29
|
-
if (show) {
|
|
30
|
-
Swal.fire({
|
|
31
|
-
title: mes,
|
|
32
|
-
icon: 'error',
|
|
33
|
-
timer: 3000,
|
|
34
|
-
timerProgressBar: true,
|
|
35
|
-
showConfirmButton: false,
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
return mes;
|
|
39
|
-
};
|
|
40
|
-
const currentYear = new Date().getFullYear();
|
|
41
|
-
export const getToken = () => {
|
|
42
|
-
if (typeof window === "undefined") {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
return Cookies.get("token") || localStorage.getItem("token");
|
|
46
|
-
};
|
|
47
|
-
export const getUser = () => {
|
|
48
|
-
// const token = typeof window !== 'undefined' ? localStorage.getItem("token") : null;
|
|
49
|
-
const token = getToken();
|
|
50
|
-
const user = token ? jwtDecode(token) : null;
|
|
51
|
-
return user;
|
|
52
|
-
};
|
|
53
|
-
export const getStoredFont = (FONTS) => {
|
|
54
|
-
if (typeof window === "undefined")
|
|
55
|
-
return FONTS[0]; // Default for SSR
|
|
56
|
-
const up = localStorage.getItem("user-pref");
|
|
57
|
-
if (!up)
|
|
58
|
-
return FONTS[5]; // Default to Ubuntu (6th index)
|
|
59
|
-
const fontIndex = parseInt(up[1]) - 1;
|
|
60
|
-
return FONTS[fontIndex] || FONTS[5];
|
|
61
|
-
};
|
|
62
|
-
export const getTimeAgo = (dateStr, type) => {
|
|
63
|
-
if (!dateStr)
|
|
64
|
-
return "";
|
|
65
|
-
const now = new Date();
|
|
66
|
-
const item = new Date(dateStr);
|
|
67
|
-
let diffMs = 0;
|
|
68
|
-
if (type === "past")
|
|
69
|
-
diffMs = now.getTime() - item.getTime();
|
|
70
|
-
if (type === "future")
|
|
71
|
-
diffMs = item.getTime() - now.getTime();
|
|
72
|
-
// Handle invalid date ranges
|
|
73
|
-
if (diffMs < 0) {
|
|
74
|
-
return type === "past" ? "just now" : "Closed";
|
|
75
|
-
}
|
|
76
|
-
const seconds = Math.floor(diffMs / 1000);
|
|
77
|
-
const minutes = Math.floor(seconds / 60);
|
|
78
|
-
const hours = Math.floor(minutes / 60);
|
|
79
|
-
const days = Math.floor(hours / 24);
|
|
80
|
-
const weeks = Math.floor(days / 7);
|
|
81
|
-
const months = Math.floor(days / 30.44); // Use average month length
|
|
82
|
-
const years = Math.floor(days / 365.25); // Account for leap years
|
|
83
|
-
const suffix = type === "past" ? "ago" : "left";
|
|
84
|
-
// 1. Logic for Past Dates (e.g., Posted 2 days ago)
|
|
85
|
-
if (type === "past") {
|
|
86
|
-
if (days === 0)
|
|
87
|
-
return "Today";
|
|
88
|
-
if (days === 1)
|
|
89
|
-
return "Yesterday";
|
|
90
|
-
if (days < 7)
|
|
91
|
-
return `${days} days ago`;
|
|
92
|
-
if (weeks < 5)
|
|
93
|
-
return `${weeks} week${weeks > 1 ? "s" : ""} ago`;
|
|
94
|
-
if (months < 12)
|
|
95
|
-
return `${months} month${months > 1 ? "s" : ""} ago`;
|
|
96
|
-
return `${years} year${years > 1 ? "s" : ""} ago`;
|
|
97
|
-
}
|
|
98
|
-
// 2. Logic for Future Dates (e.g., 5 days left)
|
|
99
|
-
if (days === 0)
|
|
100
|
-
return "Ends today";
|
|
101
|
-
if (days === 1)
|
|
102
|
-
return "1 day left";
|
|
103
|
-
if (days < 7)
|
|
104
|
-
return `${days} days left`;
|
|
105
|
-
if (weeks < 5)
|
|
106
|
-
return `${weeks} week${weeks > 1 ? "s" : ""} left`;
|
|
107
|
-
if (months < 12)
|
|
108
|
-
return `${months} month${months > 1 ? "s" : ""} left`;
|
|
109
|
-
return `${years} year${years > 1 ? "s" : ""} left`;
|
|
110
|
-
};
|
|
111
|
-
export const getLanguage = () => {
|
|
112
|
-
if (typeof window === "undefined")
|
|
113
|
-
return "en";
|
|
114
|
-
const up = localStorage.getItem("user-pref");
|
|
115
|
-
if (!up)
|
|
116
|
-
return "en";
|
|
117
|
-
const langNumber = parseInt(up[0]);
|
|
118
|
-
return langNumber === 1 ? "en" : "fr";
|
|
119
|
-
};
|
|
120
|
-
export const getTemplate = () => {
|
|
121
|
-
if (typeof window === "undefined")
|
|
122
|
-
return 1;
|
|
123
|
-
const up = localStorage.getItem("user-pref");
|
|
124
|
-
if (!up)
|
|
125
|
-
return 1;
|
|
126
|
-
return parseInt(up[2]);
|
|
127
|
-
};
|
|
128
|
-
export const decodeUrlID = (urlID) => {
|
|
129
|
-
const base64DecodedString = decodeURIComponent(urlID); // Decodes %3D%3D to ==
|
|
130
|
-
const id = Buffer.from(base64DecodedString, 'base64').toString('utf-8'); // Decoding from base64
|
|
131
|
-
return id.split(":")[1];
|
|
132
|
-
};
|
|
133
|
-
export function getAcademicYear() {
|
|
134
|
-
const today = new Date();
|
|
135
|
-
const year = today.getFullYear();
|
|
136
|
-
const month = today.getMonth(); // 0 = January, 7 = August
|
|
137
|
-
if (month < 7) {
|
|
138
|
-
return `${year - 1}/${year}`;
|
|
139
|
-
}
|
|
140
|
-
else {
|
|
141
|
-
return `${year}/${year + 1}`;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
export const removeEmptyFields = (obj) => {
|
|
145
|
-
const newObj = {};
|
|
146
|
-
for (const key in obj) {
|
|
147
|
-
// Keep File objects and non-empty values
|
|
148
|
-
if (obj[key] instanceof File ||
|
|
149
|
-
(obj[key] !== null && obj[key] !== undefined && obj[key] !== '')) {
|
|
150
|
-
newObj[key] = obj[key];
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return newObj;
|
|
154
|
-
};
|
|
155
|
-
export const Alert = ({ title, duration, status = true }) => {
|
|
156
|
-
Swal.fire({
|
|
157
|
-
title: capitalizeEachWord(title),
|
|
158
|
-
timer: duration,
|
|
159
|
-
timerProgressBar: true,
|
|
160
|
-
showConfirmButton: false,
|
|
161
|
-
icon: status ? 'success' : 'error',
|
|
162
|
-
});
|
|
163
|
-
};
|
|
164
|
-
export const validateFormFields = (formData, currentStepFields) => {
|
|
165
|
-
const newErrors = {};
|
|
166
|
-
// 1. Flatten the fields from all rows in the step
|
|
167
|
-
const fieldsToValidate = currentStepFields.flatMap(row => row.fields);
|
|
168
|
-
fieldsToValidate.forEach((field) => {
|
|
169
|
-
// Skip validation if the field is hidden
|
|
170
|
-
if (field.show === false)
|
|
171
|
-
return;
|
|
172
|
-
// 2. Get the value from formData
|
|
173
|
-
const value = formData[field.name];
|
|
174
|
-
// 3. Check Required
|
|
175
|
-
if (field.required && (!value || value.toString().trim() === "")) {
|
|
176
|
-
newErrors[field.name] = `${capitalizeEachWord(field.label)} is required`;
|
|
177
|
-
// return;
|
|
178
|
-
}
|
|
179
|
-
// 4. Check Type (Email)
|
|
180
|
-
if (field.type === 'email' && value) {
|
|
181
|
-
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
182
|
-
if (!emailRegex.test(value)) {
|
|
183
|
-
newErrors[field.name] = "Invalid email format";
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
// 5. Check Numbers (Min/Max)
|
|
187
|
-
if (field.type === 'number' && value) {
|
|
188
|
-
const numValue = Number(value);
|
|
189
|
-
if (field.min !== undefined && numValue < field.min) {
|
|
190
|
-
newErrors[field.name] = `Minimum value is ${field.min}`;
|
|
191
|
-
}
|
|
192
|
-
if (field.max !== undefined && numValue > field.max) {
|
|
193
|
-
newErrors[field.name] = `Maximum value is ${field.max}`;
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
// 6. Check Length (Strings)
|
|
197
|
-
if (field.type === 'text' && value) {
|
|
198
|
-
if (field.min !== undefined && value.length < field.min) {
|
|
199
|
-
newErrors[field.name] = `Too short (min ${field.min} chars)`;
|
|
200
|
-
}
|
|
201
|
-
if (field.max !== undefined && value.length > field.max) {
|
|
202
|
-
newErrors[field.name] = `Too long (min ${field.max} chars)`;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
return newErrors;
|
|
207
|
-
};
|
|
208
|
-
export const lastYears = (number) => {
|
|
209
|
-
return Array.from({ length: number }, (_, i) => ({
|
|
210
|
-
value: (currentYear - i).toString(),
|
|
211
|
-
label: (currentYear - i).toString()
|
|
212
|
-
})).sort((a, b) => Number(b.value) - Number(a.value));
|
|
213
|
-
};
|
|
214
|
-
export const formatText = (text) => {
|
|
215
|
-
return text;
|
|
216
|
-
if (!text || typeof text !== "string")
|
|
217
|
-
return "";
|
|
218
|
-
return text
|
|
219
|
-
// Ensure space after "." if it's not part of abbreviation/number
|
|
220
|
-
.replace(/\.(?!\s|$|[a-zA-Z0-9])/g, ". ")
|
|
221
|
-
// Split sentences by ". " but preserve delimiter
|
|
222
|
-
.split(/(\. )/g)
|
|
223
|
-
.map((segment) => {
|
|
224
|
-
if (segment === ". ")
|
|
225
|
-
return segment; // keep delimiter
|
|
226
|
-
return segment
|
|
227
|
-
.split(" ")
|
|
228
|
-
.map((word, i) => {
|
|
229
|
-
if (word === "")
|
|
230
|
-
return word;
|
|
231
|
-
// Preserve existing ALL CAPS words
|
|
232
|
-
if (word.length > 1 && /^[A-Z]+$/.test(word)) {
|
|
233
|
-
return word;
|
|
234
|
-
}
|
|
235
|
-
// Abbreviations like g.a, n.l.n
|
|
236
|
-
if (/^([a-zA-Z]\.)+[a-zA-Z]?$/.test(word)) {
|
|
237
|
-
return word.toUpperCase(); // → G.A, N.L.N
|
|
238
|
-
}
|
|
239
|
-
// Version numbers like 3.0.1
|
|
240
|
-
if (/^\d+(\.\d+)+$/.test(word)) {
|
|
241
|
-
return word;
|
|
242
|
-
}
|
|
243
|
-
// Mixed abbrev/numbers (like p.m2.5 or covid-19 v2.0)
|
|
244
|
-
if (/[a-zA-Z]\.\d/.test(word) || /\d+\.\d+[a-zA-Z]?/.test(word)) {
|
|
245
|
-
return word; // leave as-is
|
|
246
|
-
}
|
|
247
|
-
// First word of sentence → capitalize first letter
|
|
248
|
-
if (i === 0) {
|
|
249
|
-
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
|
|
250
|
-
}
|
|
251
|
-
// Other words → lowercase
|
|
252
|
-
return word.toLowerCase();
|
|
253
|
-
})
|
|
254
|
-
.join(" ");
|
|
255
|
-
})
|
|
256
|
-
.join("");
|
|
257
|
-
};
|