auth.ts (hoppscotch-2.0.0) | : | auth.ts (hoppscotch-2.1.0) | ||
---|---|---|---|---|
import firebase from "firebase/app" | import { | |||
import "firebase/firestore" | User, | |||
import "firebase/auth" | getAuth, | |||
onAuthStateChanged, | ||||
onIdTokenChanged, | ||||
signInWithPopup, | ||||
GoogleAuthProvider, | ||||
GithubAuthProvider, | ||||
signInWithEmailAndPassword as signInWithEmailAndPass, | ||||
isSignInWithEmailLink as isSignInWithEmailLinkFB, | ||||
fetchSignInMethodsForEmail, | ||||
sendSignInLinkToEmail, | ||||
signInWithEmailLink as signInWithEmailLinkFB, | ||||
ActionCodeSettings, | ||||
signOut, | ||||
linkWithCredential, | ||||
AuthCredential, | ||||
UserCredential, | ||||
updateProfile, | ||||
} from "firebase/auth" | ||||
import { | ||||
onSnapshot, | ||||
getFirestore, | ||||
setDoc, | ||||
doc, | ||||
updateDoc, | ||||
} from "firebase/firestore" | ||||
import { | import { | |||
BehaviorSubject, | BehaviorSubject, | |||
distinctUntilChanged, | distinctUntilChanged, | |||
filter, | filter, | |||
map, | map, | |||
Subject, | Subject, | |||
Subscription, | Subscription, | |||
} from "rxjs" | } from "rxjs" | |||
import { onBeforeUnmount, onMounted } from "@nuxtjs/composition-api" | import { onBeforeUnmount, onMounted } from "@nuxtjs/composition-api" | |||
import { | ||||
setLocalConfig, | ||||
getLocalConfig, | ||||
removeLocalConfig, | ||||
} from "~/newstore/localpersistence" | ||||
export type HoppUser = firebase.User & { | export type HoppUser = User & { | |||
provider?: string | provider?: string | |||
accessToken?: string | accessToken?: string | |||
} | } | |||
type AuthEvents = | type AuthEvents = | |||
| { event: "login"; user: HoppUser } | | { event: "probable_login"; user: HoppUser } // We have previous login state, | |||
| { event: "logout" } | but the app is waiting for authentication | |||
| { event: "authTokenUpdate"; user: HoppUser; newToken: string | null } | | { event: "login"; user: HoppUser } // We are authenticated | |||
| { event: "logout" } // No authentication and we have no previous state | ||||
| { 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<AuthEvents>() | |||
/** | /** | |||
* Like currentUser$ but also gives probable user value | ||||
*/ | ||||
export const probableUser$ = new BehaviorSubject<HoppUser | null>(null) | ||||
/** | ||||
* Resolves when the probable login resolves into proper login | ||||
*/ | ||||
export const waitProbableLoginToConfirm = () => | ||||
new Promise<void>((resolve, reject) => { | ||||
if (authIdToken$.value) resolve() | ||||
if (!probableUser$.value) reject(new Error("no_probable_user")) | ||||
const sub = authIdToken$.pipe(filter((token) => !!token)).subscribe(() => { | ||||
sub?.unsubscribe() | ||||
resolve() | ||||
}) | ||||
}) | ||||
/** | ||||
* Initializes the firebase authentication related subjects | * Initializes the firebase authentication related subjects | |||
*/ | */ | |||
export function initAuth() { | export function initAuth() { | |||
const auth = getAuth() | ||||
const firestore = getFirestore() | ||||
let extraSnapshotStop: (() => void) | null = null | let extraSnapshotStop: (() => void) | null = null | |||
firebase.auth().onAuthStateChanged((user) => { | probableUser$.next(JSON.parse(getLocalConfig("login_state") ?? "null")) | |||
onAuthStateChanged(auth, (user) => { | ||||
/** Whether the user was logged in before */ | /** Whether the user was logged in before */ | |||
const wasLoggedIn = currentUser$.value !== null | const wasLoggedIn = currentUser$.value !== null | |||
if (user) { | ||||
probableUser$.next(user) | ||||
} else { | ||||
probableUser$.next(null) | ||||
removeLocalConfig("login_state") | ||||
} | ||||
if (!user && extraSnapshotStop) { | if (!user && extraSnapshotStop) { | |||
extraSnapshotStop() | extraSnapshotStop() | |||
extraSnapshotStop = null | extraSnapshotStop = null | |||
} else if (user) { | } else if (user) { | |||
// Merge all the user info from all the authenticated providers | // Merge all the user info from all the authenticated providers | |||
user.providerData.forEach((profile) => { | user.providerData.forEach((profile) => { | |||
if (!profile) return | if (!profile) return | |||
const us = { | const us = { | |||
updatedOn: new Date(), | updatedOn: new Date(), | |||
provider: profile.providerId, | provider: profile.providerId, | |||
name: profile.displayName, | name: profile.displayName, | |||
email: profile.email, | email: profile.email, | |||
photoUrl: profile.photoURL, | photoUrl: profile.photoURL, | |||
uid: profile.uid, | uid: profile.uid, | |||
} | } | |||
firebase | setDoc(doc(firestore, "users", user.uid), us, { merge: true }).catch( | |||
.firestore() | (e) => console.error("error updating", us, e) | |||
.collection("users") | ) | |||
.doc(user.uid) | ||||
.set(us, { merge: true }) | ||||
.catch((e) => console.error("error updating", us, e)) | ||||
}) | }) | |||
extraSnapshotStop = firebase | extraSnapshotStop = onSnapshot( | |||
.firestore() | doc(firestore, "users", user.uid), | |||
.collection("users") | (doc) => { | |||
.doc(user.uid) | ||||
.onSnapshot((doc) => { | ||||
const data = doc.data() | const data = doc.data() | |||
const userUpdate: HoppUser = user | const userUpdate: HoppUser = user | |||
if (data) { | if (data) { | |||
// Write extra provider data | // Write extra provider data | |||
userUpdate.provider = data.provider | userUpdate.provider = data.provider | |||
userUpdate.accessToken = data.accessToken | userUpdate.accessToken = data.accessToken | |||
} | } | |||
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", | |||
}) | }) | |||
} | } | |||
}) | }) | |||
firebase.auth().onIdTokenChanged(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)) | ||||
} else { | } else { | |||
authIdToken$.next(null) | authIdToken$.next(null) | |||
} | } | |||
}) | }) | |||
} | } | |||
export function getAuthIDToken(): string | null { | ||||
return authIdToken$.getValue() | ||||
} | ||||
/** | /** | |||
* Sign user in with a popup using Google | * Sign user in with a popup using Google | |||
*/ | */ | |||
export async function signInUserWithGoogle() { | export async function signInUserWithGoogle() { | |||
return await firebase | return await signInWithPopup(getAuth(), new GoogleAuthProvider()) | |||
.auth() | ||||
.signInWithPopup(new firebase.auth.GoogleAuthProvider()) | ||||
} | } | |||
/** | /** | |||
* 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 firebase | return await signInWithPopup( | |||
.auth() | getAuth(), | |||
.signInWithPopup(new firebase.auth.GithubAuthProvider().addScope("gist")) | new GithubAuthProvider().addScope("gist") | |||
) | ||||
} | } | |||
/** | /** | |||
* 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 firebase.auth().signInWithEmailAndPassword(email, password) | return await signInWithEmailAndPass(getAuth(), email, password) | |||
} | } | |||
/** | /** | |||
* Gets the sign in methods for a given email address | * Gets the sign in methods for a given email address | |||
* | * | |||
* @param email - Email to get the methods of | * @param email - Email to get the methods of | |||
* | * | |||
* @returns Promise for string array of the auth provider methods accessible | * @returns Promise for string array of the auth provider methods accessible | |||
*/ | */ | |||
export async function getSignInMethodsForEmail(email: string) { | export async function getSignInMethodsForEmail(email: string) { | |||
return await firebase.auth().fetchSignInMethodsForEmail(email) | return await fetchSignInMethodsForEmail(getAuth(), email) | |||
} | ||||
export async function linkWithFBCredential( | ||||
user: User, | ||||
credential: AuthCredential | ||||
) { | ||||
return await linkWithCredential(user, credential) | ||||
} | } | |||
/** | /** | |||
* 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: firebase.auth.ActionCodeSettings | actionCodeSettings: ActionCodeSettings | |||
) { | ) { | |||
return await firebase.auth().sendSignInLinkToEmail(email, actionCodeSettings) | return await sendSignInLinkToEmail(getAuth(), email, actionCodeSettings) | |||
} | } | |||
/** | /** | |||
* Checks and returns whether the sign in link is an email link | * Checks and returns whether the sign in link is an email link | |||
* | * | |||
* @param url - The URL to look in | * @param url - The URL to look in | |||
*/ | */ | |||
export function isSignInWithEmailLink(url: string) { | export function isSignInWithEmailLink(url: string) { | |||
return firebase.auth().isSignInWithEmailLink(url) | return isSignInWithEmailLinkFB(getAuth(), url) | |||
} | } | |||
/** | /** | |||
* Sends an email with sign in with email link | * Sends an email with sign in with email link | |||
* | * | |||
* @param email - Email to log in to | * @param email - Email to log in to | |||
* @param url - The action URL which is used to validate login | * @param url - The action URL which is used to validate login | |||
*/ | */ | |||
export async function signInWithEmailLink(email: string, url: string) { | export async function signInWithEmailLink(email: string, url: string) { | |||
return await firebase.auth().signInWithEmailLink(email, url) | return await signInWithEmailLinkFB(getAuth(), email, url) | |||
} | } | |||
/** | /** | |||
* Signs out the user | * Signs out the user | |||
*/ | */ | |||
export async function signOutUser() { | export async function signOutUser() { | |||
if (!currentUser$.value) throw new Error("No user has logged in") | if (!currentUser$.value) throw new Error("No user has logged in") | |||
await firebase.auth().signOut() | await signOut(getAuth()) | |||
} | } | |||
/** | /** | |||
* Sets the provider id and relevant provider auth token | * Sets the provider id and relevant provider auth token | |||
* as user metadata | * as user metadata | |||
* | * | |||
* @param id - The provider ID | * @param id - The provider ID | |||
* @param token - The relevant auth token for the given provider | * @param token - The relevant auth token for the given provider | |||
*/ | */ | |||
export async function setProviderInfo(id: string, token: string) { | export async function setProviderInfo(id: string, token: string) { | |||
if (!currentUser$.value) throw new Error("No user has logged in") | if (!currentUser$.value) throw new Error("No user has logged in") | |||
const us = { | const us = { | |||
updatedOn: new Date(), | updatedOn: new Date(), | |||
provider: id, | provider: id, | |||
accessToken: token, | accessToken: token, | |||
} | } | |||
try { | try { | |||
await firebase | await updateDoc( | |||
.firestore() | doc(getFirestore(), "users", currentUser$.value.uid), | |||
.collection("users") | us | |||
.doc(currentUser$.value.uid) | ).catch((e) => console.error("error updating", us, e)) | |||
.update(us) | ||||
.catch((e) => console.error("error updating", us, e)) | ||||
} catch (e) { | } catch (e) { | |||
console.error("error updating", e) | console.error("error updating", e) | |||
throw e | throw e | |||
} | } | |||
} | } | |||
/** | /** | |||
* Sets the user's display name | ||||
* | ||||
* @param name - The new display name | ||||
*/ | ||||
export async function setDisplayName(name: string) { | ||||
if (!currentUser$.value) throw new Error("No user has logged in") | ||||
const us = { | ||||
displayName: name, | ||||
} | ||||
try { | ||||
await updateProfile(currentUser$.value, us).catch((e) => | ||||
console.error("error updating", us, e) | ||||
) | ||||
} catch (e) { | ||||
console.error("error updating", e) | ||||
throw e | ||||
} | ||||
} | ||||
export function getGithubCredentialFromResult(result: UserCredential) { | ||||
return GithubAuthProvider.credentialFromResult(result) | ||||
} | ||||
/** | ||||
* A Vue composable function that is called when the auth status | * A Vue composable function that is called when the auth status | |||
* is being updated to being logged in (fired multiple times), | * is being updated to being logged in (fired multiple times), | |||
* this is also called on component mount if the login | * this is also called on component mount if the login | |||
* was already resolved before mount. | * was already resolved before mount. | |||
*/ | */ | |||
export function onLoggedIn(exec: (user: HoppUser) => void) { | export function onLoggedIn(exec: (user: HoppUser) => void) { | |||
let sub: Subscription | null = null | let sub: Subscription | null = null | |||
onMounted(() => { | onMounted(() => { | |||
sub = currentUser$ | sub = currentUser$ | |||
End of changes. 25 change blocks. | ||||
40 lines changed or deleted | 136 lines changed or added |