auth.ts (hoppscotch-2.2.1) | : | auth.ts (hoppscotch-3.0.0) | ||
---|---|---|---|---|
import { | import { | |||
User, | User, | |||
getAuth, | getAuth, | |||
onAuthStateChanged, | onAuthStateChanged, | |||
onIdTokenChanged, | onIdTokenChanged, | |||
signInWithPopup, | signInWithPopup, | |||
GoogleAuthProvider, | GoogleAuthProvider, | |||
GithubAuthProvider, | GithubAuthProvider, | |||
OAuthProvider, | ||||
signInWithEmailAndPassword as signInWithEmailAndPass, | signInWithEmailAndPassword as signInWithEmailAndPass, | |||
isSignInWithEmailLink as isSignInWithEmailLinkFB, | isSignInWithEmailLink as isSignInWithEmailLinkFB, | |||
fetchSignInMethodsForEmail, | fetchSignInMethodsForEmail, | |||
sendSignInLinkToEmail, | sendSignInLinkToEmail, | |||
signInWithEmailLink as signInWithEmailLinkFB, | signInWithEmailLink as signInWithEmailLinkFB, | |||
ActionCodeSettings, | ActionCodeSettings, | |||
signOut, | signOut, | |||
linkWithCredential, | linkWithCredential, | |||
AuthCredential, | AuthCredential, | |||
AuthError, | ||||
UserCredential, | UserCredential, | |||
updateProfile, | updateProfile, | |||
updateEmail, | updateEmail, | |||
sendEmailVerification, | sendEmailVerification, | |||
reauthenticateWithCredential, | reauthenticateWithCredential, | |||
} from "firebase/auth" | } from "firebase/auth" | |||
import { | import { | |||
onSnapshot, | onSnapshot, | |||
getFirestore, | getFirestore, | |||
setDoc, | setDoc, | |||
doc, | doc, | |||
updateDoc, | updateDoc, | |||
} from "firebase/firestore" | } from "firebase/firestore" | |||
import { | import { BehaviorSubject, filter, Subject, Subscription } from "rxjs" | |||
BehaviorSubject, | ||||
distinctUntilChanged, | ||||
filter, | ||||
map, | ||||
Subject, | ||||
Subscription, | ||||
} from "rxjs" | ||||
import { onBeforeUnmount, onMounted } from "@nuxtjs/composition-api" | ||||
import { | import { | |||
setLocalConfig, | setLocalConfig, | |||
getLocalConfig, | getLocalConfig, | |||
removeLocalConfig, | removeLocalConfig, | |||
} from "~/newstore/localpersistence" | } from "~/newstore/localpersistence" | |||
export type HoppUser = User & { | export type HoppUser = User & { | |||
provider?: string | provider?: string | |||
accessToken?: string | accessToken?: string | |||
} | } | |||
type AuthEvents = | export type AuthEvent = | |||
| { event: "probable_login"; user: HoppUser } // We have previous login state, but the app is waiting for authentication | | { event: "probable_login"; user: HoppUser } // We have previous login state, but the app is waiting for authentication | |||
| { event: "login"; user: HoppUser } // We are authenticated | | { event: "login"; user: HoppUser } // We are authenticated | |||
| { event: "logout" } // No authentication and we have no previous state | | { event: "logout" } // No authentication and we have no previous state | |||
| { event: "authTokenUpdate"; user: HoppUser; newToken: string | null } // Tok en has been updated | | { event: "authTokenUpdate"; user: HoppUser; newToken: string | null } // Tok en has been updated | |||
/** | /** | |||
* A BehaviorSubject emitting the currently logged in user (or null if not logge d in) | * A BehaviorSubject emitting the currently logged in user (or null if not logge d in) | |||
*/ | */ | |||
export const currentUser$ = new BehaviorSubject<HoppUser | null>(null) | export const currentUser$ = new BehaviorSubject<HoppUser | null>(null) | |||
/** | /** | |||
* A BehaviorSubject emitting the current idToken | * A BehaviorSubject emitting the current idToken | |||
*/ | */ | |||
export const authIdToken$ = new BehaviorSubject<string | null>(null) | export const authIdToken$ = new BehaviorSubject<string | null>(null) | |||
/** | /** | |||
* A subject that emits events related to authentication flows | * A subject that emits events related to authentication flows | |||
*/ | */ | |||
export const authEvents$ = new Subject<AuthEvents>() | export const authEvents$ = new Subject<AuthEvent>() | |||
/** | /** | |||
* Like currentUser$ but also gives probable user value | * Like currentUser$ but also gives probable user value | |||
*/ | */ | |||
export const probableUser$ = new BehaviorSubject<HoppUser | null>(null) | export const probableUser$ = new BehaviorSubject<HoppUser | null>(null) | |||
/** | /** | |||
* Resolves when the probable login resolves into proper login | * Resolves when the probable login resolves into proper login | |||
*/ | */ | |||
export const waitProbableLoginToConfirm = () => | export const waitProbableLoginToConfirm = () => | |||
new Promise<void>((resolve, reject) => { | new Promise<void>((resolve, reject) => { | |||
if (authIdToken$.value) resolve() | if (authIdToken$.value) resolve() | |||
if (!probableUser$.value) reject(new Error("no_probable_user")) | if (!probableUser$.value) reject(new Error("no_probable_user")) | |||
const sub = authIdToken$.pipe(filter((token) => !!token)).subscribe(() => { | let sub: Subscription | null = null | |||
sub = authIdToken$.pipe(filter((token) => !!token)).subscribe(() => { | ||||
sub?.unsubscribe() | sub?.unsubscribe() | |||
resolve() | resolve() | |||
}) | }) | |||
}) | }) | |||
/** | /** | |||
* Initializes the firebase authentication related subjects | * Initializes the firebase authentication related subjects | |||
*/ | */ | |||
export function initAuth() { | export function initAuth() { | |||
const auth = getAuth() | const auth = getAuth() | |||
skipping to change at line 158 | skipping to change at line 154 | |||
currentUser$.next(userUpdate) | currentUser$.next(userUpdate) | |||
} | } | |||
) | ) | |||
} | } | |||
currentUser$.next(user) | currentUser$.next(user) | |||
// User wasn't found before, but now is there (login happened) | // User wasn't found before, but now is there (login happened) | |||
if (!wasLoggedIn && user) { | if (!wasLoggedIn && user) { | |||
authEvents$.next({ | authEvents$.next({ | |||
event: "login", | event: "login", | |||
user: currentUser$.value!!, | user: currentUser$.value!, | |||
}) | }) | |||
} else if (wasLoggedIn && !user) { | } else if (wasLoggedIn && !user) { | |||
// User was found before, but now is not there (logout happened) | // User was found before, but now is not there (logout happened) | |||
authEvents$.next({ | authEvents$.next({ | |||
event: "logout", | event: "logout", | |||
}) | }) | |||
} | } | |||
}) | }) | |||
onIdTokenChanged(auth, async (user) => { | onIdTokenChanged(auth, async (user) => { | |||
if (user) { | if (user) { | |||
authIdToken$.next(await user.getIdToken()) | authIdToken$.next(await user.getIdToken()) | |||
authEvents$.next({ | authEvents$.next({ | |||
event: "authTokenUpdate", | event: "authTokenUpdate", | |||
newToken: authIdToken$.value, | newToken: authIdToken$.value, | |||
user: currentUser$.value!!, // Force not-null because user is defined | user: currentUser$.value!, // Force not-null because user is defined | |||
}) | }) | |||
setLocalConfig("login_state", JSON.stringify(user)) | setLocalConfig("login_state", JSON.stringify(user)) | |||
} else { | } else { | |||
authIdToken$.next(null) | authIdToken$.next(null) | |||
} | } | |||
}) | }) | |||
} | } | |||
export function getAuthIDToken(): string | null { | export function getAuthIDToken(): string | null { | |||
skipping to change at line 207 | skipping to change at line 203 | |||
* Sign user in with a popup using Github | * Sign user in with a popup using Github | |||
*/ | */ | |||
export async function signInUserWithGithub() { | export async function signInUserWithGithub() { | |||
return await signInWithPopup( | return await signInWithPopup( | |||
getAuth(), | getAuth(), | |||
new GithubAuthProvider().addScope("gist") | new GithubAuthProvider().addScope("gist") | |||
) | ) | |||
} | } | |||
/** | /** | |||
* Sign user in with a popup using Microsoft | ||||
*/ | ||||
export async function signInUserWithMicrosoft() { | ||||
return await signInWithPopup(getAuth(), new OAuthProvider("microsoft.com")) | ||||
} | ||||
/** | ||||
* Sign user in with email and password | * Sign user in with email and password | |||
*/ | */ | |||
export async function signInWithEmailAndPassword( | export async function signInWithEmailAndPassword( | |||
email: string, | email: string, | |||
password: string | password: string | |||
) { | ) { | |||
return await signInWithEmailAndPass(getAuth(), email, password) | return await signInWithEmailAndPass(getAuth(), email, password) | |||
} | } | |||
/** | /** | |||
skipping to change at line 235 | skipping to change at line 238 | |||
} | } | |||
export async function linkWithFBCredential( | export async function linkWithFBCredential( | |||
user: User, | user: User, | |||
credential: AuthCredential | credential: AuthCredential | |||
) { | ) { | |||
return await linkWithCredential(user, credential) | return await linkWithCredential(user, credential) | |||
} | } | |||
/** | /** | |||
* Links account with another account given in a auth/account-exists-with-differ | ||||
ent-credential error | ||||
* | ||||
* @param user - User who has the errors | ||||
* | ||||
* @param error - Error caught after trying to login | ||||
* | ||||
* @returns Promise of UserCredential | ||||
*/ | ||||
export async function linkWithFBCredentialFromAuthError( | ||||
user: User, | ||||
error: unknown | ||||
) { | ||||
// Marked as not null since this function is supposed to be called after an au | ||||
th/account-exists-with-different-credential error, ie credentials actually exist | ||||
const credentials = OAuthProvider.credentialFromError(error as AuthError)! | ||||
return await linkWithCredential(user, credentials) | ||||
} | ||||
/** | ||||
* Sends an email with the signin link to the user | * Sends an email with the signin link to the user | |||
* | * | |||
* @param email - Email to send the email to | * @param email - Email to send the email to | |||
* @param actionCodeSettings - The settings to apply to the link | * @param actionCodeSettings - The settings to apply to the link | |||
*/ | */ | |||
export async function signInWithEmail( | export async function signInWithEmail( | |||
email: string, | email: string, | |||
actionCodeSettings: ActionCodeSettings | actionCodeSettings: ActionCodeSettings | |||
) { | ) { | |||
return await sendSignInLinkToEmail(getAuth(), email, actionCodeSettings) | return await sendSignInLinkToEmail(getAuth(), email, actionCodeSettings) | |||
skipping to change at line 366 | skipping to change at line 387 | |||
async function reauthenticateUser() { | async function reauthenticateUser() { | |||
if (!currentUser$.value) throw new Error("No user has logged in") | if (!currentUser$.value) throw new Error("No user has logged in") | |||
const currentAuthMethod = currentUser$.value.provider | const currentAuthMethod = currentUser$.value.provider | |||
let credential | let credential | |||
if (currentAuthMethod === "google.com") { | if (currentAuthMethod === "google.com") { | |||
const result = await signInUserWithGithub() | const result = await signInUserWithGithub() | |||
credential = GithubAuthProvider.credentialFromResult(result) | credential = GithubAuthProvider.credentialFromResult(result) | |||
} else if (currentAuthMethod === "github.com") { | } else if (currentAuthMethod === "github.com") { | |||
const result = await signInUserWithGoogle() | const result = await signInUserWithGoogle() | |||
credential = GoogleAuthProvider.credentialFromResult(result) | credential = GoogleAuthProvider.credentialFromResult(result) | |||
} else if (currentAuthMethod === "microsoft.com") { | ||||
const result = await signInUserWithMicrosoft() | ||||
credential = OAuthProvider.credentialFromResult(result) | ||||
} else if (currentAuthMethod === "password") { | } else if (currentAuthMethod === "password") { | |||
const email = prompt( | const email = prompt( | |||
"Reauthenticate your account using your current email:" | "Reauthenticate your account using your current email:" | |||
) | ) | |||
const actionCodeSettings = { | const actionCodeSettings = { | |||
url: `${process.env.BASE_URL}/enter`, | url: `${process.env.BASE_URL}/enter`, | |||
handleCodeInApp: true, | handleCodeInApp: true, | |||
} | } | |||
await signInWithEmail(email as string, actionCodeSettings) | await signInWithEmail(email as string, actionCodeSettings) | |||
.then(() => | .then(() => | |||
skipping to change at line 400 | skipping to change at line 424 | |||
) | ) | |||
} catch (e) { | } catch (e) { | |||
console.error("error updating", e) | console.error("error updating", e) | |||
throw e | throw e | |||
} | } | |||
} | } | |||
export function getGithubCredentialFromResult(result: UserCredential) { | export function getGithubCredentialFromResult(result: UserCredential) { | |||
return GithubAuthProvider.credentialFromResult(result) | return GithubAuthProvider.credentialFromResult(result) | |||
} | } | |||
/** | ||||
* A Vue composable function that is called when the auth status | ||||
* is being updated to being logged in (fired multiple times), | ||||
* this is also called on component mount if the login | ||||
* was already resolved before mount. | ||||
*/ | ||||
export function onLoggedIn(exec: (user: HoppUser) => void) { | ||||
let sub: Subscription | null = null | ||||
onMounted(() => { | ||||
sub = currentUser$ | ||||
.pipe( | ||||
map((user) => !!user), // Get a logged in status (true or false) | ||||
distinctUntilChanged(), // Don't propagate unless the status updates | ||||
filter((x) => x) // Don't propagate unless it is logged in | ||||
) | ||||
.subscribe(() => { | ||||
exec(currentUser$.value!) | ||||
}) | ||||
}) | ||||
onBeforeUnmount(() => { | ||||
sub?.unsubscribe() | ||||
}) | ||||
} | ||||
End of changes. 12 change blocks. | ||||
14 lines changed or deleted | 40 lines changed or added |