@djangocfg/api 2.1.37 → 2.1.39

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 CHANGED
@@ -88,13 +88,13 @@ This is handled automatically by `createExtensionAPI()` from `@djangocfg/ext-bas
88
88
 
89
89
  ## Auth Module
90
90
 
91
- Complete authentication system with React contexts, hooks, and utilities.
91
+ Complete OTP-based authentication system with React contexts, hooks, and utilities.
92
92
 
93
93
  ### Setup
94
94
 
95
95
  ```tsx
96
96
  // app/layout.tsx
97
- import { AuthProvider, AccountsProvider } from '@djangocfg/api/auth';
97
+ import { AuthProvider } from '@djangocfg/api/auth';
98
98
 
99
99
  export default function RootLayout({ children }) {
100
100
  return (
@@ -108,9 +108,7 @@ export default function RootLayout({ children }) {
108
108
  }
109
109
  }}
110
110
  >
111
- <AccountsProvider>
112
- {children}
113
- </AccountsProvider>
111
+ {children}
114
112
  </AuthProvider>
115
113
  </body>
116
114
  </html>
@@ -122,9 +120,9 @@ export default function RootLayout({ children }) {
122
120
 
123
121
  ```typescript
124
122
  import {
125
- useAuth, // Main auth context (user, isAuthenticated, login, logout)
123
+ useAuth, // Main auth context (user, isAuthenticated, OTP methods)
126
124
  useAuthGuard, // Protect routes (redirect if not authenticated)
127
- useAuthForm, // Form state management (login, register, reset)
125
+ useAuthForm, // OTP form state management (identifier, otp, steps)
128
126
  useGithubAuth, // GitHub OAuth integration
129
127
  useAutoAuth, // Auto-authentication on mount
130
128
  useLocalStorage, // localStorage helper
@@ -134,7 +132,7 @@ import {
134
132
 
135
133
  ### useAuth
136
134
 
137
- Main authentication hook:
135
+ Main authentication hook with OTP support:
138
136
 
139
137
  ```tsx
140
138
  'use client';
@@ -145,16 +143,15 @@ export function UserProfile() {
145
143
  user, // UserProfile | null
146
144
  isAuthenticated, // boolean
147
145
  isLoading, // boolean
148
- login, // (email, password) => Promise<void>
146
+ requestOTP, // (identifier, channel, sourceUrl?) => Promise<{ success, message }>
147
+ verifyOTP, // (identifier, otp, channel, sourceUrl?, redirectUrl?) => Promise<{ success, message, user? }>
149
148
  logout, // () => Promise<void>
150
- register, // (data) => Promise<void>
151
- updateProfile, // (data) => Promise<void>
152
149
  } = useAuth();
153
150
 
154
151
  if (isLoading) return <div>Loading...</div>;
155
152
 
156
153
  if (!isAuthenticated) {
157
- return <button onClick={() => login('user@example.com', 'password')}>Login</button>;
154
+ return <div>Please sign in</div>;
158
155
  }
159
156
 
160
157
  return (
@@ -188,46 +185,98 @@ export default function DashboardPage() {
188
185
 
189
186
  ### useAuthForm
190
187
 
191
- Manage authentication form state:
188
+ Manage OTP authentication form state (two-step flow):
192
189
 
193
190
  ```tsx
194
191
  'use client';
195
192
  import { useAuthForm } from '@djangocfg/api/auth';
196
193
 
197
- export function LoginForm() {
194
+ export function OTPLoginForm() {
198
195
  const {
199
- email,
200
- password,
201
- error,
196
+ // State
197
+ identifier, // Email or phone number
198
+ channel, // 'email' | 'phone'
199
+ otp, // 6-digit OTP code
200
+ step, // 'identifier' | 'otp'
202
201
  isLoading,
203
- setEmail,
204
- setPassword,
205
- handleLogin,
206
- handleRegister,
207
- handlePasswordReset,
208
- } = useAuthForm();
202
+ error,
203
+ acceptedTerms,
204
+
205
+ // Setters
206
+ setIdentifier,
207
+ setChannel,
208
+ setOtp,
209
+ setAcceptedTerms,
210
+
211
+ // Handlers
212
+ handleIdentifierSubmit, // Request OTP
213
+ handleOTPSubmit, // Verify OTP
214
+ handleResendOTP, // Resend OTP
215
+ handleBackToIdentifier, // Go back to step 1
216
+
217
+ // Utilities
218
+ detectChannelFromIdentifier,
219
+ validateIdentifier,
220
+ } = useAuthForm({
221
+ sourceUrl: window.location.origin,
222
+ requireTermsAcceptance: false, // Set true if terms/privacy links provided
223
+ onIdentifierSuccess: (identifier, channel) => {
224
+ console.log('OTP sent to', identifier);
225
+ },
226
+ onOTPSuccess: () => {
227
+ console.log('Login successful');
228
+ },
229
+ });
230
+
231
+ if (step === 'identifier') {
232
+ return (
233
+ <form onSubmit={handleIdentifierSubmit}>
234
+ <input
235
+ type="email"
236
+ value={identifier}
237
+ onChange={(e) => setIdentifier(e.target.value)}
238
+ placeholder="Enter email"
239
+ />
240
+ {error && <p className="text-red-500">{error}</p>}
241
+ <button type="submit" disabled={isLoading}>
242
+ {isLoading ? 'Sending...' : 'Send verification code'}
243
+ </button>
244
+ </form>
245
+ );
246
+ }
209
247
 
210
248
  return (
211
- <form onSubmit={(e) => { e.preventDefault(); handleLogin(); }}>
249
+ <form onSubmit={handleOTPSubmit}>
250
+ <p>Enter the code sent to {identifier}</p>
212
251
  <input
213
- type="email"
214
- value={email}
215
- onChange={(e) => setEmail(e.target.value)}
252
+ type="text"
253
+ value={otp}
254
+ onChange={(e) => setOtp(e.target.value)}
255
+ placeholder="000000"
256
+ maxLength={6}
216
257
  />
217
- <input
218
- type="password"
219
- value={password}
220
- onChange={(e) => setPassword(e.target.value)}
221
- />
222
- {error && <p>{error}</p>}
223
- <button type="submit" disabled={isLoading}>
224
- {isLoading ? 'Loading...' : 'Login'}
258
+ {error && <p className="text-red-500">{error}</p>}
259
+ <button type="submit" disabled={isLoading || otp.length < 6}>
260
+ {isLoading ? 'Verifying...' : 'Verify'}
225
261
  </button>
262
+ <button type="button" onClick={handleResendOTP}>Resend</button>
263
+ <button type="button" onClick={handleBackToIdentifier}>Back</button>
226
264
  </form>
227
265
  );
228
266
  }
229
267
  ```
230
268
 
269
+ #### useAuthForm Options
270
+
271
+ | Option | Type | Default | Description |
272
+ |--------|------|---------|-------------|
273
+ | `sourceUrl` | `string` | required | Application URL for OTP emails |
274
+ | `redirectUrl` | `string` | - | URL to redirect after successful OTP verification |
275
+ | `requireTermsAcceptance` | `boolean` | `false` | Require terms acceptance before submit |
276
+ | `onIdentifierSuccess` | `function` | - | Callback after OTP sent |
277
+ | `onOTPSuccess` | `function` | - | Callback after successful verification |
278
+ | `onError` | `function` | - | Callback on error |
279
+
231
280
  ### useGithubAuth
232
281
 
233
282
  GitHub OAuth integration:
@@ -238,34 +287,23 @@ import { useGithubAuth } from '@djangocfg/api/auth';
238
287
 
239
288
  export function GithubLoginButton() {
240
289
  const {
241
- initiateGithubAuth, // () => Promise<void>
242
- isLoading, // boolean
243
- error, // string | null
244
- } = useGithubAuth();
290
+ startGithubAuth, // () => void - redirects to GitHub
291
+ isLoading, // boolean
292
+ error, // string | null
293
+ } = useGithubAuth({
294
+ sourceUrl: window.location.origin,
295
+ });
245
296
 
246
297
  return (
247
- <button onClick={initiateGithubAuth} disabled={isLoading}>
248
- {isLoading ? 'Connecting...' : 'Login with GitHub'}
298
+ <button onClick={startGithubAuth} disabled={isLoading}>
299
+ {isLoading ? 'Connecting...' : 'Continue with GitHub'}
249
300
  </button>
250
301
  );
251
302
  }
252
-
253
- // app/auth/callback/github/page.tsx
254
- 'use client';
255
- import { useGithubAuth } from '@djangocfg/api/auth';
256
- import { useEffect } from 'react';
257
-
258
- export default function GithubCallbackPage() {
259
- const { handleGithubCallback } = useGithubAuth();
260
-
261
- useEffect(() => {
262
- handleGithubCallback();
263
- }, []);
264
-
265
- return <div>Processing GitHub login...</div>;
266
- }
267
303
  ```
268
304
 
305
+ OAuth callback is handled automatically by `@djangocfg/layouts` AuthLayout component.
306
+
269
307
  ## Server Components & API Routes
270
308
 
271
309
  Use fetchers for server-side data fetching:
package/dist/auth.cjs CHANGED
@@ -3953,7 +3953,6 @@ var defaultRoutes = {
3953
3953
  var AuthContext = (0, import_react2.createContext)(void 0);
3954
3954
  var EMAIL_STORAGE_KEY = "auth_email";
3955
3955
  var PHONE_STORAGE_KEY = "auth_phone";
3956
- var AUTH_REDIRECT_KEY = "auth_redirect_url";
3957
3956
  var hasValidTokens = /* @__PURE__ */ __name(() => {
3958
3957
  if (typeof window === "undefined") return false;
3959
3958
  return api.isAuthenticated();
@@ -3973,7 +3972,6 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
3973
3972
  const queryParams = (0, import_hooks2.useQueryParams)();
3974
3973
  const [storedEmail, setStoredEmail, clearStoredEmail] = (0, import_hooks2.useLocalStorage)(EMAIL_STORAGE_KEY, null);
3975
3974
  const [storedPhone, setStoredPhone, clearStoredPhone] = (0, import_hooks2.useLocalStorage)(PHONE_STORAGE_KEY, null);
3976
- const [redirectUrl, setRedirectUrl, clearRedirectUrl] = (0, import_hooks2.useLocalStorage)(AUTH_REDIRECT_KEY, null);
3977
3975
  const user = accounts.profile;
3978
3976
  const userRef = (0, import_react2.useRef)(user);
3979
3977
  const configRef = (0, import_react2.useRef)(config);
@@ -4155,7 +4153,7 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
4155
4153
  [accounts]
4156
4154
  );
4157
4155
  const verifyOTP = (0, import_react2.useCallback)(
4158
- async (identifier, otpCode, channel, sourceUrl) => {
4156
+ async (identifier, otpCode, channel, sourceUrl, redirectUrl) => {
4159
4157
  try {
4160
4158
  const channelValue = channel === "phone" ? enums_exports.OTPVerifyRequestChannel.PHONE : enums_exports.OTPVerifyRequestChannel.EMAIL;
4161
4159
  const result = await accounts.verifyOTP({
@@ -4185,13 +4183,8 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
4185
4183
  if (result.user?.id) {
4186
4184
  Analytics.setUser(String(result.user.id));
4187
4185
  }
4188
- const defaultCallback = config?.routes?.defaultCallback || defaultRoutes.defaultCallback;
4189
- if (redirectUrl && redirectUrl !== defaultCallback) {
4190
- clearRedirectUrl();
4191
- router.hardPush(redirectUrl);
4192
- } else {
4193
- router.hardPush(defaultCallback);
4194
- }
4186
+ const finalRedirectUrl = redirectUrl || config?.routes?.defaultCallback || defaultRoutes.defaultCallback;
4187
+ router.hardPush(finalRedirectUrl);
4195
4188
  return {
4196
4189
  success: true,
4197
4190
  message: "Login successful",
@@ -4209,7 +4202,7 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
4209
4202
  };
4210
4203
  }
4211
4204
  },
4212
- [setStoredEmail, setStoredPhone, clearStoredEmail, clearStoredPhone, redirectUrl, clearRedirectUrl, config?.routes?.defaultCallback, accounts]
4205
+ [setStoredEmail, setStoredPhone, clearStoredEmail, clearStoredPhone, config?.routes?.defaultCallback, accounts, router]
4213
4206
  );
4214
4207
  const refreshToken = (0, import_react2.useCallback)(async () => {
4215
4208
  try {
@@ -4244,15 +4237,6 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
4244
4237
  };
4245
4238
  }
4246
4239
  }, [clearAuthState, accounts]);
4247
- const clearRedirect = (0, import_react2.useCallback)(() => {
4248
- clearRedirectUrl();
4249
- }, [clearRedirectUrl]);
4250
- const saveCurrentUrlForRedirect = (0, import_react2.useCallback)(() => {
4251
- if (typeof window !== "undefined") {
4252
- const currentUrl = window.location.pathname + window.location.search;
4253
- setRedirectUrl(currentUrl);
4254
- }
4255
- }, [setRedirectUrl]);
4256
4240
  const logout = (0, import_react2.useCallback)(async () => {
4257
4241
  const performLogout = /* @__PURE__ */ __name(() => {
4258
4242
  Analytics.event("auth_logout" /* AUTH_LOGOUT */, {
@@ -4282,31 +4266,6 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
4282
4266
  }
4283
4267
  }
4284
4268
  }, [accounts, config?.routes?.defaultAuthCallback, router]);
4285
- const getSavedRedirectUrl = (0, import_react2.useCallback)(() => {
4286
- if (typeof window !== "undefined") {
4287
- return sessionStorage.getItem(AUTH_REDIRECT_KEY);
4288
- }
4289
- return null;
4290
- }, []);
4291
- const saveRedirectUrl = (0, import_react2.useCallback)((url) => {
4292
- if (typeof window !== "undefined") {
4293
- sessionStorage.setItem(AUTH_REDIRECT_KEY, url);
4294
- }
4295
- }, []);
4296
- const clearSavedRedirectUrl = (0, import_react2.useCallback)(() => {
4297
- if (typeof window !== "undefined") {
4298
- sessionStorage.removeItem(AUTH_REDIRECT_KEY);
4299
- }
4300
- }, []);
4301
- const getFinalRedirectUrl = (0, import_react2.useCallback)(() => {
4302
- const savedUrl = getSavedRedirectUrl();
4303
- return savedUrl || (config?.routes?.defaultCallback || defaultRoutes.defaultCallback);
4304
- }, [getSavedRedirectUrl, config?.routes?.defaultCallback]);
4305
- const useAndClearRedirectUrl = (0, import_react2.useCallback)(() => {
4306
- const finalUrl = getFinalRedirectUrl();
4307
- clearSavedRedirectUrl();
4308
- return finalUrl;
4309
- }, [getFinalRedirectUrl, clearSavedRedirectUrl]);
4310
4269
  const isAdminUser = (0, import_react2.useMemo)(() => {
4311
4270
  return Boolean(user?.is_staff || user?.is_superuser);
4312
4271
  }, [user]);
@@ -4330,13 +4289,7 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
4330
4289
  requestOTP,
4331
4290
  verifyOTP,
4332
4291
  refreshToken,
4333
- logout,
4334
- getSavedRedirectUrl,
4335
- saveRedirectUrl,
4336
- clearSavedRedirectUrl,
4337
- getFinalRedirectUrl,
4338
- useAndClearRedirectUrl,
4339
- saveCurrentUrlForRedirect
4292
+ logout
4340
4293
  }),
4341
4294
  [
4342
4295
  user,
@@ -4353,13 +4306,7 @@ var AuthProviderInternal = /* @__PURE__ */ __name(({ children, config }) => {
4353
4306
  requestOTP,
4354
4307
  verifyOTP,
4355
4308
  refreshToken,
4356
- logout,
4357
- getSavedRedirectUrl,
4358
- saveRedirectUrl,
4359
- clearSavedRedirectUrl,
4360
- getFinalRedirectUrl,
4361
- useAndClearRedirectUrl,
4362
- saveCurrentUrlForRedirect
4309
+ logout
4363
4310
  ]
4364
4311
  );
4365
4312
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(AuthContext.Provider, { value, children });
@@ -4517,10 +4464,10 @@ function useSessionStorage(key, initialValue) {
4517
4464
  __name(useSessionStorage, "useSessionStorage");
4518
4465
 
4519
4466
  // src/auth/hooks/useAuthRedirect.ts
4520
- var AUTH_REDIRECT_KEY2 = "auth_redirect_url";
4467
+ var AUTH_REDIRECT_KEY = "auth_redirect_url";
4521
4468
  var useAuthRedirectManager = /* @__PURE__ */ __name((options = {}) => {
4522
4469
  const { fallbackUrl = "/dashboard", clearOnUse = true } = options;
4523
- const [redirectUrl, setRedirectUrl, removeRedirectUrl] = useSessionStorage(AUTH_REDIRECT_KEY2, "");
4470
+ const [redirectUrl, setRedirectUrl, removeRedirectUrl] = useSessionStorage(AUTH_REDIRECT_KEY, "");
4524
4471
  const setRedirect = /* @__PURE__ */ __name((url) => {
4525
4472
  setRedirectUrl(url);
4526
4473
  }, "setRedirect");
@@ -4764,7 +4711,7 @@ var useAutoAuth = /* @__PURE__ */ __name((options = {}) => {
4764
4711
 
4765
4712
  // src/auth/hooks/useAuthForm.ts
4766
4713
  var useAuthForm = /* @__PURE__ */ __name((options) => {
4767
- const { onIdentifierSuccess, onOTPSuccess, onError, sourceUrl } = options;
4714
+ const { onIdentifierSuccess, onOTPSuccess, onError, sourceUrl, redirectUrl, requireTermsAcceptance = false } = options;
4768
4715
  const [identifier, setIdentifier] = (0, import_react7.useState)("");
4769
4716
  const [channel, setChannel] = (0, import_react7.useState)("email");
4770
4717
  const [otp, setOtp] = (0, import_react7.useState)("");
@@ -4833,7 +4780,7 @@ var useAuthForm = /* @__PURE__ */ __name((options) => {
4833
4780
  onError?.(message);
4834
4781
  return;
4835
4782
  }
4836
- if (!acceptedTerms) {
4783
+ if (requireTermsAcceptance && !acceptedTerms) {
4837
4784
  const message = "Please accept the Terms of Service and Privacy Policy";
4838
4785
  setError(message);
4839
4786
  onError?.(message);
@@ -4877,7 +4824,7 @@ var useAuthForm = /* @__PURE__ */ __name((options) => {
4877
4824
  setIsLoading(true);
4878
4825
  clearError();
4879
4826
  try {
4880
- const result = await verifyOTP(identifier, otp, channel, sourceUrl);
4827
+ const result = await verifyOTP(identifier, otp, channel, sourceUrl, redirectUrl);
4881
4828
  if (result.success) {
4882
4829
  if (channel === "email") {
4883
4830
  setSavedEmail(identifier);
@@ -4898,7 +4845,7 @@ var useAuthForm = /* @__PURE__ */ __name((options) => {
4898
4845
  } finally {
4899
4846
  setIsLoading(false);
4900
4847
  }
4901
- }, [identifier, otp, channel, verifyOTP, clearError, setSavedEmail, onOTPSuccess, onError, sourceUrl]);
4848
+ }, [identifier, otp, channel, verifyOTP, clearError, setSavedEmail, onOTPSuccess, onError, sourceUrl, redirectUrl]);
4902
4849
  const handleResendOTP = (0, import_react7.useCallback)(async () => {
4903
4850
  setIsLoading(true);
4904
4851
  clearError();