@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 +11 -6
- package/dist/index.esm.js +117 -2
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +120 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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? `
|
|
135
|
-
`
|
|
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)
|
|
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
|