@startsimpli/auth 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.
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Client-side authentication manager
3
+ * Handles JWT tokens, login/logout, and token refresh
4
+ */
5
+
6
+ import type {
7
+ AuthConfig,
8
+ Session,
9
+ LoginResponse,
10
+ RefreshResponse,
11
+ AuthUser,
12
+ } from '../types';
13
+ import { isTokenExpired, getTokenExpiresAt, shouldRefreshToken } from '../utils';
14
+
15
+ export class AuthClient {
16
+ private config: Required<AuthConfig>;
17
+ private session: Session | null = null;
18
+ private refreshTimer: NodeJS.Timeout | null = null;
19
+ private isRefreshing = false;
20
+ private refreshPromise: Promise<string> | null = null;
21
+
22
+ constructor(config: AuthConfig) {
23
+ this.config = {
24
+ tokenRefreshInterval: 4 * 60 * 1000, // 4 minutes
25
+ onSessionExpired: () => {},
26
+ onUnauthorized: () => {},
27
+ ...config,
28
+ };
29
+ }
30
+
31
+ /**
32
+ * Login with email and password
33
+ */
34
+ async login(email: string, password: string): Promise<Session> {
35
+ const response = await fetch(`${this.config.apiBaseUrl}/api/v1/auth/token/`, {
36
+ method: 'POST',
37
+ headers: { 'Content-Type': 'application/json' },
38
+ credentials: 'include', // Include cookies for refresh token
39
+ body: JSON.stringify({ email, password }),
40
+ });
41
+
42
+ if (!response.ok) {
43
+ const error = await response.json().catch(() => ({ detail: 'Login failed' }));
44
+ throw new Error(error.detail || 'Login failed');
45
+ }
46
+
47
+ const data: any = await response.json();
48
+ const expiresAt = getTokenExpiresAt(data.access);
49
+
50
+ if (!expiresAt) {
51
+ throw new Error('Invalid token received');
52
+ }
53
+
54
+ // Django might not include user in token response, so fetch it separately
55
+ const tempSession = {
56
+ user: data.user || { id: '', email: '', firstName: '', lastName: '', isEmailVerified: false, createdAt: '', updatedAt: '' },
57
+ accessToken: data.access,
58
+ expiresAt,
59
+ };
60
+
61
+ this.session = tempSession;
62
+
63
+ // Fetch user data if not included in login response
64
+ if (!data.user) {
65
+ try {
66
+ const user = await this.getCurrentUser();
67
+ this.session.user = user;
68
+ } catch (error) {
69
+ console.error('Failed to fetch user data after login:', error);
70
+ // Continue anyway - some user data is better than none
71
+ }
72
+ }
73
+
74
+ this.startRefreshTimer();
75
+ return this.session;
76
+ }
77
+
78
+ /**
79
+ * Logout and clear session
80
+ */
81
+ async logout(): Promise<void> {
82
+ try {
83
+ await fetch(`${this.config.apiBaseUrl}/api/v1/auth/logout/`, {
84
+ method: 'POST',
85
+ headers: this.getAuthHeaders(),
86
+ credentials: 'include',
87
+ });
88
+ } catch (error) {
89
+ console.error('Logout error:', error);
90
+ } finally {
91
+ this.clearSession();
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Refresh access token using refresh token cookie
97
+ */
98
+ async refreshToken(): Promise<string> {
99
+ // Prevent multiple simultaneous refresh requests
100
+ if (this.isRefreshing && this.refreshPromise) {
101
+ return this.refreshPromise;
102
+ }
103
+
104
+ this.isRefreshing = true;
105
+ this.refreshPromise = this.performTokenRefresh();
106
+
107
+ try {
108
+ const newToken = await this.refreshPromise;
109
+ return newToken;
110
+ } finally {
111
+ this.isRefreshing = false;
112
+ this.refreshPromise = null;
113
+ }
114
+ }
115
+
116
+ private async performTokenRefresh(): Promise<string> {
117
+ const response = await fetch(`${this.config.apiBaseUrl}/api/v1/auth/token/refresh/`, {
118
+ method: 'POST',
119
+ headers: { 'Content-Type': 'application/json' },
120
+ credentials: 'include', // Send refresh token cookie
121
+ });
122
+
123
+ if (!response.ok) {
124
+ this.clearSession();
125
+ this.config.onSessionExpired();
126
+ throw new Error('Token refresh failed');
127
+ }
128
+
129
+ const data: RefreshResponse = await response.json();
130
+ const expiresAt = getTokenExpiresAt(data.access);
131
+
132
+ if (!expiresAt) {
133
+ throw new Error('Invalid token received');
134
+ }
135
+
136
+ if (this.session) {
137
+ this.session.accessToken = data.access;
138
+ this.session.expiresAt = expiresAt;
139
+ }
140
+
141
+ this.startRefreshTimer();
142
+ return data.access;
143
+ }
144
+
145
+ /**
146
+ * Get current user data from backend
147
+ */
148
+ async getCurrentUser(): Promise<AuthUser> {
149
+ const response = await fetch(`${this.config.apiBaseUrl}/api/v1/auth/me/`, {
150
+ headers: this.getAuthHeaders(),
151
+ credentials: 'include',
152
+ });
153
+
154
+ if (!response.ok) {
155
+ if (response.status === 401) {
156
+ this.config.onUnauthorized();
157
+ }
158
+ throw new Error('Failed to fetch user data');
159
+ }
160
+
161
+ const user: AuthUser = await response.json();
162
+
163
+ if (this.session) {
164
+ this.session.user = user;
165
+ }
166
+
167
+ return user;
168
+ }
169
+
170
+ /**
171
+ * Get current session
172
+ */
173
+ getSession(): Session | null {
174
+ if (!this.session) {
175
+ return null;
176
+ }
177
+
178
+ if (isTokenExpired(this.session.accessToken)) {
179
+ this.clearSession();
180
+ this.config.onSessionExpired();
181
+ return null;
182
+ }
183
+
184
+ return this.session;
185
+ }
186
+
187
+ /**
188
+ * Set session (for SSR/hydration)
189
+ */
190
+ setSession(session: Session): void {
191
+ this.session = session;
192
+ this.startRefreshTimer();
193
+ }
194
+
195
+ /**
196
+ * Get auth headers for API requests
197
+ */
198
+ getAuthHeaders(): Record<string, string> {
199
+ const session = this.getSession();
200
+ if (!session) {
201
+ return {};
202
+ }
203
+
204
+ return {
205
+ Authorization: `Bearer ${session.accessToken}`,
206
+ };
207
+ }
208
+
209
+ /**
210
+ * Get valid access token (refreshes if needed)
211
+ */
212
+ async getAccessToken(): Promise<string | null> {
213
+ const session = this.getSession();
214
+ if (!session) {
215
+ return null;
216
+ }
217
+
218
+ if (shouldRefreshToken(session.accessToken)) {
219
+ try {
220
+ return await this.refreshToken();
221
+ } catch (error) {
222
+ console.error('Token refresh failed:', error);
223
+ return null;
224
+ }
225
+ }
226
+
227
+ return session.accessToken;
228
+ }
229
+
230
+ /**
231
+ * Start automatic token refresh timer
232
+ */
233
+ private startRefreshTimer(): void {
234
+ if (this.refreshTimer) {
235
+ clearInterval(this.refreshTimer);
236
+ }
237
+
238
+ this.refreshTimer = setInterval(() => {
239
+ const session = this.getSession();
240
+ if (session && shouldRefreshToken(session.accessToken)) {
241
+ this.refreshToken().catch((error) => {
242
+ console.error('Auto-refresh failed:', error);
243
+ });
244
+ }
245
+ }, this.config.tokenRefreshInterval);
246
+ }
247
+
248
+ /**
249
+ * Clear session and stop refresh timer
250
+ */
251
+ private clearSession(): void {
252
+ this.session = null;
253
+ if (this.refreshTimer) {
254
+ clearInterval(this.refreshTimer);
255
+ this.refreshTimer = null;
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Cleanup resources
261
+ */
262
+ destroy(): void {
263
+ this.clearSession();
264
+ }
265
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * React context provider for authentication
3
+ */
4
+
5
+ 'use client';
6
+
7
+ import {
8
+ createContext,
9
+ useContext,
10
+ useEffect,
11
+ useState,
12
+ useCallback,
13
+ type ReactNode,
14
+ } from 'react';
15
+ import { AuthClient } from './auth-client';
16
+ import type { AuthConfig, AuthState, Session, AuthUser } from '../types';
17
+
18
+ interface AuthContextValue extends AuthState {
19
+ login: (email: string, password: string) => Promise<void>;
20
+ logout: () => Promise<void>;
21
+ refreshUser: () => Promise<void>;
22
+ getAccessToken: () => Promise<string | null>;
23
+ }
24
+
25
+ const AuthContext = createContext<AuthContextValue | undefined>(undefined);
26
+
27
+ interface AuthProviderProps {
28
+ children: ReactNode;
29
+ config: AuthConfig;
30
+ initialSession?: Session | null;
31
+ }
32
+
33
+ export function AuthProvider({
34
+ children,
35
+ config,
36
+ initialSession,
37
+ }: AuthProviderProps) {
38
+ const [authClient] = useState(() => new AuthClient(config));
39
+ const [state, setState] = useState<AuthState>(() => ({
40
+ session: initialSession || null,
41
+ isLoading: !initialSession,
42
+ isAuthenticated: !!initialSession,
43
+ }));
44
+
45
+ // Initialize session from SSR or check for existing session
46
+ useEffect(() => {
47
+ if (initialSession) {
48
+ authClient.setSession(initialSession);
49
+ setState({
50
+ session: initialSession,
51
+ isLoading: false,
52
+ isAuthenticated: true,
53
+ });
54
+ } else {
55
+ // Try to get session from client
56
+ const session = authClient.getSession();
57
+ setState({
58
+ session,
59
+ isLoading: false,
60
+ isAuthenticated: !!session,
61
+ });
62
+ }
63
+ }, [authClient, initialSession]);
64
+
65
+ // Session expiration handler
66
+ useEffect(() => {
67
+ const originalOnExpired = config.onSessionExpired;
68
+ config.onSessionExpired = () => {
69
+ setState({
70
+ session: null,
71
+ isLoading: false,
72
+ isAuthenticated: false,
73
+ });
74
+ originalOnExpired?.();
75
+ };
76
+
77
+ return () => {
78
+ authClient.destroy();
79
+ };
80
+ }, [authClient, config]);
81
+
82
+ const login = useCallback(
83
+ async (email: string, password: string) => {
84
+ setState((prev) => ({ ...prev, isLoading: true }));
85
+
86
+ try {
87
+ const session = await authClient.login(email, password);
88
+ setState({
89
+ session,
90
+ isLoading: false,
91
+ isAuthenticated: true,
92
+ });
93
+ } catch (error) {
94
+ setState((prev) => ({ ...prev, isLoading: false }));
95
+ throw error;
96
+ }
97
+ },
98
+ [authClient]
99
+ );
100
+
101
+ const logout = useCallback(async () => {
102
+ setState((prev) => ({ ...prev, isLoading: true }));
103
+
104
+ try {
105
+ await authClient.logout();
106
+ } finally {
107
+ setState({
108
+ session: null,
109
+ isLoading: false,
110
+ isAuthenticated: false,
111
+ });
112
+ }
113
+ }, [authClient]);
114
+
115
+ const refreshUser = useCallback(async () => {
116
+ try {
117
+ const user = await authClient.getCurrentUser();
118
+ setState((prev) => ({
119
+ ...prev,
120
+ session: prev.session
121
+ ? { ...prev.session, user }
122
+ : null,
123
+ }));
124
+ } catch (error) {
125
+ console.error('Failed to refresh user:', error);
126
+ }
127
+ }, [authClient]);
128
+
129
+ const getAccessToken = useCallback(async () => {
130
+ return authClient.getAccessToken();
131
+ }, [authClient]);
132
+
133
+ const value: AuthContextValue = {
134
+ ...state,
135
+ login,
136
+ logout,
137
+ refreshUser,
138
+ getAccessToken,
139
+ };
140
+
141
+ return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
142
+ }
143
+
144
+ /**
145
+ * Hook to access auth context
146
+ */
147
+ export function useAuthContext(): AuthContextValue {
148
+ const context = useContext(AuthContext);
149
+ if (!context) {
150
+ throw new Error('useAuthContext must be used within AuthProvider');
151
+ }
152
+ return context;
153
+ }