@ciromaciel/auth-react 1.2.0 → 1.3.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 CHANGED
@@ -121,18 +121,23 @@ email form.
121
121
  - An account is saved only after a session exists: the code confirmed, or the provider's token
122
122
  back. A mistyped email never becomes a suggestion.
123
123
  - The list keeps the five most recent accounts. Each entry holds the email, the way in and the
124
- time of the last sign-in. It lives in `localStorage` under `auth:recent-accounts`, so each
125
- origin keeps its own list.
126
- - Signing out keeps the list. "Gerenciar" on the screen removes an account; nothing else about
127
- that account changes.
124
+ time of the last sign-in.
125
+ - Every app of the same application shares the list, even across origins: the Auth worker keeps
126
+ it in an HttpOnly cookie on its own host and answers only to the origins the application
127
+ allows. A copy in `localStorage` (`auth:recent-accounts`) renders at once while the shared list
128
+ loads, and stands in when the worker cannot be reached.
129
+ - Signing out keeps the list. "Gerenciar" on the screen removes an account from every app of the
130
+ application; nothing else about that account changes.
128
131
 
129
132
  ```jsx
130
133
  <SignIn /> {/* shows and saves recent accounts */}
131
134
  <SignIn recentAccounts={false} /> {/* shared computers: neither shows nor saves */}
132
135
  ```
133
136
 
134
- Building your own screen? `listRecentAccounts()`, `rememberAccount(email, method)` and
135
- `forgetAccount(email)` read and write the same list.
137
+ Building your own screen? `fetchRecentAccounts()`, `saveRecentAccount(method)` and
138
+ `deleteRecentAccount(email)` talk to the shared list; `listRecentAccounts()`,
139
+ `rememberAccount(email, method)`, `forgetAccount(email)` and `adoptRecentAccounts(list)` manage
140
+ the local copy.
136
141
 
137
142
  ## API
138
143
 
package/dist/index.esm.js CHANGED
@@ -110,6 +110,28 @@ function forgetAccount(email) {
110
110
  return next;
111
111
  }
112
112
 
113
+ /**
114
+ * Replaces the local copy with the list the worker returned.
115
+ *
116
+ * The worker's list is the one every panel shares, so when it has accounts it
117
+ * wins: an account removed on another panel must not come back from this
118
+ * panel's stale copy. An EMPTY answer does not wipe the local one — it is what
119
+ * a browser that signed in before the shared list existed gets, and those
120
+ * shortcuts are still true.
121
+ *
122
+ * @returns the list to show
123
+ */
124
+ function adoptRecentAccounts(remote) {
125
+ if (!Array.isArray(remote) || remote.length === 0) return listRecentAccounts();
126
+ const next = remote.filter(account => account && typeof account.email === 'string' && account.email.includes('@')).map(account => ({
127
+ email: normalizeEmail(account.email),
128
+ method: typeof account.method === 'string' && account.method ? account.method : 'code',
129
+ lastUsedAt: Number(account.lastUsedAt) || 0
130
+ })).sort((a, b) => b.lastUsedAt - a.lastUsedAt).slice(0, MAX_RECENT_ACCOUNTS);
131
+ writeRecentAccounts(next);
132
+ return next;
133
+ }
134
+
113
135
  /** Records the provider this tab is leaving for. */
114
136
  function markSocialDeparture(provider) {
115
137
  try {
@@ -691,6 +713,64 @@ const updateProfile = async data => {
691
713
  });
692
714
  };
693
715
 
716
+ /*--- Recent accounts ------------------------------------------------------*/
717
+
718
+ /*
719
+ * The server's copy of this browser's sign-in shortcuts.
720
+ *
721
+ * `localStorage` is per origin, so a list kept only there stayed on the panel
722
+ * where the person signed in. The worker keeps it in an HttpOnly cookie on its
723
+ * own host, which every panel of the application reaches — and answers only to
724
+ * the origins the application allows (`routes/recent-accounts.js`).
725
+ *
726
+ * All three fail soft: the shortcuts are a convenience, and a network error or
727
+ * an origin the worker refuses must never cost the sign-in. `null` means "no
728
+ * answer", which the screen reads as "keep what you have".
729
+ */
730
+
731
+ /** The list for this application, or `null` when the worker did not answer. */
732
+ const fetchRecentAccounts = async () => {
733
+ try {
734
+ const response = await api('/auth/recent-accounts');
735
+ return Array.isArray(response?.items) ? response.items : null;
736
+ } catch {
737
+ return null;
738
+ }
739
+ };
740
+
741
+ /**
742
+ * Records the account of the CURRENT session. The worker reads the email from
743
+ * the session, never from here. `keepalive` because the screen is usually
744
+ * navigating away at this very moment.
745
+ */
746
+ const saveRecentAccount = async (method = 'code') => {
747
+ try {
748
+ const response = await api('/auth/recent-accounts', {
749
+ method: 'POST',
750
+ body: JSON.stringify({
751
+ method
752
+ }),
753
+ keepalive: true
754
+ });
755
+ return Array.isArray(response?.items) ? response.items : null;
756
+ } catch {
757
+ return null;
758
+ }
759
+ };
760
+
761
+ /** Takes one account off this browser's list, on every panel. */
762
+ const deleteRecentAccount = async email => {
763
+ try {
764
+ await api(`/auth/recent-accounts/${encodeURIComponent(email)}`, {
765
+ method: 'DELETE',
766
+ keepalive: true
767
+ });
768
+ return true;
769
+ } catch {
770
+ return false;
771
+ }
772
+ };
773
+
694
774
  /*--- Social sign-in -------------------------------------------------------*/
695
775
 
696
776
  /**
@@ -799,6 +879,9 @@ const consumeSocialToken = () => {
799
879
  if (provider) {
800
880
  const email = decodeJWT(token)?.email;
801
881
  if (email) rememberAccount(email, provider);
882
+ // The shared copy, so the other panels learn it too. Not awaited: the
883
+ // token is already stored, and the sign-in must not wait on a shortcut.
884
+ saveRecentAccount(provider);
802
885
  }
803
886
  params.delete('token');
804
887
  const rest = params.toString();
@@ -2782,6 +2865,32 @@ function SignIn({
2782
2865
  // eslint-disable-next-line react-hooks/exhaustive-deps -- redirectOrigins enters through the serialized key above
2783
2866
  }, [authLoading, user, authenticatedRedirect, handleRedirect, redirectOriginsKey, navigate]);
2784
2867
 
2868
+ /*
2869
+ * The shared list, from the worker.
2870
+ *
2871
+ * The local copy renders at once; this replaces it when the answer
2872
+ * arrives. That is what makes an account used on the Auth panel appear
2873
+ * on Hoster: `localStorage` never crosses between the two origins.
2874
+ *
2875
+ * If the person has already started typing an email, the list does not
2876
+ * yank the form away from under them — it only feeds the "Contas salvas"
2877
+ * link, one click away.
2878
+ */
2879
+ useEffect(() => {
2880
+ if (!recentAccounts) return;
2881
+ let isActive = true;
2882
+ fetchRecentAccounts().then(remote => {
2883
+ if (!isActive || remote === null) return;
2884
+ const next = adoptRecentAccounts(remote);
2885
+ if (form.isDirty()) setIsChoosingOther(true);
2886
+ setAccounts(next);
2887
+ });
2888
+ return () => {
2889
+ isActive = false;
2890
+ };
2891
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- once per mount, like the local read
2892
+ }, [recentAccounts]);
2893
+
2785
2894
  // Step 1 — ask for the code.
2786
2895
  const handleRequest = async values => {
2787
2896
  if (sending) return false;
@@ -2841,6 +2950,9 @@ function SignIn({
2841
2950
  const handleForget = account => {
2842
2951
  const next = forgetAccount(account.email);
2843
2952
  setAccounts(next);
2953
+ // On every panel, not just this one. The local removal above already
2954
+ // took it off this screen, so a failure here costs nothing visible.
2955
+ deleteRecentAccount(account.email);
2844
2956
  if (next.length === 0) setIsManaging(false);
2845
2957
  };
2846
2958
 
@@ -2869,7 +2981,10 @@ function SignIn({
2869
2981
  // Only now, with a session: an email that never received a valid
2870
2982
  // code never becomes a suggestion. Written before the redirect,
2871
2983
  // which may unmount this screen.
2872
- if (recentAccounts) setAccounts(rememberAccount(sentTo, 'code'));
2984
+ if (recentAccounts) {
2985
+ setAccounts(rememberAccount(sentTo, 'code'));
2986
+ saveRecentAccount('code');
2987
+ }
2873
2988
  const target = handleRedirect ? getRedirectFromLocation(redirectOrigins) : null;
2874
2989
  if (target) applyRedirect(target, navigate);
2875
2990
  onSuccess?.(result?.user ?? null, {
@@ -4100,5 +4215,5 @@ function SignOutButton({
4100
4215
  });
4101
4216
  }
4102
4217
 
4103
- export { AuthCard, AuthLoaded, AuthLoading, AuthProvider, GuestOnly, IDENTITY_CHANGED_EVENT, MAX_RECENT_ACCOUNTS, Protect, RECENT_ACCOUNTS_KEY, SignIn, SignInButton, SignOutButton, SignedIn, SignedOut, SocialButtons, TOKEN_STORAGE_KEY, UserInformation, UserProfile, Wordmark, announceIdentityChange, applyRedirect, clearIdentitySwitching, configure, consumeSocialError, consumeSocialToken, decodeJWT, endImpersonation, forgetAccount, getApiUrl, getApplicationInfo, getCurrentUser, getLinkedProviders, getRedirectFromLocation, getSession, getSocialProviders, isAuthenticated, isIdentitySwitching, isInternal, listRecentAccounts, listSessions, markIdentitySwitching, pollCode, refreshToken, rememberAccount, requestCode, resolveRedirect, revokeOtherSessions, revokeSession, setStoredToken, shouldSignOutOn401, signOut, startSocialLink, startSocialSignIn, unlinkSocialProvider, updateProfile, useApplicationLogo, useAuth, useAuthLoading, useAuthStore, useCheckToken, useImpersonation, useSession, useSessions, useSignIn, useSignOut, useUser, verifyCode };
4218
+ export { AuthCard, AuthLoaded, AuthLoading, AuthProvider, GuestOnly, IDENTITY_CHANGED_EVENT, MAX_RECENT_ACCOUNTS, Protect, RECENT_ACCOUNTS_KEY, SignIn, SignInButton, SignOutButton, SignedIn, SignedOut, SocialButtons, TOKEN_STORAGE_KEY, UserInformation, UserProfile, Wordmark, adoptRecentAccounts, announceIdentityChange, applyRedirect, clearIdentitySwitching, configure, consumeSocialError, consumeSocialToken, decodeJWT, deleteRecentAccount, endImpersonation, fetchRecentAccounts, forgetAccount, getApiUrl, getApplicationInfo, getCurrentUser, getLinkedProviders, getRedirectFromLocation, getSession, getSocialProviders, isAuthenticated, isIdentitySwitching, isInternal, listRecentAccounts, listSessions, markIdentitySwitching, pollCode, refreshToken, rememberAccount, requestCode, resolveRedirect, revokeOtherSessions, revokeSession, saveRecentAccount, setStoredToken, shouldSignOutOn401, signOut, startSocialLink, startSocialSignIn, unlinkSocialProvider, updateProfile, useApplicationLogo, useAuth, useAuthLoading, useAuthStore, useCheckToken, useImpersonation, useSession, useSessions, useSignIn, useSignOut, useUser, verifyCode };
4104
4219
  //# sourceMappingURL=index.esm.js.map