nexu-app 2.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/README.md +149 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1192 -0
- package/package.json +43 -0
- package/templates/default/.changeset/config.json +11 -0
- package/templates/default/.eslintignore +16 -0
- package/templates/default/.eslintrc.js +67 -0
- package/templates/default/.github/actions/build/action.yml +35 -0
- package/templates/default/.github/actions/quality/action.yml +53 -0
- package/templates/default/.github/dependabot.yml +51 -0
- package/templates/default/.github/workflows/deploy-dev.yml +83 -0
- package/templates/default/.github/workflows/deploy-prod.yml +83 -0
- package/templates/default/.github/workflows/deploy-rec.yml +83 -0
- package/templates/default/.husky/commit-msg +1 -0
- package/templates/default/.husky/pre-commit +1 -0
- package/templates/default/.nexu-version +1 -0
- package/templates/default/.prettierignore +7 -0
- package/templates/default/.prettierrc +19 -0
- package/templates/default/.vscode/extensions.json +14 -0
- package/templates/default/.vscode/settings.json +36 -0
- package/templates/default/apps/gitkeep +0 -0
- package/templates/default/commitlint.config.js +26 -0
- package/templates/default/docker/docker-compose.dev.yml +49 -0
- package/templates/default/docker/docker-compose.prod.yml +64 -0
- package/templates/default/docker/docker-compose.yml +6 -0
- package/templates/default/docs/architecture.md +452 -0
- package/templates/default/docs/cli.md +330 -0
- package/templates/default/docs/contributing.md +462 -0
- package/templates/default/docs/scripts.md +460 -0
- package/templates/default/gitignore +44 -0
- package/templates/default/lintstagedrc.cjs +4 -0
- package/templates/default/package.json +51 -0
- package/templates/default/packages/auth/package.json +61 -0
- package/templates/default/packages/auth/src/components/ProtectedRoute.tsx +75 -0
- package/templates/default/packages/auth/src/components/SignInForm.tsx +153 -0
- package/templates/default/packages/auth/src/components/SignUpForm.tsx +179 -0
- package/templates/default/packages/auth/src/components/SocialButtons.tsx +147 -0
- package/templates/default/packages/auth/src/components/index.ts +4 -0
- package/templates/default/packages/auth/src/hooks/index.ts +4 -0
- package/templates/default/packages/auth/src/hooks/useAuth.ts +51 -0
- package/templates/default/packages/auth/src/hooks/useRequireAuth.ts +54 -0
- package/templates/default/packages/auth/src/hooks/useSession.ts +48 -0
- package/templates/default/packages/auth/src/hooks/useUser.ts +48 -0
- package/templates/default/packages/auth/src/index.ts +45 -0
- package/templates/default/packages/auth/src/next/index.ts +18 -0
- package/templates/default/packages/auth/src/next/middleware.ts +183 -0
- package/templates/default/packages/auth/src/next/server.ts +219 -0
- package/templates/default/packages/auth/src/providers/AuthContext.tsx +435 -0
- package/templates/default/packages/auth/src/providers/index.ts +1 -0
- package/templates/default/packages/auth/src/types/index.ts +284 -0
- package/templates/default/packages/auth/src/utils/api.ts +228 -0
- package/templates/default/packages/auth/src/utils/index.ts +3 -0
- package/templates/default/packages/auth/src/utils/oauth.ts +230 -0
- package/templates/default/packages/auth/src/utils/token.ts +204 -0
- package/templates/default/packages/auth/tsconfig.json +14 -0
- package/templates/default/packages/auth/tsup.config.ts +18 -0
- package/templates/default/packages/cache/package.json +26 -0
- package/templates/default/packages/cache/src/index.ts +137 -0
- package/templates/default/packages/cache/tsconfig.json +9 -0
- package/templates/default/packages/cache/tsup.config.ts +9 -0
- package/templates/default/packages/config/eslint/index.js +20 -0
- package/templates/default/packages/config/package.json +9 -0
- package/templates/default/packages/config/typescript/base.json +26 -0
- package/templates/default/packages/constants/package.json +26 -0
- package/templates/default/packages/constants/src/index.ts +121 -0
- package/templates/default/packages/constants/tsconfig.json +9 -0
- package/templates/default/packages/constants/tsup.config.ts +9 -0
- package/templates/default/packages/logger/package.json +27 -0
- package/templates/default/packages/logger/src/index.ts +197 -0
- package/templates/default/packages/logger/tsconfig.json +11 -0
- package/templates/default/packages/logger/tsup.config.ts +9 -0
- package/templates/default/packages/result/package.json +26 -0
- package/templates/default/packages/result/src/index.ts +142 -0
- package/templates/default/packages/result/tsconfig.json +9 -0
- package/templates/default/packages/result/tsup.config.ts +9 -0
- package/templates/default/packages/types/package.json +26 -0
- package/templates/default/packages/types/src/index.ts +78 -0
- package/templates/default/packages/types/tsconfig.json +9 -0
- package/templates/default/packages/types/tsup.config.ts +10 -0
- package/templates/default/packages/ui/package.json +38 -0
- package/templates/default/packages/ui/src/components/Button.tsx +58 -0
- package/templates/default/packages/ui/src/components/Card.tsx +85 -0
- package/templates/default/packages/ui/src/components/Input.tsx +45 -0
- package/templates/default/packages/ui/src/index.ts +15 -0
- package/templates/default/packages/ui/tsconfig.json +11 -0
- package/templates/default/packages/ui/tsup.config.ts +11 -0
- package/templates/default/packages/utils/package.json +30 -0
- package/templates/default/packages/utils/src/index.test.ts +130 -0
- package/templates/default/packages/utils/src/index.ts +154 -0
- package/templates/default/packages/utils/tsconfig.json +10 -0
- package/templates/default/packages/utils/tsup.config.ts +10 -0
- package/templates/default/pnpm-workspace.yaml +3 -0
- package/templates/default/scripts/audit.mjs +700 -0
- package/templates/default/scripts/deploy.mjs +40 -0
- package/templates/default/scripts/generate-app.mjs +808 -0
- package/templates/default/scripts/lib/package-manager.mjs +186 -0
- package/templates/default/scripts/setup.mjs +102 -0
- package/templates/default/services/.env.example +16 -0
- package/templates/default/services/docker-compose.yml +207 -0
- package/templates/default/services/grafana/provisioning/dashboards/dashboards.yml +11 -0
- package/templates/default/services/grafana/provisioning/datasources/datasources.yml +9 -0
- package/templates/default/services/postgres/init/gitkeep +2 -0
- package/templates/default/services/prometheus/prometheus.yml +13 -0
- package/templates/default/tsconfig.json +27 -0
- package/templates/default/turbo.json +40 -0
- package/templates/default/vitest.config.ts +15 -0
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, {
|
|
4
|
+
createContext,
|
|
5
|
+
useCallback,
|
|
6
|
+
useContext,
|
|
7
|
+
useEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useReducer,
|
|
10
|
+
useRef,
|
|
11
|
+
} from 'react';
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
AuthConfig,
|
|
15
|
+
AuthError,
|
|
16
|
+
AuthProvider as AuthProviderType,
|
|
17
|
+
AuthResponse,
|
|
18
|
+
AuthSession,
|
|
19
|
+
AuthState,
|
|
20
|
+
AuthUser,
|
|
21
|
+
SignInCredentials,
|
|
22
|
+
SignUpCredentials,
|
|
23
|
+
UpdatePasswordRequest,
|
|
24
|
+
UseAuthReturn,
|
|
25
|
+
} from '../types';
|
|
26
|
+
import { AuthApiClient, createAuthApiClient } from '../utils/api';
|
|
27
|
+
import {
|
|
28
|
+
clearOAuthState,
|
|
29
|
+
getProviderConfig,
|
|
30
|
+
getStoredOAuthState,
|
|
31
|
+
initiateOAuthFlow,
|
|
32
|
+
parseOAuthCallback,
|
|
33
|
+
} from '../utils/oauth';
|
|
34
|
+
import { createTokenManager, TokenManager } from '../utils/token';
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// State Management
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
type AuthAction =
|
|
41
|
+
| { type: 'LOADING' }
|
|
42
|
+
| { type: 'AUTHENTICATED'; user: AuthUser; session: AuthSession }
|
|
43
|
+
| { type: 'UNAUTHENTICATED' }
|
|
44
|
+
| { type: 'ERROR'; error: AuthError }
|
|
45
|
+
| { type: 'UPDATE_USER'; user: AuthUser }
|
|
46
|
+
| { type: 'CLEAR_ERROR' };
|
|
47
|
+
|
|
48
|
+
const initialState: AuthState = {
|
|
49
|
+
user: null,
|
|
50
|
+
session: null,
|
|
51
|
+
isAuthenticated: false,
|
|
52
|
+
isLoading: true,
|
|
53
|
+
error: null,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function authReducer(state: AuthState, action: AuthAction): AuthState {
|
|
57
|
+
switch (action.type) {
|
|
58
|
+
case 'LOADING':
|
|
59
|
+
return { ...state, isLoading: true, error: null };
|
|
60
|
+
case 'AUTHENTICATED':
|
|
61
|
+
return {
|
|
62
|
+
user: action.user,
|
|
63
|
+
session: action.session,
|
|
64
|
+
isAuthenticated: true,
|
|
65
|
+
isLoading: false,
|
|
66
|
+
error: null,
|
|
67
|
+
};
|
|
68
|
+
case 'UNAUTHENTICATED':
|
|
69
|
+
return {
|
|
70
|
+
user: null,
|
|
71
|
+
session: null,
|
|
72
|
+
isAuthenticated: false,
|
|
73
|
+
isLoading: false,
|
|
74
|
+
error: null,
|
|
75
|
+
};
|
|
76
|
+
case 'ERROR':
|
|
77
|
+
return { ...state, isLoading: false, error: action.error };
|
|
78
|
+
case 'UPDATE_USER':
|
|
79
|
+
return { ...state, user: action.user };
|
|
80
|
+
case 'CLEAR_ERROR':
|
|
81
|
+
return { ...state, error: null };
|
|
82
|
+
default:
|
|
83
|
+
return state;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// Context
|
|
89
|
+
// ============================================================================
|
|
90
|
+
|
|
91
|
+
interface AuthContextValue extends UseAuthReturn {
|
|
92
|
+
config: AuthConfig;
|
|
93
|
+
tokenManager: TokenManager;
|
|
94
|
+
apiClient: AuthApiClient;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const AuthContext = createContext<AuthContextValue | null>(null);
|
|
98
|
+
|
|
99
|
+
// ============================================================================
|
|
100
|
+
// Provider
|
|
101
|
+
// ============================================================================
|
|
102
|
+
|
|
103
|
+
interface AuthProviderProps {
|
|
104
|
+
children: React.ReactNode;
|
|
105
|
+
config: AuthConfig;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function AuthProvider({ children, config }: AuthProviderProps) {
|
|
109
|
+
const [state, dispatch] = useReducer(authReducer, initialState);
|
|
110
|
+
|
|
111
|
+
// Create stable instances
|
|
112
|
+
const tokenManager = useMemo(
|
|
113
|
+
() => createTokenManager(config),
|
|
114
|
+
[config.tokenStorage, config.cookieOptions]
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const apiClient = useMemo(
|
|
118
|
+
() => createAuthApiClient(config, tokenManager),
|
|
119
|
+
[config, tokenManager]
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Ref for auto-refresh interval
|
|
123
|
+
const refreshIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
|
124
|
+
|
|
125
|
+
// Helper to create session from response
|
|
126
|
+
const createSession = useCallback(
|
|
127
|
+
(response: AuthResponse, provider: AuthProviderType = 'email'): AuthSession => {
|
|
128
|
+
return {
|
|
129
|
+
user: response.user,
|
|
130
|
+
accessToken: response.accessToken,
|
|
131
|
+
refreshToken: response.refreshToken,
|
|
132
|
+
expiresAt: Date.now() + response.expiresIn * 1000,
|
|
133
|
+
provider,
|
|
134
|
+
};
|
|
135
|
+
},
|
|
136
|
+
[]
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Handle successful authentication
|
|
140
|
+
const handleAuthSuccess = useCallback(
|
|
141
|
+
async (response: AuthResponse, provider: AuthProviderType = 'email') => {
|
|
142
|
+
const session = createSession(response, provider);
|
|
143
|
+
|
|
144
|
+
tokenManager.setAccessToken(response.accessToken, response.expiresIn);
|
|
145
|
+
if (response.refreshToken) {
|
|
146
|
+
tokenManager.setRefreshToken(response.refreshToken);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
dispatch({ type: 'AUTHENTICATED', user: response.user, session });
|
|
150
|
+
|
|
151
|
+
// Call callback if provided
|
|
152
|
+
if (config.callbacks?.onSignIn) {
|
|
153
|
+
await config.callbacks.onSignIn(response.user, session);
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
[config.callbacks, tokenManager, createSession]
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
// Sign in with credentials
|
|
160
|
+
const signIn = useCallback(
|
|
161
|
+
async (credentials: SignInCredentials): Promise<AuthResponse> => {
|
|
162
|
+
dispatch({ type: 'LOADING' });
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const response = await apiClient.signIn(credentials);
|
|
166
|
+
await handleAuthSuccess(response, 'email');
|
|
167
|
+
return response;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
const authError = error as AuthError;
|
|
170
|
+
dispatch({ type: 'ERROR', error: authError });
|
|
171
|
+
config.callbacks?.onError?.(authError);
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
[apiClient, handleAuthSuccess, config.callbacks]
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// Sign up with credentials
|
|
179
|
+
const signUp = useCallback(
|
|
180
|
+
async (credentials: SignUpCredentials): Promise<AuthResponse> => {
|
|
181
|
+
dispatch({ type: 'LOADING' });
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const response = await apiClient.signUp(credentials);
|
|
185
|
+
await handleAuthSuccess(response, 'email');
|
|
186
|
+
return response;
|
|
187
|
+
} catch (error) {
|
|
188
|
+
const authError = error as AuthError;
|
|
189
|
+
dispatch({ type: 'ERROR', error: authError });
|
|
190
|
+
config.callbacks?.onError?.(authError);
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
[apiClient, handleAuthSuccess, config.callbacks]
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Sign out
|
|
198
|
+
const signOut = useCallback(async (): Promise<void> => {
|
|
199
|
+
try {
|
|
200
|
+
await apiClient.signOut();
|
|
201
|
+
} catch {
|
|
202
|
+
// Ignore sign out errors
|
|
203
|
+
} finally {
|
|
204
|
+
tokenManager.clearTokens();
|
|
205
|
+
dispatch({ type: 'UNAUTHENTICATED' });
|
|
206
|
+
void config.callbacks?.onSignOut?.();
|
|
207
|
+
}
|
|
208
|
+
}, [apiClient, tokenManager, config.callbacks]);
|
|
209
|
+
|
|
210
|
+
// Sign in with OAuth provider
|
|
211
|
+
const signInWithProvider = useCallback(
|
|
212
|
+
async (provider: AuthProviderType): Promise<void> => {
|
|
213
|
+
const providerConfig = getProviderConfig(provider, config.providers);
|
|
214
|
+
|
|
215
|
+
if (!providerConfig) {
|
|
216
|
+
throw new Error(`Provider ${provider} is not configured`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
await initiateOAuthFlow(provider, providerConfig);
|
|
220
|
+
},
|
|
221
|
+
[config.providers]
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
// Refresh session
|
|
225
|
+
const refreshSession = useCallback(async (): Promise<AuthSession | null> => {
|
|
226
|
+
try {
|
|
227
|
+
const response = await apiClient.refreshToken();
|
|
228
|
+
const session = createSession(response);
|
|
229
|
+
|
|
230
|
+
tokenManager.setAccessToken(response.accessToken, response.expiresIn);
|
|
231
|
+
if (response.refreshToken) {
|
|
232
|
+
tokenManager.setRefreshToken(response.refreshToken);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
dispatch({ type: 'AUTHENTICATED', user: response.user, session });
|
|
236
|
+
|
|
237
|
+
return session;
|
|
238
|
+
} catch {
|
|
239
|
+
tokenManager.clearTokens();
|
|
240
|
+
dispatch({ type: 'UNAUTHENTICATED' });
|
|
241
|
+
config.callbacks?.onSessionExpired?.();
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}, [apiClient, tokenManager, createSession, config.callbacks]);
|
|
245
|
+
|
|
246
|
+
// Reset password
|
|
247
|
+
const resetPassword = useCallback(
|
|
248
|
+
async (email: string): Promise<void> => {
|
|
249
|
+
await apiClient.resetPassword(email);
|
|
250
|
+
},
|
|
251
|
+
[apiClient]
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
// Update password
|
|
255
|
+
const updatePassword = useCallback(
|
|
256
|
+
async (request: UpdatePasswordRequest): Promise<void> => {
|
|
257
|
+
await apiClient.updatePassword(request);
|
|
258
|
+
},
|
|
259
|
+
[apiClient]
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
// Update user
|
|
263
|
+
const updateUser = useCallback(
|
|
264
|
+
async (data: Partial<AuthUser>): Promise<AuthUser> => {
|
|
265
|
+
const user = await apiClient.updateUser(data);
|
|
266
|
+
dispatch({ type: 'UPDATE_USER', user });
|
|
267
|
+
return user;
|
|
268
|
+
},
|
|
269
|
+
[apiClient]
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
// Verify email
|
|
273
|
+
const verifyEmail = useCallback(
|
|
274
|
+
async (token: string): Promise<void> => {
|
|
275
|
+
await apiClient.verifyEmail(token);
|
|
276
|
+
},
|
|
277
|
+
[apiClient]
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
// Resend verification
|
|
281
|
+
const resendVerification = useCallback(async (): Promise<void> => {
|
|
282
|
+
await apiClient.resendVerification();
|
|
283
|
+
}, [apiClient]);
|
|
284
|
+
|
|
285
|
+
// Initialize auth state
|
|
286
|
+
useEffect(() => {
|
|
287
|
+
const initializeAuth = async () => {
|
|
288
|
+
// Check for OAuth callback
|
|
289
|
+
const callbackParams = parseOAuthCallback();
|
|
290
|
+
if (callbackParams?.code) {
|
|
291
|
+
const pathParts = window.location.pathname.split('/');
|
|
292
|
+
const provider = pathParts[pathParts.length - 1] as AuthProviderType;
|
|
293
|
+
const storedState = getStoredOAuthState(provider);
|
|
294
|
+
|
|
295
|
+
if (storedState && callbackParams.state === storedState.state) {
|
|
296
|
+
try {
|
|
297
|
+
const response = await apiClient.exchangeOAuthCode(
|
|
298
|
+
provider,
|
|
299
|
+
callbackParams.code,
|
|
300
|
+
callbackParams.state
|
|
301
|
+
);
|
|
302
|
+
await handleAuthSuccess(response, provider);
|
|
303
|
+
clearOAuthState(provider);
|
|
304
|
+
|
|
305
|
+
// Clean URL
|
|
306
|
+
window.history.replaceState({}, '', window.location.pathname);
|
|
307
|
+
return;
|
|
308
|
+
} catch (error) {
|
|
309
|
+
clearOAuthState(provider);
|
|
310
|
+
dispatch({ type: 'ERROR', error: error as AuthError });
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Check for existing token
|
|
317
|
+
const token = tokenManager.getAccessToken();
|
|
318
|
+
|
|
319
|
+
if (!token) {
|
|
320
|
+
dispatch({ type: 'UNAUTHENTICATED' });
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Validate token and get user
|
|
325
|
+
if (tokenManager.isTokenExpired(token)) {
|
|
326
|
+
// Try to refresh
|
|
327
|
+
const refreshToken = tokenManager.getRefreshToken();
|
|
328
|
+
if (refreshToken) {
|
|
329
|
+
await refreshSession();
|
|
330
|
+
} else {
|
|
331
|
+
tokenManager.clearTokens();
|
|
332
|
+
dispatch({ type: 'UNAUTHENTICATED' });
|
|
333
|
+
}
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Token is valid, get user
|
|
338
|
+
try {
|
|
339
|
+
const user = await apiClient.getUser();
|
|
340
|
+
const session: AuthSession = {
|
|
341
|
+
user,
|
|
342
|
+
accessToken: token,
|
|
343
|
+
refreshToken: tokenManager.getRefreshToken() || undefined,
|
|
344
|
+
expiresAt: tokenManager.getTokenExpiration(token) || Date.now() + 3600000,
|
|
345
|
+
provider: 'email', // Will be updated from token if available
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
dispatch({ type: 'AUTHENTICATED', user, session });
|
|
349
|
+
} catch {
|
|
350
|
+
tokenManager.clearTokens();
|
|
351
|
+
dispatch({ type: 'UNAUTHENTICATED' });
|
|
352
|
+
}
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
void initializeAuth();
|
|
356
|
+
}, [apiClient, tokenManager, refreshSession, handleAuthSuccess]);
|
|
357
|
+
|
|
358
|
+
// Set up auto-refresh
|
|
359
|
+
useEffect(() => {
|
|
360
|
+
if (config.autoRefresh === false) return;
|
|
361
|
+
if (!state.isAuthenticated) return;
|
|
362
|
+
|
|
363
|
+
const threshold = (config.refreshThreshold || 300) * 1000;
|
|
364
|
+
const checkInterval = Math.min(threshold / 2, 60000); // Check at least every minute
|
|
365
|
+
|
|
366
|
+
refreshIntervalRef.current = setInterval(() => {
|
|
367
|
+
if (tokenManager.shouldRefresh(config.refreshThreshold || 300)) {
|
|
368
|
+
void refreshSession();
|
|
369
|
+
}
|
|
370
|
+
}, checkInterval);
|
|
371
|
+
|
|
372
|
+
return () => {
|
|
373
|
+
if (refreshIntervalRef.current) {
|
|
374
|
+
clearInterval(refreshIntervalRef.current);
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
}, [
|
|
378
|
+
config.autoRefresh,
|
|
379
|
+
config.refreshThreshold,
|
|
380
|
+
state.isAuthenticated,
|
|
381
|
+
tokenManager,
|
|
382
|
+
refreshSession,
|
|
383
|
+
]);
|
|
384
|
+
|
|
385
|
+
const value: AuthContextValue = useMemo(
|
|
386
|
+
() => ({
|
|
387
|
+
...state,
|
|
388
|
+
config,
|
|
389
|
+
tokenManager,
|
|
390
|
+
apiClient,
|
|
391
|
+
signIn,
|
|
392
|
+
signUp,
|
|
393
|
+
signOut,
|
|
394
|
+
signInWithProvider,
|
|
395
|
+
refreshSession,
|
|
396
|
+
resetPassword,
|
|
397
|
+
updatePassword,
|
|
398
|
+
updateUser,
|
|
399
|
+
verifyEmail,
|
|
400
|
+
resendVerification,
|
|
401
|
+
}),
|
|
402
|
+
[
|
|
403
|
+
state,
|
|
404
|
+
config,
|
|
405
|
+
tokenManager,
|
|
406
|
+
apiClient,
|
|
407
|
+
signIn,
|
|
408
|
+
signUp,
|
|
409
|
+
signOut,
|
|
410
|
+
signInWithProvider,
|
|
411
|
+
refreshSession,
|
|
412
|
+
resetPassword,
|
|
413
|
+
updatePassword,
|
|
414
|
+
updateUser,
|
|
415
|
+
verifyEmail,
|
|
416
|
+
resendVerification,
|
|
417
|
+
]
|
|
418
|
+
);
|
|
419
|
+
|
|
420
|
+
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ============================================================================
|
|
424
|
+
// Hook
|
|
425
|
+
// ============================================================================
|
|
426
|
+
|
|
427
|
+
export function useAuthContext(): AuthContextValue {
|
|
428
|
+
const context = useContext(AuthContext);
|
|
429
|
+
|
|
430
|
+
if (!context) {
|
|
431
|
+
throw new Error('useAuthContext must be used within an AuthProvider');
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return context;
|
|
435
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { AuthProvider, useAuthContext } from './AuthContext';
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// User Types
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
export interface AuthUser {
|
|
6
|
+
id: string;
|
|
7
|
+
email: string;
|
|
8
|
+
name?: string;
|
|
9
|
+
avatar?: string;
|
|
10
|
+
emailVerified?: boolean;
|
|
11
|
+
provider?: AuthProvider;
|
|
12
|
+
metadata?: Record<string, unknown>;
|
|
13
|
+
createdAt?: string;
|
|
14
|
+
updatedAt?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ============================================================================
|
|
18
|
+
// Provider Types
|
|
19
|
+
// ============================================================================
|
|
20
|
+
|
|
21
|
+
export type AuthProvider =
|
|
22
|
+
| 'email'
|
|
23
|
+
| 'google'
|
|
24
|
+
| 'github'
|
|
25
|
+
| 'facebook'
|
|
26
|
+
| 'apple'
|
|
27
|
+
| 'twitter'
|
|
28
|
+
| 'custom';
|
|
29
|
+
|
|
30
|
+
export interface OAuthProviderConfig {
|
|
31
|
+
clientId: string;
|
|
32
|
+
redirectUri?: string;
|
|
33
|
+
scope?: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AuthProviderConfig {
|
|
37
|
+
google?: OAuthProviderConfig;
|
|
38
|
+
github?: OAuthProviderConfig;
|
|
39
|
+
facebook?: OAuthProviderConfig;
|
|
40
|
+
apple?: OAuthProviderConfig;
|
|
41
|
+
twitter?: OAuthProviderConfig;
|
|
42
|
+
custom?: {
|
|
43
|
+
name: string;
|
|
44
|
+
authorizationUrl: string;
|
|
45
|
+
tokenUrl: string;
|
|
46
|
+
clientId: string;
|
|
47
|
+
redirectUri?: string;
|
|
48
|
+
scope?: string[];
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ============================================================================
|
|
53
|
+
// Session & Token Types
|
|
54
|
+
// ============================================================================
|
|
55
|
+
|
|
56
|
+
export interface AuthSession {
|
|
57
|
+
user: AuthUser;
|
|
58
|
+
accessToken: string;
|
|
59
|
+
refreshToken?: string;
|
|
60
|
+
expiresAt: number;
|
|
61
|
+
provider: AuthProvider;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface TokenPayload {
|
|
65
|
+
sub: string;
|
|
66
|
+
email?: string;
|
|
67
|
+
name?: string;
|
|
68
|
+
exp: number;
|
|
69
|
+
iat: number;
|
|
70
|
+
[key: string]: unknown;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type TokenStorage = 'localStorage' | 'cookie' | 'memory';
|
|
74
|
+
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// Auth State Types
|
|
77
|
+
// ============================================================================
|
|
78
|
+
|
|
79
|
+
export interface AuthState {
|
|
80
|
+
user: AuthUser | null;
|
|
81
|
+
session: AuthSession | null;
|
|
82
|
+
isAuthenticated: boolean;
|
|
83
|
+
isLoading: boolean;
|
|
84
|
+
error: AuthError | null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface AuthError {
|
|
88
|
+
code: AuthErrorCode;
|
|
89
|
+
message: string;
|
|
90
|
+
details?: unknown;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type AuthErrorCode =
|
|
94
|
+
| 'INVALID_CREDENTIALS'
|
|
95
|
+
| 'USER_NOT_FOUND'
|
|
96
|
+
| 'USER_ALREADY_EXISTS'
|
|
97
|
+
| 'EMAIL_NOT_VERIFIED'
|
|
98
|
+
| 'INVALID_TOKEN'
|
|
99
|
+
| 'TOKEN_EXPIRED'
|
|
100
|
+
| 'NETWORK_ERROR'
|
|
101
|
+
| 'PROVIDER_ERROR'
|
|
102
|
+
| 'UNAUTHORIZED'
|
|
103
|
+
| 'FORBIDDEN'
|
|
104
|
+
| 'UNKNOWN_ERROR';
|
|
105
|
+
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// Auth Config Types
|
|
108
|
+
// ============================================================================
|
|
109
|
+
|
|
110
|
+
export interface AuthConfig {
|
|
111
|
+
/**
|
|
112
|
+
* Base URL for API requests (e.g., '/api/auth' or 'https://api.example.com/auth')
|
|
113
|
+
*/
|
|
114
|
+
apiBaseUrl: string;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* OAuth providers configuration
|
|
118
|
+
*/
|
|
119
|
+
providers?: AuthProviderConfig;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Token storage method
|
|
123
|
+
* @default 'cookie'
|
|
124
|
+
*/
|
|
125
|
+
tokenStorage?: TokenStorage;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Cookie options (only used when tokenStorage is 'cookie')
|
|
129
|
+
*/
|
|
130
|
+
cookieOptions?: {
|
|
131
|
+
secure?: boolean;
|
|
132
|
+
sameSite?: 'strict' | 'lax' | 'none';
|
|
133
|
+
domain?: string;
|
|
134
|
+
path?: string;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Auto refresh token before expiration
|
|
139
|
+
* @default true
|
|
140
|
+
*/
|
|
141
|
+
autoRefresh?: boolean;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Refresh token before this many seconds before expiration
|
|
145
|
+
* @default 300 (5 minutes)
|
|
146
|
+
*/
|
|
147
|
+
refreshThreshold?: number;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Callback URLs
|
|
151
|
+
*/
|
|
152
|
+
callbacks?: {
|
|
153
|
+
onSignIn?: (user: AuthUser, session: AuthSession) => void | Promise<void>;
|
|
154
|
+
onSignOut?: () => void | Promise<void>;
|
|
155
|
+
onError?: (error: AuthError) => void;
|
|
156
|
+
onSessionExpired?: () => void;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Custom headers to include in API requests
|
|
161
|
+
*/
|
|
162
|
+
headers?: Record<string, string>;
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Enable debug mode
|
|
166
|
+
* @default false
|
|
167
|
+
*/
|
|
168
|
+
debug?: boolean;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ============================================================================
|
|
172
|
+
// API Request/Response Types
|
|
173
|
+
// ============================================================================
|
|
174
|
+
|
|
175
|
+
export interface SignInCredentials {
|
|
176
|
+
email: string;
|
|
177
|
+
password: string;
|
|
178
|
+
remember?: boolean;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export interface SignUpCredentials {
|
|
182
|
+
email: string;
|
|
183
|
+
password: string;
|
|
184
|
+
name?: string;
|
|
185
|
+
metadata?: Record<string, unknown>;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface ResetPasswordRequest {
|
|
189
|
+
email: string;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface UpdatePasswordRequest {
|
|
193
|
+
currentPassword?: string;
|
|
194
|
+
newPassword: string;
|
|
195
|
+
token?: string;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface AuthResponse {
|
|
199
|
+
user: AuthUser;
|
|
200
|
+
accessToken: string;
|
|
201
|
+
refreshToken?: string;
|
|
202
|
+
expiresIn: number;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface OAuthCallbackParams {
|
|
206
|
+
code: string;
|
|
207
|
+
state?: string;
|
|
208
|
+
error?: string;
|
|
209
|
+
errorDescription?: string;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ============================================================================
|
|
213
|
+
// Hook Return Types
|
|
214
|
+
// ============================================================================
|
|
215
|
+
|
|
216
|
+
export interface UseAuthReturn extends AuthState {
|
|
217
|
+
signIn: (credentials: SignInCredentials) => Promise<AuthResponse>;
|
|
218
|
+
signUp: (credentials: SignUpCredentials) => Promise<AuthResponse>;
|
|
219
|
+
signOut: () => Promise<void>;
|
|
220
|
+
signInWithProvider: (provider: AuthProvider) => Promise<void>;
|
|
221
|
+
refreshSession: () => Promise<AuthSession | null>;
|
|
222
|
+
resetPassword: (email: string) => Promise<void>;
|
|
223
|
+
updatePassword: (request: UpdatePasswordRequest) => Promise<void>;
|
|
224
|
+
updateUser: (data: Partial<AuthUser>) => Promise<AuthUser>;
|
|
225
|
+
verifyEmail: (token: string) => Promise<void>;
|
|
226
|
+
resendVerification: () => Promise<void>;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export interface UseSessionReturn {
|
|
230
|
+
session: AuthSession | null;
|
|
231
|
+
isLoading: boolean;
|
|
232
|
+
refresh: () => Promise<AuthSession | null>;
|
|
233
|
+
isValid: () => boolean;
|
|
234
|
+
getToken: () => string | null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface UseUserReturn {
|
|
238
|
+
user: AuthUser | null;
|
|
239
|
+
isLoading: boolean;
|
|
240
|
+
update: (data: Partial<AuthUser>) => Promise<AuthUser>;
|
|
241
|
+
refresh: () => Promise<AuthUser | null>;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ============================================================================
|
|
245
|
+
// Component Props Types
|
|
246
|
+
// ============================================================================
|
|
247
|
+
|
|
248
|
+
export interface SignInFormProps {
|
|
249
|
+
onSuccess?: (response: AuthResponse) => void;
|
|
250
|
+
onError?: (error: AuthError) => void;
|
|
251
|
+
providers?: AuthProvider[];
|
|
252
|
+
showRememberMe?: boolean;
|
|
253
|
+
showForgotPassword?: boolean;
|
|
254
|
+
forgotPasswordUrl?: string;
|
|
255
|
+
signUpUrl?: string;
|
|
256
|
+
redirectUrl?: string;
|
|
257
|
+
className?: string;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export interface SignUpFormProps {
|
|
261
|
+
onSuccess?: (response: AuthResponse) => void;
|
|
262
|
+
onError?: (error: AuthError) => void;
|
|
263
|
+
providers?: AuthProvider[];
|
|
264
|
+
showName?: boolean;
|
|
265
|
+
signInUrl?: string;
|
|
266
|
+
redirectUrl?: string;
|
|
267
|
+
className?: string;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export interface SocialButtonsProps {
|
|
271
|
+
providers: AuthProvider[];
|
|
272
|
+
onSuccess?: (provider: AuthProvider) => void;
|
|
273
|
+
onError?: (error: AuthError) => void;
|
|
274
|
+
mode?: 'signin' | 'signup';
|
|
275
|
+
className?: string;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export interface ProtectedRouteProps {
|
|
279
|
+
children: React.ReactNode;
|
|
280
|
+
fallback?: React.ReactNode;
|
|
281
|
+
redirectTo?: string;
|
|
282
|
+
roles?: string[];
|
|
283
|
+
permissions?: string[];
|
|
284
|
+
}
|