@ph-cms/client-sdk 0.1.6 → 0.1.8
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 +266 -160
- package/dist/auth/base-provider.d.ts +151 -0
- package/dist/auth/base-provider.js +210 -0
- package/dist/auth/firebase-provider.d.ts +46 -12
- package/dist/auth/firebase-provider.js +54 -42
- package/dist/auth/interfaces.d.ts +25 -2
- package/dist/auth/jwt-utils.d.ts +53 -0
- package/dist/auth/jwt-utils.js +85 -0
- package/dist/auth/local-provider.d.ts +34 -17
- package/dist/auth/local-provider.js +45 -39
- package/dist/client.d.ts +23 -0
- package/dist/client.js +116 -26
- package/dist/context.d.ts +12 -0
- package/dist/context.js +19 -7
- package/dist/hooks/useAuth.d.ts +26 -29
- package/dist/hooks/useAuth.js +6 -6
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/modules/auth.d.ts +2 -2
- package/dist/modules/auth.js +11 -20
- package/dist/types.d.ts +15 -1
- package/dist/types.js +7 -15
- package/package.json +1 -1
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Lightweight JWT utilities for client-side token inspection.
|
|
4
|
+
*
|
|
5
|
+
* These helpers parse the **payload** of a JWT (the middle segment) using
|
|
6
|
+
* plain base64 decoding. They do NOT verify the signature — that is the
|
|
7
|
+
* server's responsibility. The client only needs to know *when* a token
|
|
8
|
+
* expires so it can proactively refresh before sending an API request.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.decodeJwtPayload = decodeJwtPayload;
|
|
12
|
+
exports.getTokenExpirationMs = getTokenExpirationMs;
|
|
13
|
+
exports.isTokenExpired = isTokenExpired;
|
|
14
|
+
exports.getTokenTTL = getTokenTTL;
|
|
15
|
+
/**
|
|
16
|
+
* Decode the payload (second segment) of a JWT string.
|
|
17
|
+
*
|
|
18
|
+
* Returns `null` if the token is malformed or cannot be decoded.
|
|
19
|
+
* This function works in both browser and Node.js environments.
|
|
20
|
+
*/
|
|
21
|
+
function decodeJwtPayload(token) {
|
|
22
|
+
try {
|
|
23
|
+
const parts = token.split('.');
|
|
24
|
+
if (parts.length !== 3)
|
|
25
|
+
return null;
|
|
26
|
+
const base64Url = parts[1];
|
|
27
|
+
// Convert base64url → base64
|
|
28
|
+
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
|
29
|
+
let jsonStr;
|
|
30
|
+
if (typeof atob === 'function') {
|
|
31
|
+
// Browser (and Node >= 16 with global atob)
|
|
32
|
+
jsonStr = atob(base64);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// Fallback for older Node.js environments
|
|
36
|
+
jsonStr = Buffer.from(base64, 'base64').toString('utf-8');
|
|
37
|
+
}
|
|
38
|
+
return JSON.parse(jsonStr);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Returns the expiration time of a JWT as a Unix timestamp **in milliseconds**.
|
|
46
|
+
*
|
|
47
|
+
* Returns `null` if the token is malformed or does not contain an `exp` claim.
|
|
48
|
+
*/
|
|
49
|
+
function getTokenExpirationMs(token) {
|
|
50
|
+
const payload = decodeJwtPayload(token);
|
|
51
|
+
if (!payload || typeof payload.exp !== 'number')
|
|
52
|
+
return null;
|
|
53
|
+
// JWT `exp` is in seconds; convert to milliseconds.
|
|
54
|
+
return payload.exp * 1000;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Check whether a token has expired (or will expire within `bufferMs`).
|
|
58
|
+
*
|
|
59
|
+
* @param token Raw JWT string
|
|
60
|
+
* @param bufferMs Grace period in milliseconds. If the token expires within
|
|
61
|
+
* this window it is considered "expired" so the caller can
|
|
62
|
+
* refresh proactively. Defaults to **60 000 ms (1 minute)**.
|
|
63
|
+
* @returns
|
|
64
|
+
* - `true` — the token is expired or will expire within `bufferMs`
|
|
65
|
+
* - `false` — the token is still valid beyond the buffer window
|
|
66
|
+
* - `null` — the token could not be parsed (treat as expired for safety)
|
|
67
|
+
*/
|
|
68
|
+
function isTokenExpired(token, bufferMs = 60000) {
|
|
69
|
+
const expiresAtMs = getTokenExpirationMs(token);
|
|
70
|
+
if (expiresAtMs === null)
|
|
71
|
+
return null;
|
|
72
|
+
return Date.now() >= expiresAtMs - bufferMs;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Returns the number of milliseconds until the token expires.
|
|
76
|
+
*
|
|
77
|
+
* Returns `null` if the token is malformed.
|
|
78
|
+
* Returns a negative value if the token is already expired.
|
|
79
|
+
*/
|
|
80
|
+
function getTokenTTL(token) {
|
|
81
|
+
const expiresAtMs = getTokenExpirationMs(token);
|
|
82
|
+
if (expiresAtMs === null)
|
|
83
|
+
return null;
|
|
84
|
+
return expiresAtMs - Date.now();
|
|
85
|
+
}
|
|
@@ -1,19 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
import { BaseAuthProvider } from './base-provider';
|
|
2
|
+
/**
|
|
3
|
+
* Authentication provider that stores PH-CMS tokens in `localStorage`.
|
|
4
|
+
*
|
|
5
|
+
* This is the default provider for applications that use email/password
|
|
6
|
+
* or any non-Firebase authentication flow.
|
|
7
|
+
*
|
|
8
|
+
* Token lifecycle:
|
|
9
|
+
* 1. On `getToken()`, the stored access token's `exp` claim is checked.
|
|
10
|
+
* 2. If the token is expired (or will expire within `expiryBufferMs`),
|
|
11
|
+
* an automatic refresh is attempted via the registered `refreshFn`.
|
|
12
|
+
* 3. If refresh fails, tokens are cleared and the `onExpiredCallback` is
|
|
13
|
+
* invoked so the UI layer can redirect to a login page.
|
|
14
|
+
*/
|
|
15
|
+
export declare class LocalAuthProvider extends BaseAuthProvider {
|
|
16
|
+
readonly type: "LOCAL";
|
|
17
|
+
constructor(storageKeyPrefix?: string, options?: {
|
|
18
|
+
/**
|
|
19
|
+
* How many ms before actual expiration the token is considered expired.
|
|
20
|
+
* @default 60_000 (1 minute)
|
|
21
|
+
*/
|
|
22
|
+
expiryBufferMs?: number;
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* Returns a valid access token.
|
|
26
|
+
*
|
|
27
|
+
* Before returning the stored access token this method checks its `exp`
|
|
28
|
+
* claim. If the token is expired (or will expire within `expiryBufferMs`)
|
|
29
|
+
* **and** a refresh function has been registered via `setRefreshFn()`,
|
|
30
|
+
* it automatically refreshes the token pair.
|
|
31
|
+
*
|
|
32
|
+
* Concurrent callers share the same in-flight refresh promise so that
|
|
33
|
+
* multiple parallel requests don't each trigger their own refresh.
|
|
34
|
+
*/
|
|
15
35
|
getToken(): Promise<string | null>;
|
|
16
|
-
onTokenExpired(callback: () => Promise<void>): void;
|
|
17
|
-
logout(): Promise<void>;
|
|
18
|
-
getRefreshToken(): string | null;
|
|
19
36
|
}
|
|
@@ -1,49 +1,55 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.LocalAuthProvider = void 0;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
const base_provider_1 = require("./base-provider");
|
|
5
|
+
/**
|
|
6
|
+
* Authentication provider that stores PH-CMS tokens in `localStorage`.
|
|
7
|
+
*
|
|
8
|
+
* This is the default provider for applications that use email/password
|
|
9
|
+
* or any non-Firebase authentication flow.
|
|
10
|
+
*
|
|
11
|
+
* Token lifecycle:
|
|
12
|
+
* 1. On `getToken()`, the stored access token's `exp` claim is checked.
|
|
13
|
+
* 2. If the token is expired (or will expire within `expiryBufferMs`),
|
|
14
|
+
* an automatic refresh is attempted via the registered `refreshFn`.
|
|
15
|
+
* 3. If refresh fails, tokens are cleared and the `onExpiredCallback` is
|
|
16
|
+
* invoked so the UI layer can redirect to a login page.
|
|
17
|
+
*/
|
|
18
|
+
class LocalAuthProvider extends base_provider_1.BaseAuthProvider {
|
|
19
|
+
constructor(storageKeyPrefix = 'ph_cms_', options) {
|
|
20
|
+
super(storageKeyPrefix, options?.expiryBufferMs);
|
|
8
21
|
this.type = 'LOCAL';
|
|
9
|
-
this.accessToken = null;
|
|
10
|
-
this.refreshToken = null;
|
|
11
|
-
this.onExpiredCallback = null;
|
|
12
|
-
if (typeof window !== 'undefined') {
|
|
13
|
-
this.accessToken = localStorage.getItem(`${this.storageKeyPrefix}access_token`);
|
|
14
|
-
this.refreshToken = localStorage.getItem(`${this.storageKeyPrefix}refresh_token`);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
setTokens(accessToken, refreshToken) {
|
|
18
|
-
this.accessToken = accessToken;
|
|
19
|
-
this.refreshToken = refreshToken;
|
|
20
|
-
if (typeof window !== 'undefined') {
|
|
21
|
-
localStorage.setItem(`${this.storageKeyPrefix}access_token`, accessToken);
|
|
22
|
-
localStorage.setItem(`${this.storageKeyPrefix}refresh_token`, refreshToken);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
hasToken() {
|
|
26
|
-
return this.accessToken !== null || this.refreshToken !== null;
|
|
27
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Returns a valid access token.
|
|
25
|
+
*
|
|
26
|
+
* Before returning the stored access token this method checks its `exp`
|
|
27
|
+
* claim. If the token is expired (or will expire within `expiryBufferMs`)
|
|
28
|
+
* **and** a refresh function has been registered via `setRefreshFn()`,
|
|
29
|
+
* it automatically refreshes the token pair.
|
|
30
|
+
*
|
|
31
|
+
* Concurrent callers share the same in-flight refresh promise so that
|
|
32
|
+
* multiple parallel requests don't each trigger their own refresh.
|
|
33
|
+
*/
|
|
28
34
|
async getToken() {
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
// Fast path: no access token at all.
|
|
36
|
+
if (!this.accessToken)
|
|
37
|
+
return null;
|
|
38
|
+
// Check expiration.
|
|
39
|
+
const expired = this.isCurrentTokenExpired();
|
|
40
|
+
// `expired === null` means the token could not be parsed — treat as
|
|
41
|
+
// potentially valid and let the server decide (it might still be fine
|
|
42
|
+
// if the format changed, or the server will 401 and the interceptor
|
|
43
|
+
// will handle it).
|
|
44
|
+
if (expired === true) {
|
|
45
|
+
const refreshed = await this.tryRefresh();
|
|
46
|
+
if (refreshed)
|
|
47
|
+
return refreshed;
|
|
48
|
+
// Refresh failed — return null so the request goes out without a
|
|
49
|
+
// token (will result in 401 which the interceptor can handle).
|
|
50
|
+
return null;
|
|
42
51
|
}
|
|
43
|
-
|
|
44
|
-
// Helper for the client to trigger refresh manually if needed
|
|
45
|
-
getRefreshToken() {
|
|
46
|
-
return this.refreshToken;
|
|
52
|
+
return this.accessToken;
|
|
47
53
|
}
|
|
48
54
|
}
|
|
49
55
|
exports.LocalAuthProvider = LocalAuthProvider;
|
package/dist/client.d.ts
CHANGED
|
@@ -19,7 +19,30 @@ export declare class PHCMSClient {
|
|
|
19
19
|
readonly channel: ChannelModule;
|
|
20
20
|
readonly terms: TermsModule;
|
|
21
21
|
readonly media: MediaModule;
|
|
22
|
+
/**
|
|
23
|
+
* Whether a token refresh is currently in flight (used by the 401
|
|
24
|
+
* interceptor to de-duplicate concurrent refresh attempts).
|
|
25
|
+
*/
|
|
26
|
+
private isRefreshing;
|
|
27
|
+
/**
|
|
28
|
+
* Queue of requests waiting for the current refresh to complete.
|
|
29
|
+
* Each entry will be resolved/rejected once the single in-flight
|
|
30
|
+
* refresh call finishes.
|
|
31
|
+
*/
|
|
32
|
+
private refreshQueue;
|
|
22
33
|
/** Exposes the auth provider so UI layers can check token state synchronously. */
|
|
23
34
|
get authProvider(): AuthProvider | undefined;
|
|
24
35
|
constructor(config: PHCMSClientConfig);
|
|
36
|
+
/**
|
|
37
|
+
* If a refresh is already in flight, queue the caller. Otherwise kick
|
|
38
|
+
* off a fresh refresh request and resolve all queued callers when it
|
|
39
|
+
* completes.
|
|
40
|
+
*
|
|
41
|
+
* This method reuses the **provider's** `tryRefresh()` path (via
|
|
42
|
+
* `getToken()` → proactive refresh) when possible. However since we
|
|
43
|
+
* arrive here because the access token already failed server-side
|
|
44
|
+
* validation (401), we perform an explicit refresh using the raw
|
|
45
|
+
* axios instance with `_skipAuth` to avoid circular `getToken()` calls.
|
|
46
|
+
*/
|
|
47
|
+
private coordinatedRefresh;
|
|
25
48
|
}
|
package/dist/client.js
CHANGED
|
@@ -18,6 +18,17 @@ class PHCMSClient {
|
|
|
18
18
|
}
|
|
19
19
|
constructor(config) {
|
|
20
20
|
this.config = config;
|
|
21
|
+
/**
|
|
22
|
+
* Whether a token refresh is currently in flight (used by the 401
|
|
23
|
+
* interceptor to de-duplicate concurrent refresh attempts).
|
|
24
|
+
*/
|
|
25
|
+
this.isRefreshing = false;
|
|
26
|
+
/**
|
|
27
|
+
* Queue of requests waiting for the current refresh to complete.
|
|
28
|
+
* Each entry will be resolved/rejected once the single in-flight
|
|
29
|
+
* refresh call finishes.
|
|
30
|
+
*/
|
|
31
|
+
this.refreshQueue = [];
|
|
21
32
|
const normalizedApiPrefix = `/${(config.apiPrefix || '/api').replace(/^\/+|\/+$/g, '')}`;
|
|
22
33
|
this.axiosInstance = axios_1.default.create({
|
|
23
34
|
baseURL: config.baseURL,
|
|
@@ -26,8 +37,33 @@ class PHCMSClient {
|
|
|
26
37
|
'Content-Type': 'application/json',
|
|
27
38
|
},
|
|
28
39
|
});
|
|
29
|
-
//
|
|
40
|
+
// Initialize Modules (before interceptors so the refresh wiring
|
|
41
|
+
// can reference `this.auth`).
|
|
42
|
+
this.auth = new auth_1.AuthModule(this.axiosInstance, config.auth);
|
|
43
|
+
this.content = new content_1.ContentModule(this.axiosInstance);
|
|
44
|
+
this.channel = new channel_1.ChannelModule(this.axiosInstance);
|
|
45
|
+
this.terms = new terms_1.TermsModule(this.axiosInstance);
|
|
46
|
+
this.media = new media_1.MediaModule(this.axiosInstance);
|
|
47
|
+
// Wire the refresh function into the auth provider so it can
|
|
48
|
+
// proactively refresh tokens inside `getToken()` without needing
|
|
49
|
+
// a direct reference to the AuthModule / axios instance.
|
|
50
|
+
//
|
|
51
|
+
// The `_skipAuth` flag ensures the refresh request itself does NOT
|
|
52
|
+
// pass through the "attach auth token" logic in the request
|
|
53
|
+
// interceptor, avoiding a recursive `getToken()` call.
|
|
54
|
+
if (config.auth) {
|
|
55
|
+
config.auth.setRefreshFn(async (refreshToken) => {
|
|
56
|
+
const response = await this.axiosInstance.post('/api/auth/refresh', { refreshToken }, { _skipAuth: true });
|
|
57
|
+
// The response interceptor unwraps `response.data`, so at
|
|
58
|
+
// this point `response` is already the data payload.
|
|
59
|
+
return response;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// ---------------------------------------------------------------
|
|
63
|
+
// Request Interceptor: Rewrite API prefix & attach auth token
|
|
64
|
+
// ---------------------------------------------------------------
|
|
30
65
|
this.axiosInstance.interceptors.request.use(async (reqConfig) => {
|
|
66
|
+
// --- URL rewriting ---
|
|
31
67
|
if (typeof reqConfig.url === 'string') {
|
|
32
68
|
if (reqConfig.url === '/api') {
|
|
33
69
|
reqConfig.url = normalizedApiPrefix;
|
|
@@ -36,7 +72,9 @@ class PHCMSClient {
|
|
|
36
72
|
reqConfig.url = `${normalizedApiPrefix}${reqConfig.url.slice('/api'.length)}`;
|
|
37
73
|
}
|
|
38
74
|
}
|
|
39
|
-
|
|
75
|
+
// --- Auth header ---
|
|
76
|
+
// Skip auth for requests explicitly flagged (e.g. refresh calls)
|
|
77
|
+
if (!reqConfig._skipAuth && this.config.auth) {
|
|
40
78
|
const token = await this.config.auth.getToken();
|
|
41
79
|
if (token) {
|
|
42
80
|
reqConfig.headers.Authorization = `Bearer ${token}`;
|
|
@@ -44,37 +82,89 @@ class PHCMSClient {
|
|
|
44
82
|
}
|
|
45
83
|
return reqConfig;
|
|
46
84
|
});
|
|
47
|
-
//
|
|
85
|
+
// ---------------------------------------------------------------
|
|
86
|
+
// Response Interceptor: Unwrap data, handle errors, 401 auto-retry
|
|
87
|
+
// ---------------------------------------------------------------
|
|
48
88
|
this.axiosInstance.interceptors.response.use((response) => {
|
|
49
|
-
// Return the data directly, stripping Axios wrapper
|
|
89
|
+
// Return the data directly, stripping the Axios wrapper.
|
|
50
90
|
return response.data;
|
|
51
91
|
}, async (error) => {
|
|
52
|
-
if (error.response) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const message = data?.message || error.message;
|
|
56
|
-
// Handle 401 Token Expiry if provider supports it
|
|
57
|
-
if (status === 401 && this.config.auth) {
|
|
58
|
-
// Logic to handle token refresh could be here or triggered by the app.
|
|
59
|
-
// For now just throw.
|
|
92
|
+
if (!error.response) {
|
|
93
|
+
if (error.request) {
|
|
94
|
+
throw new errors_1.PHCMSError('No response received from server');
|
|
60
95
|
}
|
|
61
|
-
throw new errors_1.ApiError(message, status, data);
|
|
62
|
-
}
|
|
63
|
-
else if (error.request) {
|
|
64
|
-
// Request made but no response
|
|
65
|
-
throw new errors_1.PHCMSError('No response received from server');
|
|
66
|
-
}
|
|
67
|
-
else {
|
|
68
|
-
// Something happened in setting up the request
|
|
69
96
|
throw new errors_1.PHCMSError(error.message);
|
|
70
97
|
}
|
|
98
|
+
const { status, data } = error.response;
|
|
99
|
+
const message = data?.message || error.message;
|
|
100
|
+
const originalRequest = error.config;
|
|
101
|
+
// ----------------------------------------------------------
|
|
102
|
+
// 401 handling: attempt one transparent token refresh
|
|
103
|
+
// ----------------------------------------------------------
|
|
104
|
+
if (status === 401 &&
|
|
105
|
+
this.config.auth &&
|
|
106
|
+
!originalRequest._retry &&
|
|
107
|
+
!originalRequest._skipAuth // don't retry refresh requests
|
|
108
|
+
) {
|
|
109
|
+
originalRequest._retry = true;
|
|
110
|
+
const refreshToken = this.config.auth.getRefreshToken();
|
|
111
|
+
if (refreshToken) {
|
|
112
|
+
try {
|
|
113
|
+
const newToken = await this.coordinatedRefresh(refreshToken);
|
|
114
|
+
if (newToken) {
|
|
115
|
+
// Re-issue the original request with the fresh token.
|
|
116
|
+
originalRequest.headers.Authorization = `Bearer ${newToken}`;
|
|
117
|
+
return this.axiosInstance(originalRequest);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Refresh itself failed — fall through to throw ApiError.
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
throw new errors_1.ApiError(message, status, data);
|
|
71
126
|
});
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
127
|
+
}
|
|
128
|
+
// -------------------------------------------------------------------
|
|
129
|
+
// Private: coordinated token refresh (de-duplicates concurrent 401s)
|
|
130
|
+
// -------------------------------------------------------------------
|
|
131
|
+
/**
|
|
132
|
+
* If a refresh is already in flight, queue the caller. Otherwise kick
|
|
133
|
+
* off a fresh refresh request and resolve all queued callers when it
|
|
134
|
+
* completes.
|
|
135
|
+
*
|
|
136
|
+
* This method reuses the **provider's** `tryRefresh()` path (via
|
|
137
|
+
* `getToken()` → proactive refresh) when possible. However since we
|
|
138
|
+
* arrive here because the access token already failed server-side
|
|
139
|
+
* validation (401), we perform an explicit refresh using the raw
|
|
140
|
+
* axios instance with `_skipAuth` to avoid circular `getToken()` calls.
|
|
141
|
+
*/
|
|
142
|
+
async coordinatedRefresh(refreshToken) {
|
|
143
|
+
if (this.isRefreshing) {
|
|
144
|
+
// Another request already triggered a refresh — wait for it.
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
this.refreshQueue.push({ resolve, reject });
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
this.isRefreshing = true;
|
|
150
|
+
try {
|
|
151
|
+
const result = await this.axiosInstance.post('/api/auth/refresh', { refreshToken }, { _skipAuth: true });
|
|
152
|
+
// Store the new tokens in the provider.
|
|
153
|
+
this.config.auth.setTokens(result.accessToken, result.refreshToken);
|
|
154
|
+
// Resolve all queued callers.
|
|
155
|
+
this.refreshQueue.forEach((q) => q.resolve(result.accessToken));
|
|
156
|
+
this.refreshQueue = [];
|
|
157
|
+
return result.accessToken;
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
// Notify all queued callers of the failure.
|
|
161
|
+
this.refreshQueue.forEach((q) => q.reject(err));
|
|
162
|
+
this.refreshQueue = [];
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
this.isRefreshing = false;
|
|
167
|
+
}
|
|
78
168
|
}
|
|
79
169
|
}
|
|
80
170
|
exports.PHCMSClient = PHCMSClient;
|
package/dist/context.d.ts
CHANGED
|
@@ -2,11 +2,23 @@ import { UserDto } from '@ph-cms/api-contract';
|
|
|
2
2
|
import { QueryClient } from '@tanstack/react-query';
|
|
3
3
|
import React, { ReactNode } from 'react';
|
|
4
4
|
import { PHCMSClient } from './client';
|
|
5
|
+
export type AuthStatus = 'loading' | 'authenticated' | 'unauthenticated';
|
|
5
6
|
export interface PHCMSContextType {
|
|
6
7
|
client: PHCMSClient;
|
|
7
8
|
user: UserDto | null;
|
|
8
9
|
isAuthenticated: boolean;
|
|
9
10
|
isLoading: boolean;
|
|
11
|
+
/** Whether the user profile fetch failed. */
|
|
12
|
+
isError: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Fine-grained authentication status.
|
|
15
|
+
* - `'loading'`: Token exists, `/auth/me` is being fetched.
|
|
16
|
+
* - `'authenticated'`: User profile successfully loaded.
|
|
17
|
+
* - `'unauthenticated'`: No token, or token validation failed.
|
|
18
|
+
*/
|
|
19
|
+
authStatus: AuthStatus;
|
|
20
|
+
/** Whether the auth provider currently holds any token (access or refresh). */
|
|
21
|
+
hasToken: boolean;
|
|
10
22
|
/** Manually trigger a refetch of the current user profile. */
|
|
11
23
|
refreshUser: () => Promise<void>;
|
|
12
24
|
}
|
package/dist/context.js
CHANGED
|
@@ -76,13 +76,25 @@ const PHCMSInternalProvider = ({ client, children }) => {
|
|
|
76
76
|
}, 2000); // Check every 2s as a fallback
|
|
77
77
|
return () => clearInterval(interval);
|
|
78
78
|
}, [client.authProvider, hasToken]);
|
|
79
|
-
const value = (0, react_1.useMemo)(() =>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
79
|
+
const value = (0, react_1.useMemo)(() => {
|
|
80
|
+
const authStatus = (() => {
|
|
81
|
+
if (hasToken && isLoading)
|
|
82
|
+
return 'loading';
|
|
83
|
+
if (!!user && !isError && hasToken)
|
|
84
|
+
return 'authenticated';
|
|
85
|
+
return 'unauthenticated';
|
|
86
|
+
})();
|
|
87
|
+
return {
|
|
88
|
+
client,
|
|
89
|
+
user: user || null,
|
|
90
|
+
isAuthenticated: authStatus === 'authenticated',
|
|
91
|
+
isLoading: authStatus === 'loading',
|
|
92
|
+
isError,
|
|
93
|
+
authStatus,
|
|
94
|
+
hasToken,
|
|
95
|
+
refreshUser,
|
|
96
|
+
};
|
|
97
|
+
}, [client, user, isError, isLoading, hasToken, refreshUser]);
|
|
86
98
|
return react_1.default.createElement(PHCMSContext.Provider, { value: value }, children);
|
|
87
99
|
};
|
|
88
100
|
/**
|
package/dist/hooks/useAuth.d.ts
CHANGED
|
@@ -205,34 +205,31 @@ export declare const useUser: () => {
|
|
|
205
205
|
} | null;
|
|
206
206
|
isLoading: boolean;
|
|
207
207
|
isAuthenticated: boolean;
|
|
208
|
+
error: boolean;
|
|
208
209
|
};
|
|
209
|
-
export declare const useLogin: () => {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
uid: string;
|
|
214
|
-
email: string;
|
|
215
|
-
username: string | null;
|
|
216
|
-
display_name: string;
|
|
217
|
-
avatar_url: string | null;
|
|
218
|
-
phone_number: string | null;
|
|
219
|
-
email_verified_at: string | null;
|
|
220
|
-
phone_verified_at: string | null;
|
|
221
|
-
locale: string;
|
|
222
|
-
timezone: string;
|
|
223
|
-
status: string;
|
|
224
|
-
role: string[];
|
|
225
|
-
profile_data: Record<string, any>;
|
|
226
|
-
last_login_at: string | null;
|
|
227
|
-
created_at: string;
|
|
228
|
-
updated_at: string;
|
|
229
|
-
};
|
|
230
|
-
accessToken: string;
|
|
231
|
-
}, Error, {
|
|
210
|
+
export declare const useLogin: () => import("@tanstack/react-query").UseMutationResult<{
|
|
211
|
+
refreshToken: string;
|
|
212
|
+
user: {
|
|
213
|
+
uid: string;
|
|
232
214
|
email: string;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
215
|
+
username: string | null;
|
|
216
|
+
display_name: string;
|
|
217
|
+
avatar_url: string | null;
|
|
218
|
+
phone_number: string | null;
|
|
219
|
+
email_verified_at: string | null;
|
|
220
|
+
phone_verified_at: string | null;
|
|
221
|
+
locale: string;
|
|
222
|
+
timezone: string;
|
|
223
|
+
status: string;
|
|
224
|
+
role: string[];
|
|
225
|
+
profile_data: Record<string, any>;
|
|
226
|
+
last_login_at: string | null;
|
|
227
|
+
created_at: string;
|
|
228
|
+
updated_at: string;
|
|
229
|
+
};
|
|
230
|
+
accessToken: string;
|
|
231
|
+
}, Error, {
|
|
232
|
+
email: string;
|
|
233
|
+
password: string;
|
|
234
|
+
}, unknown>;
|
|
235
|
+
export declare const useLogout: () => import("@tanstack/react-query").UseMutationResult<void, Error, void, unknown>;
|
package/dist/hooks/useAuth.js
CHANGED
|
@@ -55,17 +55,17 @@ const useAuth = () => {
|
|
|
55
55
|
exports.useAuth = useAuth;
|
|
56
56
|
// Keep individual hooks for backward compatibility or more specific use cases
|
|
57
57
|
const useUser = () => {
|
|
58
|
-
const { user, isLoading, isAuthenticated } = (0, context_1.usePHCMSContext)();
|
|
59
|
-
return { data: user, isLoading, isAuthenticated };
|
|
58
|
+
const { user, isLoading, isAuthenticated, isError } = (0, context_1.usePHCMSContext)();
|
|
59
|
+
return { data: user, isLoading, isAuthenticated, error: isError };
|
|
60
60
|
};
|
|
61
61
|
exports.useUser = useUser;
|
|
62
62
|
const useLogin = () => {
|
|
63
|
-
const {
|
|
64
|
-
return
|
|
63
|
+
const { loginStatus } = (0, exports.useAuth)();
|
|
64
|
+
return loginStatus;
|
|
65
65
|
};
|
|
66
66
|
exports.useLogin = useLogin;
|
|
67
67
|
const useLogout = () => {
|
|
68
|
-
const {
|
|
69
|
-
return
|
|
68
|
+
const { logoutStatus } = (0, exports.useAuth)();
|
|
69
|
+
return logoutStatus;
|
|
70
70
|
};
|
|
71
71
|
exports.useLogout = useLogout;
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -14,8 +14,10 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./auth/base-provider"), exports);
|
|
17
18
|
__exportStar(require("./auth/firebase-provider"), exports);
|
|
18
19
|
__exportStar(require("./auth/interfaces"), exports);
|
|
20
|
+
__exportStar(require("./auth/jwt-utils"), exports);
|
|
19
21
|
__exportStar(require("./auth/local-provider"), exports);
|
|
20
22
|
__exportStar(require("./client"), exports);
|
|
21
23
|
__exportStar(require("./errors"), exports);
|
package/dist/modules/auth.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
+
import { AuthResponse, FirebaseExchangeRequest, LoginRequest, RegisterRequest, UserDto } from "@ph-cms/api-contract";
|
|
1
2
|
import { AxiosInstance } from "axios";
|
|
2
3
|
import { AuthProvider } from "../auth/interfaces";
|
|
3
|
-
import { LoginRequest, RegisterRequest, AuthResponse, UserDto, FirebaseExchangeRequest } from "@ph-cms/api-contract";
|
|
4
4
|
export declare class AuthModule {
|
|
5
5
|
private client;
|
|
6
6
|
private provider?;
|
|
7
7
|
constructor(client: AxiosInstance, provider?: AuthProvider | undefined);
|
|
8
8
|
/**
|
|
9
|
-
* Logs in the user and updates the AuthProvider
|
|
9
|
+
* Logs in the user and updates the AuthProvider tokens automatically.
|
|
10
10
|
*/
|
|
11
11
|
login(data: LoginRequest): Promise<AuthResponse>;
|
|
12
12
|
/**
|