auth.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import {
  2. User,
  3. getAuth,
  4. onAuthStateChanged,
  5. onIdTokenChanged,
  6. signInWithPopup,
  7. GoogleAuthProvider,
  8. GithubAuthProvider,
  9. signInWithEmailAndPassword as signInWithEmailAndPass,
  10. isSignInWithEmailLink as isSignInWithEmailLinkFB,
  11. fetchSignInMethodsForEmail,
  12. sendSignInLinkToEmail,
  13. signInWithEmailLink as signInWithEmailLinkFB,
  14. ActionCodeSettings,
  15. signOut,
  16. linkWithCredential,
  17. AuthCredential,
  18. UserCredential,
  19. } from "firebase/auth"
  20. import {
  21. onSnapshot,
  22. getFirestore,
  23. setDoc,
  24. doc,
  25. updateDoc,
  26. } from "firebase/firestore"
  27. import {
  28. BehaviorSubject,
  29. distinctUntilChanged,
  30. filter,
  31. map,
  32. Subject,
  33. Subscription,
  34. } from "rxjs"
  35. import { onBeforeUnmount, onMounted } from "@nuxtjs/composition-api"
  36. export type HoppUser = User & {
  37. provider?: string
  38. accessToken?: string
  39. }
  40. type AuthEvents =
  41. | { event: "login"; user: HoppUser }
  42. | { event: "logout" }
  43. | { event: "authTokenUpdate"; user: HoppUser; newToken: string | null }
  44. /**
  45. * A BehaviorSubject emitting the currently logged in user (or null if not logged in)
  46. */
  47. export const currentUser$ = new BehaviorSubject<HoppUser | null>(null)
  48. /**
  49. * A BehaviorSubject emitting the current idToken
  50. */
  51. export const authIdToken$ = new BehaviorSubject<string | null>(null)
  52. /**
  53. * A subject that emits events related to authentication flows
  54. */
  55. export const authEvents$ = new Subject<AuthEvents>()
  56. /**
  57. * Initializes the firebase authentication related subjects
  58. */
  59. export function initAuth() {
  60. const auth = getAuth()
  61. const firestore = getFirestore()
  62. let extraSnapshotStop: (() => void) | null = null
  63. onAuthStateChanged(auth, (user) => {
  64. /** Whether the user was logged in before */
  65. const wasLoggedIn = currentUser$.value !== null
  66. if (!user && extraSnapshotStop) {
  67. extraSnapshotStop()
  68. extraSnapshotStop = null
  69. } else if (user) {
  70. // Merge all the user info from all the authenticated providers
  71. user.providerData.forEach((profile) => {
  72. if (!profile) return
  73. const us = {
  74. updatedOn: new Date(),
  75. provider: profile.providerId,
  76. name: profile.displayName,
  77. email: profile.email,
  78. photoUrl: profile.photoURL,
  79. uid: profile.uid,
  80. }
  81. setDoc(doc(firestore, "users", user.uid), us, { merge: true }).catch(
  82. (e) => console.error("error updating", us, e)
  83. )
  84. })
  85. extraSnapshotStop = onSnapshot(
  86. doc(firestore, "users", user.uid),
  87. (doc) => {
  88. const data = doc.data()
  89. const userUpdate: HoppUser = user
  90. if (data) {
  91. // Write extra provider data
  92. userUpdate.provider = data.provider
  93. userUpdate.accessToken = data.accessToken
  94. }
  95. currentUser$.next(userUpdate)
  96. }
  97. )
  98. }
  99. currentUser$.next(user)
  100. // User wasn't found before, but now is there (login happened)
  101. if (!wasLoggedIn && user) {
  102. authEvents$.next({
  103. event: "login",
  104. user: currentUser$.value!!,
  105. })
  106. } else if (wasLoggedIn && !user) {
  107. // User was found before, but now is not there (logout happened)
  108. authEvents$.next({
  109. event: "logout",
  110. })
  111. }
  112. })
  113. onIdTokenChanged(auth, async (user) => {
  114. if (user) {
  115. authIdToken$.next(await user.getIdToken())
  116. authEvents$.next({
  117. event: "authTokenUpdate",
  118. newToken: authIdToken$.value,
  119. user: currentUser$.value!!, // Force not-null because user is defined
  120. })
  121. } else {
  122. authIdToken$.next(null)
  123. }
  124. })
  125. }
  126. /**
  127. * Sign user in with a popup using Google
  128. */
  129. export async function signInUserWithGoogle() {
  130. return await signInWithPopup(getAuth(), new GoogleAuthProvider())
  131. }
  132. /**
  133. * Sign user in with a popup using Github
  134. */
  135. export async function signInUserWithGithub() {
  136. return await signInWithPopup(
  137. getAuth(),
  138. new GithubAuthProvider().addScope("gist")
  139. )
  140. }
  141. /**
  142. * Sign user in with email and password
  143. */
  144. export async function signInWithEmailAndPassword(
  145. email: string,
  146. password: string
  147. ) {
  148. return await signInWithEmailAndPass(getAuth(), email, password)
  149. }
  150. /**
  151. * Gets the sign in methods for a given email address
  152. *
  153. * @param email - Email to get the methods of
  154. *
  155. * @returns Promise for string array of the auth provider methods accessible
  156. */
  157. export async function getSignInMethodsForEmail(email: string) {
  158. return await fetchSignInMethodsForEmail(getAuth(), email)
  159. }
  160. export async function linkWithFBCredential(
  161. user: User,
  162. credential: AuthCredential
  163. ) {
  164. return await linkWithCredential(user, credential)
  165. }
  166. /**
  167. * Sends an email with the signin link to the user
  168. *
  169. * @param email - Email to send the email to
  170. * @param actionCodeSettings - The settings to apply to the link
  171. */
  172. export async function signInWithEmail(
  173. email: string,
  174. actionCodeSettings: ActionCodeSettings
  175. ) {
  176. return await sendSignInLinkToEmail(getAuth(), email, actionCodeSettings)
  177. }
  178. /**
  179. * Checks and returns whether the sign in link is an email link
  180. *
  181. * @param url - The URL to look in
  182. */
  183. export function isSignInWithEmailLink(url: string) {
  184. return isSignInWithEmailLinkFB(getAuth(), url)
  185. }
  186. /**
  187. * Sends an email with sign in with email link
  188. *
  189. * @param email - Email to log in to
  190. * @param url - The action URL which is used to validate login
  191. */
  192. export async function signInWithEmailLink(email: string, url: string) {
  193. return await signInWithEmailLinkFB(getAuth(), email, url)
  194. }
  195. /**
  196. * Signs out the user
  197. */
  198. export async function signOutUser() {
  199. if (!currentUser$.value) throw new Error("No user has logged in")
  200. await signOut(getAuth())
  201. }
  202. /**
  203. * Sets the provider id and relevant provider auth token
  204. * as user metadata
  205. *
  206. * @param id - The provider ID
  207. * @param token - The relevant auth token for the given provider
  208. */
  209. export async function setProviderInfo(id: string, token: string) {
  210. if (!currentUser$.value) throw new Error("No user has logged in")
  211. const us = {
  212. updatedOn: new Date(),
  213. provider: id,
  214. accessToken: token,
  215. }
  216. try {
  217. await updateDoc(
  218. doc(getFirestore(), "users", currentUser$.value.uid),
  219. us
  220. ).catch((e) => console.error("error updating", us, e))
  221. } catch (e) {
  222. console.error("error updating", e)
  223. throw e
  224. }
  225. }
  226. export function getGithubCredentialFromResult(result: UserCredential) {
  227. return GithubAuthProvider.credentialFromResult(result)
  228. }
  229. /**
  230. * A Vue composable function that is called when the auth status
  231. * is being updated to being logged in (fired multiple times),
  232. * this is also called on component mount if the login
  233. * was already resolved before mount.
  234. */
  235. export function onLoggedIn(exec: (user: HoppUser) => void) {
  236. let sub: Subscription | null = null
  237. onMounted(() => {
  238. sub = currentUser$
  239. .pipe(
  240. map((user) => !!user), // Get a logged in status (true or false)
  241. distinctUntilChanged(), // Don't propagate unless the status updates
  242. filter((x) => x) // Don't propagate unless it is logged in
  243. )
  244. .subscribe(() => {
  245. exec(currentUser$.value!)
  246. })
  247. })
  248. onBeforeUnmount(() => {
  249. sub?.unsubscribe()
  250. })
  251. }