chore: reintroduce updated auth mechanism
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
CombinedError,
|
||||
Operation,
|
||||
OperationResult,
|
||||
Client,
|
||||
} from "@urql/core"
|
||||
import { authExchange } from "@urql/exchange-auth"
|
||||
import { devtoolsExchange } from "@urql/devtools"
|
||||
@@ -21,12 +22,7 @@ import * as TE from "fp-ts/TaskEither"
|
||||
import { pipe, constVoid, flow } from "fp-ts/function"
|
||||
import { subscribe, pipe as wonkaPipe } from "wonka"
|
||||
import { filter, map, Subject } from "rxjs"
|
||||
import {
|
||||
authIdToken$,
|
||||
getAuthIDToken,
|
||||
probableUser$,
|
||||
waitProbableLoginToConfirm,
|
||||
} from "~/helpers/fb/auth"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
// TODO: Implement caching
|
||||
|
||||
@@ -57,11 +53,7 @@ export const gqlClientError$ = new Subject<GQLClientErrorEvent>()
|
||||
const createSubscriptionClient = () => {
|
||||
return new SubscriptionClient(BACKEND_WS_URL, {
|
||||
reconnect: true,
|
||||
connectionParams: () => {
|
||||
return {
|
||||
authorization: `Bearer ${authIdToken$.value}`,
|
||||
}
|
||||
},
|
||||
connectionParams: () => platform.auth.getBackendHeaders(),
|
||||
connectionCallback(error) {
|
||||
if (error?.length > 0) {
|
||||
gqlClientError$.next({
|
||||
@@ -79,7 +71,7 @@ const createHoppClient = () => {
|
||||
dedupExchange,
|
||||
authExchange({
|
||||
addAuthToOperation({ authState, operation }) {
|
||||
if (!authState || !authState.authToken) {
|
||||
if (!authState) {
|
||||
return operation
|
||||
}
|
||||
|
||||
@@ -88,28 +80,29 @@ const createHoppClient = () => {
|
||||
? operation.context.fetchOptions()
|
||||
: operation.context.fetchOptions || {}
|
||||
|
||||
const authHeaders = platform.auth.getBackendHeaders()
|
||||
|
||||
return makeOperation(operation.kind, operation, {
|
||||
...operation.context,
|
||||
fetchOptions: {
|
||||
...fetchOptions,
|
||||
headers: {
|
||||
...fetchOptions.headers,
|
||||
Authorization: `Bearer ${authState.authToken}`,
|
||||
...authHeaders,
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
willAuthError({ authState }) {
|
||||
return !authState || !authState.authToken
|
||||
willAuthError() {
|
||||
return platform.auth.willBackendHaveAuthError()
|
||||
},
|
||||
getAuth: async () => {
|
||||
if (!probableUser$.value) return { authToken: null }
|
||||
const probableUser = platform.auth.getProbableUser()
|
||||
|
||||
await waitProbableLoginToConfirm()
|
||||
if (probableUser !== null)
|
||||
await platform.auth.waitProbableLoginToConfirm()
|
||||
|
||||
return {
|
||||
authToken: getAuthIDToken(),
|
||||
}
|
||||
return {}
|
||||
},
|
||||
}),
|
||||
fetchExchange,
|
||||
@@ -137,31 +130,40 @@ const createHoppClient = () => {
|
||||
return createClient({
|
||||
url: BACKEND_GQL_URL,
|
||||
exchanges,
|
||||
...(platform.auth.getGQLClientOptions
|
||||
? platform.auth.getGQLClientOptions()
|
||||
: {}),
|
||||
})
|
||||
}
|
||||
|
||||
let subscriptionClient: SubscriptionClient | null
|
||||
export const client = ref(createHoppClient())
|
||||
|
||||
authIdToken$.subscribe((idToken) => {
|
||||
// triggering reconnect by closing the websocket client
|
||||
if (idToken && subscriptionClient) {
|
||||
subscriptionClient?.client?.close()
|
||||
}
|
||||
|
||||
// creating new subscription
|
||||
if (idToken && !subscriptionClient) {
|
||||
subscriptionClient = createSubscriptionClient()
|
||||
}
|
||||
|
||||
// closing existing subscription client.
|
||||
if (!idToken && subscriptionClient) {
|
||||
subscriptionClient.close()
|
||||
subscriptionClient = null
|
||||
}
|
||||
export const client = ref<Client>()
|
||||
|
||||
export function initBackendGQLClient() {
|
||||
client.value = createHoppClient()
|
||||
})
|
||||
|
||||
platform.auth.onBackendGQLClientShouldReconnect(() => {
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
// triggering reconnect by closing the websocket client
|
||||
if (currentUser && subscriptionClient) {
|
||||
subscriptionClient?.client?.close()
|
||||
}
|
||||
|
||||
// creating new subscription
|
||||
if (currentUser && !subscriptionClient) {
|
||||
subscriptionClient = createSubscriptionClient()
|
||||
}
|
||||
|
||||
// closing existing subscription client.
|
||||
if (!currentUser && subscriptionClient) {
|
||||
subscriptionClient.close()
|
||||
subscriptionClient = null
|
||||
}
|
||||
|
||||
client.value = createHoppClient()
|
||||
})
|
||||
}
|
||||
|
||||
type RunQueryOptions<T = any, V = object> = {
|
||||
query: TypedDocumentNode<T, V>
|
||||
@@ -185,7 +187,7 @@ export const runGQLQuery = <DocType, DocVarType, DocErrorType extends string>(
|
||||
args: RunQueryOptions<DocType, DocVarType>
|
||||
): Promise<E.Either<GQLError<DocErrorType>, DocType>> => {
|
||||
const request = createRequest<DocType, DocVarType>(args.query, args.variables)
|
||||
const source = client.value.executeQuery(request, {
|
||||
const source = client.value!.executeQuery(request, {
|
||||
requestPolicy: "network-only",
|
||||
})
|
||||
|
||||
@@ -250,7 +252,7 @@ export const runGQLSubscription = <
|
||||
) => {
|
||||
const result$ = new Subject<E.Either<GQLError<DocErrorType>, DocType>>()
|
||||
|
||||
const source = client.value.executeSubscription(
|
||||
const source = client.value!.executeSubscription(
|
||||
createRequest(args.query, args.variables)
|
||||
)
|
||||
|
||||
@@ -342,8 +344,8 @@ export const runMutation = <
|
||||
pipe(
|
||||
TE.tryCatch(
|
||||
() =>
|
||||
client.value
|
||||
.mutation(mutation, variables, {
|
||||
client
|
||||
.value!.mutation(mutation, variables, {
|
||||
requestPolicy: "cache-and-network",
|
||||
...additionalConfig,
|
||||
})
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
setUserId,
|
||||
setUserProperties,
|
||||
} from "firebase/analytics"
|
||||
import { authEvents$ } from "./auth"
|
||||
import { platform } from "~/platform"
|
||||
import {
|
||||
HoppAccentColor,
|
||||
HoppBgColor,
|
||||
@@ -42,13 +42,15 @@ export function initAnalytics() {
|
||||
}
|
||||
|
||||
function initLoginListeners() {
|
||||
const authEvents$ = platform.auth.getAuthEventsStream()
|
||||
|
||||
authEvents$.subscribe((ev) => {
|
||||
if (ev.event === "login") {
|
||||
if (settingsStore.value.TELEMETRY_ENABLED && analytics) {
|
||||
setUserId(analytics, ev.user.uid)
|
||||
|
||||
logEvent(analytics, "login", {
|
||||
method: ev.user.providerData[0]?.providerId, // Assume the first provider is the login provider
|
||||
method: ev.user.provider, // Assume the first provider is the login provider
|
||||
})
|
||||
}
|
||||
} else if (ev.event === "logout") {
|
||||
|
||||
@@ -1,434 +0,0 @@
|
||||
import {
|
||||
User,
|
||||
getAuth,
|
||||
onAuthStateChanged,
|
||||
onIdTokenChanged,
|
||||
signInWithPopup,
|
||||
GoogleAuthProvider,
|
||||
GithubAuthProvider,
|
||||
OAuthProvider,
|
||||
signInWithEmailAndPassword as signInWithEmailAndPass,
|
||||
isSignInWithEmailLink as isSignInWithEmailLinkFB,
|
||||
fetchSignInMethodsForEmail,
|
||||
sendSignInLinkToEmail,
|
||||
signInWithEmailLink as signInWithEmailLinkFB,
|
||||
ActionCodeSettings,
|
||||
signOut,
|
||||
linkWithCredential,
|
||||
AuthCredential,
|
||||
AuthError,
|
||||
UserCredential,
|
||||
updateProfile,
|
||||
updateEmail,
|
||||
sendEmailVerification,
|
||||
reauthenticateWithCredential,
|
||||
} from "firebase/auth"
|
||||
import {
|
||||
onSnapshot,
|
||||
getFirestore,
|
||||
setDoc,
|
||||
doc,
|
||||
updateDoc,
|
||||
} from "firebase/firestore"
|
||||
import { BehaviorSubject, filter, Subject, Subscription } from "rxjs"
|
||||
import {
|
||||
setLocalConfig,
|
||||
getLocalConfig,
|
||||
removeLocalConfig,
|
||||
} from "~/newstore/localpersistence"
|
||||
|
||||
export type HoppUser = User & {
|
||||
provider?: string
|
||||
accessToken?: string
|
||||
}
|
||||
|
||||
export type AuthEvent =
|
||||
| { 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: "logout" } // No authentication and we have no previous state
|
||||
| { event: "authTokenUpdate"; user: HoppUser; newToken: string | null } // Token has been updated
|
||||
|
||||
/**
|
||||
* A BehaviorSubject emitting the currently logged in user (or null if not logged in)
|
||||
*/
|
||||
export const currentUser$ = new BehaviorSubject<HoppUser | null>(null)
|
||||
/**
|
||||
* A BehaviorSubject emitting the current idToken
|
||||
*/
|
||||
export const authIdToken$ = new BehaviorSubject<string | null>(null)
|
||||
|
||||
/**
|
||||
* A subject that emits events related to authentication flows
|
||||
*/
|
||||
export const authEvents$ = new Subject<AuthEvent>()
|
||||
|
||||
/**
|
||||
* 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"))
|
||||
|
||||
let sub: Subscription | null = null
|
||||
|
||||
sub = authIdToken$.pipe(filter((token) => !!token)).subscribe(() => {
|
||||
sub?.unsubscribe()
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* Initializes the firebase authentication related subjects
|
||||
*/
|
||||
export function initAuth() {
|
||||
const auth = getAuth()
|
||||
const firestore = getFirestore()
|
||||
|
||||
let extraSnapshotStop: (() => void) | null = null
|
||||
|
||||
probableUser$.next(JSON.parse(getLocalConfig("login_state") ?? "null"))
|
||||
|
||||
onAuthStateChanged(auth, (user) => {
|
||||
/** Whether the user was logged in before */
|
||||
const wasLoggedIn = currentUser$.value !== null
|
||||
|
||||
if (user) {
|
||||
probableUser$.next(user)
|
||||
} else {
|
||||
probableUser$.next(null)
|
||||
removeLocalConfig("login_state")
|
||||
}
|
||||
|
||||
if (!user && extraSnapshotStop) {
|
||||
extraSnapshotStop()
|
||||
extraSnapshotStop = null
|
||||
} else if (user) {
|
||||
// Merge all the user info from all the authenticated providers
|
||||
user.providerData.forEach((profile) => {
|
||||
if (!profile) return
|
||||
|
||||
const us = {
|
||||
updatedOn: new Date(),
|
||||
provider: profile.providerId,
|
||||
name: profile.displayName,
|
||||
email: profile.email,
|
||||
photoUrl: profile.photoURL,
|
||||
uid: profile.uid,
|
||||
}
|
||||
|
||||
setDoc(doc(firestore, "users", user.uid), us, { merge: true }).catch(
|
||||
(e) => console.error("error updating", us, e)
|
||||
)
|
||||
})
|
||||
|
||||
extraSnapshotStop = onSnapshot(
|
||||
doc(firestore, "users", user.uid),
|
||||
(doc) => {
|
||||
const data = doc.data()
|
||||
|
||||
const userUpdate: HoppUser = user
|
||||
|
||||
if (data) {
|
||||
// Write extra provider data
|
||||
userUpdate.provider = data.provider
|
||||
userUpdate.accessToken = data.accessToken
|
||||
}
|
||||
|
||||
currentUser$.next(userUpdate)
|
||||
}
|
||||
)
|
||||
}
|
||||
currentUser$.next(user)
|
||||
|
||||
// User wasn't found before, but now is there (login happened)
|
||||
if (!wasLoggedIn && user) {
|
||||
authEvents$.next({
|
||||
event: "login",
|
||||
user: currentUser$.value!,
|
||||
})
|
||||
} else if (wasLoggedIn && !user) {
|
||||
// User was found before, but now is not there (logout happened)
|
||||
authEvents$.next({
|
||||
event: "logout",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onIdTokenChanged(auth, async (user) => {
|
||||
if (user) {
|
||||
authIdToken$.next(await user.getIdToken())
|
||||
|
||||
authEvents$.next({
|
||||
event: "authTokenUpdate",
|
||||
newToken: authIdToken$.value,
|
||||
user: currentUser$.value!, // Force not-null because user is defined
|
||||
})
|
||||
|
||||
setLocalConfig("login_state", JSON.stringify(user))
|
||||
} else {
|
||||
authIdToken$.next(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function getAuthIDToken(): string | null {
|
||||
return authIdToken$.getValue()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign user in with a popup using Google
|
||||
*/
|
||||
export async function signInUserWithGoogle() {
|
||||
return await signInWithPopup(getAuth(), new GoogleAuthProvider())
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign user in with a popup using Github
|
||||
*/
|
||||
export async function signInUserWithGithub() {
|
||||
return await signInWithPopup(
|
||||
getAuth(),
|
||||
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
|
||||
*/
|
||||
export async function signInWithEmailAndPassword(
|
||||
email: string,
|
||||
password: string
|
||||
) {
|
||||
return await signInWithEmailAndPass(getAuth(), email, password)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the sign in methods for a given email address
|
||||
*
|
||||
* @param email - Email to get the methods of
|
||||
*
|
||||
* @returns Promise for string array of the auth provider methods accessible
|
||||
*/
|
||||
export async function getSignInMethodsForEmail(email: string) {
|
||||
return await fetchSignInMethodsForEmail(getAuth(), email)
|
||||
}
|
||||
|
||||
export async function linkWithFBCredential(
|
||||
user: User,
|
||||
credential: AuthCredential
|
||||
) {
|
||||
return await linkWithCredential(user, credential)
|
||||
}
|
||||
|
||||
/**
|
||||
* Links account with another account given in a auth/account-exists-with-different-credential error
|
||||
*
|
||||
* @param error - Error caught after trying to login
|
||||
*
|
||||
* @returns Promise of UserCredential
|
||||
*/
|
||||
export async function linkWithFBCredentialFromAuthError(error: unknown) {
|
||||
// credential is not null since this function is called after an auth/account-exists-with-different-credential error, ie credentials actually exist
|
||||
const credentials = OAuthProvider.credentialFromError(error as AuthError)!
|
||||
|
||||
const otherLinkedProviders = (
|
||||
await getSignInMethodsForEmail((error as AuthError).customData.email!)
|
||||
).filter((providerId) => credentials.providerId !== providerId)
|
||||
|
||||
let user: User | null = null
|
||||
|
||||
if (otherLinkedProviders.indexOf("google.com") >= -1) {
|
||||
user = (await signInUserWithGoogle()).user
|
||||
} else if (otherLinkedProviders.indexOf("github.com") >= -1) {
|
||||
user = (await signInUserWithGithub()).user
|
||||
} else if (otherLinkedProviders.indexOf("microsoft.com") >= -1) {
|
||||
user = (await signInUserWithMicrosoft()).user
|
||||
}
|
||||
|
||||
// user is not null since going through each provider will return a user
|
||||
return await linkWithCredential(user!, credentials)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an email with the signin link to the user
|
||||
*
|
||||
* @param email - Email to send the email to
|
||||
* @param actionCodeSettings - The settings to apply to the link
|
||||
*/
|
||||
export async function signInWithEmail(
|
||||
email: string,
|
||||
actionCodeSettings: ActionCodeSettings
|
||||
) {
|
||||
return await sendSignInLinkToEmail(getAuth(), email, actionCodeSettings)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks and returns whether the sign in link is an email link
|
||||
*
|
||||
* @param url - The URL to look in
|
||||
*/
|
||||
export function isSignInWithEmailLink(url: string) {
|
||||
return isSignInWithEmailLinkFB(getAuth(), url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an email with sign in with email link
|
||||
*
|
||||
* @param email - Email to log in to
|
||||
* @param url - The action URL which is used to validate login
|
||||
*/
|
||||
export async function signInWithEmailLink(email: string, url: string) {
|
||||
return await signInWithEmailLinkFB(getAuth(), email, url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs out the user
|
||||
*/
|
||||
export async function signOutUser() {
|
||||
if (!currentUser$.value) throw new Error("No user has logged in")
|
||||
|
||||
await signOut(getAuth())
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the provider id and relevant provider auth token
|
||||
* as user metadata
|
||||
*
|
||||
* @param id - The provider ID
|
||||
* @param token - The relevant auth token for the given provider
|
||||
*/
|
||||
export async function setProviderInfo(id: string, token: string) {
|
||||
if (!currentUser$.value) throw new Error("No user has logged in")
|
||||
|
||||
const us = {
|
||||
updatedOn: new Date(),
|
||||
provider: id,
|
||||
accessToken: token,
|
||||
}
|
||||
|
||||
try {
|
||||
await updateDoc(
|
||||
doc(getFirestore(), "users", currentUser$.value.uid),
|
||||
us
|
||||
).catch((e) => console.error("error updating", us, e))
|
||||
} catch (e) {
|
||||
console.error("error updating", 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", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send user's email address verification mail
|
||||
*/
|
||||
export async function verifyEmailAddress() {
|
||||
if (!currentUser$.value) throw new Error("No user has logged in")
|
||||
|
||||
try {
|
||||
await sendEmailVerification(currentUser$.value)
|
||||
} catch (e) {
|
||||
console.error("error updating", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the user's email address
|
||||
*
|
||||
* @param email - The new email address
|
||||
*/
|
||||
export async function setEmailAddress(email: string) {
|
||||
if (!currentUser$.value) throw new Error("No user has logged in")
|
||||
|
||||
try {
|
||||
await updateEmail(currentUser$.value, email)
|
||||
} catch (e) {
|
||||
await reauthenticateUser()
|
||||
console.error("error updating", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reauthenticate the user with the given credential
|
||||
*/
|
||||
async function reauthenticateUser() {
|
||||
if (!currentUser$.value) throw new Error("No user has logged in")
|
||||
const currentAuthMethod = currentUser$.value.provider
|
||||
let credential
|
||||
if (currentAuthMethod === "google.com") {
|
||||
const result = await signInUserWithGithub()
|
||||
credential = GithubAuthProvider.credentialFromResult(result)
|
||||
} else if (currentAuthMethod === "github.com") {
|
||||
const result = await signInUserWithGoogle()
|
||||
credential = GoogleAuthProvider.credentialFromResult(result)
|
||||
} else if (currentAuthMethod === "microsoft.com") {
|
||||
const result = await signInUserWithMicrosoft()
|
||||
credential = OAuthProvider.credentialFromResult(result)
|
||||
} else if (currentAuthMethod === "password") {
|
||||
const email = prompt(
|
||||
"Reauthenticate your account using your current email:"
|
||||
)
|
||||
const actionCodeSettings = {
|
||||
url: `${process.env.BASE_URL}/enter`,
|
||||
handleCodeInApp: true,
|
||||
}
|
||||
await signInWithEmail(email as string, actionCodeSettings)
|
||||
.then(() =>
|
||||
alert(
|
||||
`Check your inbox - we sent an email to ${email}. It contains a magic link that will reauthenticate your account.`
|
||||
)
|
||||
)
|
||||
.catch((e) => {
|
||||
alert(`Error: ${e.message}`)
|
||||
console.error(e)
|
||||
})
|
||||
return
|
||||
}
|
||||
try {
|
||||
await reauthenticateWithCredential(
|
||||
currentUser$.value,
|
||||
credential as AuthCredential
|
||||
)
|
||||
} catch (e) {
|
||||
console.error("error updating", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export function getGithubCredentialFromResult(result: UserCredential) {
|
||||
return GithubAuthProvider.credentialFromResult(result)
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
translateToNewRESTCollection,
|
||||
translateToNewGQLCollection,
|
||||
} from "@hoppscotch/data"
|
||||
import { currentUser$ } from "./auth"
|
||||
import { platform } from "~/platform"
|
||||
import {
|
||||
restCollections$,
|
||||
graphqlCollections$,
|
||||
@@ -44,20 +44,22 @@ export async function writeCollections(
|
||||
collection: any[],
|
||||
flag: CollectionFlags
|
||||
) {
|
||||
if (currentUser$.value === null)
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
if (currentUser === null)
|
||||
throw new Error("User not logged in to write collections")
|
||||
|
||||
const cl = {
|
||||
updatedOn: new Date(),
|
||||
author: currentUser$.value.uid,
|
||||
author_name: currentUser$.value.displayName,
|
||||
author_image: currentUser$.value.photoURL,
|
||||
author: currentUser.uid,
|
||||
author_name: currentUser.displayName,
|
||||
author_image: currentUser.photoURL,
|
||||
collection,
|
||||
}
|
||||
|
||||
try {
|
||||
await setDoc(
|
||||
doc(getFirestore(), "users", currentUser$.value.uid, flag, "sync"),
|
||||
doc(getFirestore(), "users", currentUser.uid, flag, "sync"),
|
||||
cl
|
||||
)
|
||||
} catch (e) {
|
||||
@@ -67,10 +69,14 @@ export async function writeCollections(
|
||||
}
|
||||
|
||||
export function initCollections() {
|
||||
const currentUser$ = platform.auth.getCurrentUserStream()
|
||||
|
||||
const restCollSub = restCollections$.subscribe((collections) => {
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
if (
|
||||
loadedRESTCollections &&
|
||||
currentUser$.value &&
|
||||
currentUser &&
|
||||
settingsStore.value.syncCollections
|
||||
) {
|
||||
writeCollections(collections, "collections")
|
||||
@@ -80,7 +86,7 @@ export function initCollections() {
|
||||
const gqlCollSub = graphqlCollections$.subscribe((collections) => {
|
||||
if (
|
||||
loadedGraphqlCollections &&
|
||||
currentUser$.value &&
|
||||
currentUser &&
|
||||
settingsStore.value.syncCollections
|
||||
) {
|
||||
writeCollections(collections, "collectionsGraphql")
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
onSnapshot,
|
||||
setDoc,
|
||||
} from "firebase/firestore"
|
||||
import { currentUser$ } from "./auth"
|
||||
import { platform } from "~/platform"
|
||||
import {
|
||||
environments$,
|
||||
globalEnv$,
|
||||
@@ -32,26 +32,22 @@ let loadedEnvironments = false
|
||||
let loadedGlobals = true
|
||||
|
||||
async function writeEnvironments(environment: Environment[]) {
|
||||
if (currentUser$.value == null)
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
if (currentUser === null)
|
||||
throw new Error("Cannot write environments when signed out")
|
||||
|
||||
const ev = {
|
||||
updatedOn: new Date(),
|
||||
author: currentUser$.value.uid,
|
||||
author_name: currentUser$.value.displayName,
|
||||
author_image: currentUser$.value.photoURL,
|
||||
author: currentUser.uid,
|
||||
author_name: currentUser.displayName,
|
||||
author_image: currentUser.photoURL,
|
||||
environment,
|
||||
}
|
||||
|
||||
try {
|
||||
await setDoc(
|
||||
doc(
|
||||
getFirestore(),
|
||||
"users",
|
||||
currentUser$.value.uid,
|
||||
"environments",
|
||||
"sync"
|
||||
),
|
||||
doc(getFirestore(), "users", currentUser.uid, "environments", "sync"),
|
||||
ev
|
||||
)
|
||||
} catch (e) {
|
||||
@@ -61,20 +57,22 @@ async function writeEnvironments(environment: Environment[]) {
|
||||
}
|
||||
|
||||
async function writeGlobalEnvironment(variables: Environment["variables"]) {
|
||||
if (currentUser$.value == null)
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
if (currentUser === null)
|
||||
throw new Error("Cannot write global environment when signed out")
|
||||
|
||||
const ev = {
|
||||
updatedOn: new Date(),
|
||||
author: currentUser$.value.uid,
|
||||
author_name: currentUser$.value.displayName,
|
||||
author_image: currentUser$.value.photoURL,
|
||||
author: currentUser.uid,
|
||||
author_name: currentUser.displayName,
|
||||
author_image: currentUser.photoURL,
|
||||
variables,
|
||||
}
|
||||
|
||||
try {
|
||||
await setDoc(
|
||||
doc(getFirestore(), "users", currentUser$.value.uid, "globalEnv", "sync"),
|
||||
doc(getFirestore(), "users", currentUser.uid, "globalEnv", "sync"),
|
||||
ev
|
||||
)
|
||||
} catch (e) {
|
||||
@@ -84,9 +82,13 @@ async function writeGlobalEnvironment(variables: Environment["variables"]) {
|
||||
}
|
||||
|
||||
export function initEnvironments() {
|
||||
const currentUser$ = platform.auth.getCurrentUserStream()
|
||||
|
||||
const envListenSub = environments$.subscribe((envs) => {
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
if (
|
||||
currentUser$.value &&
|
||||
currentUser &&
|
||||
settingsStore.value.syncEnvironments &&
|
||||
loadedEnvironments
|
||||
) {
|
||||
@@ -95,11 +97,9 @@ export function initEnvironments() {
|
||||
})
|
||||
|
||||
const globalListenSub = globalEnv$.subscribe((vars) => {
|
||||
if (
|
||||
currentUser$.value &&
|
||||
settingsStore.value.syncEnvironments &&
|
||||
loadedGlobals
|
||||
) {
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
if (currentUser && settingsStore.value.syncEnvironments && loadedGlobals) {
|
||||
writeGlobalEnvironment(vars)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
updateDoc,
|
||||
} from "firebase/firestore"
|
||||
import { FormDataKeyValue } from "@hoppscotch/data"
|
||||
import { currentUser$ } from "./auth"
|
||||
import { platform } from "~/platform"
|
||||
import { getSettingSubject, settingsStore } from "~/newstore/settings"
|
||||
import {
|
||||
GQLHistoryEntry,
|
||||
@@ -76,7 +76,9 @@ async function writeHistory(
|
||||
? purgeFormDataFromRequest(entry as RESTHistoryEntry)
|
||||
: entry
|
||||
|
||||
if (currentUser$.value == null)
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
if (currentUser === null)
|
||||
throw new Error("User not logged in to sync history")
|
||||
|
||||
const hs = {
|
||||
@@ -85,10 +87,7 @@ async function writeHistory(
|
||||
}
|
||||
|
||||
try {
|
||||
await addDoc(
|
||||
collection(getFirestore(), "users", currentUser$.value.uid, col),
|
||||
hs
|
||||
)
|
||||
await addDoc(collection(getFirestore(), "users", currentUser.uid, col), hs)
|
||||
} catch (e) {
|
||||
console.error("error writing to history", hs, e)
|
||||
throw e
|
||||
@@ -99,12 +98,14 @@ async function deleteHistory(
|
||||
entry: (RESTHistoryEntry | GQLHistoryEntry) & { id: string },
|
||||
col: HistoryFBCollections
|
||||
) {
|
||||
if (currentUser$.value == null)
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
if (currentUser === null)
|
||||
throw new Error("User not logged in to delete history")
|
||||
|
||||
try {
|
||||
await deleteDoc(
|
||||
doc(getFirestore(), "users", currentUser$.value.uid, col, entry.id)
|
||||
doc(getFirestore(), "users", currentUser.uid, col, entry.id)
|
||||
)
|
||||
} catch (e) {
|
||||
console.error("error deleting history", entry, e)
|
||||
@@ -113,11 +114,13 @@ async function deleteHistory(
|
||||
}
|
||||
|
||||
async function clearHistory(col: HistoryFBCollections) {
|
||||
if (currentUser$.value == null)
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
if (currentUser === null)
|
||||
throw new Error("User not logged in to clear history")
|
||||
|
||||
const { docs } = await getDocs(
|
||||
collection(getFirestore(), "users", currentUser$.value.uid, col)
|
||||
collection(getFirestore(), "users", currentUser.uid, col)
|
||||
)
|
||||
|
||||
await Promise.all(docs.map((e) => deleteHistory(e as any, col)))
|
||||
@@ -127,12 +130,13 @@ async function toggleStar(
|
||||
entry: (RESTHistoryEntry | GQLHistoryEntry) & { id: string },
|
||||
col: HistoryFBCollections
|
||||
) {
|
||||
if (currentUser$.value == null)
|
||||
throw new Error("User not logged in to toggle star")
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
if (currentUser === null) throw new Error("User not logged in to toggle star")
|
||||
|
||||
try {
|
||||
await updateDoc(
|
||||
doc(getFirestore(), "users", currentUser$.value.uid, col, entry.id),
|
||||
doc(getFirestore(), "users", currentUser.uid, col, entry.id),
|
||||
{ star: !entry.star }
|
||||
)
|
||||
} catch (e) {
|
||||
@@ -142,12 +146,12 @@ async function toggleStar(
|
||||
}
|
||||
|
||||
export function initHistory() {
|
||||
const currentUser$ = platform.auth.getCurrentUserStream()
|
||||
|
||||
const restHistorySub = restHistoryStore.dispatches$.subscribe((dispatch) => {
|
||||
if (
|
||||
loadedRESTHistory &&
|
||||
currentUser$.value &&
|
||||
settingsStore.value.syncHistory
|
||||
) {
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
if (loadedRESTHistory && currentUser && settingsStore.value.syncHistory) {
|
||||
if (dispatch.dispatcher === "addEntry") {
|
||||
writeHistory(dispatch.payload.entry, "history")
|
||||
} else if (dispatch.dispatcher === "deleteEntry") {
|
||||
@@ -162,9 +166,11 @@ export function initHistory() {
|
||||
|
||||
const gqlHistorySub = graphqlHistoryStore.dispatches$.subscribe(
|
||||
(dispatch) => {
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
if (
|
||||
loadedGraphqlHistory &&
|
||||
currentUser$.value &&
|
||||
currentUser &&
|
||||
settingsStore.value.syncHistory
|
||||
) {
|
||||
if (dispatch.dispatcher === "addEntry") {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { initializeApp } from "firebase/app"
|
||||
import { platform } from "~/platform"
|
||||
import { initAnalytics } from "./analytics"
|
||||
import { initAuth } from "./auth"
|
||||
import { initCollections } from "./collections"
|
||||
import { initEnvironments } from "./environments"
|
||||
import { initHistory } from "./history"
|
||||
@@ -24,7 +24,7 @@ export function initializeFirebase() {
|
||||
try {
|
||||
initializeApp(firebaseConfig)
|
||||
|
||||
initAuth()
|
||||
platform.auth.performAuthInit()
|
||||
initSettings()
|
||||
initCollections()
|
||||
initHistory()
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
import { doc, getDoc, getFirestore, setDoc } from "firebase/firestore"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { HoppRESTRequest, translateToNewRequest } from "@hoppscotch/data"
|
||||
import { currentUser$, HoppUser } from "./auth"
|
||||
import { platform } from "~/platform"
|
||||
import { HoppUser } from "~/platform/auth"
|
||||
import { restRequest$ } from "~/newstore/RESTSession"
|
||||
|
||||
/**
|
||||
@@ -44,7 +45,7 @@ function writeCurrentRequest(user: HoppUser, request: HoppRESTRequest) {
|
||||
* @returns Fetched request object if exists else null
|
||||
*/
|
||||
export async function loadRequestFromSync(): Promise<HoppRESTRequest | null> {
|
||||
const currentUser = currentUser$.value
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
if (!currentUser)
|
||||
throw new Error("Cannot load request from sync without login")
|
||||
@@ -66,6 +67,8 @@ export async function loadRequestFromSync(): Promise<HoppRESTRequest | null> {
|
||||
* Unsubscribe to stop syncing.
|
||||
*/
|
||||
export function startRequestSync(): Subscription {
|
||||
const currentUser$ = platform.auth.getCurrentUserStream()
|
||||
|
||||
const sub = combineLatest([
|
||||
currentUser$,
|
||||
restRequest$.pipe(distinctUntilChanged()),
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
onSnapshot,
|
||||
setDoc,
|
||||
} from "firebase/firestore"
|
||||
import { currentUser$ } from "./auth"
|
||||
import { platform } from "~/platform"
|
||||
import { applySetting, settingsStore, SettingsType } from "~/newstore/settings"
|
||||
|
||||
/**
|
||||
@@ -20,21 +20,23 @@ let loadedSettings = false
|
||||
* Write Transform
|
||||
*/
|
||||
async function writeSettings(setting: string, value: any) {
|
||||
if (currentUser$.value === null)
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
if (currentUser === null)
|
||||
throw new Error("Cannot write setting, user not signed in")
|
||||
|
||||
const st = {
|
||||
updatedOn: new Date(),
|
||||
author: currentUser$.value.uid,
|
||||
author_name: currentUser$.value.displayName,
|
||||
author_image: currentUser$.value.photoURL,
|
||||
author: currentUser.uid,
|
||||
author_name: currentUser.displayName,
|
||||
author_image: currentUser.photoURL,
|
||||
name: setting,
|
||||
value,
|
||||
}
|
||||
|
||||
try {
|
||||
await setDoc(
|
||||
doc(getFirestore(), "users", currentUser$.value.uid, "settings", setting),
|
||||
doc(getFirestore(), "users", currentUser.uid, "settings", setting),
|
||||
st
|
||||
)
|
||||
} catch (e) {
|
||||
@@ -44,8 +46,12 @@ async function writeSettings(setting: string, value: any) {
|
||||
}
|
||||
|
||||
export function initSettings() {
|
||||
const currentUser$ = platform.auth.getCurrentUserStream()
|
||||
|
||||
settingsStore.dispatches$.subscribe((dispatch) => {
|
||||
if (currentUser$.value && loadedSettings) {
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
if (currentUser && loadedSettings) {
|
||||
if (dispatch.dispatcher === "bulkApplySettings") {
|
||||
Object.keys(dispatch.payload).forEach((key) => {
|
||||
writeSettings(key, dispatch.payload[key])
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as E from "fp-ts/Either"
|
||||
import { BehaviorSubject } from "rxjs"
|
||||
import { authIdToken$ } from "../fb/auth"
|
||||
import { runGQLQuery } from "../backend/GQLClient"
|
||||
import { GetUserInfoDocument } from "../backend/graphql"
|
||||
|
||||
/*
|
||||
* This file deals with interfacing data provided by the
|
||||
* Hoppscotch Backend server
|
||||
*/
|
||||
|
||||
/**
|
||||
* Defines the information provided about a user
|
||||
*/
|
||||
export interface UserInfo {
|
||||
/**
|
||||
* UID of the user
|
||||
*/
|
||||
uid: string
|
||||
/**
|
||||
* Displayable name of the user (or null if none available)
|
||||
*/
|
||||
displayName: string | null
|
||||
/**
|
||||
* Email of the user (or null if none available)
|
||||
*/
|
||||
email: string | null
|
||||
/**
|
||||
* URL to the profile photo of the user (or null if none available)
|
||||
*/
|
||||
photoURL: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* An observable subject onto the currently logged in user info (is null if not logged in)
|
||||
*/
|
||||
export const currentUserInfo$ = new BehaviorSubject<UserInfo | null>(null)
|
||||
|
||||
/**
|
||||
* Initializes the currenUserInfo$ view and sets up its update mechanism
|
||||
*/
|
||||
export function initUserInfo() {
|
||||
authIdToken$.subscribe((token) => {
|
||||
if (token) {
|
||||
updateUserInfo()
|
||||
} else {
|
||||
currentUserInfo$.next(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the actual user info fetching
|
||||
*/
|
||||
async function updateUserInfo() {
|
||||
const result = await runGQLQuery({
|
||||
query: GetUserInfoDocument,
|
||||
})
|
||||
|
||||
currentUserInfo$.next(
|
||||
pipe(
|
||||
result,
|
||||
E.matchW(
|
||||
() => null,
|
||||
(x) => ({
|
||||
uid: x.me.uid,
|
||||
displayName: x.me.displayName ?? null,
|
||||
email: x.me.email ?? null,
|
||||
photoURL: x.me.photoURL ?? null,
|
||||
})
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import * as E from "fp-ts/Either"
|
||||
import { BehaviorSubject } from "rxjs"
|
||||
import { GQLError, runGQLQuery } from "../backend/GQLClient"
|
||||
import { GetMyTeamsDocument, GetMyTeamsQuery } from "../backend/graphql"
|
||||
import { authIdToken$ } from "~/helpers/fb/auth"
|
||||
import { platform } from "~/platform"
|
||||
|
||||
const BACKEND_PAGE_SIZE = 10
|
||||
const POLL_DURATION = 10000
|
||||
@@ -47,8 +47,10 @@ export default class TeamListAdapter {
|
||||
}
|
||||
|
||||
async fetchList() {
|
||||
const currentUser = platform.auth.getCurrentUser()
|
||||
|
||||
// if the authIdToken is not present, don't fetch the teams list, as it will fail anyway
|
||||
if (!authIdToken$.value) return
|
||||
if (!currentUser) return
|
||||
|
||||
this.loading$.next(true)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user