diff --git a/frontend/src/ts/controllers/account-controller.ts b/frontend/src/ts/controllers/account-controller.ts
index b9971a7c7169..b81e3d751ee5 100644
--- a/frontend/src/ts/controllers/account-controller.ts
+++ b/frontend/src/ts/controllers/account-controller.ts
@@ -20,6 +20,7 @@ import * as Account from "../pages/account";
import * as Alerts from "../elements/alerts";
import {
GoogleAuthProvider,
+ GithubAuthProvider,
browserSessionPersistence,
browserLocalPersistence,
createUserWithEmailAndPassword,
@@ -31,6 +32,7 @@ import {
getAdditionalUserInfo,
User as UserType,
Unsubscribe,
+ AuthProvider,
} from "firebase/auth";
import { Auth, getAuthenticatedUser, isAuthenticated } from "../firebase";
import { dispatch as dispatchSignUpEvent } from "../observables/google-sign-up-event";
@@ -45,6 +47,7 @@ import { getHtmlByUserFlags } from "./user-flag-controller";
let signedOutThisSession = false;
export const gmailProvider = new GoogleAuthProvider();
+export const githubProvider = new GithubAuthProvider();
async function sendVerificationEmail(): Promise
{
if (Auth === undefined) {
@@ -266,6 +269,8 @@ if (Auth && ConnectionState.get()) {
// ChallengeController.setup(challengeName);
// }, 1000);
}
+
+ Settings.updateAuthSections();
});
} else {
$("nav .signInOut").addClass("hidden");
@@ -357,7 +362,7 @@ async function signIn(): Promise {
});
}
-async function signInWithGoogle(): Promise {
+async function signInWithProvider(provider: AuthProvider): Promise {
if (Auth === undefined) {
Notifications.add("Authentication uninitialized", -1, {
duration: 3,
@@ -382,7 +387,7 @@ async function signInWithGoogle(): Promise {
: browserSessionPersistence;
await setPersistence(Auth, persistence);
- signInWithPopup(Auth, gmailProvider)
+ signInWithPopup(Auth, provider)
.then(async (signedInUser) => {
if (getAdditionalUserInfo(signedInUser)?.isNewUser) {
dispatchSignUpEvent(signedInUser, true);
@@ -405,6 +410,11 @@ async function signInWithGoogle(): Promise {
} else if (error.code === "auth/user-cancelled") {
// message = "User refused to sign in";
return;
+ } else if (
+ error.code === "auth/account-exists-with-different-credential"
+ ) {
+ message =
+ "Account already exists, but its using a different authentication method. Try signing in with a different method";
}
Notifications.add(message, -1);
LoginPage.hidePreloader();
@@ -413,7 +423,32 @@ async function signInWithGoogle(): Promise {
});
}
+async function signInWithGoogle(): Promise {
+ return signInWithProvider(gmailProvider);
+}
+
+async function signInWithGitHub(): Promise {
+ return signInWithProvider(githubProvider);
+}
+
async function addGoogleAuth(): Promise {
+ return addAuthProvider("Google", gmailProvider);
+}
+
+async function addGithubAuth(): Promise {
+ return addAuthProvider("GitHub", githubProvider);
+}
+
+async function addAuthProvider(
+ providerName: string,
+ provider: AuthProvider
+): Promise {
+ if (!ConnectionState.get()) {
+ Notifications.add("You are offline", 0, {
+ duration: 2,
+ });
+ return;
+ }
if (Auth === undefined) {
Notifications.add("Authentication uninitialized", -1, {
duration: 3,
@@ -422,16 +457,16 @@ async function addGoogleAuth(): Promise {
}
Loader.show();
if (!isAuthenticated()) return;
- linkWithPopup(getAuthenticatedUser(), gmailProvider)
+ linkWithPopup(getAuthenticatedUser(), provider)
.then(function () {
Loader.hide();
- Notifications.add("Google authentication added", 1);
+ Notifications.add(`${providerName} authentication added`, 1);
Settings.updateAuthSections();
})
.catch(function (error) {
Loader.hide();
Notifications.add(
- "Failed to add Google authentication: " + error.message,
+ `Failed to add ${providerName} authentication: ` + error.message,
-1
);
});
@@ -620,9 +655,9 @@ $(".pageLogin .login button.signInWithGoogle").on("click", () => {
void signInWithGoogle();
});
-// $(".pageLogin .login .button.signInWithGitHub").on("click",(e) => {
-// signInWithGitHub();
-// });
+$(".pageLogin .login button.signInWithGitHub").on("click", () => {
+ void signInWithGitHub();
+});
$("header .signInOut").on("click", () => {
if (Auth === undefined) {
@@ -645,15 +680,13 @@ $(".pageLogin .register form").on("submit", (e) => {
});
$(".pageSettings #addGoogleAuth").on("click", async () => {
- if (!ConnectionState.get()) {
- Notifications.add("You are offline", 0, {
- duration: 2,
- });
- return;
- }
void addGoogleAuth();
});
+$(".pageSettings #addGithubAuth").on("click", async () => {
+ void addGithubAuth();
+});
+
$(".pageAccount").on("click", ".sendVerificationEmail", () => {
if (!ConnectionState.get()) {
Notifications.add("You are offline", 0, {
diff --git a/frontend/src/ts/pages/settings.ts b/frontend/src/ts/pages/settings.ts
index 944c5dc7f9e7..c295d7f00d57 100644
--- a/frontend/src/ts/pages/settings.ts
+++ b/frontend/src/ts/pages/settings.ts
@@ -734,16 +734,20 @@ export function updateDiscordSection(): void {
export function updateAuthSections(): void {
$(".pageSettings .section.passwordAuthSettings button").addClass("hidden");
$(".pageSettings .section.googleAuthSettings button").addClass("hidden");
+ $(".pageSettings .section.githubAuthSettings button").addClass("hidden");
if (!isAuthenticated()) return;
const user = getAuthenticatedUser();
- const passwordProvider = user.providerData.find(
+ const passwordProvider = user.providerData.some(
(provider) => provider.providerId === "password"
);
- const googleProvider = user.providerData.find(
+ const googleProvider = user.providerData.some(
(provider) => provider.providerId === "google.com"
);
+ const githubProvider = user.providerData.some(
+ (provider) => provider.providerId === "github.com"
+ );
if (passwordProvider) {
$(
@@ -762,7 +766,7 @@ export function updateAuthSections(): void {
$(
".pageSettings .section.googleAuthSettings #removeGoogleAuth"
).removeClass("hidden");
- if (passwordProvider) {
+ if (passwordProvider || githubProvider) {
$(
".pageSettings .section.googleAuthSettings #removeGoogleAuth"
).removeClass("disabled");
@@ -776,6 +780,24 @@ export function updateAuthSections(): void {
"hidden"
);
}
+ if (githubProvider) {
+ $(
+ ".pageSettings .section.githubAuthSettings #removeGithubAuth"
+ ).removeClass("hidden");
+ if (passwordProvider || googleProvider) {
+ $(
+ ".pageSettings .section.githubAuthSettings #removeGithubAuth"
+ ).removeClass("disabled");
+ } else {
+ $(".pageSettings .section.githubAuthSettings #removeGithubAuth").addClass(
+ "disabled"
+ );
+ }
+ } else {
+ $(".pageSettings .section.githubAuthSettings #addGithubAuth").removeClass(
+ "hidden"
+ );
+ }
}
function setActiveFunboxButton(): void {
diff --git a/frontend/src/ts/popups/simple-popups.ts b/frontend/src/ts/popups/simple-popups.ts
index 976a38a983c1..3e841453d27b 100644
--- a/frontend/src/ts/popups/simple-popups.ts
+++ b/frontend/src/ts/popups/simple-popups.ts
@@ -59,6 +59,7 @@ type PopupKey =
| "updateName"
| "updatePassword"
| "removeGoogleAuth"
+ | "removeGithubAuth"
| "addPasswordAuth"
| "deleteAccount"
| "resetAccount"
@@ -86,6 +87,7 @@ const list: Record = {
updateName: undefined,
updatePassword: undefined,
removeGoogleAuth: undefined,
+ removeGithubAuth: undefined,
addPasswordAuth: undefined,
deleteAccount: undefined,
resetAccount: undefined,
@@ -372,7 +374,7 @@ function hide(): void {
}
}
-type ReauthMethod = "passwordOnly" | "passwordFirst";
+type AuthMethod = "password" | "github.com" | "google.com";
type ReauthSuccess = {
status: 1;
@@ -385,9 +387,44 @@ type ReauthFailed = {
message: string;
};
+type ReauthenticateOptions = {
+ excludeMethod?: AuthMethod;
+ password?: string;
+};
+
+function getPreferredAuthenticationMethod(
+ exclude?: AuthMethod
+): AuthMethod | undefined {
+ const authMethods = ["password", "github.com", "google.com"] as AuthMethod[];
+ const filteredMethods = authMethods.filter((it) => it !== exclude);
+ for (const method of filteredMethods) {
+ if (isUsingAuthentication(method)) return method;
+ }
+ return undefined;
+}
+
+function isUsingPasswordAuthentication(): boolean {
+ return isUsingAuthentication("password");
+}
+
+function isUsingGithubAuthentication(): boolean {
+ return isUsingAuthentication("github.com");
+}
+
+function isUsingGoogleAuthentication(): boolean {
+ return isUsingAuthentication("google.com");
+}
+
+function isUsingAuthentication(authProvider: AuthMethod): boolean {
+ return (
+ Auth?.currentUser?.providerData.some(
+ (p) => p.providerId === authProvider
+ ) || false
+ );
+}
+
async function reauthenticate(
- method: ReauthMethod,
- password: string
+ options: ReauthenticateOptions
): Promise {
if (Auth === undefined) {
return {
@@ -403,28 +440,35 @@ async function reauthenticate(
};
}
const user = getAuthenticatedUser();
+ const authMethod = getPreferredAuthenticationMethod(options.excludeMethod);
try {
- const passwordAuthEnabled = user.providerData.some(
- (p) => p?.providerId === "password"
- );
-
- if (!passwordAuthEnabled && method === "passwordOnly") {
+ if (authMethod === undefined) {
return {
status: -1,
message:
- "Failed to reauthenticate in password only mode: password authentication is not enabled on this account",
+ "Failed to reauthenticate: there is no valid authentication present on the account.",
};
}
- if (passwordAuthEnabled) {
+ if (authMethod === "password") {
+ if (options.password === undefined) {
+ return {
+ status: -1,
+ message: "Failed to reauthenticate using password: password missing.",
+ };
+ }
const credential = EmailAuthProvider.credential(
user.email as string,
- password
+ options.password
);
await reauthenticateWithCredential(user, credential);
- } else if (method === "passwordFirst") {
- await reauthenticateWithPopup(user, AccountController.gmailProvider);
+ } else {
+ const authProvider =
+ authMethod === "github.com"
+ ? AccountController.githubProvider
+ : AccountController.gmailProvider;
+ await reauthenticateWithPopup(user, authProvider);
}
return {
@@ -484,7 +528,7 @@ list.updateEmail = new SimplePopup({
};
}
- const reauth = await reauthenticate("passwordOnly", password);
+ const reauth = await reauthenticate({ password });
if (reauth.status !== 1) {
return {
status: reauth.status,
@@ -535,7 +579,10 @@ list.removeGoogleAuth = new SimplePopup({
onlineOnly: true,
buttonText: "remove",
execFn: async (_thisPopup, password): Promise => {
- const reauth = await reauthenticate("passwordOnly", password);
+ const reauth = await reauthenticate({
+ password,
+ excludeMethod: "google.com",
+ });
if (reauth.status !== 1) {
return {
status: reauth.status,
@@ -565,8 +612,65 @@ list.removeGoogleAuth = new SimplePopup({
if (!isAuthenticated()) return;
if (!isUsingPasswordAuthentication()) {
thisPopup.inputs = [];
- thisPopup.buttonText = "";
- thisPopup.text = "Password authentication is not enabled";
+ if (!isUsingGithubAuthentication()) {
+ thisPopup.buttonText = "";
+ thisPopup.text = "Password or GitHub authentication is not enabled";
+ }
+ }
+ },
+});
+
+list.removeGithubAuth = new SimplePopup({
+ id: "removeGithubAuth",
+ type: "text",
+ title: "Remove GitHub authentication",
+ inputs: [
+ {
+ placeholder: "Password",
+ type: "password",
+ initVal: "",
+ },
+ ],
+ onlineOnly: true,
+ buttonText: "remove",
+ execFn: async (_thisPopup, password): Promise => {
+ const reauth = await reauthenticate({
+ password,
+ excludeMethod: "github.com",
+ });
+ if (reauth.status !== 1) {
+ return {
+ status: reauth.status,
+ message: reauth.message,
+ };
+ }
+
+ try {
+ await unlink(reauth.user, "github.com");
+ } catch (e) {
+ const message = createErrorMessage(e, "Failed to unlink GitHub account");
+ return {
+ status: -1,
+ message,
+ };
+ }
+
+ Settings.updateAuthSections();
+
+ reloadAfter(3);
+ return {
+ status: 1,
+ message: "GitHub authentication removed",
+ };
+ },
+ beforeInitFn: (thisPopup): void => {
+ if (!isAuthenticated()) return;
+ if (!isUsingPasswordAuthentication()) {
+ thisPopup.inputs = [];
+ if (!isUsingGoogleAuthentication()) {
+ thisPopup.buttonText = "";
+ thisPopup.text = "Password or Google authentication is not enabled";
+ }
}
},
});
@@ -589,8 +693,8 @@ list.updateName = new SimplePopup({
],
buttonText: "update",
onlineOnly: true,
- execFn: async (_thisPopup, pass, newName): Promise => {
- const reauth = await reauthenticate("passwordFirst", pass);
+ execFn: async (_thisPopup, password, newName): Promise => {
+ const reauth = await reauthenticate({ password });
if (reauth.status !== 1) {
return {
status: reauth.status,
@@ -700,7 +804,7 @@ list.updatePassword = new SimplePopup({
};
}
- const reauth = await reauthenticate("passwordOnly", previousPass);
+ const reauth = await reauthenticate({ password: previousPass });
if (reauth.status !== 1) {
return {
status: reauth.status,
@@ -767,7 +871,7 @@ list.addPasswordAuth = new SimplePopup({
_thisPopup,
email,
emailConfirm,
- pass,
+ password,
passConfirm
): Promise => {
if (email !== emailConfirm) {
@@ -777,14 +881,14 @@ list.addPasswordAuth = new SimplePopup({
};
}
- if (pass !== passConfirm) {
+ if (password !== passConfirm) {
return {
status: 0,
message: "Passwords don't match",
};
}
- const reauth = await reauthenticate("passwordFirst", pass);
+ const reauth = await reauthenticate({ password });
if (reauth.status !== 1) {
return {
status: reauth.status,
@@ -793,7 +897,7 @@ list.addPasswordAuth = new SimplePopup({
}
try {
- const credential = EmailAuthProvider.credential(email, pass);
+ const credential = EmailAuthProvider.credential(email, password);
await linkWithCredential(reauth.user, credential);
} catch (e) {
const message = createErrorMessage(
@@ -842,7 +946,7 @@ list.deleteAccount = new SimplePopup({
buttonText: "delete",
onlineOnly: true,
execFn: async (_thisPopup, password): Promise => {
- const reauth = await reauthenticate("passwordFirst", password);
+ const reauth = await reauthenticate({ password });
if (reauth.status !== 1) {
return {
status: reauth.status,
@@ -891,7 +995,7 @@ list.resetAccount = new SimplePopup({
buttonText: "reset",
onlineOnly: true,
execFn: async (_thisPopup, password): Promise => {
- const reauth = await reauthenticate("passwordFirst", password);
+ const reauth = await reauthenticate({ password });
if (reauth.status !== 1) {
return {
status: reauth.status,
@@ -942,7 +1046,7 @@ list.optOutOfLeaderboards = new SimplePopup({
buttonText: "opt out",
onlineOnly: true,
execFn: async (_thisPopup, password): Promise => {
- const reauth = await reauthenticate("passwordFirst", password);
+ const reauth = await reauthenticate({ password });
if (reauth.status !== 1) {
return {
status: reauth.status,
@@ -1049,7 +1153,7 @@ list.resetPersonalBests = new SimplePopup({
buttonText: "reset",
onlineOnly: true,
execFn: async (_thisPopup, password): Promise => {
- const reauth = await reauthenticate("passwordFirst", password);
+ const reauth = await reauthenticate({ password });
if (reauth.status !== 1) {
return {
status: reauth.status,
@@ -1126,7 +1230,7 @@ list.revokeAllTokens = new SimplePopup({
buttonText: "revoke all",
onlineOnly: true,
execFn: async (_thisPopup, password): Promise => {
- const reauth = await reauthenticate("passwordFirst", password);
+ const reauth = await reauthenticate({ password });
if (reauth.status !== 1) {
return {
status: reauth.status,
@@ -1541,14 +1645,6 @@ list.forgotPassword = new SimplePopup({
},
});
-function isUsingPasswordAuthentication(): boolean {
- return (
- Auth?.currentUser?.providerData.find(
- (p) => p?.providerId === "password"
- ) !== undefined
- );
-}
-
export function showPopup(
key: PopupKey,
showParams = [] as string[],
@@ -1582,6 +1678,10 @@ $(".pageSettings #removeGoogleAuth").on("click", () => {
showPopup("removeGoogleAuth");
});
+$(".pageSettings #removeGithubAuth").on("click", () => {
+ showPopup("removeGithubAuth");
+});
+
$("#resetSettingsButton").on("click", () => {
showPopup("resetSettings");
});