mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-12-27 18:38:37 +08:00
feat(authentication): add signin with github (fehmer) (#5239)
This commit is contained in:
parent
a6ccb2cead
commit
7635d37848
5 changed files with 226 additions and 56 deletions
|
|
@ -77,10 +77,10 @@
|
|||
<i class="fab fa-google"></i>
|
||||
Google Sign In
|
||||
</button>
|
||||
<!-- <div class="button signInWithGitHub">
|
||||
<button class="signInWithGitHub">
|
||||
<i class="fab fa-github"></i>
|
||||
GitHub Sign In
|
||||
</div> -->
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1635,6 +1635,21 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section githubAuthSettings needsAccount hidden">
|
||||
<div class="groupTitle">
|
||||
<i class="fab fa-github"></i>
|
||||
<span>github authentication settings</span>
|
||||
</div>
|
||||
<div class="text">Add or remove GitHub authentication.</div>
|
||||
<div class="buttons vertical">
|
||||
<button class="danger" id="addGithubAuth">
|
||||
add github authentication
|
||||
</button>
|
||||
<button class="danger" id="removeGithubAuth">
|
||||
remove github authentication
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section revokeAllTokens">
|
||||
<div class="groupTitle">
|
||||
<i class="fas fa-user-slash"></i>
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
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<void> {
|
|||
});
|
||||
}
|
||||
|
||||
async function signInWithGoogle(): Promise<void> {
|
||||
async function signInWithProvider(provider: AuthProvider): Promise<void> {
|
||||
if (Auth === undefined) {
|
||||
Notifications.add("Authentication uninitialized", -1, {
|
||||
duration: 3,
|
||||
|
|
@ -382,7 +387,7 @@ async function signInWithGoogle(): Promise<void> {
|
|||
: 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<void> {
|
|||
} 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<void> {
|
|||
});
|
||||
}
|
||||
|
||||
async function signInWithGoogle(): Promise<void> {
|
||||
return signInWithProvider(gmailProvider);
|
||||
}
|
||||
|
||||
async function signInWithGitHub(): Promise<void> {
|
||||
return signInWithProvider(githubProvider);
|
||||
}
|
||||
|
||||
async function addGoogleAuth(): Promise<void> {
|
||||
return addAuthProvider("Google", gmailProvider);
|
||||
}
|
||||
|
||||
async function addGithubAuth(): Promise<void> {
|
||||
return addAuthProvider("GitHub", githubProvider);
|
||||
}
|
||||
|
||||
async function addAuthProvider(
|
||||
providerName: string,
|
||||
provider: AuthProvider
|
||||
): Promise<void> {
|
||||
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<void> {
|
|||
}
|
||||
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, {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ type PopupKey =
|
|||
| "updateName"
|
||||
| "updatePassword"
|
||||
| "removeGoogleAuth"
|
||||
| "removeGithubAuth"
|
||||
| "addPasswordAuth"
|
||||
| "deleteAccount"
|
||||
| "resetAccount"
|
||||
|
|
@ -86,6 +87,7 @@ const list: Record<PopupKey, SimplePopup | undefined> = {
|
|||
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<ReauthSuccess | ReauthFailed> {
|
||||
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<ExecReturn> => {
|
||||
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<ExecReturn> => {
|
||||
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<ExecReturn> => {
|
||||
const reauth = await reauthenticate("passwordFirst", pass);
|
||||
execFn: async (_thisPopup, password, newName): Promise<ExecReturn> => {
|
||||
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<ExecReturn> => {
|
||||
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<ExecReturn> => {
|
||||
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<ExecReturn> => {
|
||||
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<ExecReturn> => {
|
||||
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<ExecReturn> => {
|
||||
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<ExecReturn> => {
|
||||
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");
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue