Skip to content

Commit e838f71

Browse files
fehmerMiodechottekbyseif21
authored
fix: authentication issues when using multiple tabs (@fehmer) (monkeytypegame#6790)
fixes monkeytypegame#6279 - store the last use "remember login" state in localstorage - initialize firebase auth with correct persistence (LOCAL if "remember me" is set, SESSION otherwise) initialization of `Auth` needs to by awaited. This required some refactoring. During debugging it was useful to have easier control over the `Auth` object. Summary of the refactoring: - don't expose firebase `App` or `Auth` (except for email-handler) - initialise firebase in async method that can be awaited to ensure setup is done before any call to firebase - move `authStateChanged` handling from account-controller to our firebase module which then calls `account-controller.readyFunction`. - update all direct calls to `Auth` to use functions of our firebase module - move error handling and interpretation of `FirebaseError` to our module and removed duplicate code - use tryCatch helper on refactored code instead of native `try... catch` --------- Co-authored-by: Miodec <[email protected]> Co-authored-by: Lukas <[email protected]> Co-authored-by: Seif Soliman <[email protected]>
1 parent 541f29c commit e838f71

18 files changed

+731
-521
lines changed

frontend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@
9595
"color-blend": "4.0.0",
9696
"damerau-levenshtein": "1.0.8",
9797
"date-fns": "3.6.0",
98-
"firebase": "10.12.4",
98+
"firebase": "12.0.0",
9999
"hangul-js": "0.2.6",
100100
"howler": "2.2.3",
101101
"html2canvas": "1.4.1",

frontend/src/email-handler.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@
166166
</body>
167167
<script type="module">
168168
import $ from "jquery";
169-
import { Auth } from "./ts/firebase";
169+
import { _Auth as Auth } from "./ts/firebase";
170170
import {
171171
applyActionCode,
172172
verifyPasswordResetCode,

frontend/src/ts/ape/adapters/ts-rest-adapter.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ import {
44
tsRestFetchApi,
55
type ApiFetcherArgs,
66
} from "@ts-rest/core";
7-
import { getIdToken } from "firebase/auth";
87
import { envConfig } from "../../constants/env-config";
9-
import { getAuthenticatedUser, isAuthenticated } from "../../firebase";
8+
import { getIdToken } from "../../firebase";
109
import {
1110
COMPATIBILITY_CHECK,
1211
COMPATIBILITY_CHECK_HEADER,
@@ -30,8 +29,8 @@ function buildApi(timeout: number): (args: ApiFetcherArgs) => Promise<{
3029
}> {
3130
return async (request: ApiFetcherArgs) => {
3231
try {
33-
if (isAuthenticated()) {
34-
const token = await getIdToken(getAuthenticatedUser());
32+
const token = await getIdToken();
33+
if (token !== null) {
3534
request.headers["Authorization"] = `Bearer ${token}`;
3635
}
3736

frontend/src/ts/controllers/account-controller.ts

Lines changed: 69 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -22,38 +22,38 @@ import * as AccountSettings from "../pages/account-settings";
2222
import {
2323
GoogleAuthProvider,
2424
GithubAuthProvider,
25-
browserSessionPersistence,
26-
browserLocalPersistence,
27-
createUserWithEmailAndPassword,
28-
signInWithEmailAndPassword,
29-
signInWithPopup,
30-
setPersistence,
3125
updateProfile,
3226
linkWithPopup,
33-
getAdditionalUserInfo,
3427
User as UserType,
35-
Unsubscribe,
3628
AuthProvider,
3729
} from "firebase/auth";
38-
import { Auth, getAuthenticatedUser, isAuthenticated } from "../firebase";
39-
import { dispatch as dispatchSignUpEvent } from "../observables/google-sign-up-event";
30+
import {
31+
isAuthAvailable,
32+
getAuthenticatedUser,
33+
isAuthenticated,
34+
signOut as authSignOut,
35+
signInWithEmailAndPassword,
36+
createUserWithEmailAndPassword,
37+
signInWithPopup,
38+
resetIgnoreAuthCallback,
39+
} from "../firebase";
4040
import {
4141
hideFavoriteQuoteLength,
4242
showFavoriteQuoteLength,
4343
} from "../test/test-config";
4444
import * as ConnectionState from "../states/connection";
4545
import { navigate } from "./route-controller";
46-
import { FirebaseError } from "firebase/app";
4746
import * as PSA from "../elements/psa";
4847
import { getActiveFunboxesWithFunction } from "../test/funbox/list";
4948
import { Snapshot } from "../constants/default-snapshot";
5049
import * as Sentry from "../sentry";
50+
import { tryCatch } from "@monkeytype/util/trycatch";
5151

5252
export const gmailProvider = new GoogleAuthProvider();
5353
export const githubProvider = new GithubAuthProvider();
5454

5555
async function sendVerificationEmail(): Promise<void> {
56-
if (Auth === undefined) {
56+
if (!isAuthAvailable()) {
5757
Notifications.add("Authentication uninitialized", -1, {
5858
duration: 3,
5959
});
@@ -200,7 +200,7 @@ export async function loadUser(_user: UserType): Promise<void> {
200200
}
201201
}
202202

203-
async function readyFunction(
203+
export async function onAuthStateChanged(
204204
authInitialisedAndConnected: boolean,
205205
user: UserType | null
206206
): Promise<void> {
@@ -217,10 +217,14 @@ async function readyFunction(
217217
if (window.location.pathname === "/account") {
218218
window.history.replaceState("", "", "/login");
219219
}
220+
220221
Settings.hideAccountSection();
221222
AccountButton.update(undefined);
222223
DB.setSnapshot(undefined);
223224
Sentry.clearUser();
225+
setTimeout(() => {
226+
hideFavoriteQuoteLength();
227+
}, 125);
224228
PageTransition.set(false);
225229
navigate();
226230
}
@@ -242,20 +246,8 @@ async function readyFunction(
242246
AccountSettings.updateUI();
243247
}
244248

245-
let disableAuthListener: Unsubscribe;
246-
247-
if (Auth && ConnectionState.get()) {
248-
disableAuthListener = Auth?.onAuthStateChanged(function (user) {
249-
void readyFunction(true, user);
250-
});
251-
} else {
252-
$((): void => {
253-
void readyFunction(false, null);
254-
});
255-
}
256-
257249
export async function signIn(email: string, password: string): Promise<void> {
258-
if (Auth === undefined) {
250+
if (!isAuthAvailable()) {
259251
Notifications.add("Authentication uninitialized", -1);
260252
return;
261253
}
@@ -266,7 +258,6 @@ export async function signIn(email: string, password: string): Promise<void> {
266258
return;
267259
}
268260

269-
disableAuthListener();
270261
LoginPage.showPreloader();
271262
LoginPage.disableInputs();
272263
LoginPage.disableSignUpButton();
@@ -279,45 +270,25 @@ export async function signIn(email: string, password: string): Promise<void> {
279270
return;
280271
}
281272

282-
const persistence = ($(".pageLogin .login #rememberMe input").prop(
273+
const rememberMe = $(".pageLogin .login #rememberMe input").prop(
283274
"checked"
284-
) as boolean)
285-
? browserLocalPersistence
286-
: browserSessionPersistence;
287-
288-
await setPersistence(Auth, persistence);
289-
return signInWithEmailAndPassword(Auth, email, password)
290-
.then(async (e) => {
291-
await loadUser(e.user);
292-
})
293-
.catch(function (error: unknown) {
294-
console.error(error);
295-
let message = Misc.createErrorMessage(
296-
error,
297-
"Failed to sign in with email and password"
298-
);
299-
if (error instanceof FirebaseError) {
300-
if (error.code === "auth/wrong-password") {
301-
message = "Incorrect password";
302-
} else if (error.code === "auth/user-not-found") {
303-
message = "User not found";
304-
} else if (error.code === "auth/invalid-email") {
305-
message =
306-
"Invalid email format (make sure you are using your email to login - not your username)";
307-
} else if (error.code === "auth/invalid-credential") {
308-
message =
309-
"Email/password is incorrect or your account does not have password authentication enabled.";
310-
}
311-
}
312-
Notifications.add(message, -1);
313-
LoginPage.hidePreloader();
314-
LoginPage.enableInputs();
315-
LoginPage.updateSignupButton();
316-
});
275+
) as boolean;
276+
277+
const { error } = await tryCatch(
278+
signInWithEmailAndPassword(email, password, rememberMe)
279+
);
280+
281+
if (error !== null) {
282+
Notifications.add(error.message, -1);
283+
LoginPage.hidePreloader();
284+
LoginPage.enableInputs();
285+
LoginPage.updateSignupButton();
286+
return;
287+
}
317288
}
318289

319290
async function signInWithProvider(provider: AuthProvider): Promise<void> {
320-
if (Auth === undefined) {
291+
if (!isAuthAvailable()) {
321292
Notifications.add("Authentication uninitialized", -1, {
322293
duration: 3,
323294
});
@@ -333,61 +304,21 @@ async function signInWithProvider(provider: AuthProvider): Promise<void> {
333304
LoginPage.showPreloader();
334305
LoginPage.disableInputs();
335306
LoginPage.disableSignUpButton();
336-
disableAuthListener();
337-
const persistence = ($(".pageLogin .login #rememberMe input").prop(
307+
const rememberMe = $(".pageLogin .login #rememberMe input").prop(
338308
"checked"
339-
) as boolean)
340-
? browserLocalPersistence
341-
: browserSessionPersistence;
342-
343-
await setPersistence(Auth, persistence);
344-
signInWithPopup(Auth, provider)
345-
.then(async (signedInUser) => {
346-
if (getAdditionalUserInfo(signedInUser)?.isNewUser) {
347-
dispatchSignUpEvent(signedInUser, true);
348-
} else {
349-
await loadUser(signedInUser.user);
350-
}
351-
})
352-
.catch((error: unknown) => {
353-
console.log(error);
354-
let message = Misc.createErrorMessage(
355-
error,
356-
"Failed to sign in with popup"
357-
);
358-
if (error instanceof FirebaseError) {
359-
if (error.code === "auth/wrong-password") {
360-
message = "Incorrect password";
361-
} else if (error.code === "auth/user-not-found") {
362-
message = "User not found";
363-
} else if (error.code === "auth/invalid-email") {
364-
message =
365-
"Invalid email format (make sure you are using your email to login - not your username)";
366-
} else if (error.code === "auth/popup-closed-by-user") {
367-
message = "";
368-
// message = "Popup closed by user";
369-
// return;
370-
} else if (error.code === "auth/popup-blocked") {
371-
message =
372-
"Sign in popup was blocked by the browser. Check the address bar for a blocked popup icon, or update your browser settings to allow popups.";
373-
} else if (error.code === "auth/user-cancelled") {
374-
message = "";
375-
// message = "User refused to sign in";
376-
// return;
377-
} else if (
378-
error.code === "auth/account-exists-with-different-credential"
379-
) {
380-
message =
381-
"Account already exists, but its using a different authentication method. Try signing in with a different method";
382-
}
383-
}
384-
if (message !== "") {
385-
Notifications.add(message, -1);
386-
}
387-
LoginPage.hidePreloader();
388-
LoginPage.enableInputs();
389-
LoginPage.updateSignupButton();
390-
});
309+
) as boolean;
310+
311+
const { error } = await tryCatch(signInWithPopup(provider, rememberMe));
312+
313+
if (error !== null) {
314+
if (error.message !== "") {
315+
Notifications.add(error.message, -1);
316+
}
317+
LoginPage.hidePreloader();
318+
LoginPage.enableInputs();
319+
LoginPage.updateSignupButton();
320+
return;
321+
}
391322
}
392323

393324
async function signInWithGoogle(): Promise<void> {
@@ -416,60 +347,43 @@ async function addAuthProvider(
416347
});
417348
return;
418349
}
419-
if (Auth === undefined) {
350+
if (!isAuthAvailable()) {
420351
Notifications.add("Authentication uninitialized", -1, {
421352
duration: 3,
422353
});
423354
return;
424355
}
425356
Loader.show();
426-
if (!isAuthenticated()) return;
427-
linkWithPopup(getAuthenticatedUser(), provider)
428-
.then(function () {
429-
Loader.hide();
430-
Notifications.add(`${providerName} authentication added`, 1);
431-
AccountSettings.updateUI();
432-
})
433-
.catch(function (error: unknown) {
434-
Loader.hide();
435-
const message = Misc.createErrorMessage(
436-
error,
437-
`Failed to add ${providerName} authentication`
438-
);
439-
Notifications.add(message, -1);
440-
});
357+
const user = getAuthenticatedUser();
358+
if (!user) return;
359+
try {
360+
await linkWithPopup(user, provider);
361+
Loader.hide();
362+
Notifications.add(`${providerName} authentication added`, 1);
363+
AccountSettings.updateUI();
364+
} catch (error) {
365+
Loader.hide();
366+
const message = Misc.createErrorMessage(
367+
error,
368+
`Failed to add ${providerName} authentication`
369+
);
370+
Notifications.add(message, -1);
371+
}
441372
}
442373

443374
export function signOut(): void {
444-
if (Auth === undefined) {
375+
if (!isAuthAvailable()) {
445376
Notifications.add("Authentication uninitialized", -1, {
446377
duration: 3,
447378
});
448379
return;
449380
}
450381
if (!isAuthenticated()) return;
451-
Auth.signOut()
452-
.then(function () {
453-
Notifications.add("Signed out", 0, {
454-
duration: 2,
455-
});
456-
Sentry.clearUser();
457-
Settings.hideAccountSection();
458-
AccountButton.update(undefined);
459-
navigate("/login");
460-
DB.setSnapshot(undefined);
461-
setTimeout(() => {
462-
hideFavoriteQuoteLength();
463-
}, 125);
464-
})
465-
.catch(function (error: unknown) {
466-
const message = Misc.createErrorMessage(error, `Failed to sign out`);
467-
Notifications.add(message, -1);
468-
});
382+
void authSignOut();
469383
}
470384

471385
async function signUp(): Promise<void> {
472-
if (Auth === undefined) {
386+
if (!isAuthAvailable()) {
473387
Notifications.add("Authentication uninitialized", -1, {
474388
duration: 3,
475389
});
@@ -551,11 +465,8 @@ async function signUp(): Promise<void> {
551465
return;
552466
}
553467

554-
disableAuthListener();
555-
556468
try {
557469
const createdAuthUser = await createUserWithEmailAndPassword(
558-
Auth,
559470
email,
560471
password
561472
);
@@ -575,7 +486,8 @@ async function signUp(): Promise<void> {
575486
await updateProfile(createdAuthUser.user, { displayName: nname });
576487
await sendVerificationEmail();
577488
LoginPage.hidePreloader();
578-
await loadUser(createdAuthUser.user);
489+
await onAuthStateChanged(true, createdAuthUser.user);
490+
resetIgnoreAuthCallback();
579491

580492
Notifications.add("Account created", 1);
581493
} catch (e) {
@@ -617,7 +529,7 @@ $(".pageLogin .login button.signInWithGitHub").on("click", () => {
617529
});
618530

619531
$("nav .accountButtonAndMenu .menu button.signOut").on("click", () => {
620-
if (Auth === undefined) {
532+
if (!isAuthAvailable()) {
621533
Notifications.add("Authentication uninitialized", -1, {
622534
duration: 3,
623535
});

0 commit comments

Comments
 (0)