@vylth/nexus-react 1.0.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/dist/index.d.mts +86 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +472 -0
- package/dist/index.mjs +434 -0
- package/package.json +25 -0
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface NexusUser {
|
|
5
|
+
id: string;
|
|
6
|
+
email: string;
|
|
7
|
+
username: string;
|
|
8
|
+
firstName: string;
|
|
9
|
+
lastName: string;
|
|
10
|
+
avatar: string;
|
|
11
|
+
globalRole: string;
|
|
12
|
+
emailVerified: boolean;
|
|
13
|
+
affiliateCode: string;
|
|
14
|
+
twoFactorEnabled: boolean;
|
|
15
|
+
appRoles?: Record<string, {
|
|
16
|
+
role: string;
|
|
17
|
+
permissions: string[];
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
interface NexusConfig {
|
|
21
|
+
clientId: string;
|
|
22
|
+
redirectUri: string;
|
|
23
|
+
nexusUrl?: string;
|
|
24
|
+
apiUrl?: string;
|
|
25
|
+
}
|
|
26
|
+
interface NexusAuthResult {
|
|
27
|
+
access_token: string;
|
|
28
|
+
refresh_token: string;
|
|
29
|
+
expires_in: number;
|
|
30
|
+
investor: {
|
|
31
|
+
id: string;
|
|
32
|
+
email: string;
|
|
33
|
+
first_name: string;
|
|
34
|
+
last_name: string;
|
|
35
|
+
username: string;
|
|
36
|
+
avatar_url: string;
|
|
37
|
+
email_verified: boolean;
|
|
38
|
+
two_factor_enabled: boolean;
|
|
39
|
+
global_role: string;
|
|
40
|
+
};
|
|
41
|
+
session_id: string;
|
|
42
|
+
}
|
|
43
|
+
interface NexusContextValue {
|
|
44
|
+
user: NexusUser | null;
|
|
45
|
+
isAuthenticated: boolean;
|
|
46
|
+
isLoading: boolean;
|
|
47
|
+
accessToken: string | null;
|
|
48
|
+
login: () => void;
|
|
49
|
+
logout: () => void;
|
|
50
|
+
refreshUser: () => Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface NexusProviderProps extends NexusConfig {
|
|
54
|
+
children: ReactNode;
|
|
55
|
+
onLogin?: (user: NexusUser) => void;
|
|
56
|
+
onLogout?: () => void;
|
|
57
|
+
}
|
|
58
|
+
declare function NexusProvider({ children, clientId, redirectUri, nexusUrl, apiUrl, onLogin, onLogout, }: NexusProviderProps): react_jsx_runtime.JSX.Element;
|
|
59
|
+
|
|
60
|
+
interface NexusCallbackProps {
|
|
61
|
+
onSuccess?: () => void;
|
|
62
|
+
onError?: (error: string) => void;
|
|
63
|
+
}
|
|
64
|
+
declare function NexusCallback({ onSuccess, onError }: NexusCallbackProps): react_jsx_runtime.JSX.Element;
|
|
65
|
+
|
|
66
|
+
interface NexusLoginButtonProps {
|
|
67
|
+
label?: string;
|
|
68
|
+
size?: 'sm' | 'md' | 'lg';
|
|
69
|
+
variant?: 'dark' | 'light' | 'outline';
|
|
70
|
+
className?: string;
|
|
71
|
+
style?: React.CSSProperties;
|
|
72
|
+
}
|
|
73
|
+
declare function NexusLoginButton({ label, size, variant, className, style: customStyle, }: NexusLoginButtonProps): react_jsx_runtime.JSX.Element;
|
|
74
|
+
|
|
75
|
+
declare function useNexus(): NexusContextValue;
|
|
76
|
+
|
|
77
|
+
declare function setTokens(access: string, refresh: string): void;
|
|
78
|
+
declare function getAccessToken(): string | null;
|
|
79
|
+
declare function getRefreshToken(): string | null;
|
|
80
|
+
declare function clearTokens(): void;
|
|
81
|
+
declare function isTokenValid(): boolean;
|
|
82
|
+
declare function decodeUser(token: string): NexusUser | null;
|
|
83
|
+
declare function refreshTokens(apiUrl: string): Promise<boolean>;
|
|
84
|
+
declare function nexusFetch<T>(apiUrl: string, path: string, options?: RequestInit, onAuthFailure?: () => void): Promise<T>;
|
|
85
|
+
|
|
86
|
+
export { type NexusAuthResult, NexusCallback, type NexusConfig, type NexusContextValue, NexusLoginButton, NexusProvider, type NexusUser, clearTokens, decodeUser, getAccessToken, getRefreshToken, isTokenValid, nexusFetch, refreshTokens, setTokens, useNexus };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
interface NexusUser {
|
|
5
|
+
id: string;
|
|
6
|
+
email: string;
|
|
7
|
+
username: string;
|
|
8
|
+
firstName: string;
|
|
9
|
+
lastName: string;
|
|
10
|
+
avatar: string;
|
|
11
|
+
globalRole: string;
|
|
12
|
+
emailVerified: boolean;
|
|
13
|
+
affiliateCode: string;
|
|
14
|
+
twoFactorEnabled: boolean;
|
|
15
|
+
appRoles?: Record<string, {
|
|
16
|
+
role: string;
|
|
17
|
+
permissions: string[];
|
|
18
|
+
}>;
|
|
19
|
+
}
|
|
20
|
+
interface NexusConfig {
|
|
21
|
+
clientId: string;
|
|
22
|
+
redirectUri: string;
|
|
23
|
+
nexusUrl?: string;
|
|
24
|
+
apiUrl?: string;
|
|
25
|
+
}
|
|
26
|
+
interface NexusAuthResult {
|
|
27
|
+
access_token: string;
|
|
28
|
+
refresh_token: string;
|
|
29
|
+
expires_in: number;
|
|
30
|
+
investor: {
|
|
31
|
+
id: string;
|
|
32
|
+
email: string;
|
|
33
|
+
first_name: string;
|
|
34
|
+
last_name: string;
|
|
35
|
+
username: string;
|
|
36
|
+
avatar_url: string;
|
|
37
|
+
email_verified: boolean;
|
|
38
|
+
two_factor_enabled: boolean;
|
|
39
|
+
global_role: string;
|
|
40
|
+
};
|
|
41
|
+
session_id: string;
|
|
42
|
+
}
|
|
43
|
+
interface NexusContextValue {
|
|
44
|
+
user: NexusUser | null;
|
|
45
|
+
isAuthenticated: boolean;
|
|
46
|
+
isLoading: boolean;
|
|
47
|
+
accessToken: string | null;
|
|
48
|
+
login: () => void;
|
|
49
|
+
logout: () => void;
|
|
50
|
+
refreshUser: () => Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface NexusProviderProps extends NexusConfig {
|
|
54
|
+
children: ReactNode;
|
|
55
|
+
onLogin?: (user: NexusUser) => void;
|
|
56
|
+
onLogout?: () => void;
|
|
57
|
+
}
|
|
58
|
+
declare function NexusProvider({ children, clientId, redirectUri, nexusUrl, apiUrl, onLogin, onLogout, }: NexusProviderProps): react_jsx_runtime.JSX.Element;
|
|
59
|
+
|
|
60
|
+
interface NexusCallbackProps {
|
|
61
|
+
onSuccess?: () => void;
|
|
62
|
+
onError?: (error: string) => void;
|
|
63
|
+
}
|
|
64
|
+
declare function NexusCallback({ onSuccess, onError }: NexusCallbackProps): react_jsx_runtime.JSX.Element;
|
|
65
|
+
|
|
66
|
+
interface NexusLoginButtonProps {
|
|
67
|
+
label?: string;
|
|
68
|
+
size?: 'sm' | 'md' | 'lg';
|
|
69
|
+
variant?: 'dark' | 'light' | 'outline';
|
|
70
|
+
className?: string;
|
|
71
|
+
style?: React.CSSProperties;
|
|
72
|
+
}
|
|
73
|
+
declare function NexusLoginButton({ label, size, variant, className, style: customStyle, }: NexusLoginButtonProps): react_jsx_runtime.JSX.Element;
|
|
74
|
+
|
|
75
|
+
declare function useNexus(): NexusContextValue;
|
|
76
|
+
|
|
77
|
+
declare function setTokens(access: string, refresh: string): void;
|
|
78
|
+
declare function getAccessToken(): string | null;
|
|
79
|
+
declare function getRefreshToken(): string | null;
|
|
80
|
+
declare function clearTokens(): void;
|
|
81
|
+
declare function isTokenValid(): boolean;
|
|
82
|
+
declare function decodeUser(token: string): NexusUser | null;
|
|
83
|
+
declare function refreshTokens(apiUrl: string): Promise<boolean>;
|
|
84
|
+
declare function nexusFetch<T>(apiUrl: string, path: string, options?: RequestInit, onAuthFailure?: () => void): Promise<T>;
|
|
85
|
+
|
|
86
|
+
export { type NexusAuthResult, NexusCallback, type NexusConfig, type NexusContextValue, NexusLoginButton, NexusProvider, type NexusUser, clearTokens, decodeUser, getAccessToken, getRefreshToken, isTokenValid, nexusFetch, refreshTokens, setTokens, useNexus };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
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
|
+
NexusCallback: () => NexusCallback,
|
|
24
|
+
NexusLoginButton: () => NexusLoginButton,
|
|
25
|
+
NexusProvider: () => NexusProvider,
|
|
26
|
+
clearTokens: () => clearTokens,
|
|
27
|
+
decodeUser: () => decodeUser,
|
|
28
|
+
getAccessToken: () => getAccessToken,
|
|
29
|
+
getRefreshToken: () => getRefreshToken,
|
|
30
|
+
isTokenValid: () => isTokenValid,
|
|
31
|
+
nexusFetch: () => nexusFetch,
|
|
32
|
+
refreshTokens: () => refreshTokens,
|
|
33
|
+
setTokens: () => setTokens,
|
|
34
|
+
useNexus: () => useNexus
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
|
|
38
|
+
// src/NexusProvider.tsx
|
|
39
|
+
var import_react = require("react");
|
|
40
|
+
|
|
41
|
+
// src/client.ts
|
|
42
|
+
var TOKEN_KEY = "nexus_access_token";
|
|
43
|
+
var REFRESH_KEY = "nexus_refresh_token";
|
|
44
|
+
var STATE_KEY = "nexus_oauth_state";
|
|
45
|
+
var memoryAccessToken = null;
|
|
46
|
+
var memoryRefreshToken = null;
|
|
47
|
+
var refreshPromise = null;
|
|
48
|
+
function setTokens(access, refresh) {
|
|
49
|
+
memoryAccessToken = access;
|
|
50
|
+
memoryRefreshToken = refresh;
|
|
51
|
+
localStorage.setItem(TOKEN_KEY, access);
|
|
52
|
+
localStorage.setItem(REFRESH_KEY, refresh);
|
|
53
|
+
}
|
|
54
|
+
function getAccessToken() {
|
|
55
|
+
if (!memoryAccessToken) {
|
|
56
|
+
memoryAccessToken = localStorage.getItem(TOKEN_KEY);
|
|
57
|
+
}
|
|
58
|
+
return memoryAccessToken;
|
|
59
|
+
}
|
|
60
|
+
function getRefreshToken() {
|
|
61
|
+
if (!memoryRefreshToken) {
|
|
62
|
+
memoryRefreshToken = localStorage.getItem(REFRESH_KEY);
|
|
63
|
+
}
|
|
64
|
+
return memoryRefreshToken;
|
|
65
|
+
}
|
|
66
|
+
function clearTokens() {
|
|
67
|
+
memoryAccessToken = null;
|
|
68
|
+
memoryRefreshToken = null;
|
|
69
|
+
localStorage.removeItem(TOKEN_KEY);
|
|
70
|
+
localStorage.removeItem(REFRESH_KEY);
|
|
71
|
+
}
|
|
72
|
+
function isTokenValid() {
|
|
73
|
+
const token = getAccessToken();
|
|
74
|
+
if (!token) return false;
|
|
75
|
+
try {
|
|
76
|
+
const payload = JSON.parse(atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")));
|
|
77
|
+
return payload.exp * 1e3 > Date.now() + 3e4;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function decodeUser(token) {
|
|
83
|
+
try {
|
|
84
|
+
const payload = JSON.parse(atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")));
|
|
85
|
+
return {
|
|
86
|
+
id: payload.sub,
|
|
87
|
+
email: payload.email,
|
|
88
|
+
username: payload.username || "",
|
|
89
|
+
firstName: "",
|
|
90
|
+
lastName: "",
|
|
91
|
+
avatar: payload.avatar || "",
|
|
92
|
+
globalRole: payload.global_role || "citizen",
|
|
93
|
+
emailVerified: payload.email_verified || false,
|
|
94
|
+
affiliateCode: payload.affiliate_code || "",
|
|
95
|
+
twoFactorEnabled: payload.two_factor_verified || false,
|
|
96
|
+
appRoles: payload.app_roles
|
|
97
|
+
};
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function generateState() {
|
|
103
|
+
const state = Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2);
|
|
104
|
+
sessionStorage.setItem(STATE_KEY, state);
|
|
105
|
+
return state;
|
|
106
|
+
}
|
|
107
|
+
function validateState(state) {
|
|
108
|
+
const saved = sessionStorage.getItem(STATE_KEY);
|
|
109
|
+
sessionStorage.removeItem(STATE_KEY);
|
|
110
|
+
return saved === state;
|
|
111
|
+
}
|
|
112
|
+
function getLoginUrl(config) {
|
|
113
|
+
const nexusUrl = config.nexusUrl || "https://accounts.vylth.com";
|
|
114
|
+
const state = generateState();
|
|
115
|
+
const params = new URLSearchParams({
|
|
116
|
+
client_id: config.clientId,
|
|
117
|
+
redirect_uri: config.redirectUri,
|
|
118
|
+
state
|
|
119
|
+
});
|
|
120
|
+
return `${nexusUrl}/oauth/authorize?${params.toString()}`;
|
|
121
|
+
}
|
|
122
|
+
async function exchangeCode(code, apiUrl) {
|
|
123
|
+
const res = await fetch(`${apiUrl}/token/exchange`, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: { "Content-Type": "application/json" },
|
|
126
|
+
body: JSON.stringify({ code })
|
|
127
|
+
});
|
|
128
|
+
if (!res.ok) {
|
|
129
|
+
const err = await res.json().catch(() => ({ error: "Token exchange failed" }));
|
|
130
|
+
throw new Error(err.error || "Token exchange failed");
|
|
131
|
+
}
|
|
132
|
+
const data = await res.json();
|
|
133
|
+
setTokens(data.access_token, data.refresh_token);
|
|
134
|
+
return data;
|
|
135
|
+
}
|
|
136
|
+
async function doRefresh(apiUrl) {
|
|
137
|
+
const rt = getRefreshToken();
|
|
138
|
+
if (!rt) return false;
|
|
139
|
+
try {
|
|
140
|
+
const res = await fetch(`${apiUrl}/token/refresh`, {
|
|
141
|
+
method: "POST",
|
|
142
|
+
headers: { "Content-Type": "application/json" },
|
|
143
|
+
body: JSON.stringify({ refresh_token: rt })
|
|
144
|
+
});
|
|
145
|
+
if (res.ok) {
|
|
146
|
+
const data = await res.json();
|
|
147
|
+
setTokens(data.access_token, data.refresh_token);
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
} catch {
|
|
151
|
+
}
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
function refreshTokens(apiUrl) {
|
|
155
|
+
if (!refreshPromise) {
|
|
156
|
+
refreshPromise = doRefresh(apiUrl).finally(() => {
|
|
157
|
+
refreshPromise = null;
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return refreshPromise;
|
|
161
|
+
}
|
|
162
|
+
async function nexusFetch(apiUrl, path, options = {}, onAuthFailure) {
|
|
163
|
+
const token = getAccessToken();
|
|
164
|
+
const headers = {
|
|
165
|
+
"Content-Type": "application/json",
|
|
166
|
+
...options.headers
|
|
167
|
+
};
|
|
168
|
+
if (token) {
|
|
169
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
170
|
+
}
|
|
171
|
+
const res = await fetch(`${apiUrl}${path}`, { ...options, headers });
|
|
172
|
+
if (res.status === 401 && token) {
|
|
173
|
+
const refreshed = await refreshTokens(apiUrl);
|
|
174
|
+
if (refreshed) {
|
|
175
|
+
headers["Authorization"] = `Bearer ${getAccessToken()}`;
|
|
176
|
+
const retry = await fetch(`${apiUrl}${path}`, { ...options, headers });
|
|
177
|
+
if (!retry.ok) {
|
|
178
|
+
const err = await retry.json().catch(() => ({ error: "Request failed" }));
|
|
179
|
+
throw new Error(err.error || "Request failed");
|
|
180
|
+
}
|
|
181
|
+
return retry.json();
|
|
182
|
+
}
|
|
183
|
+
clearTokens();
|
|
184
|
+
onAuthFailure?.();
|
|
185
|
+
throw new Error("Session expired");
|
|
186
|
+
}
|
|
187
|
+
if (!res.ok) {
|
|
188
|
+
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
|
|
189
|
+
throw new Error(err.error || `HTTP ${res.status}`);
|
|
190
|
+
}
|
|
191
|
+
return res.json();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/NexusProvider.tsx
|
|
195
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
196
|
+
var NexusContext = (0, import_react.createContext)(null);
|
|
197
|
+
function NexusProvider({
|
|
198
|
+
children,
|
|
199
|
+
clientId,
|
|
200
|
+
redirectUri,
|
|
201
|
+
nexusUrl,
|
|
202
|
+
apiUrl,
|
|
203
|
+
onLogin,
|
|
204
|
+
onLogout
|
|
205
|
+
}) {
|
|
206
|
+
const resolvedApiUrl = apiUrl || "https://auth.vylth.com/api/nexus";
|
|
207
|
+
const config = { clientId, redirectUri, nexusUrl, apiUrl: resolvedApiUrl };
|
|
208
|
+
const [user, setUser] = (0, import_react.useState)(null);
|
|
209
|
+
const [isLoading, setIsLoading] = (0, import_react.useState)(true);
|
|
210
|
+
const initAuth = (0, import_react.useCallback)(async () => {
|
|
211
|
+
const token = getAccessToken();
|
|
212
|
+
if (!token) {
|
|
213
|
+
setIsLoading(false);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (isTokenValid()) {
|
|
217
|
+
const decoded = decodeUser(token);
|
|
218
|
+
if (decoded) {
|
|
219
|
+
setUser(decoded);
|
|
220
|
+
setIsLoading(false);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const refreshed = await refreshTokens(resolvedApiUrl);
|
|
225
|
+
if (refreshed) {
|
|
226
|
+
const newToken = getAccessToken();
|
|
227
|
+
if (newToken) {
|
|
228
|
+
const decoded = decodeUser(newToken);
|
|
229
|
+
setUser(decoded);
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
clearTokens();
|
|
233
|
+
}
|
|
234
|
+
setIsLoading(false);
|
|
235
|
+
}, [resolvedApiUrl]);
|
|
236
|
+
(0, import_react.useEffect)(() => {
|
|
237
|
+
initAuth();
|
|
238
|
+
}, [initAuth]);
|
|
239
|
+
const login = (0, import_react.useCallback)(() => {
|
|
240
|
+
window.location.href = getLoginUrl(config);
|
|
241
|
+
}, [config]);
|
|
242
|
+
const logout = (0, import_react.useCallback)(() => {
|
|
243
|
+
clearTokens();
|
|
244
|
+
setUser(null);
|
|
245
|
+
onLogout?.();
|
|
246
|
+
}, [onLogout]);
|
|
247
|
+
const refreshUser = (0, import_react.useCallback)(async () => {
|
|
248
|
+
const refreshed = await refreshTokens(resolvedApiUrl);
|
|
249
|
+
if (refreshed) {
|
|
250
|
+
const token = getAccessToken();
|
|
251
|
+
if (token) {
|
|
252
|
+
const decoded = decodeUser(token);
|
|
253
|
+
setUser(decoded);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}, [resolvedApiUrl]);
|
|
257
|
+
const setUserWithCallback = (0, import_react.useCallback)(
|
|
258
|
+
(u) => {
|
|
259
|
+
setUser(u);
|
|
260
|
+
onLogin?.(u);
|
|
261
|
+
},
|
|
262
|
+
[onLogin]
|
|
263
|
+
);
|
|
264
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
265
|
+
NexusContext.Provider,
|
|
266
|
+
{
|
|
267
|
+
value: {
|
|
268
|
+
user,
|
|
269
|
+
isAuthenticated: !!user,
|
|
270
|
+
isLoading,
|
|
271
|
+
accessToken: getAccessToken(),
|
|
272
|
+
login,
|
|
273
|
+
logout,
|
|
274
|
+
refreshUser
|
|
275
|
+
},
|
|
276
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NexusConfigContext.Provider, { value: { ...config, setUser: setUserWithCallback }, children })
|
|
277
|
+
}
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
var NexusConfigContext = (0, import_react.createContext)(null);
|
|
281
|
+
|
|
282
|
+
// src/NexusCallback.tsx
|
|
283
|
+
var import_react2 = require("react");
|
|
284
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
285
|
+
function NexusCallback({ onSuccess, onError }) {
|
|
286
|
+
const config = (0, import_react2.useContext)(NexusConfigContext);
|
|
287
|
+
const [error, setError] = (0, import_react2.useState)("");
|
|
288
|
+
(0, import_react2.useEffect)(() => {
|
|
289
|
+
if (!config) return;
|
|
290
|
+
const params = new URLSearchParams(window.location.search);
|
|
291
|
+
const code = params.get("code");
|
|
292
|
+
const state = params.get("state");
|
|
293
|
+
const oauthError = params.get("error");
|
|
294
|
+
if (oauthError) {
|
|
295
|
+
const msg = `Authentication denied: ${oauthError}`;
|
|
296
|
+
setError(msg);
|
|
297
|
+
onError?.(msg);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
if (!code) {
|
|
301
|
+
const msg = "No authorization code received";
|
|
302
|
+
setError(msg);
|
|
303
|
+
onError?.(msg);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (state && !validateState(state)) {
|
|
307
|
+
const msg = "Invalid state parameter";
|
|
308
|
+
setError(msg);
|
|
309
|
+
onError?.(msg);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const apiUrl = config.apiUrl || "https://auth.vylth.com/api/nexus";
|
|
313
|
+
exchangeCode(code, apiUrl).then((result) => {
|
|
314
|
+
const user = decodeUser(result.access_token);
|
|
315
|
+
if (user) {
|
|
316
|
+
user.firstName = result.investor.first_name;
|
|
317
|
+
user.lastName = result.investor.last_name;
|
|
318
|
+
user.avatar = result.investor.avatar_url || user.avatar;
|
|
319
|
+
user.emailVerified = result.investor.email_verified;
|
|
320
|
+
config.setUser(user);
|
|
321
|
+
}
|
|
322
|
+
onSuccess?.();
|
|
323
|
+
}).catch((err) => {
|
|
324
|
+
const msg = err.message || "Authentication failed";
|
|
325
|
+
setError(msg);
|
|
326
|
+
onError?.(msg);
|
|
327
|
+
});
|
|
328
|
+
}, [config]);
|
|
329
|
+
if (error) {
|
|
330
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", minHeight: "100vh", fontFamily: "system-ui, sans-serif", background: "#0a0a0a", color: "#fff" }, children: [
|
|
331
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: { color: "#ef4444", marginBottom: "16px" }, children: error }),
|
|
332
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
333
|
+
"button",
|
|
334
|
+
{
|
|
335
|
+
onClick: () => window.location.href = "/",
|
|
336
|
+
style: { padding: "8px 24px", borderRadius: "8px", border: "1px solid rgba(255,255,255,0.1)", background: "rgba(255,255,255,0.05)", color: "#fff", cursor: "pointer" },
|
|
337
|
+
children: "Try Again"
|
|
338
|
+
}
|
|
339
|
+
)
|
|
340
|
+
] });
|
|
341
|
+
}
|
|
342
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { display: "flex", alignItems: "center", justifyContent: "center", minHeight: "100vh", background: "#0a0a0a", color: "#fff" }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: { textAlign: "center" }, children: [
|
|
343
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: { width: "32px", height: "32px", border: "3px solid rgba(255,255,255,0.1)", borderTopColor: "#d4a574", borderRadius: "50%", animation: "nexus-spin 0.8s linear infinite", margin: "0 auto 16px" } }),
|
|
344
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { style: { fontFamily: "monospace", fontSize: "14px", opacity: 0.6 }, children: "Authenticating with Nexus..." }),
|
|
345
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: `@keyframes nexus-spin { to { transform: rotate(360deg) } }` })
|
|
346
|
+
] }) });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/NexusLoginButton.tsx
|
|
350
|
+
var import_react3 = require("react");
|
|
351
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
352
|
+
var NEXUS_LOGO = `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>`)}`;
|
|
353
|
+
var sizes = {
|
|
354
|
+
sm: { padding: "8px 16px", fontSize: "13px", iconSize: 16, gap: 6 },
|
|
355
|
+
md: { padding: "10px 24px", fontSize: "14px", iconSize: 18, gap: 8 },
|
|
356
|
+
lg: { padding: "12px 32px", fontSize: "15px", iconSize: 20, gap: 10 }
|
|
357
|
+
};
|
|
358
|
+
var variants = {
|
|
359
|
+
dark: {
|
|
360
|
+
background: "#1a1a1a",
|
|
361
|
+
color: "#ffffff",
|
|
362
|
+
border: "1px solid rgba(255,255,255,0.1)",
|
|
363
|
+
hoverBg: "#252525"
|
|
364
|
+
},
|
|
365
|
+
light: {
|
|
366
|
+
background: "#ffffff",
|
|
367
|
+
color: "#0a0a0a",
|
|
368
|
+
border: "1px solid rgba(0,0,0,0.1)",
|
|
369
|
+
hoverBg: "#f5f5f5"
|
|
370
|
+
},
|
|
371
|
+
outline: {
|
|
372
|
+
background: "transparent",
|
|
373
|
+
color: "#d4a574",
|
|
374
|
+
border: "1px solid #d4a574",
|
|
375
|
+
hoverBg: "rgba(212,165,116,0.1)"
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
function NexusLoginButton({
|
|
379
|
+
label = "Sign in with Nexus",
|
|
380
|
+
size = "md",
|
|
381
|
+
variant = "dark",
|
|
382
|
+
className,
|
|
383
|
+
style: customStyle
|
|
384
|
+
}) {
|
|
385
|
+
const context = (0, import_react3.useContext)(NexusContext);
|
|
386
|
+
const s = sizes[size];
|
|
387
|
+
const v = variants[variant];
|
|
388
|
+
const handleClick = () => {
|
|
389
|
+
context?.login();
|
|
390
|
+
};
|
|
391
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
392
|
+
"button",
|
|
393
|
+
{
|
|
394
|
+
onClick: handleClick,
|
|
395
|
+
className,
|
|
396
|
+
style: {
|
|
397
|
+
display: "inline-flex",
|
|
398
|
+
alignItems: "center",
|
|
399
|
+
justifyContent: "center",
|
|
400
|
+
gap: `${s.gap}px`,
|
|
401
|
+
padding: s.padding,
|
|
402
|
+
fontSize: s.fontSize,
|
|
403
|
+
fontFamily: "'JetBrains Mono', 'SF Mono', 'Fira Code', monospace",
|
|
404
|
+
fontWeight: 500,
|
|
405
|
+
letterSpacing: "0.02em",
|
|
406
|
+
background: v.background,
|
|
407
|
+
color: v.color,
|
|
408
|
+
border: v.border,
|
|
409
|
+
borderRadius: "12px",
|
|
410
|
+
cursor: "pointer",
|
|
411
|
+
transition: "all 0.2s ease",
|
|
412
|
+
textDecoration: "none",
|
|
413
|
+
whiteSpace: "nowrap",
|
|
414
|
+
...customStyle
|
|
415
|
+
},
|
|
416
|
+
onMouseEnter: (e) => {
|
|
417
|
+
e.target.style.background = v.hoverBg;
|
|
418
|
+
e.target.style.transform = "translateY(-1px)";
|
|
419
|
+
},
|
|
420
|
+
onMouseLeave: (e) => {
|
|
421
|
+
e.target.style.background = v.background;
|
|
422
|
+
e.target.style.transform = "translateY(0)";
|
|
423
|
+
},
|
|
424
|
+
children: [
|
|
425
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
426
|
+
"svg",
|
|
427
|
+
{
|
|
428
|
+
width: s.iconSize,
|
|
429
|
+
height: s.iconSize,
|
|
430
|
+
viewBox: "0 0 24 24",
|
|
431
|
+
fill: "none",
|
|
432
|
+
stroke: "#d4a574",
|
|
433
|
+
strokeWidth: "2",
|
|
434
|
+
strokeLinecap: "round",
|
|
435
|
+
strokeLinejoin: "round",
|
|
436
|
+
children: [
|
|
437
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M12 2L2 7l10 5 10-5-10-5z" }),
|
|
438
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M2 17l10 5 10-5" }),
|
|
439
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("path", { d: "M2 12l10 5 10-5" })
|
|
440
|
+
]
|
|
441
|
+
}
|
|
442
|
+
),
|
|
443
|
+
label
|
|
444
|
+
]
|
|
445
|
+
}
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/useNexus.ts
|
|
450
|
+
var import_react4 = require("react");
|
|
451
|
+
function useNexus() {
|
|
452
|
+
const context = (0, import_react4.useContext)(NexusContext);
|
|
453
|
+
if (!context) {
|
|
454
|
+
throw new Error("useNexus must be used within a <NexusProvider>");
|
|
455
|
+
}
|
|
456
|
+
return context;
|
|
457
|
+
}
|
|
458
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
459
|
+
0 && (module.exports = {
|
|
460
|
+
NexusCallback,
|
|
461
|
+
NexusLoginButton,
|
|
462
|
+
NexusProvider,
|
|
463
|
+
clearTokens,
|
|
464
|
+
decodeUser,
|
|
465
|
+
getAccessToken,
|
|
466
|
+
getRefreshToken,
|
|
467
|
+
isTokenValid,
|
|
468
|
+
nexusFetch,
|
|
469
|
+
refreshTokens,
|
|
470
|
+
setTokens,
|
|
471
|
+
useNexus
|
|
472
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
// src/NexusProvider.tsx
|
|
2
|
+
import { createContext, useCallback, useEffect, useState } from "react";
|
|
3
|
+
|
|
4
|
+
// src/client.ts
|
|
5
|
+
var TOKEN_KEY = "nexus_access_token";
|
|
6
|
+
var REFRESH_KEY = "nexus_refresh_token";
|
|
7
|
+
var STATE_KEY = "nexus_oauth_state";
|
|
8
|
+
var memoryAccessToken = null;
|
|
9
|
+
var memoryRefreshToken = null;
|
|
10
|
+
var refreshPromise = null;
|
|
11
|
+
function setTokens(access, refresh) {
|
|
12
|
+
memoryAccessToken = access;
|
|
13
|
+
memoryRefreshToken = refresh;
|
|
14
|
+
localStorage.setItem(TOKEN_KEY, access);
|
|
15
|
+
localStorage.setItem(REFRESH_KEY, refresh);
|
|
16
|
+
}
|
|
17
|
+
function getAccessToken() {
|
|
18
|
+
if (!memoryAccessToken) {
|
|
19
|
+
memoryAccessToken = localStorage.getItem(TOKEN_KEY);
|
|
20
|
+
}
|
|
21
|
+
return memoryAccessToken;
|
|
22
|
+
}
|
|
23
|
+
function getRefreshToken() {
|
|
24
|
+
if (!memoryRefreshToken) {
|
|
25
|
+
memoryRefreshToken = localStorage.getItem(REFRESH_KEY);
|
|
26
|
+
}
|
|
27
|
+
return memoryRefreshToken;
|
|
28
|
+
}
|
|
29
|
+
function clearTokens() {
|
|
30
|
+
memoryAccessToken = null;
|
|
31
|
+
memoryRefreshToken = null;
|
|
32
|
+
localStorage.removeItem(TOKEN_KEY);
|
|
33
|
+
localStorage.removeItem(REFRESH_KEY);
|
|
34
|
+
}
|
|
35
|
+
function isTokenValid() {
|
|
36
|
+
const token = getAccessToken();
|
|
37
|
+
if (!token) return false;
|
|
38
|
+
try {
|
|
39
|
+
const payload = JSON.parse(atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")));
|
|
40
|
+
return payload.exp * 1e3 > Date.now() + 3e4;
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function decodeUser(token) {
|
|
46
|
+
try {
|
|
47
|
+
const payload = JSON.parse(atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")));
|
|
48
|
+
return {
|
|
49
|
+
id: payload.sub,
|
|
50
|
+
email: payload.email,
|
|
51
|
+
username: payload.username || "",
|
|
52
|
+
firstName: "",
|
|
53
|
+
lastName: "",
|
|
54
|
+
avatar: payload.avatar || "",
|
|
55
|
+
globalRole: payload.global_role || "citizen",
|
|
56
|
+
emailVerified: payload.email_verified || false,
|
|
57
|
+
affiliateCode: payload.affiliate_code || "",
|
|
58
|
+
twoFactorEnabled: payload.two_factor_verified || false,
|
|
59
|
+
appRoles: payload.app_roles
|
|
60
|
+
};
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function generateState() {
|
|
66
|
+
const state = Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2);
|
|
67
|
+
sessionStorage.setItem(STATE_KEY, state);
|
|
68
|
+
return state;
|
|
69
|
+
}
|
|
70
|
+
function validateState(state) {
|
|
71
|
+
const saved = sessionStorage.getItem(STATE_KEY);
|
|
72
|
+
sessionStorage.removeItem(STATE_KEY);
|
|
73
|
+
return saved === state;
|
|
74
|
+
}
|
|
75
|
+
function getLoginUrl(config) {
|
|
76
|
+
const nexusUrl = config.nexusUrl || "https://accounts.vylth.com";
|
|
77
|
+
const state = generateState();
|
|
78
|
+
const params = new URLSearchParams({
|
|
79
|
+
client_id: config.clientId,
|
|
80
|
+
redirect_uri: config.redirectUri,
|
|
81
|
+
state
|
|
82
|
+
});
|
|
83
|
+
return `${nexusUrl}/oauth/authorize?${params.toString()}`;
|
|
84
|
+
}
|
|
85
|
+
async function exchangeCode(code, apiUrl) {
|
|
86
|
+
const res = await fetch(`${apiUrl}/token/exchange`, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: { "Content-Type": "application/json" },
|
|
89
|
+
body: JSON.stringify({ code })
|
|
90
|
+
});
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
const err = await res.json().catch(() => ({ error: "Token exchange failed" }));
|
|
93
|
+
throw new Error(err.error || "Token exchange failed");
|
|
94
|
+
}
|
|
95
|
+
const data = await res.json();
|
|
96
|
+
setTokens(data.access_token, data.refresh_token);
|
|
97
|
+
return data;
|
|
98
|
+
}
|
|
99
|
+
async function doRefresh(apiUrl) {
|
|
100
|
+
const rt = getRefreshToken();
|
|
101
|
+
if (!rt) return false;
|
|
102
|
+
try {
|
|
103
|
+
const res = await fetch(`${apiUrl}/token/refresh`, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: { "Content-Type": "application/json" },
|
|
106
|
+
body: JSON.stringify({ refresh_token: rt })
|
|
107
|
+
});
|
|
108
|
+
if (res.ok) {
|
|
109
|
+
const data = await res.json();
|
|
110
|
+
setTokens(data.access_token, data.refresh_token);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
}
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
function refreshTokens(apiUrl) {
|
|
118
|
+
if (!refreshPromise) {
|
|
119
|
+
refreshPromise = doRefresh(apiUrl).finally(() => {
|
|
120
|
+
refreshPromise = null;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return refreshPromise;
|
|
124
|
+
}
|
|
125
|
+
async function nexusFetch(apiUrl, path, options = {}, onAuthFailure) {
|
|
126
|
+
const token = getAccessToken();
|
|
127
|
+
const headers = {
|
|
128
|
+
"Content-Type": "application/json",
|
|
129
|
+
...options.headers
|
|
130
|
+
};
|
|
131
|
+
if (token) {
|
|
132
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
133
|
+
}
|
|
134
|
+
const res = await fetch(`${apiUrl}${path}`, { ...options, headers });
|
|
135
|
+
if (res.status === 401 && token) {
|
|
136
|
+
const refreshed = await refreshTokens(apiUrl);
|
|
137
|
+
if (refreshed) {
|
|
138
|
+
headers["Authorization"] = `Bearer ${getAccessToken()}`;
|
|
139
|
+
const retry = await fetch(`${apiUrl}${path}`, { ...options, headers });
|
|
140
|
+
if (!retry.ok) {
|
|
141
|
+
const err = await retry.json().catch(() => ({ error: "Request failed" }));
|
|
142
|
+
throw new Error(err.error || "Request failed");
|
|
143
|
+
}
|
|
144
|
+
return retry.json();
|
|
145
|
+
}
|
|
146
|
+
clearTokens();
|
|
147
|
+
onAuthFailure?.();
|
|
148
|
+
throw new Error("Session expired");
|
|
149
|
+
}
|
|
150
|
+
if (!res.ok) {
|
|
151
|
+
const err = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
|
|
152
|
+
throw new Error(err.error || `HTTP ${res.status}`);
|
|
153
|
+
}
|
|
154
|
+
return res.json();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/NexusProvider.tsx
|
|
158
|
+
import { jsx } from "react/jsx-runtime";
|
|
159
|
+
var NexusContext = createContext(null);
|
|
160
|
+
function NexusProvider({
|
|
161
|
+
children,
|
|
162
|
+
clientId,
|
|
163
|
+
redirectUri,
|
|
164
|
+
nexusUrl,
|
|
165
|
+
apiUrl,
|
|
166
|
+
onLogin,
|
|
167
|
+
onLogout
|
|
168
|
+
}) {
|
|
169
|
+
const resolvedApiUrl = apiUrl || "https://auth.vylth.com/api/nexus";
|
|
170
|
+
const config = { clientId, redirectUri, nexusUrl, apiUrl: resolvedApiUrl };
|
|
171
|
+
const [user, setUser] = useState(null);
|
|
172
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
173
|
+
const initAuth = useCallback(async () => {
|
|
174
|
+
const token = getAccessToken();
|
|
175
|
+
if (!token) {
|
|
176
|
+
setIsLoading(false);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (isTokenValid()) {
|
|
180
|
+
const decoded = decodeUser(token);
|
|
181
|
+
if (decoded) {
|
|
182
|
+
setUser(decoded);
|
|
183
|
+
setIsLoading(false);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const refreshed = await refreshTokens(resolvedApiUrl);
|
|
188
|
+
if (refreshed) {
|
|
189
|
+
const newToken = getAccessToken();
|
|
190
|
+
if (newToken) {
|
|
191
|
+
const decoded = decodeUser(newToken);
|
|
192
|
+
setUser(decoded);
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
clearTokens();
|
|
196
|
+
}
|
|
197
|
+
setIsLoading(false);
|
|
198
|
+
}, [resolvedApiUrl]);
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
initAuth();
|
|
201
|
+
}, [initAuth]);
|
|
202
|
+
const login = useCallback(() => {
|
|
203
|
+
window.location.href = getLoginUrl(config);
|
|
204
|
+
}, [config]);
|
|
205
|
+
const logout = useCallback(() => {
|
|
206
|
+
clearTokens();
|
|
207
|
+
setUser(null);
|
|
208
|
+
onLogout?.();
|
|
209
|
+
}, [onLogout]);
|
|
210
|
+
const refreshUser = useCallback(async () => {
|
|
211
|
+
const refreshed = await refreshTokens(resolvedApiUrl);
|
|
212
|
+
if (refreshed) {
|
|
213
|
+
const token = getAccessToken();
|
|
214
|
+
if (token) {
|
|
215
|
+
const decoded = decodeUser(token);
|
|
216
|
+
setUser(decoded);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}, [resolvedApiUrl]);
|
|
220
|
+
const setUserWithCallback = useCallback(
|
|
221
|
+
(u) => {
|
|
222
|
+
setUser(u);
|
|
223
|
+
onLogin?.(u);
|
|
224
|
+
},
|
|
225
|
+
[onLogin]
|
|
226
|
+
);
|
|
227
|
+
return /* @__PURE__ */ jsx(
|
|
228
|
+
NexusContext.Provider,
|
|
229
|
+
{
|
|
230
|
+
value: {
|
|
231
|
+
user,
|
|
232
|
+
isAuthenticated: !!user,
|
|
233
|
+
isLoading,
|
|
234
|
+
accessToken: getAccessToken(),
|
|
235
|
+
login,
|
|
236
|
+
logout,
|
|
237
|
+
refreshUser
|
|
238
|
+
},
|
|
239
|
+
children: /* @__PURE__ */ jsx(NexusConfigContext.Provider, { value: { ...config, setUser: setUserWithCallback }, children })
|
|
240
|
+
}
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
var NexusConfigContext = createContext(null);
|
|
244
|
+
|
|
245
|
+
// src/NexusCallback.tsx
|
|
246
|
+
import { useContext, useEffect as useEffect2, useState as useState2 } from "react";
|
|
247
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
248
|
+
function NexusCallback({ onSuccess, onError }) {
|
|
249
|
+
const config = useContext(NexusConfigContext);
|
|
250
|
+
const [error, setError] = useState2("");
|
|
251
|
+
useEffect2(() => {
|
|
252
|
+
if (!config) return;
|
|
253
|
+
const params = new URLSearchParams(window.location.search);
|
|
254
|
+
const code = params.get("code");
|
|
255
|
+
const state = params.get("state");
|
|
256
|
+
const oauthError = params.get("error");
|
|
257
|
+
if (oauthError) {
|
|
258
|
+
const msg = `Authentication denied: ${oauthError}`;
|
|
259
|
+
setError(msg);
|
|
260
|
+
onError?.(msg);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (!code) {
|
|
264
|
+
const msg = "No authorization code received";
|
|
265
|
+
setError(msg);
|
|
266
|
+
onError?.(msg);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (state && !validateState(state)) {
|
|
270
|
+
const msg = "Invalid state parameter";
|
|
271
|
+
setError(msg);
|
|
272
|
+
onError?.(msg);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const apiUrl = config.apiUrl || "https://auth.vylth.com/api/nexus";
|
|
276
|
+
exchangeCode(code, apiUrl).then((result) => {
|
|
277
|
+
const user = decodeUser(result.access_token);
|
|
278
|
+
if (user) {
|
|
279
|
+
user.firstName = result.investor.first_name;
|
|
280
|
+
user.lastName = result.investor.last_name;
|
|
281
|
+
user.avatar = result.investor.avatar_url || user.avatar;
|
|
282
|
+
user.emailVerified = result.investor.email_verified;
|
|
283
|
+
config.setUser(user);
|
|
284
|
+
}
|
|
285
|
+
onSuccess?.();
|
|
286
|
+
}).catch((err) => {
|
|
287
|
+
const msg = err.message || "Authentication failed";
|
|
288
|
+
setError(msg);
|
|
289
|
+
onError?.(msg);
|
|
290
|
+
});
|
|
291
|
+
}, [config]);
|
|
292
|
+
if (error) {
|
|
293
|
+
return /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", minHeight: "100vh", fontFamily: "system-ui, sans-serif", background: "#0a0a0a", color: "#fff" }, children: [
|
|
294
|
+
/* @__PURE__ */ jsx2("p", { style: { color: "#ef4444", marginBottom: "16px" }, children: error }),
|
|
295
|
+
/* @__PURE__ */ jsx2(
|
|
296
|
+
"button",
|
|
297
|
+
{
|
|
298
|
+
onClick: () => window.location.href = "/",
|
|
299
|
+
style: { padding: "8px 24px", borderRadius: "8px", border: "1px solid rgba(255,255,255,0.1)", background: "rgba(255,255,255,0.05)", color: "#fff", cursor: "pointer" },
|
|
300
|
+
children: "Try Again"
|
|
301
|
+
}
|
|
302
|
+
)
|
|
303
|
+
] });
|
|
304
|
+
}
|
|
305
|
+
return /* @__PURE__ */ jsx2("div", { style: { display: "flex", alignItems: "center", justifyContent: "center", minHeight: "100vh", background: "#0a0a0a", color: "#fff" }, children: /* @__PURE__ */ jsxs("div", { style: { textAlign: "center" }, children: [
|
|
306
|
+
/* @__PURE__ */ jsx2("div", { style: { width: "32px", height: "32px", border: "3px solid rgba(255,255,255,0.1)", borderTopColor: "#d4a574", borderRadius: "50%", animation: "nexus-spin 0.8s linear infinite", margin: "0 auto 16px" } }),
|
|
307
|
+
/* @__PURE__ */ jsx2("p", { style: { fontFamily: "monospace", fontSize: "14px", opacity: 0.6 }, children: "Authenticating with Nexus..." }),
|
|
308
|
+
/* @__PURE__ */ jsx2("style", { children: `@keyframes nexus-spin { to { transform: rotate(360deg) } }` })
|
|
309
|
+
] }) });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// src/NexusLoginButton.tsx
|
|
313
|
+
import { useContext as useContext2 } from "react";
|
|
314
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
315
|
+
var NEXUS_LOGO = `data:image/svg+xml,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>`)}`;
|
|
316
|
+
var sizes = {
|
|
317
|
+
sm: { padding: "8px 16px", fontSize: "13px", iconSize: 16, gap: 6 },
|
|
318
|
+
md: { padding: "10px 24px", fontSize: "14px", iconSize: 18, gap: 8 },
|
|
319
|
+
lg: { padding: "12px 32px", fontSize: "15px", iconSize: 20, gap: 10 }
|
|
320
|
+
};
|
|
321
|
+
var variants = {
|
|
322
|
+
dark: {
|
|
323
|
+
background: "#1a1a1a",
|
|
324
|
+
color: "#ffffff",
|
|
325
|
+
border: "1px solid rgba(255,255,255,0.1)",
|
|
326
|
+
hoverBg: "#252525"
|
|
327
|
+
},
|
|
328
|
+
light: {
|
|
329
|
+
background: "#ffffff",
|
|
330
|
+
color: "#0a0a0a",
|
|
331
|
+
border: "1px solid rgba(0,0,0,0.1)",
|
|
332
|
+
hoverBg: "#f5f5f5"
|
|
333
|
+
},
|
|
334
|
+
outline: {
|
|
335
|
+
background: "transparent",
|
|
336
|
+
color: "#d4a574",
|
|
337
|
+
border: "1px solid #d4a574",
|
|
338
|
+
hoverBg: "rgba(212,165,116,0.1)"
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
function NexusLoginButton({
|
|
342
|
+
label = "Sign in with Nexus",
|
|
343
|
+
size = "md",
|
|
344
|
+
variant = "dark",
|
|
345
|
+
className,
|
|
346
|
+
style: customStyle
|
|
347
|
+
}) {
|
|
348
|
+
const context = useContext2(NexusContext);
|
|
349
|
+
const s = sizes[size];
|
|
350
|
+
const v = variants[variant];
|
|
351
|
+
const handleClick = () => {
|
|
352
|
+
context?.login();
|
|
353
|
+
};
|
|
354
|
+
return /* @__PURE__ */ jsxs2(
|
|
355
|
+
"button",
|
|
356
|
+
{
|
|
357
|
+
onClick: handleClick,
|
|
358
|
+
className,
|
|
359
|
+
style: {
|
|
360
|
+
display: "inline-flex",
|
|
361
|
+
alignItems: "center",
|
|
362
|
+
justifyContent: "center",
|
|
363
|
+
gap: `${s.gap}px`,
|
|
364
|
+
padding: s.padding,
|
|
365
|
+
fontSize: s.fontSize,
|
|
366
|
+
fontFamily: "'JetBrains Mono', 'SF Mono', 'Fira Code', monospace",
|
|
367
|
+
fontWeight: 500,
|
|
368
|
+
letterSpacing: "0.02em",
|
|
369
|
+
background: v.background,
|
|
370
|
+
color: v.color,
|
|
371
|
+
border: v.border,
|
|
372
|
+
borderRadius: "12px",
|
|
373
|
+
cursor: "pointer",
|
|
374
|
+
transition: "all 0.2s ease",
|
|
375
|
+
textDecoration: "none",
|
|
376
|
+
whiteSpace: "nowrap",
|
|
377
|
+
...customStyle
|
|
378
|
+
},
|
|
379
|
+
onMouseEnter: (e) => {
|
|
380
|
+
e.target.style.background = v.hoverBg;
|
|
381
|
+
e.target.style.transform = "translateY(-1px)";
|
|
382
|
+
},
|
|
383
|
+
onMouseLeave: (e) => {
|
|
384
|
+
e.target.style.background = v.background;
|
|
385
|
+
e.target.style.transform = "translateY(0)";
|
|
386
|
+
},
|
|
387
|
+
children: [
|
|
388
|
+
/* @__PURE__ */ jsxs2(
|
|
389
|
+
"svg",
|
|
390
|
+
{
|
|
391
|
+
width: s.iconSize,
|
|
392
|
+
height: s.iconSize,
|
|
393
|
+
viewBox: "0 0 24 24",
|
|
394
|
+
fill: "none",
|
|
395
|
+
stroke: "#d4a574",
|
|
396
|
+
strokeWidth: "2",
|
|
397
|
+
strokeLinecap: "round",
|
|
398
|
+
strokeLinejoin: "round",
|
|
399
|
+
children: [
|
|
400
|
+
/* @__PURE__ */ jsx3("path", { d: "M12 2L2 7l10 5 10-5-10-5z" }),
|
|
401
|
+
/* @__PURE__ */ jsx3("path", { d: "M2 17l10 5 10-5" }),
|
|
402
|
+
/* @__PURE__ */ jsx3("path", { d: "M2 12l10 5 10-5" })
|
|
403
|
+
]
|
|
404
|
+
}
|
|
405
|
+
),
|
|
406
|
+
label
|
|
407
|
+
]
|
|
408
|
+
}
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/useNexus.ts
|
|
413
|
+
import { useContext as useContext3 } from "react";
|
|
414
|
+
function useNexus() {
|
|
415
|
+
const context = useContext3(NexusContext);
|
|
416
|
+
if (!context) {
|
|
417
|
+
throw new Error("useNexus must be used within a <NexusProvider>");
|
|
418
|
+
}
|
|
419
|
+
return context;
|
|
420
|
+
}
|
|
421
|
+
export {
|
|
422
|
+
NexusCallback,
|
|
423
|
+
NexusLoginButton,
|
|
424
|
+
NexusProvider,
|
|
425
|
+
clearTokens,
|
|
426
|
+
decodeUser,
|
|
427
|
+
getAccessToken,
|
|
428
|
+
getRefreshToken,
|
|
429
|
+
isTokenValid,
|
|
430
|
+
nexusFetch,
|
|
431
|
+
refreshTokens,
|
|
432
|
+
setTokens,
|
|
433
|
+
useNexus
|
|
434
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vylth/nexus-react",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Nexus SSO SDK for React — Sign in with Nexus",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": ["dist"],
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
11
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch"
|
|
12
|
+
},
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"react": ">=18.0.0",
|
|
15
|
+
"react-dom": ">=18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"react": "^18.3.1",
|
|
19
|
+
"react-dom": "^18.3.1",
|
|
20
|
+
"@types/react": "^18.3.3",
|
|
21
|
+
"typescript": "^5.5.0",
|
|
22
|
+
"tsup": "^8.0.0"
|
|
23
|
+
},
|
|
24
|
+
"license": "MIT"
|
|
25
|
+
}
|