@startsimpli/auth 0.4.5 → 0.4.7

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/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@startsimpli/auth",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Shared authentication package for StartSimpli Next.js apps",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
7
7
  "exports": {
8
8
  ".": "./src/index.ts",
9
9
  "./client": "./src/client/index.ts",
10
+ "./components": "./src/components/index.ts",
10
11
  "./server": "./src/server/index.ts",
11
12
  "./types": "./src/types/index.ts",
12
13
  "./email": "./src/email/index.ts"
@@ -4,7 +4,7 @@ import { AuthClient } from '../client/auth-client';
4
4
  // Helpers
5
5
  function makeToken(exp: number): string {
6
6
  const header = btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));
7
- const body = btoa(JSON.stringify({ token_type: 'access', exp, iat: exp - 3600, jti: 'test', user_id: '123' }));
7
+ const body = btoa(JSON.stringify({ tokenType: 'access', exp, iat: exp - 3600, jti: 'test', userId: '123' }));
8
8
  return `${header}.${body}.signature`;
9
9
  }
10
10
 
@@ -36,8 +36,8 @@ function makeJwt(payload: object): string {
36
36
  return `${encode({ alg: 'HS256' })}.${encode(payload)}.sig`;
37
37
  }
38
38
 
39
- const VALID_TOKEN = makeJwt({ exp: Math.floor(Date.now() / 1000) + 3600, user_id: '1' });
40
- const REFRESHED_TOKEN = makeJwt({ exp: Math.floor(Date.now() / 1000) + 7200, user_id: '1' });
39
+ const VALID_TOKEN = makeJwt({ exp: Math.floor(Date.now() / 1000) + 3600, userId: '1' });
40
+ const REFRESHED_TOKEN = makeJwt({ exp: Math.floor(Date.now() / 1000) + 7200, userId: '1' });
41
41
 
42
42
  // Stub the CSRF token helper so refreshAccessToken() doesn't fail
43
43
  vi.mock('../utils/cookies', () => ({
@@ -83,20 +83,17 @@ function makeJwt(payload: object): string {
83
83
  return `${encode({ alg: 'HS256' })}.${encode(payload)}.sig`;
84
84
  }
85
85
 
86
- const VALID_TOKEN = makeJwt({ exp: Math.floor(Date.now() / 1000) + 3600, user_id: '1' });
86
+ const VALID_TOKEN = makeJwt({ exp: Math.floor(Date.now() / 1000) + 3600, userId: '1' });
87
87
 
88
- describe('CSRF token on signin/register', () => {
88
+ describe('CSRF not required for signin/register (endpoints are @csrf_exempt)', () => {
89
89
  beforeEach(() => {
90
90
  vi.clearAllMocks();
91
91
  mockSessionStorage.clear();
92
92
  mockLocalStorage.clear();
93
93
  });
94
94
 
95
- it('signInWithCredentials sends X-CSRFToken header', async () => {
96
- mockFetch.mockImplementation((url: string) => {
97
- if (url.includes('/csrf/')) {
98
- return Promise.resolve({ ok: true });
99
- }
95
+ it('signInWithCredentials does not fetch or send CSRF token', async () => {
96
+ mockFetch.mockImplementation(() => {
100
97
  return Promise.resolve({
101
98
  ok: true,
102
99
  status: 200,
@@ -106,20 +103,23 @@ describe('CSRF token on signin/register', () => {
106
103
 
107
104
  await signInWithCredentials('test@test.com', 'password');
108
105
 
109
- // Find the token endpoint call (not the csrf call)
106
+ // Should NOT call the CSRF endpoint
107
+ const csrfCall = mockFetch.mock.calls.find(
108
+ (c: unknown[]) => typeof c[0] === 'string' && (c[0] as string).includes('/csrf/')
109
+ );
110
+ expect(csrfCall).toBeUndefined();
111
+
112
+ // Token endpoint call should not include X-CSRFToken header
110
113
  const tokenCall = mockFetch.mock.calls.find(
111
114
  (c: unknown[]) => typeof c[0] === 'string' && (c[0] as string).includes('/auth/token/') && !(c[0] as string).includes('csrf') && !(c[0] as string).includes('refresh')
112
115
  );
113
116
  expect(tokenCall).toBeDefined();
114
117
  const headers = tokenCall![1]?.headers;
115
- expect(headers['X-CSRFToken']).toBe('test-csrf');
118
+ expect(headers['X-CSRFToken']).toBeUndefined();
116
119
  });
117
120
 
118
- it('registerAccount sends X-CSRFToken header', async () => {
119
- mockFetch.mockImplementation((url: string) => {
120
- if (url.includes('/csrf/')) {
121
- return Promise.resolve({ ok: true });
122
- }
121
+ it('registerAccount does not fetch or send CSRF token', async () => {
122
+ mockFetch.mockImplementation(() => {
123
123
  return Promise.resolve({
124
124
  ok: true,
125
125
  status: 200,
@@ -133,12 +133,18 @@ describe('CSRF token on signin/register', () => {
133
133
  passwordConfirm: 'securepassword',
134
134
  });
135
135
 
136
+ // Should NOT call the CSRF endpoint
137
+ const csrfCall = mockFetch.mock.calls.find(
138
+ (c: unknown[]) => typeof c[0] === 'string' && (c[0] as string).includes('/csrf/')
139
+ );
140
+ expect(csrfCall).toBeUndefined();
141
+
136
142
  const registerCall = mockFetch.mock.calls.find(
137
143
  (c: unknown[]) => typeof c[0] === 'string' && (c[0] as string).includes('/auth/register/')
138
144
  );
139
145
  expect(registerCall).toBeDefined();
140
146
  const headers = registerCall![1]?.headers;
141
- expect(headers['X-CSRFToken']).toBe('test-csrf');
147
+ expect(headers['X-CSRFToken']).toBeUndefined();
142
148
  });
143
149
  });
144
150
 
@@ -10,16 +10,16 @@ describe('Token utilities', () => {
10
10
  describe('decodeToken', () => {
11
11
  it('should decode valid JWT token', () => {
12
12
  const token =
13
- 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzM5MjI3MjAwLCJpYXQiOjE3MzkxNDA4MDAsImp0aSI6InRlc3QtanRpIiwidXNlcl9pZCI6IjEyMzQ1In0.test-signature';
13
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlblR5cGUiOiJhY2Nlc3MiLCJleHAiOjE3MzkyMjcyMDAsImlhdCI6MTczOTE0MDgwMCwianRpIjoidGVzdC1qdGkiLCJ1c2VySWQiOiIxMjM0NSJ9.test-signature';
14
14
 
15
15
  const payload = decodeToken(token);
16
16
 
17
17
  expect(payload).toEqual({
18
- token_type: 'access',
18
+ tokenType: 'access',
19
19
  exp: 1739227200,
20
20
  iat: 1739140800,
21
21
  jti: 'test-jti',
22
- user_id: '12345',
22
+ userId: '12345',
23
23
  });
24
24
  });
25
25
 
@@ -243,13 +243,11 @@ function parseAuthResponse(data: unknown): { access?: string; user?: AuthUser }
243
243
  // --- Auth functions ---
244
244
 
245
245
  export async function signInWithCredentials(email: string, password: string) {
246
- await fetchCsrfToken();
247
- const csrfToken = getCsrfToken();
246
+ // No CSRF needed — Django's /auth/token/ is @csrf_exempt (JWT endpoint)
248
247
  const response = await fetchWithTimeout(resolveAuthUrl(AUTH_PATHS.TOKEN), {
249
248
  method: 'POST',
250
249
  headers: {
251
250
  'Content-Type': 'application/json',
252
- ...(csrfToken ? { 'X-CSRFToken': csrfToken } : {}),
253
251
  },
254
252
  credentials: 'include',
255
253
  body: JSON.stringify({ email, password }),
@@ -286,13 +284,11 @@ export async function registerAccount(payload: {
286
284
  const [firstFromName, ...rest] = rawName ? rawName.split(/\s+/) : [];
287
285
  const lastFromName = rest.length ? rest.join(' ') : undefined;
288
286
 
289
- await fetchCsrfToken();
290
- const csrfToken = getCsrfToken();
287
+ // No CSRF needed — Django's /auth/register/ is @csrf_exempt
291
288
  const response = await fetchWithTimeout(resolveAuthUrl(AUTH_PATHS.REGISTER), {
292
289
  method: 'POST',
293
290
  headers: {
294
291
  'Content-Type': 'application/json',
295
- ...(csrfToken ? { 'X-CSRFToken': csrfToken } : {}),
296
292
  },
297
293
  credentials: 'include',
298
294
  body: JSON.stringify({
@@ -0,0 +1,78 @@
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { initiateGoogleOAuth } from '../client/functions'
5
+
6
+ export interface GoogleSignInButtonProps {
7
+ /** Path for the OAuth callback page (default: "/auth/callback") */
8
+ callbackPath?: string
9
+ /** URL to redirect to after successful auth */
10
+ redirectTo?: string
11
+ /** Error handler */
12
+ onError?: (message: string) => void
13
+ /** Button text (default: "Continue with Google") */
14
+ children?: React.ReactNode
15
+ /** Additional class names */
16
+ className?: string
17
+ /** Disabled state */
18
+ disabled?: boolean
19
+ }
20
+
21
+ /**
22
+ * Google Sign-In button that handles the full OAuth initiate flow:
23
+ * 1. Calls backend to get authorization URL with PKCE
24
+ * 2. Stores state nonce in sessionStorage for CSRF protection
25
+ * 3. Redirects to Google consent screen
26
+ */
27
+ export function GoogleSignInButton({
28
+ callbackPath = '/auth/callback',
29
+ redirectTo,
30
+ onError,
31
+ children,
32
+ className,
33
+ disabled,
34
+ }: GoogleSignInButtonProps) {
35
+ const [isLoading, setIsLoading] = useState(false)
36
+
37
+ const handleClick = async () => {
38
+ setIsLoading(true)
39
+ try {
40
+ // Use a clean callback URL with no query params — Google requires exact redirect_uri match
41
+ const redirectUri = new URL(callbackPath, window.location.origin).toString()
42
+
43
+ const data = await initiateGoogleOAuth(redirectUri)
44
+ if (!data?.authorization_url) {
45
+ throw new Error('Missing authorization URL from server')
46
+ }
47
+
48
+ // Store state nonce for CSRF validation on the callback page
49
+ const authUrlParams = new URL(data.authorization_url).searchParams
50
+ const stateNonce = authUrlParams.get('state')
51
+ if (stateNonce) {
52
+ sessionStorage.setItem('oauth_state_nonce', stateNonce)
53
+ }
54
+
55
+ // Store post-auth redirect separately (not in the redirect_uri)
56
+ if (redirectTo) {
57
+ sessionStorage.setItem('oauth_redirect_to', redirectTo)
58
+ }
59
+
60
+ window.location.href = data.authorization_url
61
+ } catch (err) {
62
+ setIsLoading(false)
63
+ const message = err instanceof Error ? err.message : 'Failed to initiate Google sign-in'
64
+ onError?.(message)
65
+ }
66
+ }
67
+
68
+ return (
69
+ <button
70
+ type="button"
71
+ onClick={handleClick}
72
+ disabled={disabled || isLoading}
73
+ className={className}
74
+ >
75
+ {children ?? 'Continue with Google'}
76
+ </button>
77
+ )
78
+ }
@@ -0,0 +1,4 @@
1
+ export { GoogleSignInButton, type GoogleSignInButtonProps } from './google-sign-in-button'
2
+ export { OAuthCallback, type OAuthCallbackProps } from './oauth-callback'
3
+ export { useOAuthCallback, type UseOAuthCallbackOptions, type UseOAuthCallbackReturn, type OAuthCallbackResult } from './use-oauth-callback'
4
+ export { OAuthConnectionCard, type OAuthConnectionCardProps } from './oauth-connection-card'
@@ -0,0 +1,95 @@
1
+ 'use client'
2
+
3
+ import type { AuthUser } from '../types'
4
+ import { useOAuthCallback, type OAuthCallbackResult } from './use-oauth-callback'
5
+
6
+ export interface OAuthCallbackProps {
7
+ /** Code from OAuth redirect URL params */
8
+ code: string | null
9
+ /** State from OAuth redirect URL params */
10
+ state: string | null
11
+ /** Called on successful authentication. redirectTo is the path stored by GoogleSignInButton. */
12
+ onSuccess: (result: OAuthCallbackResult & { redirectTo: string }) => void
13
+ /** Called on error */
14
+ onError?: (message: string) => void
15
+ /** Path to redirect to on "Back to Sign In" (default: "/auth/signin") */
16
+ signInPath?: string
17
+ /** Custom loading content */
18
+ loadingContent?: React.ReactNode
19
+ /** Custom error content renderer */
20
+ renderError?: (error: string, signInPath: string) => React.ReactNode
21
+ }
22
+
23
+ /**
24
+ * OAuth callback handler component with built-in loading/error UI.
25
+ *
26
+ * Handles the full callback flow: state validation, token exchange, user fetch.
27
+ * Drop into your /auth/callback page:
28
+ *
29
+ * ```tsx
30
+ * const searchParams = useSearchParams()
31
+ * <OAuthCallback
32
+ * code={searchParams.get('code')}
33
+ * state={searchParams.get('state')}
34
+ * onSuccess={({ access, user }) => {
35
+ * setAccessToken(access); setUser(user); router.replace('/dashboard')
36
+ * }}
37
+ * />
38
+ * ```
39
+ */
40
+ export function OAuthCallback({
41
+ code,
42
+ state,
43
+ onSuccess,
44
+ onError,
45
+ signInPath = '/auth/signin',
46
+ loadingContent,
47
+ renderError,
48
+ }: OAuthCallbackProps) {
49
+ const { error, isProcessing, redirectTo } = useOAuthCallback(
50
+ { code, state },
51
+ {
52
+ onSuccess: (result) => onSuccess({ ...result, redirectTo }),
53
+ onError,
54
+ },
55
+ )
56
+
57
+ if (error) {
58
+ if (renderError) {
59
+ return <>{renderError(error, signInPath)}</>
60
+ }
61
+
62
+ return (
63
+ <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100 p-4">
64
+ <div className="w-full max-w-md rounded-2xl bg-white p-8 shadow-xl shadow-slate-200/50 ring-1 ring-slate-200 text-center">
65
+ <h1 className="text-2xl font-bold text-slate-900 mb-2">Authentication failed</h1>
66
+ <p className="text-slate-600 mb-6">{error}</p>
67
+ <a
68
+ href={signInPath}
69
+ className="inline-block w-full rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"
70
+ >
71
+ Back to Sign In
72
+ </a>
73
+ </div>
74
+ </div>
75
+ )
76
+ }
77
+
78
+ if (isProcessing) {
79
+ if (loadingContent) {
80
+ return <>{loadingContent}</>
81
+ }
82
+
83
+ return (
84
+ <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100 p-4">
85
+ <div className="w-full max-w-md rounded-2xl bg-white p-8 shadow-xl shadow-slate-200/50 ring-1 ring-slate-200 text-center">
86
+ <div className="mx-auto mb-4 h-12 w-12 animate-spin rounded-full border-4 border-indigo-200 border-t-indigo-600" />
87
+ <h1 className="text-xl font-semibold text-slate-900">Finishing sign in...</h1>
88
+ <p className="text-slate-600 mt-2">This will only take a moment.</p>
89
+ </div>
90
+ </div>
91
+ )
92
+ }
93
+
94
+ return null
95
+ }
@@ -0,0 +1,117 @@
1
+ 'use client'
2
+
3
+ import { useState, useCallback } from 'react'
4
+
5
+ export interface OAuthConnectionCardProps {
6
+ providerName: string
7
+ providerIcon?: React.ReactNode
8
+ description?: string
9
+ connected: boolean
10
+ accountLabel?: string
11
+ lastSync?: string
12
+ connectUrl?: string
13
+ onConnect?: () => void | Promise<void>
14
+ onDisconnect?: () => void | Promise<void>
15
+ }
16
+
17
+ /**
18
+ * Card showing OAuth service connection status with connect/disconnect actions.
19
+ * Used for email (Gmail/Outlook), calendar, CRM integrations.
20
+ */
21
+ export function OAuthConnectionCard({
22
+ providerName,
23
+ providerIcon,
24
+ description,
25
+ connected,
26
+ accountLabel,
27
+ lastSync,
28
+ connectUrl,
29
+ onConnect,
30
+ onDisconnect,
31
+ }: OAuthConnectionCardProps) {
32
+ const [isLoading, setIsLoading] = useState(false)
33
+
34
+ const handleConnect = useCallback(async () => {
35
+ if (connectUrl) {
36
+ window.location.href = connectUrl
37
+ return
38
+ }
39
+ if (onConnect) {
40
+ setIsLoading(true)
41
+ try {
42
+ await onConnect()
43
+ } finally {
44
+ setIsLoading(false)
45
+ }
46
+ }
47
+ }, [connectUrl, onConnect])
48
+
49
+ const handleDisconnect = useCallback(async () => {
50
+ if (onDisconnect) {
51
+ setIsLoading(true)
52
+ try {
53
+ await onDisconnect()
54
+ } finally {
55
+ setIsLoading(false)
56
+ }
57
+ }
58
+ }, [onDisconnect])
59
+
60
+ return (
61
+ <div className="rounded-lg border border-gray-200 bg-white p-5">
62
+ <div className="flex items-start gap-4">
63
+ {providerIcon && (
64
+ <div className="flex-shrink-0 w-10 h-10 rounded-lg bg-gray-100 flex items-center justify-center">
65
+ {providerIcon}
66
+ </div>
67
+ )}
68
+ <div className="flex-1 min-w-0">
69
+ <div className="flex items-center justify-between mb-1">
70
+ <h3 className="font-medium text-gray-900">{providerName}</h3>
71
+ <span
72
+ className={`text-xs px-2 py-0.5 rounded-full font-medium ${
73
+ connected
74
+ ? 'bg-green-100 text-green-700'
75
+ : 'bg-gray-100 text-gray-500'
76
+ }`}
77
+ >
78
+ {connected ? 'Connected' : 'Not connected'}
79
+ </span>
80
+ </div>
81
+
82
+ {description && (
83
+ <p className="text-sm text-gray-600 mb-2">{description}</p>
84
+ )}
85
+
86
+ {connected && accountLabel && (
87
+ <p className="text-sm text-gray-700 mb-1">{accountLabel}</p>
88
+ )}
89
+
90
+ {connected && lastSync && (
91
+ <p className="text-xs text-gray-400 mb-2">Last sync: {lastSync}</p>
92
+ )}
93
+
94
+ <div className="mt-3">
95
+ {connected ? (
96
+ <button
97
+ onClick={handleDisconnect}
98
+ disabled={isLoading}
99
+ className="px-3 py-1.5 text-sm font-medium text-red-700 bg-red-50 hover:bg-red-100 rounded-md disabled:opacity-50 transition-colors"
100
+ >
101
+ {isLoading ? 'Disconnecting...' : 'Disconnect'}
102
+ </button>
103
+ ) : (
104
+ <button
105
+ onClick={handleConnect}
106
+ disabled={isLoading}
107
+ className="px-3 py-1.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md disabled:opacity-50 transition-colors"
108
+ >
109
+ {isLoading ? 'Connecting...' : `Connect ${providerName}`}
110
+ </button>
111
+ )}
112
+ </div>
113
+ </div>
114
+ </div>
115
+ </div>
116
+ )
117
+ }
@@ -0,0 +1,123 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useRef, useState } from 'react'
4
+ import { completeGoogleOAuth, getMe } from '../client/functions'
5
+ import type { AuthUser } from '../types'
6
+
7
+ export interface OAuthCallbackResult {
8
+ /** Access token from successful auth */
9
+ access: string
10
+ /** Authenticated user */
11
+ user: AuthUser
12
+ }
13
+
14
+ export interface UseOAuthCallbackOptions {
15
+ /** Called on successful authentication */
16
+ onSuccess: (result: OAuthCallbackResult) => void
17
+ /** Called on error */
18
+ onError?: (message: string) => void
19
+ }
20
+
21
+ export interface UseOAuthCallbackReturn {
22
+ /** Error message if callback failed */
23
+ error: string
24
+ /** Whether the callback is still processing */
25
+ isProcessing: boolean
26
+ /** Post-auth redirect path stored by GoogleSignInButton (default: '/dashboard') */
27
+ redirectTo: string
28
+ }
29
+
30
+ /**
31
+ * Hook that handles the OAuth callback flow:
32
+ * 1. Reads code + state from URL search params
33
+ * 2. Validates state nonce against sessionStorage (CSRF protection)
34
+ * 3. Exchanges code for tokens via backend
35
+ * 4. Fetches user if not returned with tokens
36
+ * 5. Calls onSuccess with access token + user
37
+ *
38
+ * Usage in a Next.js callback page:
39
+ * ```tsx
40
+ * const searchParams = useSearchParams()
41
+ * const { error, isProcessing } = useOAuthCallback({
42
+ * code: searchParams.get('code'),
43
+ * state: searchParams.get('state'),
44
+ * onSuccess: ({ access, user }) => {
45
+ * setAccessToken(access)
46
+ * setUser(user)
47
+ * router.replace('/dashboard')
48
+ * },
49
+ * })
50
+ * ```
51
+ */
52
+ export function useOAuthCallback(
53
+ params: {
54
+ code: string | null
55
+ state: string | null
56
+ },
57
+ options: UseOAuthCallbackOptions,
58
+ ): UseOAuthCallbackReturn {
59
+ const [error, setError] = useState('')
60
+ const [isProcessing, setIsProcessing] = useState(true)
61
+ const [redirectTo] = useState(() => {
62
+ if (typeof window === 'undefined') return '/dashboard'
63
+ const stored = sessionStorage.getItem('oauth_redirect_to')
64
+ sessionStorage.removeItem('oauth_redirect_to')
65
+ return stored || '/dashboard'
66
+ })
67
+ const ranRef = useRef(false)
68
+
69
+ useEffect(() => {
70
+ // Prevent double-execution in React strict mode
71
+ if (ranRef.current) return
72
+ ranRef.current = true
73
+
74
+ const handleCallback = async () => {
75
+ const { code, state } = params
76
+ if (!code || !state) {
77
+ setError('Invalid OAuth callback — missing code or state')
78
+ setIsProcessing(false)
79
+ return
80
+ }
81
+
82
+ // CSRF check: validate state nonce against pre-auth sessionStorage value
83
+ const storedNonce = sessionStorage.getItem('oauth_state_nonce')
84
+ sessionStorage.removeItem('oauth_state_nonce')
85
+ if (!storedNonce || storedNonce !== state) {
86
+ setError('Invalid OAuth state — possible CSRF attempt')
87
+ setIsProcessing(false)
88
+ return
89
+ }
90
+
91
+ try {
92
+ const result = await completeGoogleOAuth(code, state)
93
+ const access = result?.access
94
+
95
+ if (!access) {
96
+ throw new Error('No access token returned')
97
+ }
98
+
99
+ let user = result?.user as AuthUser | undefined
100
+ if (!user) {
101
+ user = await getMe() as AuthUser | undefined
102
+ }
103
+
104
+ if (!user) {
105
+ throw new Error('Failed to retrieve user after authentication')
106
+ }
107
+
108
+ setIsProcessing(false)
109
+ options.onSuccess({ access, user })
110
+ } catch (err) {
111
+ const message = err instanceof Error ? err.message : 'Authentication failed'
112
+ setError(message)
113
+ setIsProcessing(false)
114
+ options.onError?.(message)
115
+ }
116
+ }
117
+
118
+ handleCallback()
119
+ // eslint-disable-next-line react-hooks/exhaustive-deps
120
+ }, [])
121
+
122
+ return { error, isProcessing, redirectTo }
123
+ }
@@ -58,11 +58,11 @@ export interface AuthUser {
58
58
  * JWT token payload structure
59
59
  */
60
60
  export interface TokenPayload {
61
- token_type: 'access';
61
+ tokenType: 'access';
62
62
  exp: number;
63
63
  iat: number;
64
64
  jti: string;
65
- user_id: string;
65
+ userId: string;
66
66
  }
67
67
 
68
68
  /**