@fluoce/auth-react 0.1.0
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 +94 -0
- package/dist/index.cjs +300 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +82 -0
- package/dist/index.d.ts +82 -0
- package/dist/index.js +276 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# @fluoce/auth-react
|
|
2
|
+
|
|
3
|
+
React SDK for Fluoce Auth. Wraps the exchange / me / refresh flow, stores
|
|
4
|
+
tokens in cookies, and gives you `AuthGuard` + `useFluoceAuth()`.
|
|
5
|
+
|
|
6
|
+
## Setup
|
|
7
|
+
|
|
8
|
+
```tsx
|
|
9
|
+
// App.tsx
|
|
10
|
+
import { FluoceAuthProvider } from '@fluoce/auth-react';
|
|
11
|
+
|
|
12
|
+
export function App() {
|
|
13
|
+
return (
|
|
14
|
+
<FluoceAuthProvider
|
|
15
|
+
authUrl="https://auth.fluoce.com"
|
|
16
|
+
apiUrl="https://api.fluoce.com" // your auth backend
|
|
17
|
+
appUrl="https://myapp.com/auth" // this app's callback url (the `ref`)
|
|
18
|
+
>
|
|
19
|
+
<Routes />
|
|
20
|
+
</FluoceAuthProvider>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`appUrl` must be a route in your app that stays mounted under `FluoceAuthProvider`
|
|
26
|
+
(e.g. `/auth`) — auth.fluoce.com redirects back to `${appUrl}?code=...`, and the
|
|
27
|
+
provider picks the `code` param up automatically wherever it appears, exchanges
|
|
28
|
+
it, sets cookies, and strips it from the URL.
|
|
29
|
+
|
|
30
|
+
## Protecting a route
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { AuthGuard } from '@fluoce/auth-react';
|
|
34
|
+
|
|
35
|
+
function DashboardPage() {
|
|
36
|
+
return (
|
|
37
|
+
<AuthGuard fallback={<Spinner />}>
|
|
38
|
+
<Dashboard />
|
|
39
|
+
</AuthGuard>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
If there's no valid session, `AuthGuard` redirects to
|
|
45
|
+
`${authUrl}/auth?ref=${appUrl}` for you — nothing else to wire up.
|
|
46
|
+
|
|
47
|
+
## Reading the user / calling other endpoints
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
import { useFluoceAuth } from '@fluoce/auth-react';
|
|
51
|
+
|
|
52
|
+
function Profile() {
|
|
53
|
+
const { user, loading, logout, authFetch } = useFluoceAuth();
|
|
54
|
+
|
|
55
|
+
if (loading) return <Spinner />;
|
|
56
|
+
|
|
57
|
+
const updateName = (name: string) =>
|
|
58
|
+
authFetch('/me', {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: { 'Content-Type': 'application/json' },
|
|
61
|
+
body: JSON.stringify({ name }),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<div>
|
|
66
|
+
<p>{user?.name ?? user?.email}</p>
|
|
67
|
+
<button onClick={() => logout()}>Log out</button>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`authFetch` is the same authenticated-fetch helper the SDK uses internally for
|
|
74
|
+
`/me` — it auto-refreshes on 401 and redirects to login if the refresh also
|
|
75
|
+
fails. Use it for any endpoint, including ones not wrapped yet
|
|
76
|
+
(`/me/add/email`, `/me/add/phone`, etc.) until they get first-class methods.
|
|
77
|
+
|
|
78
|
+
## Cookies
|
|
79
|
+
|
|
80
|
+
Access/refresh tokens are stored as regular (non-httpOnly — this is
|
|
81
|
+
client-side JS) cookies:
|
|
82
|
+
|
|
83
|
+
- `fluoce_access_token`
|
|
84
|
+
- `fluoce_refresh_token`
|
|
85
|
+
|
|
86
|
+
Override names/expiry via provider props: `accessTokenCookie`,
|
|
87
|
+
`refreshTokenCookie`, `accessTokenExpiryDays`, `refreshTokenExpiryDays`.
|
|
88
|
+
|
|
89
|
+
## Not yet wrapped
|
|
90
|
+
|
|
91
|
+
`/email`, `/phone`, `/google`, `/github`, `/me/add/email`, `/me/add/phone`,
|
|
92
|
+
`/logout/all` (client method exists, no dedicated hook) — call these directly
|
|
93
|
+
via `authFetch` (or plain `fetch` for the pre-login ones like `/email`,
|
|
94
|
+
`/phone`, `/google`, `/github`) until added.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AuthGuard: () => AuthGuard,
|
|
24
|
+
FluoceApiClient: () => FluoceApiClient,
|
|
25
|
+
FluoceAuthProvider: () => FluoceAuthProvider,
|
|
26
|
+
useFluoceAuth: () => useFluoceAuth
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/FluoceAuthProvider.tsx
|
|
31
|
+
var import_react2 = require("react");
|
|
32
|
+
|
|
33
|
+
// src/AuthContext.tsx
|
|
34
|
+
var import_react = require("react");
|
|
35
|
+
var FluoceAuthContext = (0, import_react.createContext)(
|
|
36
|
+
null
|
|
37
|
+
);
|
|
38
|
+
function useFluoceAuth() {
|
|
39
|
+
const ctx = (0, import_react.useContext)(FluoceAuthContext);
|
|
40
|
+
if (!ctx)
|
|
41
|
+
throw new Error("useFluoceAuth must be used within <FluoceAuthProvider>");
|
|
42
|
+
return ctx;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// src/cookies.ts
|
|
46
|
+
function setCookie(name, value, days) {
|
|
47
|
+
const expires = new Date(Date.now() + days * 864e5).toUTCString();
|
|
48
|
+
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/; SameSite=Lax`;
|
|
49
|
+
}
|
|
50
|
+
function getCookie(name) {
|
|
51
|
+
const escaped = name.replace(/([.$?*|{}()[\]\\/+^])/g, "\\$1");
|
|
52
|
+
const match = document.cookie.match(new RegExp("(?:^|; )" + escaped + "=([^;]*)"));
|
|
53
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
54
|
+
}
|
|
55
|
+
function deleteCookie(name) {
|
|
56
|
+
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/apiClient.ts
|
|
60
|
+
var FluoceApiClient = class {
|
|
61
|
+
constructor(config) {
|
|
62
|
+
this.refreshPromise = null;
|
|
63
|
+
/** set by the Provider — called once refresh fails and there's no session left */
|
|
64
|
+
this.onAuthFailure = () => {
|
|
65
|
+
};
|
|
66
|
+
this.config = {
|
|
67
|
+
accessTokenCookie: "fluoce_access_token",
|
|
68
|
+
refreshTokenCookie: "fluoce_refresh_token",
|
|
69
|
+
accessTokenExpiryDays: 1,
|
|
70
|
+
refreshTokenExpiryDays: 30,
|
|
71
|
+
...config
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
getAccessToken() {
|
|
75
|
+
return getCookie(this.config.accessTokenCookie);
|
|
76
|
+
}
|
|
77
|
+
getRefreshToken() {
|
|
78
|
+
return getCookie(this.config.refreshTokenCookie);
|
|
79
|
+
}
|
|
80
|
+
setTokens(tokens) {
|
|
81
|
+
setCookie(
|
|
82
|
+
this.config.accessTokenCookie,
|
|
83
|
+
tokens.accessToken,
|
|
84
|
+
this.config.accessTokenExpiryDays
|
|
85
|
+
);
|
|
86
|
+
setCookie(
|
|
87
|
+
this.config.refreshTokenCookie,
|
|
88
|
+
tokens.refreshToken,
|
|
89
|
+
this.config.refreshTokenExpiryDays
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
clearTokens() {
|
|
93
|
+
deleteCookie(this.config.accessTokenCookie);
|
|
94
|
+
deleteCookie(this.config.refreshTokenCookie);
|
|
95
|
+
}
|
|
96
|
+
buildLoginUrl() {
|
|
97
|
+
const ref = encodeURIComponent(this.config.appUrl);
|
|
98
|
+
return `${this.config.authUrl}/auth?ref=${ref}`;
|
|
99
|
+
}
|
|
100
|
+
async exchangeCode(code) {
|
|
101
|
+
const res = await fetch(`${this.config.apiUrl}/exchange`, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: { "Content-Type": "application/json" },
|
|
104
|
+
body: JSON.stringify({ code })
|
|
105
|
+
});
|
|
106
|
+
if (!res.ok) throw new Error(`exchange failed: ${res.status}`);
|
|
107
|
+
const data = await res.json();
|
|
108
|
+
this.setTokens(data);
|
|
109
|
+
return data;
|
|
110
|
+
}
|
|
111
|
+
async getMe() {
|
|
112
|
+
return this.authFetch("/me");
|
|
113
|
+
}
|
|
114
|
+
async updateMe(payload) {
|
|
115
|
+
return this.authFetch("/me", {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: { "Content-Type": "application/json" },
|
|
118
|
+
body: JSON.stringify(payload)
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
async refresh() {
|
|
122
|
+
if (this.refreshPromise) return this.refreshPromise;
|
|
123
|
+
const refreshToken = this.getRefreshToken();
|
|
124
|
+
if (!refreshToken) return null;
|
|
125
|
+
this.refreshPromise = (async () => {
|
|
126
|
+
try {
|
|
127
|
+
const res = await fetch(`${this.config.apiUrl}/refresh`, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: {
|
|
130
|
+
"Content-Type": "application/json",
|
|
131
|
+
Authorization: `Bearer ${refreshToken}`
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
if (!res.ok) {
|
|
135
|
+
this.clearTokens();
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const data = await res.json();
|
|
139
|
+
this.setTokens(data);
|
|
140
|
+
return data;
|
|
141
|
+
} catch {
|
|
142
|
+
this.clearTokens();
|
|
143
|
+
return null;
|
|
144
|
+
} finally {
|
|
145
|
+
this.refreshPromise = null;
|
|
146
|
+
}
|
|
147
|
+
})();
|
|
148
|
+
return this.refreshPromise;
|
|
149
|
+
}
|
|
150
|
+
async logout(allDevices = false) {
|
|
151
|
+
const refreshToken = this.getRefreshToken();
|
|
152
|
+
if (refreshToken) {
|
|
153
|
+
try {
|
|
154
|
+
await fetch(
|
|
155
|
+
`${this.config.apiUrl}${allDevices ? "/logout/all" : "/logout"}`,
|
|
156
|
+
{
|
|
157
|
+
method: "POST",
|
|
158
|
+
headers: {
|
|
159
|
+
"Content-Type": "application/json",
|
|
160
|
+
Authorization: `Bearer ${refreshToken}`
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
} catch {
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
this.clearTokens();
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Authenticated fetch. Attaches access token, retries once through /refresh
|
|
171
|
+
* on 401, and calls onAuthFailure (-> redirect to auth frontend) if that
|
|
172
|
+
* fails too. Use this for /me, /me updates, and any endpoint you add later.
|
|
173
|
+
*/
|
|
174
|
+
async authFetch(path, init = {}) {
|
|
175
|
+
const doFetch = (token) => fetch(`${this.config.apiUrl}${path}`, {
|
|
176
|
+
...init,
|
|
177
|
+
headers: {
|
|
178
|
+
...init.headers || {},
|
|
179
|
+
...token ? { Authorization: `Bearer ${token}` } : {}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
let res = await doFetch(this.getAccessToken());
|
|
183
|
+
if (res.status === 401) {
|
|
184
|
+
const refreshed = await this.refresh();
|
|
185
|
+
if (!refreshed) {
|
|
186
|
+
this.onAuthFailure();
|
|
187
|
+
throw new Error("unauthenticated");
|
|
188
|
+
}
|
|
189
|
+
res = await doFetch(refreshed.accessToken);
|
|
190
|
+
if (res.status === 401) {
|
|
191
|
+
this.clearTokens();
|
|
192
|
+
this.onAuthFailure();
|
|
193
|
+
throw new Error("unauthenticated");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (!res.ok) throw new Error(`request failed: ${res.status}`);
|
|
197
|
+
return res.json();
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
// src/FluoceAuthProvider.tsx
|
|
202
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
203
|
+
function FluoceAuthProvider({
|
|
204
|
+
children,
|
|
205
|
+
...config
|
|
206
|
+
}) {
|
|
207
|
+
const clientRef = (0, import_react2.useRef)();
|
|
208
|
+
if (!clientRef.current) clientRef.current = new FluoceApiClient(config);
|
|
209
|
+
const client = clientRef.current;
|
|
210
|
+
const [user, setUser] = (0, import_react2.useState)(null);
|
|
211
|
+
const [loading, setLoading] = (0, import_react2.useState)(true);
|
|
212
|
+
const [error, setError] = (0, import_react2.useState)(null);
|
|
213
|
+
const redirectToLogin = (0, import_react2.useCallback)(() => {
|
|
214
|
+
window.location.href = client.buildLoginUrl();
|
|
215
|
+
}, [client]);
|
|
216
|
+
client.onAuthFailure = redirectToLogin;
|
|
217
|
+
const refreshUser = (0, import_react2.useCallback)(async () => {
|
|
218
|
+
try {
|
|
219
|
+
const me = await client.getMe();
|
|
220
|
+
setUser(me);
|
|
221
|
+
setError(null);
|
|
222
|
+
} catch (e) {
|
|
223
|
+
setUser(null);
|
|
224
|
+
setError(e);
|
|
225
|
+
}
|
|
226
|
+
}, [client]);
|
|
227
|
+
const logout = (0, import_react2.useCallback)(
|
|
228
|
+
async (allDevices = false) => {
|
|
229
|
+
await client.logout(allDevices);
|
|
230
|
+
setUser(null);
|
|
231
|
+
redirectToLogin();
|
|
232
|
+
},
|
|
233
|
+
[client, redirectToLogin]
|
|
234
|
+
);
|
|
235
|
+
(0, import_react2.useEffect)(() => {
|
|
236
|
+
let cancelled = false;
|
|
237
|
+
async function init() {
|
|
238
|
+
const params = new URLSearchParams(window.location.search);
|
|
239
|
+
const code = params.get("code");
|
|
240
|
+
try {
|
|
241
|
+
if (code) {
|
|
242
|
+
await client.exchangeCode(code);
|
|
243
|
+
params.delete("code");
|
|
244
|
+
const query = params.toString();
|
|
245
|
+
const cleanUrl = window.location.pathname + (query ? `?${query}` : "") + window.location.hash;
|
|
246
|
+
window.history.replaceState({}, "", cleanUrl);
|
|
247
|
+
}
|
|
248
|
+
if (client.getAccessToken() || client.getRefreshToken()) {
|
|
249
|
+
const me = await client.getMe();
|
|
250
|
+
if (!cancelled) setUser(me);
|
|
251
|
+
}
|
|
252
|
+
} catch (e) {
|
|
253
|
+
if (!cancelled) setError(e);
|
|
254
|
+
} finally {
|
|
255
|
+
if (!cancelled) setLoading(false);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
init();
|
|
259
|
+
return () => {
|
|
260
|
+
cancelled = true;
|
|
261
|
+
};
|
|
262
|
+
}, []);
|
|
263
|
+
const value = (0, import_react2.useMemo)(
|
|
264
|
+
() => ({
|
|
265
|
+
user,
|
|
266
|
+
loading,
|
|
267
|
+
isAuthenticated: !!user,
|
|
268
|
+
error,
|
|
269
|
+
refreshUser,
|
|
270
|
+
logout,
|
|
271
|
+
redirectToLogin
|
|
272
|
+
}),
|
|
273
|
+
[user, loading, error, refreshUser, logout, redirectToLogin]
|
|
274
|
+
);
|
|
275
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FluoceAuthContext.Provider, { value, children });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/AuthGuard.tsx
|
|
279
|
+
var import_react3 = require("react");
|
|
280
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
281
|
+
function AuthGuard({ children, fallback = null }) {
|
|
282
|
+
const { loading, isAuthenticated, redirectToLogin } = useFluoceAuth();
|
|
283
|
+
(0, import_react3.useEffect)(() => {
|
|
284
|
+
if (!loading && !isAuthenticated) {
|
|
285
|
+
redirectToLogin();
|
|
286
|
+
}
|
|
287
|
+
}, [loading, isAuthenticated, redirectToLogin]);
|
|
288
|
+
if (loading || !isAuthenticated) {
|
|
289
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: fallback });
|
|
290
|
+
}
|
|
291
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children });
|
|
292
|
+
}
|
|
293
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
294
|
+
0 && (module.exports = {
|
|
295
|
+
AuthGuard,
|
|
296
|
+
FluoceApiClient,
|
|
297
|
+
FluoceAuthProvider,
|
|
298
|
+
useFluoceAuth
|
|
299
|
+
});
|
|
300
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/FluoceAuthProvider.tsx","../src/AuthContext.tsx","../src/cookies.ts","../src/apiClient.ts","../src/AuthGuard.tsx"],"sourcesContent":["export { FluoceAuthProvider } from './FluoceAuthProvider';\nexport type { FluoceAuthProviderProps } from './FluoceAuthProvider';\nexport { AuthGuard } from './AuthGuard';\nexport type { AuthGuardProps } from './AuthGuard';\nexport { useFluoceAuth } from './AuthContext';\nexport { FluoceApiClient } from './apiClient';\nexport type {\n FluoceUser,\n TokenPair,\n FluoceAuthConfig,\n FluoceAuthContextValue,\n} from './types';\n","import {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport { FluoceAuthContext } from \"./AuthContext\";\nimport { FluoceApiClient } from \"./apiClient\";\nimport type { FluoceAuthConfig, FluoceUser } from \"./types\";\n\nexport interface FluoceAuthProviderProps extends FluoceAuthConfig {\n children: ReactNode;\n}\n\nexport function FluoceAuthProvider({\n children,\n ...config\n}: FluoceAuthProviderProps) {\n const clientRef = useRef<FluoceApiClient>();\n if (!clientRef.current) clientRef.current = new FluoceApiClient(config);\n const client = clientRef.current;\n\n const [user, setUser] = useState<FluoceUser | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const redirectToLogin = useCallback(() => {\n window.location.href = client.buildLoginUrl();\n }, [client]);\n\n // keep the client's failure hook pointed at the latest redirect fn\n client.onAuthFailure = redirectToLogin;\n\n const refreshUser = useCallback(async () => {\n try {\n const me = await client.getMe();\n setUser(me);\n setError(null);\n } catch (e) {\n setUser(null);\n setError(e as Error);\n }\n }, [client]);\n\n const logout = useCallback(\n async (allDevices = false) => {\n await client.logout(allDevices);\n setUser(null);\n redirectToLogin();\n },\n [client, redirectToLogin],\n );\n\n useEffect(() => {\n let cancelled = false;\n\n async function init() {\n const params = new URLSearchParams(window.location.search);\n const code = params.get(\"code\");\n\n try {\n if (code) {\n await client.exchangeCode(code);\n params.delete(\"code\");\n const query = params.toString();\n const cleanUrl =\n window.location.pathname +\n (query ? `?${query}` : \"\") +\n window.location.hash;\n window.history.replaceState({}, \"\", cleanUrl);\n }\n\n if (client.getAccessToken() || client.getRefreshToken()) {\n const me = await client.getMe();\n if (!cancelled) setUser(me);\n }\n } catch (e) {\n if (!cancelled) setError(e as Error);\n } finally {\n if (!cancelled) setLoading(false);\n }\n }\n\n init();\n return () => {\n cancelled = true;\n };\n // run once on mount only\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // const authFetch = useCallback(\n // <T,>(path: string, init?: RequestInit) => client.authFetch<T>(path, init),\n // [client],\n // );\n\n const value = useMemo(\n () => ({\n user,\n loading,\n isAuthenticated: !!user,\n error,\n refreshUser,\n logout,\n redirectToLogin,\n }),\n [user, loading, error, refreshUser, logout, redirectToLogin],\n );\n\n return (\n <FluoceAuthContext.Provider value={value}>\n {children}\n </FluoceAuthContext.Provider>\n );\n}\n","import { createContext, useContext } from \"react\";\nimport type { FluoceAuthContextValue } from \"./types\";\n\nexport const FluoceAuthContext = createContext<FluoceAuthContextValue | null>(\n null,\n);\n\nexport function useFluoceAuth(): FluoceAuthContextValue {\n const ctx = useContext(FluoceAuthContext);\n if (!ctx)\n throw new Error(\"useFluoceAuth must be used within <FluoceAuthProvider>\");\n return ctx;\n}\n","export function setCookie(name: string, value: string, days: number): void {\n const expires = new Date(Date.now() + days * 864e5).toUTCString();\n document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/; SameSite=Lax`;\n}\n\nexport function getCookie(name: string): string | null {\n const escaped = name.replace(/([.$?*|{}()[\\]\\\\/+^])/g, '\\\\$1');\n const match = document.cookie.match(new RegExp('(?:^|; )' + escaped + '=([^;]*)'));\n return match ? decodeURIComponent(match[1]) : null;\n}\n\nexport function deleteCookie(name: string): void {\n document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;\n}\n","import { getCookie, setCookie, deleteCookie } from \"./cookies\";\nimport type { FluoceAuthConfig, FluoceUser, TokenPair } from \"./types\";\n\ntype ResolvedConfig = Required<FluoceAuthConfig>;\n\nexport class FluoceApiClient {\n private config: ResolvedConfig;\n private refreshPromise: Promise<TokenPair | null> | null = null;\n\n /** set by the Provider — called once refresh fails and there's no session left */\n onAuthFailure: () => void = () => {};\n\n constructor(config: FluoceAuthConfig) {\n this.config = {\n accessTokenCookie: \"fluoce_access_token\",\n refreshTokenCookie: \"fluoce_refresh_token\",\n accessTokenExpiryDays: 1,\n refreshTokenExpiryDays: 30,\n ...config,\n };\n }\n\n getAccessToken(): string | null {\n return getCookie(this.config.accessTokenCookie);\n }\n\n getRefreshToken(): string | null {\n return getCookie(this.config.refreshTokenCookie);\n }\n\n setTokens(tokens: TokenPair): void {\n setCookie(\n this.config.accessTokenCookie,\n tokens.accessToken,\n this.config.accessTokenExpiryDays,\n );\n setCookie(\n this.config.refreshTokenCookie,\n tokens.refreshToken,\n this.config.refreshTokenExpiryDays,\n );\n }\n\n clearTokens(): void {\n deleteCookie(this.config.accessTokenCookie);\n deleteCookie(this.config.refreshTokenCookie);\n }\n\n buildLoginUrl(): string {\n const ref = encodeURIComponent(this.config.appUrl);\n return `${this.config.authUrl}/auth?ref=${ref}`;\n }\n\n async exchangeCode(code: string): Promise<TokenPair> {\n const res = await fetch(`${this.config.apiUrl}/exchange`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ code }),\n });\n if (!res.ok) throw new Error(`exchange failed: ${res.status}`);\n const data = (await res.json()) as TokenPair;\n this.setTokens(data);\n return data;\n }\n\n async getMe(): Promise<FluoceUser> {\n return this.authFetch<FluoceUser>(\"/me\");\n }\n\n async updateMe(payload: {\n name?: string;\n photo?: string;\n }): Promise<FluoceUser> {\n return this.authFetch<FluoceUser>(\"/me\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(payload),\n });\n }\n\n async refresh(): Promise<TokenPair | null> {\n // dedupe concurrent refresh calls triggered by parallel 401s\n if (this.refreshPromise) return this.refreshPromise;\n\n const refreshToken = this.getRefreshToken();\n if (!refreshToken) return null;\n\n this.refreshPromise = (async () => {\n try {\n const res = await fetch(`${this.config.apiUrl}/refresh`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${refreshToken}`,\n },\n });\n if (!res.ok) {\n this.clearTokens();\n return null;\n }\n const data = (await res.json()) as TokenPair;\n this.setTokens(data);\n return data;\n } catch {\n this.clearTokens();\n return null;\n } finally {\n this.refreshPromise = null;\n }\n })();\n\n return this.refreshPromise;\n }\n\n async logout(allDevices = false): Promise<void> {\n const refreshToken = this.getRefreshToken();\n if (refreshToken) {\n try {\n await fetch(\n `${this.config.apiUrl}${allDevices ? \"/logout/all\" : \"/logout\"}`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${refreshToken}`,\n },\n },\n );\n } catch {\n // best-effort — clear local session regardless\n }\n }\n this.clearTokens();\n }\n\n /**\n * Authenticated fetch. Attaches access token, retries once through /refresh\n * on 401, and calls onAuthFailure (-> redirect to auth frontend) if that\n * fails too. Use this for /me, /me updates, and any endpoint you add later.\n */\n async authFetch<T = unknown>(\n path: string,\n init: RequestInit = {},\n ): Promise<T> {\n const doFetch = (token: string | null) =>\n fetch(`${this.config.apiUrl}${path}`, {\n ...init,\n headers: {\n ...(init.headers || {}),\n ...(token ? { Authorization: `Bearer ${token}` } : {}),\n },\n });\n\n let res = await doFetch(this.getAccessToken());\n\n if (res.status === 401) {\n const refreshed = await this.refresh();\n if (!refreshed) {\n this.onAuthFailure();\n throw new Error(\"unauthenticated\");\n }\n res = await doFetch(refreshed.accessToken);\n if (res.status === 401) {\n this.clearTokens();\n this.onAuthFailure();\n throw new Error(\"unauthenticated\");\n }\n }\n\n if (!res.ok) throw new Error(`request failed: ${res.status}`);\n\n return res.json() as Promise<T>;\n }\n}\n","import { useEffect, type ReactNode } from \"react\";\nimport { useFluoceAuth } from \"./AuthContext\";\n\nexport interface AuthGuardProps {\n children: ReactNode;\n /** shown while loading or right before the redirect fires */\n fallback?: ReactNode;\n}\n\nexport function AuthGuard({ children, fallback = null }: AuthGuardProps) {\n const { loading, isAuthenticated, redirectToLogin } = useFluoceAuth();\n\n useEffect(() => {\n if (!loading && !isAuthenticated) {\n redirectToLogin();\n }\n }, [loading, isAuthenticated, redirectToLogin]);\n\n if (loading || !isAuthenticated) {\n return <>{fallback}</>;\n }\n\n return <>{children}</>;\n}\n"],"mappings":"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","names":["import_react","import_react","import_jsx_runtime"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface FluoceUser {
|
|
5
|
+
id: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
email?: string;
|
|
8
|
+
phone?: string;
|
|
9
|
+
photo?: string;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
interface TokenPair {
|
|
13
|
+
accessToken: string;
|
|
14
|
+
refreshToken: string;
|
|
15
|
+
}
|
|
16
|
+
interface FluoceAuthConfig {
|
|
17
|
+
/** Auth frontend base url, e.g. "https://auth.fluoce.com" */
|
|
18
|
+
authUrl: string;
|
|
19
|
+
/** Auth backend api base url, e.g. "http://localhost:3001" */
|
|
20
|
+
apiUrl: string;
|
|
21
|
+
/**
|
|
22
|
+
* This app's own callback url, sent as `ref` and where auth.fluoce.com
|
|
23
|
+
* redirects back with `?code=`. e.g. "https://myapp.com/auth"
|
|
24
|
+
*/
|
|
25
|
+
appUrl: string;
|
|
26
|
+
accessTokenCookie?: string;
|
|
27
|
+
refreshTokenCookie?: string;
|
|
28
|
+
accessTokenExpiryDays?: number;
|
|
29
|
+
refreshTokenExpiryDays?: number;
|
|
30
|
+
}
|
|
31
|
+
interface FluoceAuthContextValue {
|
|
32
|
+
user: FluoceUser | null;
|
|
33
|
+
loading: boolean;
|
|
34
|
+
isAuthenticated: boolean;
|
|
35
|
+
error: Error | null;
|
|
36
|
+
refreshUser: () => Promise<void>;
|
|
37
|
+
logout: (allDevices?: boolean) => Promise<void>;
|
|
38
|
+
redirectToLogin: () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface FluoceAuthProviderProps extends FluoceAuthConfig {
|
|
42
|
+
children: ReactNode;
|
|
43
|
+
}
|
|
44
|
+
declare function FluoceAuthProvider({ children, ...config }: FluoceAuthProviderProps): react.JSX.Element;
|
|
45
|
+
|
|
46
|
+
interface AuthGuardProps {
|
|
47
|
+
children: ReactNode;
|
|
48
|
+
/** shown while loading or right before the redirect fires */
|
|
49
|
+
fallback?: ReactNode;
|
|
50
|
+
}
|
|
51
|
+
declare function AuthGuard({ children, fallback }: AuthGuardProps): react.JSX.Element;
|
|
52
|
+
|
|
53
|
+
declare function useFluoceAuth(): FluoceAuthContextValue;
|
|
54
|
+
|
|
55
|
+
declare class FluoceApiClient {
|
|
56
|
+
private config;
|
|
57
|
+
private refreshPromise;
|
|
58
|
+
/** set by the Provider — called once refresh fails and there's no session left */
|
|
59
|
+
onAuthFailure: () => void;
|
|
60
|
+
constructor(config: FluoceAuthConfig);
|
|
61
|
+
getAccessToken(): string | null;
|
|
62
|
+
getRefreshToken(): string | null;
|
|
63
|
+
setTokens(tokens: TokenPair): void;
|
|
64
|
+
clearTokens(): void;
|
|
65
|
+
buildLoginUrl(): string;
|
|
66
|
+
exchangeCode(code: string): Promise<TokenPair>;
|
|
67
|
+
getMe(): Promise<FluoceUser>;
|
|
68
|
+
updateMe(payload: {
|
|
69
|
+
name?: string;
|
|
70
|
+
photo?: string;
|
|
71
|
+
}): Promise<FluoceUser>;
|
|
72
|
+
refresh(): Promise<TokenPair | null>;
|
|
73
|
+
logout(allDevices?: boolean): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Authenticated fetch. Attaches access token, retries once through /refresh
|
|
76
|
+
* on 401, and calls onAuthFailure (-> redirect to auth frontend) if that
|
|
77
|
+
* fails too. Use this for /me, /me updates, and any endpoint you add later.
|
|
78
|
+
*/
|
|
79
|
+
authFetch<T = unknown>(path: string, init?: RequestInit): Promise<T>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export { AuthGuard, type AuthGuardProps, FluoceApiClient, type FluoceAuthConfig, type FluoceAuthContextValue, FluoceAuthProvider, type FluoceAuthProviderProps, type FluoceUser, type TokenPair, useFluoceAuth };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface FluoceUser {
|
|
5
|
+
id: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
email?: string;
|
|
8
|
+
phone?: string;
|
|
9
|
+
photo?: string;
|
|
10
|
+
[key: string]: unknown;
|
|
11
|
+
}
|
|
12
|
+
interface TokenPair {
|
|
13
|
+
accessToken: string;
|
|
14
|
+
refreshToken: string;
|
|
15
|
+
}
|
|
16
|
+
interface FluoceAuthConfig {
|
|
17
|
+
/** Auth frontend base url, e.g. "https://auth.fluoce.com" */
|
|
18
|
+
authUrl: string;
|
|
19
|
+
/** Auth backend api base url, e.g. "http://localhost:3001" */
|
|
20
|
+
apiUrl: string;
|
|
21
|
+
/**
|
|
22
|
+
* This app's own callback url, sent as `ref` and where auth.fluoce.com
|
|
23
|
+
* redirects back with `?code=`. e.g. "https://myapp.com/auth"
|
|
24
|
+
*/
|
|
25
|
+
appUrl: string;
|
|
26
|
+
accessTokenCookie?: string;
|
|
27
|
+
refreshTokenCookie?: string;
|
|
28
|
+
accessTokenExpiryDays?: number;
|
|
29
|
+
refreshTokenExpiryDays?: number;
|
|
30
|
+
}
|
|
31
|
+
interface FluoceAuthContextValue {
|
|
32
|
+
user: FluoceUser | null;
|
|
33
|
+
loading: boolean;
|
|
34
|
+
isAuthenticated: boolean;
|
|
35
|
+
error: Error | null;
|
|
36
|
+
refreshUser: () => Promise<void>;
|
|
37
|
+
logout: (allDevices?: boolean) => Promise<void>;
|
|
38
|
+
redirectToLogin: () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface FluoceAuthProviderProps extends FluoceAuthConfig {
|
|
42
|
+
children: ReactNode;
|
|
43
|
+
}
|
|
44
|
+
declare function FluoceAuthProvider({ children, ...config }: FluoceAuthProviderProps): react.JSX.Element;
|
|
45
|
+
|
|
46
|
+
interface AuthGuardProps {
|
|
47
|
+
children: ReactNode;
|
|
48
|
+
/** shown while loading or right before the redirect fires */
|
|
49
|
+
fallback?: ReactNode;
|
|
50
|
+
}
|
|
51
|
+
declare function AuthGuard({ children, fallback }: AuthGuardProps): react.JSX.Element;
|
|
52
|
+
|
|
53
|
+
declare function useFluoceAuth(): FluoceAuthContextValue;
|
|
54
|
+
|
|
55
|
+
declare class FluoceApiClient {
|
|
56
|
+
private config;
|
|
57
|
+
private refreshPromise;
|
|
58
|
+
/** set by the Provider — called once refresh fails and there's no session left */
|
|
59
|
+
onAuthFailure: () => void;
|
|
60
|
+
constructor(config: FluoceAuthConfig);
|
|
61
|
+
getAccessToken(): string | null;
|
|
62
|
+
getRefreshToken(): string | null;
|
|
63
|
+
setTokens(tokens: TokenPair): void;
|
|
64
|
+
clearTokens(): void;
|
|
65
|
+
buildLoginUrl(): string;
|
|
66
|
+
exchangeCode(code: string): Promise<TokenPair>;
|
|
67
|
+
getMe(): Promise<FluoceUser>;
|
|
68
|
+
updateMe(payload: {
|
|
69
|
+
name?: string;
|
|
70
|
+
photo?: string;
|
|
71
|
+
}): Promise<FluoceUser>;
|
|
72
|
+
refresh(): Promise<TokenPair | null>;
|
|
73
|
+
logout(allDevices?: boolean): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Authenticated fetch. Attaches access token, retries once through /refresh
|
|
76
|
+
* on 401, and calls onAuthFailure (-> redirect to auth frontend) if that
|
|
77
|
+
* fails too. Use this for /me, /me updates, and any endpoint you add later.
|
|
78
|
+
*/
|
|
79
|
+
authFetch<T = unknown>(path: string, init?: RequestInit): Promise<T>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export { AuthGuard, type AuthGuardProps, FluoceApiClient, type FluoceAuthConfig, type FluoceAuthContextValue, FluoceAuthProvider, type FluoceAuthProviderProps, type FluoceUser, type TokenPair, useFluoceAuth };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// src/FluoceAuthProvider.tsx
|
|
2
|
+
import {
|
|
3
|
+
useCallback,
|
|
4
|
+
useEffect,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState
|
|
8
|
+
} from "react";
|
|
9
|
+
|
|
10
|
+
// src/AuthContext.tsx
|
|
11
|
+
import { createContext, useContext } from "react";
|
|
12
|
+
var FluoceAuthContext = createContext(
|
|
13
|
+
null
|
|
14
|
+
);
|
|
15
|
+
function useFluoceAuth() {
|
|
16
|
+
const ctx = useContext(FluoceAuthContext);
|
|
17
|
+
if (!ctx)
|
|
18
|
+
throw new Error("useFluoceAuth must be used within <FluoceAuthProvider>");
|
|
19
|
+
return ctx;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/cookies.ts
|
|
23
|
+
function setCookie(name, value, days) {
|
|
24
|
+
const expires = new Date(Date.now() + days * 864e5).toUTCString();
|
|
25
|
+
document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/; SameSite=Lax`;
|
|
26
|
+
}
|
|
27
|
+
function getCookie(name) {
|
|
28
|
+
const escaped = name.replace(/([.$?*|{}()[\]\\/+^])/g, "\\$1");
|
|
29
|
+
const match = document.cookie.match(new RegExp("(?:^|; )" + escaped + "=([^;]*)"));
|
|
30
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
31
|
+
}
|
|
32
|
+
function deleteCookie(name) {
|
|
33
|
+
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/apiClient.ts
|
|
37
|
+
var FluoceApiClient = class {
|
|
38
|
+
constructor(config) {
|
|
39
|
+
this.refreshPromise = null;
|
|
40
|
+
/** set by the Provider — called once refresh fails and there's no session left */
|
|
41
|
+
this.onAuthFailure = () => {
|
|
42
|
+
};
|
|
43
|
+
this.config = {
|
|
44
|
+
accessTokenCookie: "fluoce_access_token",
|
|
45
|
+
refreshTokenCookie: "fluoce_refresh_token",
|
|
46
|
+
accessTokenExpiryDays: 1,
|
|
47
|
+
refreshTokenExpiryDays: 30,
|
|
48
|
+
...config
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
getAccessToken() {
|
|
52
|
+
return getCookie(this.config.accessTokenCookie);
|
|
53
|
+
}
|
|
54
|
+
getRefreshToken() {
|
|
55
|
+
return getCookie(this.config.refreshTokenCookie);
|
|
56
|
+
}
|
|
57
|
+
setTokens(tokens) {
|
|
58
|
+
setCookie(
|
|
59
|
+
this.config.accessTokenCookie,
|
|
60
|
+
tokens.accessToken,
|
|
61
|
+
this.config.accessTokenExpiryDays
|
|
62
|
+
);
|
|
63
|
+
setCookie(
|
|
64
|
+
this.config.refreshTokenCookie,
|
|
65
|
+
tokens.refreshToken,
|
|
66
|
+
this.config.refreshTokenExpiryDays
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
clearTokens() {
|
|
70
|
+
deleteCookie(this.config.accessTokenCookie);
|
|
71
|
+
deleteCookie(this.config.refreshTokenCookie);
|
|
72
|
+
}
|
|
73
|
+
buildLoginUrl() {
|
|
74
|
+
const ref = encodeURIComponent(this.config.appUrl);
|
|
75
|
+
return `${this.config.authUrl}/auth?ref=${ref}`;
|
|
76
|
+
}
|
|
77
|
+
async exchangeCode(code) {
|
|
78
|
+
const res = await fetch(`${this.config.apiUrl}/exchange`, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers: { "Content-Type": "application/json" },
|
|
81
|
+
body: JSON.stringify({ code })
|
|
82
|
+
});
|
|
83
|
+
if (!res.ok) throw new Error(`exchange failed: ${res.status}`);
|
|
84
|
+
const data = await res.json();
|
|
85
|
+
this.setTokens(data);
|
|
86
|
+
return data;
|
|
87
|
+
}
|
|
88
|
+
async getMe() {
|
|
89
|
+
return this.authFetch("/me");
|
|
90
|
+
}
|
|
91
|
+
async updateMe(payload) {
|
|
92
|
+
return this.authFetch("/me", {
|
|
93
|
+
method: "POST",
|
|
94
|
+
headers: { "Content-Type": "application/json" },
|
|
95
|
+
body: JSON.stringify(payload)
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async refresh() {
|
|
99
|
+
if (this.refreshPromise) return this.refreshPromise;
|
|
100
|
+
const refreshToken = this.getRefreshToken();
|
|
101
|
+
if (!refreshToken) return null;
|
|
102
|
+
this.refreshPromise = (async () => {
|
|
103
|
+
try {
|
|
104
|
+
const res = await fetch(`${this.config.apiUrl}/refresh`, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
headers: {
|
|
107
|
+
"Content-Type": "application/json",
|
|
108
|
+
Authorization: `Bearer ${refreshToken}`
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
if (!res.ok) {
|
|
112
|
+
this.clearTokens();
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const data = await res.json();
|
|
116
|
+
this.setTokens(data);
|
|
117
|
+
return data;
|
|
118
|
+
} catch {
|
|
119
|
+
this.clearTokens();
|
|
120
|
+
return null;
|
|
121
|
+
} finally {
|
|
122
|
+
this.refreshPromise = null;
|
|
123
|
+
}
|
|
124
|
+
})();
|
|
125
|
+
return this.refreshPromise;
|
|
126
|
+
}
|
|
127
|
+
async logout(allDevices = false) {
|
|
128
|
+
const refreshToken = this.getRefreshToken();
|
|
129
|
+
if (refreshToken) {
|
|
130
|
+
try {
|
|
131
|
+
await fetch(
|
|
132
|
+
`${this.config.apiUrl}${allDevices ? "/logout/all" : "/logout"}`,
|
|
133
|
+
{
|
|
134
|
+
method: "POST",
|
|
135
|
+
headers: {
|
|
136
|
+
"Content-Type": "application/json",
|
|
137
|
+
Authorization: `Bearer ${refreshToken}`
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
this.clearTokens();
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Authenticated fetch. Attaches access token, retries once through /refresh
|
|
148
|
+
* on 401, and calls onAuthFailure (-> redirect to auth frontend) if that
|
|
149
|
+
* fails too. Use this for /me, /me updates, and any endpoint you add later.
|
|
150
|
+
*/
|
|
151
|
+
async authFetch(path, init = {}) {
|
|
152
|
+
const doFetch = (token) => fetch(`${this.config.apiUrl}${path}`, {
|
|
153
|
+
...init,
|
|
154
|
+
headers: {
|
|
155
|
+
...init.headers || {},
|
|
156
|
+
...token ? { Authorization: `Bearer ${token}` } : {}
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
let res = await doFetch(this.getAccessToken());
|
|
160
|
+
if (res.status === 401) {
|
|
161
|
+
const refreshed = await this.refresh();
|
|
162
|
+
if (!refreshed) {
|
|
163
|
+
this.onAuthFailure();
|
|
164
|
+
throw new Error("unauthenticated");
|
|
165
|
+
}
|
|
166
|
+
res = await doFetch(refreshed.accessToken);
|
|
167
|
+
if (res.status === 401) {
|
|
168
|
+
this.clearTokens();
|
|
169
|
+
this.onAuthFailure();
|
|
170
|
+
throw new Error("unauthenticated");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (!res.ok) throw new Error(`request failed: ${res.status}`);
|
|
174
|
+
return res.json();
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// src/FluoceAuthProvider.tsx
|
|
179
|
+
import { jsx } from "react/jsx-runtime";
|
|
180
|
+
function FluoceAuthProvider({
|
|
181
|
+
children,
|
|
182
|
+
...config
|
|
183
|
+
}) {
|
|
184
|
+
const clientRef = useRef();
|
|
185
|
+
if (!clientRef.current) clientRef.current = new FluoceApiClient(config);
|
|
186
|
+
const client = clientRef.current;
|
|
187
|
+
const [user, setUser] = useState(null);
|
|
188
|
+
const [loading, setLoading] = useState(true);
|
|
189
|
+
const [error, setError] = useState(null);
|
|
190
|
+
const redirectToLogin = useCallback(() => {
|
|
191
|
+
window.location.href = client.buildLoginUrl();
|
|
192
|
+
}, [client]);
|
|
193
|
+
client.onAuthFailure = redirectToLogin;
|
|
194
|
+
const refreshUser = useCallback(async () => {
|
|
195
|
+
try {
|
|
196
|
+
const me = await client.getMe();
|
|
197
|
+
setUser(me);
|
|
198
|
+
setError(null);
|
|
199
|
+
} catch (e) {
|
|
200
|
+
setUser(null);
|
|
201
|
+
setError(e);
|
|
202
|
+
}
|
|
203
|
+
}, [client]);
|
|
204
|
+
const logout = useCallback(
|
|
205
|
+
async (allDevices = false) => {
|
|
206
|
+
await client.logout(allDevices);
|
|
207
|
+
setUser(null);
|
|
208
|
+
redirectToLogin();
|
|
209
|
+
},
|
|
210
|
+
[client, redirectToLogin]
|
|
211
|
+
);
|
|
212
|
+
useEffect(() => {
|
|
213
|
+
let cancelled = false;
|
|
214
|
+
async function init() {
|
|
215
|
+
const params = new URLSearchParams(window.location.search);
|
|
216
|
+
const code = params.get("code");
|
|
217
|
+
try {
|
|
218
|
+
if (code) {
|
|
219
|
+
await client.exchangeCode(code);
|
|
220
|
+
params.delete("code");
|
|
221
|
+
const query = params.toString();
|
|
222
|
+
const cleanUrl = window.location.pathname + (query ? `?${query}` : "") + window.location.hash;
|
|
223
|
+
window.history.replaceState({}, "", cleanUrl);
|
|
224
|
+
}
|
|
225
|
+
if (client.getAccessToken() || client.getRefreshToken()) {
|
|
226
|
+
const me = await client.getMe();
|
|
227
|
+
if (!cancelled) setUser(me);
|
|
228
|
+
}
|
|
229
|
+
} catch (e) {
|
|
230
|
+
if (!cancelled) setError(e);
|
|
231
|
+
} finally {
|
|
232
|
+
if (!cancelled) setLoading(false);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
init();
|
|
236
|
+
return () => {
|
|
237
|
+
cancelled = true;
|
|
238
|
+
};
|
|
239
|
+
}, []);
|
|
240
|
+
const value = useMemo(
|
|
241
|
+
() => ({
|
|
242
|
+
user,
|
|
243
|
+
loading,
|
|
244
|
+
isAuthenticated: !!user,
|
|
245
|
+
error,
|
|
246
|
+
refreshUser,
|
|
247
|
+
logout,
|
|
248
|
+
redirectToLogin
|
|
249
|
+
}),
|
|
250
|
+
[user, loading, error, refreshUser, logout, redirectToLogin]
|
|
251
|
+
);
|
|
252
|
+
return /* @__PURE__ */ jsx(FluoceAuthContext.Provider, { value, children });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/AuthGuard.tsx
|
|
256
|
+
import { useEffect as useEffect2 } from "react";
|
|
257
|
+
import { Fragment, jsx as jsx2 } from "react/jsx-runtime";
|
|
258
|
+
function AuthGuard({ children, fallback = null }) {
|
|
259
|
+
const { loading, isAuthenticated, redirectToLogin } = useFluoceAuth();
|
|
260
|
+
useEffect2(() => {
|
|
261
|
+
if (!loading && !isAuthenticated) {
|
|
262
|
+
redirectToLogin();
|
|
263
|
+
}
|
|
264
|
+
}, [loading, isAuthenticated, redirectToLogin]);
|
|
265
|
+
if (loading || !isAuthenticated) {
|
|
266
|
+
return /* @__PURE__ */ jsx2(Fragment, { children: fallback });
|
|
267
|
+
}
|
|
268
|
+
return /* @__PURE__ */ jsx2(Fragment, { children });
|
|
269
|
+
}
|
|
270
|
+
export {
|
|
271
|
+
AuthGuard,
|
|
272
|
+
FluoceApiClient,
|
|
273
|
+
FluoceAuthProvider,
|
|
274
|
+
useFluoceAuth
|
|
275
|
+
};
|
|
276
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/FluoceAuthProvider.tsx","../src/AuthContext.tsx","../src/cookies.ts","../src/apiClient.ts","../src/AuthGuard.tsx"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport { FluoceAuthContext } from \"./AuthContext\";\nimport { FluoceApiClient } from \"./apiClient\";\nimport type { FluoceAuthConfig, FluoceUser } from \"./types\";\n\nexport interface FluoceAuthProviderProps extends FluoceAuthConfig {\n children: ReactNode;\n}\n\nexport function FluoceAuthProvider({\n children,\n ...config\n}: FluoceAuthProviderProps) {\n const clientRef = useRef<FluoceApiClient>();\n if (!clientRef.current) clientRef.current = new FluoceApiClient(config);\n const client = clientRef.current;\n\n const [user, setUser] = useState<FluoceUser | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const redirectToLogin = useCallback(() => {\n window.location.href = client.buildLoginUrl();\n }, [client]);\n\n // keep the client's failure hook pointed at the latest redirect fn\n client.onAuthFailure = redirectToLogin;\n\n const refreshUser = useCallback(async () => {\n try {\n const me = await client.getMe();\n setUser(me);\n setError(null);\n } catch (e) {\n setUser(null);\n setError(e as Error);\n }\n }, [client]);\n\n const logout = useCallback(\n async (allDevices = false) => {\n await client.logout(allDevices);\n setUser(null);\n redirectToLogin();\n },\n [client, redirectToLogin],\n );\n\n useEffect(() => {\n let cancelled = false;\n\n async function init() {\n const params = new URLSearchParams(window.location.search);\n const code = params.get(\"code\");\n\n try {\n if (code) {\n await client.exchangeCode(code);\n params.delete(\"code\");\n const query = params.toString();\n const cleanUrl =\n window.location.pathname +\n (query ? `?${query}` : \"\") +\n window.location.hash;\n window.history.replaceState({}, \"\", cleanUrl);\n }\n\n if (client.getAccessToken() || client.getRefreshToken()) {\n const me = await client.getMe();\n if (!cancelled) setUser(me);\n }\n } catch (e) {\n if (!cancelled) setError(e as Error);\n } finally {\n if (!cancelled) setLoading(false);\n }\n }\n\n init();\n return () => {\n cancelled = true;\n };\n // run once on mount only\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // const authFetch = useCallback(\n // <T,>(path: string, init?: RequestInit) => client.authFetch<T>(path, init),\n // [client],\n // );\n\n const value = useMemo(\n () => ({\n user,\n loading,\n isAuthenticated: !!user,\n error,\n refreshUser,\n logout,\n redirectToLogin,\n }),\n [user, loading, error, refreshUser, logout, redirectToLogin],\n );\n\n return (\n <FluoceAuthContext.Provider value={value}>\n {children}\n </FluoceAuthContext.Provider>\n );\n}\n","import { createContext, useContext } from \"react\";\nimport type { FluoceAuthContextValue } from \"./types\";\n\nexport const FluoceAuthContext = createContext<FluoceAuthContextValue | null>(\n null,\n);\n\nexport function useFluoceAuth(): FluoceAuthContextValue {\n const ctx = useContext(FluoceAuthContext);\n if (!ctx)\n throw new Error(\"useFluoceAuth must be used within <FluoceAuthProvider>\");\n return ctx;\n}\n","export function setCookie(name: string, value: string, days: number): void {\n const expires = new Date(Date.now() + days * 864e5).toUTCString();\n document.cookie = `${name}=${encodeURIComponent(value)}; expires=${expires}; path=/; SameSite=Lax`;\n}\n\nexport function getCookie(name: string): string | null {\n const escaped = name.replace(/([.$?*|{}()[\\]\\\\/+^])/g, '\\\\$1');\n const match = document.cookie.match(new RegExp('(?:^|; )' + escaped + '=([^;]*)'));\n return match ? decodeURIComponent(match[1]) : null;\n}\n\nexport function deleteCookie(name: string): void {\n document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;\n}\n","import { getCookie, setCookie, deleteCookie } from \"./cookies\";\nimport type { FluoceAuthConfig, FluoceUser, TokenPair } from \"./types\";\n\ntype ResolvedConfig = Required<FluoceAuthConfig>;\n\nexport class FluoceApiClient {\n private config: ResolvedConfig;\n private refreshPromise: Promise<TokenPair | null> | null = null;\n\n /** set by the Provider — called once refresh fails and there's no session left */\n onAuthFailure: () => void = () => {};\n\n constructor(config: FluoceAuthConfig) {\n this.config = {\n accessTokenCookie: \"fluoce_access_token\",\n refreshTokenCookie: \"fluoce_refresh_token\",\n accessTokenExpiryDays: 1,\n refreshTokenExpiryDays: 30,\n ...config,\n };\n }\n\n getAccessToken(): string | null {\n return getCookie(this.config.accessTokenCookie);\n }\n\n getRefreshToken(): string | null {\n return getCookie(this.config.refreshTokenCookie);\n }\n\n setTokens(tokens: TokenPair): void {\n setCookie(\n this.config.accessTokenCookie,\n tokens.accessToken,\n this.config.accessTokenExpiryDays,\n );\n setCookie(\n this.config.refreshTokenCookie,\n tokens.refreshToken,\n this.config.refreshTokenExpiryDays,\n );\n }\n\n clearTokens(): void {\n deleteCookie(this.config.accessTokenCookie);\n deleteCookie(this.config.refreshTokenCookie);\n }\n\n buildLoginUrl(): string {\n const ref = encodeURIComponent(this.config.appUrl);\n return `${this.config.authUrl}/auth?ref=${ref}`;\n }\n\n async exchangeCode(code: string): Promise<TokenPair> {\n const res = await fetch(`${this.config.apiUrl}/exchange`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ code }),\n });\n if (!res.ok) throw new Error(`exchange failed: ${res.status}`);\n const data = (await res.json()) as TokenPair;\n this.setTokens(data);\n return data;\n }\n\n async getMe(): Promise<FluoceUser> {\n return this.authFetch<FluoceUser>(\"/me\");\n }\n\n async updateMe(payload: {\n name?: string;\n photo?: string;\n }): Promise<FluoceUser> {\n return this.authFetch<FluoceUser>(\"/me\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(payload),\n });\n }\n\n async refresh(): Promise<TokenPair | null> {\n // dedupe concurrent refresh calls triggered by parallel 401s\n if (this.refreshPromise) return this.refreshPromise;\n\n const refreshToken = this.getRefreshToken();\n if (!refreshToken) return null;\n\n this.refreshPromise = (async () => {\n try {\n const res = await fetch(`${this.config.apiUrl}/refresh`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${refreshToken}`,\n },\n });\n if (!res.ok) {\n this.clearTokens();\n return null;\n }\n const data = (await res.json()) as TokenPair;\n this.setTokens(data);\n return data;\n } catch {\n this.clearTokens();\n return null;\n } finally {\n this.refreshPromise = null;\n }\n })();\n\n return this.refreshPromise;\n }\n\n async logout(allDevices = false): Promise<void> {\n const refreshToken = this.getRefreshToken();\n if (refreshToken) {\n try {\n await fetch(\n `${this.config.apiUrl}${allDevices ? \"/logout/all\" : \"/logout\"}`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Authorization: `Bearer ${refreshToken}`,\n },\n },\n );\n } catch {\n // best-effort — clear local session regardless\n }\n }\n this.clearTokens();\n }\n\n /**\n * Authenticated fetch. Attaches access token, retries once through /refresh\n * on 401, and calls onAuthFailure (-> redirect to auth frontend) if that\n * fails too. Use this for /me, /me updates, and any endpoint you add later.\n */\n async authFetch<T = unknown>(\n path: string,\n init: RequestInit = {},\n ): Promise<T> {\n const doFetch = (token: string | null) =>\n fetch(`${this.config.apiUrl}${path}`, {\n ...init,\n headers: {\n ...(init.headers || {}),\n ...(token ? { Authorization: `Bearer ${token}` } : {}),\n },\n });\n\n let res = await doFetch(this.getAccessToken());\n\n if (res.status === 401) {\n const refreshed = await this.refresh();\n if (!refreshed) {\n this.onAuthFailure();\n throw new Error(\"unauthenticated\");\n }\n res = await doFetch(refreshed.accessToken);\n if (res.status === 401) {\n this.clearTokens();\n this.onAuthFailure();\n throw new Error(\"unauthenticated\");\n }\n }\n\n if (!res.ok) throw new Error(`request failed: ${res.status}`);\n\n return res.json() as Promise<T>;\n }\n}\n","import { useEffect, type ReactNode } from \"react\";\nimport { useFluoceAuth } from \"./AuthContext\";\n\nexport interface AuthGuardProps {\n children: ReactNode;\n /** shown while loading or right before the redirect fires */\n fallback?: ReactNode;\n}\n\nexport function AuthGuard({ children, fallback = null }: AuthGuardProps) {\n const { loading, isAuthenticated, redirectToLogin } = useFluoceAuth();\n\n useEffect(() => {\n if (!loading && !isAuthenticated) {\n redirectToLogin();\n }\n }, [loading, isAuthenticated, redirectToLogin]);\n\n if (loading || !isAuthenticated) {\n return <>{fallback}</>;\n }\n\n return <>{children}</>;\n}\n"],"mappings":"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","names":["useEffect","jsx","useEffect"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fluoce/auth-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "React SDK for Fluoce Auth",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"module": "./dist/index.mjs",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.mjs",
|
|
17
|
+
"require": "./dist/index.js"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
26
|
+
"dev": "tsup src/index.ts --watch --format esm,cjs --dts",
|
|
27
|
+
"prepublishOnly": "pnpm build"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"react": ">=18",
|
|
31
|
+
"react-dom": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@types/react": "^18.3.31",
|
|
35
|
+
"tsup": "^8.5.1",
|
|
36
|
+
"typescript": "^5.9.3"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "https://github.com/fluoce/auth"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/fluoce/auth",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/fluoce/auth/issues"
|
|
45
|
+
},
|
|
46
|
+
"keywords": [
|
|
47
|
+
"react",
|
|
48
|
+
"auth",
|
|
49
|
+
"authentication",
|
|
50
|
+
"oauth",
|
|
51
|
+
"sdk",
|
|
52
|
+
"fluoce"
|
|
53
|
+
],
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=18"
|
|
56
|
+
}
|
|
57
|
+
}
|