@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/dist/index.js
CHANGED
|
@@ -112,6 +112,28 @@ function forgetAccount(email) {
|
|
|
112
112
|
return next;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Replaces the local copy with the list the worker returned.
|
|
117
|
+
*
|
|
118
|
+
* The worker's list is the one every panel shares, so when it has accounts it
|
|
119
|
+
* wins: an account removed on another panel must not come back from this
|
|
120
|
+
* panel's stale copy. An EMPTY answer does not wipe the local one — it is what
|
|
121
|
+
* a browser that signed in before the shared list existed gets, and those
|
|
122
|
+
* shortcuts are still true.
|
|
123
|
+
*
|
|
124
|
+
* @returns the list to show
|
|
125
|
+
*/
|
|
126
|
+
function adoptRecentAccounts(remote) {
|
|
127
|
+
if (!Array.isArray(remote) || remote.length === 0) return listRecentAccounts();
|
|
128
|
+
const next = remote.filter(account => account && typeof account.email === 'string' && account.email.includes('@')).map(account => ({
|
|
129
|
+
email: normalizeEmail(account.email),
|
|
130
|
+
method: typeof account.method === 'string' && account.method ? account.method : 'code',
|
|
131
|
+
lastUsedAt: Number(account.lastUsedAt) || 0
|
|
132
|
+
})).sort((a, b) => b.lastUsedAt - a.lastUsedAt).slice(0, MAX_RECENT_ACCOUNTS);
|
|
133
|
+
writeRecentAccounts(next);
|
|
134
|
+
return next;
|
|
135
|
+
}
|
|
136
|
+
|
|
115
137
|
/** Records the provider this tab is leaving for. */
|
|
116
138
|
function markSocialDeparture(provider) {
|
|
117
139
|
try {
|
|
@@ -693,6 +715,64 @@ const updateProfile = async data => {
|
|
|
693
715
|
});
|
|
694
716
|
};
|
|
695
717
|
|
|
718
|
+
/*--- Recent accounts ------------------------------------------------------*/
|
|
719
|
+
|
|
720
|
+
/*
|
|
721
|
+
* The server's copy of this browser's sign-in shortcuts.
|
|
722
|
+
*
|
|
723
|
+
* `localStorage` is per origin, so a list kept only there stayed on the panel
|
|
724
|
+
* where the person signed in. The worker keeps it in an HttpOnly cookie on its
|
|
725
|
+
* own host, which every panel of the application reaches — and answers only to
|
|
726
|
+
* the origins the application allows (`routes/recent-accounts.js`).
|
|
727
|
+
*
|
|
728
|
+
* All three fail soft: the shortcuts are a convenience, and a network error or
|
|
729
|
+
* an origin the worker refuses must never cost the sign-in. `null` means "no
|
|
730
|
+
* answer", which the screen reads as "keep what you have".
|
|
731
|
+
*/
|
|
732
|
+
|
|
733
|
+
/** The list for this application, or `null` when the worker did not answer. */
|
|
734
|
+
const fetchRecentAccounts = async () => {
|
|
735
|
+
try {
|
|
736
|
+
const response = await api('/auth/recent-accounts');
|
|
737
|
+
return Array.isArray(response?.items) ? response.items : null;
|
|
738
|
+
} catch {
|
|
739
|
+
return null;
|
|
740
|
+
}
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Records the account of the CURRENT session. The worker reads the email from
|
|
745
|
+
* the session, never from here. `keepalive` because the screen is usually
|
|
746
|
+
* navigating away at this very moment.
|
|
747
|
+
*/
|
|
748
|
+
const saveRecentAccount = async (method = 'code') => {
|
|
749
|
+
try {
|
|
750
|
+
const response = await api('/auth/recent-accounts', {
|
|
751
|
+
method: 'POST',
|
|
752
|
+
body: JSON.stringify({
|
|
753
|
+
method
|
|
754
|
+
}),
|
|
755
|
+
keepalive: true
|
|
756
|
+
});
|
|
757
|
+
return Array.isArray(response?.items) ? response.items : null;
|
|
758
|
+
} catch {
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
/** Takes one account off this browser's list, on every panel. */
|
|
764
|
+
const deleteRecentAccount = async email => {
|
|
765
|
+
try {
|
|
766
|
+
await api(`/auth/recent-accounts/${encodeURIComponent(email)}`, {
|
|
767
|
+
method: 'DELETE',
|
|
768
|
+
keepalive: true
|
|
769
|
+
});
|
|
770
|
+
return true;
|
|
771
|
+
} catch {
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
774
|
+
};
|
|
775
|
+
|
|
696
776
|
/*--- Social sign-in -------------------------------------------------------*/
|
|
697
777
|
|
|
698
778
|
/**
|
|
@@ -801,6 +881,9 @@ const consumeSocialToken = () => {
|
|
|
801
881
|
if (provider) {
|
|
802
882
|
const email = decodeJWT(token)?.email;
|
|
803
883
|
if (email) rememberAccount(email, provider);
|
|
884
|
+
// The shared copy, so the other panels learn it too. Not awaited: the
|
|
885
|
+
// token is already stored, and the sign-in must not wait on a shortcut.
|
|
886
|
+
saveRecentAccount(provider);
|
|
804
887
|
}
|
|
805
888
|
params.delete('token');
|
|
806
889
|
const rest = params.toString();
|
|
@@ -2784,6 +2867,32 @@ function SignIn({
|
|
|
2784
2867
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- redirectOrigins enters through the serialized key above
|
|
2785
2868
|
}, [authLoading, user, authenticatedRedirect, handleRedirect, redirectOriginsKey, navigate]);
|
|
2786
2869
|
|
|
2870
|
+
/*
|
|
2871
|
+
* The shared list, from the worker.
|
|
2872
|
+
*
|
|
2873
|
+
* The local copy renders at once; this replaces it when the answer
|
|
2874
|
+
* arrives. That is what makes an account used on the Auth panel appear
|
|
2875
|
+
* on Hoster: `localStorage` never crosses between the two origins.
|
|
2876
|
+
*
|
|
2877
|
+
* If the person has already started typing an email, the list does not
|
|
2878
|
+
* yank the form away from under them — it only feeds the "Contas salvas"
|
|
2879
|
+
* link, one click away.
|
|
2880
|
+
*/
|
|
2881
|
+
react.useEffect(() => {
|
|
2882
|
+
if (!recentAccounts) return;
|
|
2883
|
+
let isActive = true;
|
|
2884
|
+
fetchRecentAccounts().then(remote => {
|
|
2885
|
+
if (!isActive || remote === null) return;
|
|
2886
|
+
const next = adoptRecentAccounts(remote);
|
|
2887
|
+
if (form$1.isDirty()) setIsChoosingOther(true);
|
|
2888
|
+
setAccounts(next);
|
|
2889
|
+
});
|
|
2890
|
+
return () => {
|
|
2891
|
+
isActive = false;
|
|
2892
|
+
};
|
|
2893
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- once per mount, like the local read
|
|
2894
|
+
}, [recentAccounts]);
|
|
2895
|
+
|
|
2787
2896
|
// Step 1 — ask for the code.
|
|
2788
2897
|
const handleRequest = async values => {
|
|
2789
2898
|
if (sending) return false;
|
|
@@ -2843,6 +2952,9 @@ function SignIn({
|
|
|
2843
2952
|
const handleForget = account => {
|
|
2844
2953
|
const next = forgetAccount(account.email);
|
|
2845
2954
|
setAccounts(next);
|
|
2955
|
+
// On every panel, not just this one. The local removal above already
|
|
2956
|
+
// took it off this screen, so a failure here costs nothing visible.
|
|
2957
|
+
deleteRecentAccount(account.email);
|
|
2846
2958
|
if (next.length === 0) setIsManaging(false);
|
|
2847
2959
|
};
|
|
2848
2960
|
|
|
@@ -2871,7 +2983,10 @@ function SignIn({
|
|
|
2871
2983
|
// Only now, with a session: an email that never received a valid
|
|
2872
2984
|
// code never becomes a suggestion. Written before the redirect,
|
|
2873
2985
|
// which may unmount this screen.
|
|
2874
|
-
if (recentAccounts)
|
|
2986
|
+
if (recentAccounts) {
|
|
2987
|
+
setAccounts(rememberAccount(sentTo, 'code'));
|
|
2988
|
+
saveRecentAccount('code');
|
|
2989
|
+
}
|
|
2875
2990
|
const target = handleRedirect ? getRedirectFromLocation(redirectOrigins) : null;
|
|
2876
2991
|
if (target) applyRedirect(target, navigate);
|
|
2877
2992
|
onSuccess?.(result?.user ?? null, {
|
|
@@ -4121,6 +4236,7 @@ exports.TOKEN_STORAGE_KEY = TOKEN_STORAGE_KEY;
|
|
|
4121
4236
|
exports.UserInformation = UserInformation;
|
|
4122
4237
|
exports.UserProfile = UserProfile;
|
|
4123
4238
|
exports.Wordmark = Wordmark;
|
|
4239
|
+
exports.adoptRecentAccounts = adoptRecentAccounts;
|
|
4124
4240
|
exports.announceIdentityChange = announceIdentityChange;
|
|
4125
4241
|
exports.applyRedirect = applyRedirect;
|
|
4126
4242
|
exports.clearIdentitySwitching = clearIdentitySwitching;
|
|
@@ -4128,7 +4244,9 @@ exports.configure = configure;
|
|
|
4128
4244
|
exports.consumeSocialError = consumeSocialError;
|
|
4129
4245
|
exports.consumeSocialToken = consumeSocialToken;
|
|
4130
4246
|
exports.decodeJWT = decodeJWT;
|
|
4247
|
+
exports.deleteRecentAccount = deleteRecentAccount;
|
|
4131
4248
|
exports.endImpersonation = endImpersonation;
|
|
4249
|
+
exports.fetchRecentAccounts = fetchRecentAccounts;
|
|
4132
4250
|
exports.forgetAccount = forgetAccount;
|
|
4133
4251
|
exports.getApiUrl = getApiUrl;
|
|
4134
4252
|
exports.getApplicationInfo = getApplicationInfo;
|
|
@@ -4150,6 +4268,7 @@ exports.requestCode = requestCode;
|
|
|
4150
4268
|
exports.resolveRedirect = resolveRedirect;
|
|
4151
4269
|
exports.revokeOtherSessions = revokeOtherSessions;
|
|
4152
4270
|
exports.revokeSession = revokeSession;
|
|
4271
|
+
exports.saveRecentAccount = saveRecentAccount;
|
|
4153
4272
|
exports.setStoredToken = setStoredToken;
|
|
4154
4273
|
exports.shouldSignOutOn401 = shouldSignOutOn401;
|
|
4155
4274
|
exports.signOut = signOut;
|