dauth-context-react 6.3.0 → 6.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dauth-context-react",
3
- "version": "6.3.0",
3
+ "version": "6.4.0",
4
4
  "description": "React provider and hook for passwordless authentication via the Dauth service (BFF pattern with httpOnly cookies)",
5
5
  "license": "MIT",
6
6
  "author": "David T. Pizarro Frick",
@@ -299,6 +299,7 @@ export function DauthProfileModal({
299
299
  getPasskeyCredentials,
300
300
  registerPasskey,
301
301
  deletePasskeyCredential,
302
+ uploadAvatar,
302
303
  } = useDauth();
303
304
  const isDesktop = useMediaQuery('(min-width: 641px)');
304
305
  const phase = useModalAnimation(open);
@@ -570,20 +571,22 @@ export function DauthProfileModal({
570
571
  );
571
572
 
572
573
  const handleAvatarClick = useCallback(() => {
573
- if (onAvatarUpload) {
574
- avatarInputRef.current?.click();
575
- }
576
- }, [onAvatarUpload]);
574
+ avatarInputRef.current?.click();
575
+ }, []);
577
576
 
578
577
  const handleAvatarChange = useCallback(
579
578
  async (e: React.ChangeEvent<HTMLInputElement>) => {
580
579
  const file = e.target.files?.[0];
581
- if (!file || !onAvatarUpload) return;
580
+ if (!file) return;
582
581
  setUploadingAvatar(true);
583
582
  try {
584
- const url = await onAvatarUpload(file);
585
- if (url) {
586
- await updateUser({ avatar: url } as any);
583
+ if (onAvatarUpload) {
584
+ const url = await onAvatarUpload(file);
585
+ if (url) {
586
+ await updateUser({ avatar: url } as any);
587
+ }
588
+ } else {
589
+ await uploadAvatar(file);
587
590
  }
588
591
  } catch {
589
592
  // Error handled by onError callback
@@ -593,7 +596,7 @@ export function DauthProfileModal({
593
596
  avatarInputRef.current.value = '';
594
597
  }
595
598
  },
596
- [onAvatarUpload, updateUser]
599
+ [onAvatarUpload, updateUser, uploadAvatar]
597
600
  );
598
601
 
599
602
  const handleSignOut = useCallback(() => {
@@ -776,9 +779,7 @@ export function DauthProfileModal({
776
779
  <div
777
780
  style={{
778
781
  ...avatarCircle,
779
- cursor: onAvatarUpload
780
- ? 'pointer'
781
- : 'default',
782
+ cursor: 'pointer',
782
783
  position: 'relative' as const,
783
784
  }}
784
785
  onClick={handleAvatarClick}
@@ -798,22 +799,20 @@ export function DauthProfileModal({
798
799
  ) : (
799
800
  avatarInitial
800
801
  )}
801
- {onAvatarUpload && !uploadingAvatar && (
802
+ {!uploadingAvatar && (
802
803
  <div style={avatarOverlay}>
803
804
  <IconCamera />
804
805
  </div>
805
806
  )}
806
807
  </div>
807
808
  <div style={emailText}>{user.email}</div>
808
- {onAvatarUpload && (
809
- <input
810
- ref={avatarInputRef}
811
- type="file"
812
- accept="image/*"
813
- style={{ display: 'none' }}
814
- onChange={handleAvatarChange}
815
- />
816
- )}
809
+ <input
810
+ ref={avatarInputRef}
811
+ type="file"
812
+ accept="image/*"
813
+ style={{ display: 'none' }}
814
+ onChange={handleAvatarChange}
815
+ />
817
816
  </div>
818
817
 
819
818
  {/* Status */}
@@ -8,6 +8,7 @@ import {
8
8
  IPasskeyRegistrationStartResponse,
9
9
  IPasskeyRegistrationFinishResponse,
10
10
  IDeletePasskeyResponse,
11
+ IUploadAvatarResponse,
11
12
  } from './interfaces/dauth.api.responses';
12
13
 
13
14
  function getCsrfToken(): string {
@@ -158,3 +159,19 @@ export async function deletePasskeyCredentialAPI(
158
159
  const data = await response.json();
159
160
  return { response, data };
160
161
  }
162
+
163
+ export async function uploadAvatarAPI(
164
+ basePath: string,
165
+ file: File
166
+ ): Promise<IUploadAvatarResponse> {
167
+ const formData = new FormData();
168
+ formData.append('avatar', file);
169
+ const response = await fetch(`${basePath}/avatar`, {
170
+ method: 'POST',
171
+ headers: { 'X-CSRF-Token': getCsrfToken() },
172
+ credentials: 'include',
173
+ body: formData,
174
+ });
175
+ const data = await response.json();
176
+ return { response, data };
177
+ }
@@ -69,3 +69,12 @@ export interface IDeletePasskeyResponse {
69
69
  message: string;
70
70
  };
71
71
  }
72
+
73
+ export interface IUploadAvatarResponse {
74
+ response: Response;
75
+ data: {
76
+ status: string;
77
+ user: IDauthUser;
78
+ message: string;
79
+ };
80
+ }
package/src/index.tsx CHANGED
@@ -135,6 +135,11 @@ export const DauthProvider: React.FC<IDauthProviderProps> = (
135
135
  [ctx]
136
136
  );
137
137
 
138
+ const uploadAvatar = useCallback(
139
+ (file: File) => action.uploadAvatarAction(ctx, file),
140
+ [ctx]
141
+ );
142
+
138
143
  const memoProvider = useMemo(
139
144
  () => ({
140
145
  ...dauthState,
@@ -145,6 +150,7 @@ export const DauthProvider: React.FC<IDauthProviderProps> = (
145
150
  getPasskeyCredentials,
146
151
  registerPasskey,
147
152
  deletePasskeyCredential,
153
+ uploadAvatar,
148
154
  }),
149
155
  [
150
156
  dauthState,
@@ -155,6 +161,7 @@ export const DauthProvider: React.FC<IDauthProviderProps> = (
155
161
  getPasskeyCredentials,
156
162
  registerPasskey,
157
163
  deletePasskeyCredential,
164
+ uploadAvatar,
158
165
  ]
159
166
  );
160
167
 
@@ -17,6 +17,7 @@ const initialDauthState: IDauthState = {
17
17
  getPasskeyCredentials: () => Promise.resolve([]),
18
18
  registerPasskey: () => Promise.resolve(null),
19
19
  deletePasskeyCredential: () => Promise.resolve(false),
20
+ uploadAvatar: () => Promise.resolve(false),
20
21
  };
21
22
 
22
23
  export default initialDauthState;
package/src/interfaces.ts CHANGED
@@ -89,14 +89,15 @@ export interface IDauthState {
89
89
  deletePasskeyCredential: (
90
90
  credentialId: string
91
91
  ) => Promise<boolean>;
92
+ uploadAvatar: (file: File) => Promise<boolean>;
92
93
  }
93
94
 
94
95
  export interface DauthProfileModalProps {
95
96
  open: boolean;
96
97
  onClose: () => void;
97
- /** Optional: provide a function to handle avatar upload.
98
+ /** Optional override for avatar upload.
98
99
  * Receives a File, should return the URL string.
99
- * If not provided, the avatar edit button is hidden. */
100
+ * If not provided, uses the built-in upload via the auth proxy. */
100
101
  onAvatarUpload?: (file: File) => Promise<string>;
101
102
  }
102
103
 
@@ -8,6 +8,7 @@ import {
8
8
  startPasskeyRegistrationAPI,
9
9
  finishPasskeyRegistrationAPI,
10
10
  deletePasskeyCredentialAPI,
11
+ uploadAvatarAPI,
11
12
  } from '../api/dauth.api';
12
13
  import type { IPasskeyCredential } from '../api/interfaces/dauth.api.responses';
13
14
  import { createPasskeyCredential } from '../webauthn';
@@ -239,6 +240,36 @@ export async function deletePasskeyCredentialAction(
239
240
  }
240
241
  }
241
242
 
243
+ export async function uploadAvatarAction(
244
+ ctx: ActionContext,
245
+ file: File
246
+ ): Promise<boolean> {
247
+ const { dispatch, authProxyPath, onError } = ctx;
248
+ try {
249
+ const result = await uploadAvatarAPI(authProxyPath, file);
250
+ if (result.response.status === 200) {
251
+ dispatch({
252
+ type: DauthTypes.UPDATE_USER,
253
+ payload: result.data.user,
254
+ });
255
+ return true;
256
+ }
257
+ onError(
258
+ new Error(
259
+ 'Avatar upload error: ' + result.data.message
260
+ )
261
+ );
262
+ return false;
263
+ } catch (error) {
264
+ onError(
265
+ error instanceof Error
266
+ ? error
267
+ : new Error('Avatar upload error')
268
+ );
269
+ return false;
270
+ }
271
+ }
272
+
242
273
  export const resetUser = (dispatch: React.Dispatch<any>) => {
243
274
  return dispatch({
244
275
  type: DauthTypes.LOGIN,