From 06caa21a4d938d7a0e4f6f6c1ffb4b27fa356003 Mon Sep 17 00:00:00 2001 From: Chenlei Hu Date: Tue, 15 Apr 2025 20:56:18 -0400 Subject: [PATCH] [API Nodes] Setup Google/Github login (#3471) --- .../dialog/content/SignInContent.vue | 36 ++++---- src/stores/firebaseAuthStore.ts | 21 ++++- .../tests/store/firebaseAuthStore.test.ts | 91 ++++++++++++++++++- 3 files changed, 128 insertions(+), 20 deletions(-) diff --git a/src/components/dialog/content/SignInContent.vue b/src/components/dialog/content/SignInContent.vue index d754ccbdb..64949f03f 100644 --- a/src/components/dialog/content/SignInContent.vue +++ b/src/components/dialog/content/SignInContent.vue @@ -79,6 +79,7 @@ import Divider from 'primevue/divider' import { ref } from 'vue' import { useI18n } from 'vue-i18n' +import { useErrorHandling } from '@/composables/useErrorHandling' import { SignInData, SignUpData } from '@/schemas/signInSchema' import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' @@ -92,33 +93,32 @@ const { onSuccess } = defineProps<{ }>() const firebaseAuthStore = useFirebaseAuthStore() +const { wrapWithErrorHandlingAsync } = useErrorHandling() const isSignIn = ref(true) const toggleState = () => { isSignIn.value = !isSignIn.value } -const signInWithGoogle = () => { - // Implement Google login - console.log(isSignIn.value) - console.log('Google login clicked') +const signInWithGoogle = wrapWithErrorHandlingAsync(async () => { + await firebaseAuthStore.loginWithGoogle() onSuccess() -} +}) -const signInWithGithub = () => { - // Implement Github login - console.log(isSignIn.value) - console.log('Github login clicked') +const signInWithGithub = wrapWithErrorHandlingAsync(async () => { + await firebaseAuthStore.loginWithGithub() onSuccess() -} +}) -const signInWithEmail = async (values: SignInData | SignUpData) => { - const { email, password } = values - if (isSignIn.value) { - await firebaseAuthStore.login(email, password) - } else { - await firebaseAuthStore.register(email, password) +const signInWithEmail = wrapWithErrorHandlingAsync( + async (values: SignInData | SignUpData) => { + const { email, password } = values + if (isSignIn.value) { + await firebaseAuthStore.login(email, password) + } else { + await firebaseAuthStore.register(email, password) + } + onSuccess() } - onSuccess() -} +) diff --git a/src/stores/firebaseAuthStore.ts b/src/stores/firebaseAuthStore.ts index 5108c65d2..89924dba8 100644 --- a/src/stores/firebaseAuthStore.ts +++ b/src/stores/firebaseAuthStore.ts @@ -1,10 +1,13 @@ import { type Auth, + GithubAuthProvider, + GoogleAuthProvider, type User, type UserCredential, createUserWithEmailAndPassword, onAuthStateChanged, signInWithEmailAndPassword, + signInWithPopup, signOut } from 'firebase/auth' import { defineStore } from 'pinia' @@ -18,6 +21,10 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { const currentUser = ref(null) const isInitialized = ref(false) + // Providers + const googleProvider = new GoogleAuthProvider() + const githubProvider = new GithubAuthProvider() + // Getters const isAuthenticated = computed(() => !!currentUser.value) const userEmail = computed(() => currentUser.value?.email) @@ -68,6 +75,16 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { createUserWithEmailAndPassword(authInstance, email, password) ) + const loginWithGoogle = async (): Promise => + executeAuthAction((authInstance) => + signInWithPopup(authInstance, googleProvider) + ) + + const loginWithGithub = async (): Promise => + executeAuthAction((authInstance) => + signInWithPopup(authInstance, githubProvider) + ) + const logout = async (): Promise => executeAuthAction((authInstance) => signOut(authInstance)) @@ -94,6 +111,8 @@ export const useFirebaseAuthStore = defineStore('firebaseAuth', () => { login, register, logout, - getIdToken + getIdToken, + loginWithGoogle, + loginWithGithub } }) diff --git a/tests-ui/tests/store/firebaseAuthStore.test.ts b/tests-ui/tests/store/firebaseAuthStore.test.ts index d54f23361..1050b2e58 100644 --- a/tests-ui/tests/store/firebaseAuthStore.test.ts +++ b/tests-ui/tests/store/firebaseAuthStore.test.ts @@ -13,7 +13,10 @@ vi.mock('firebase/auth', () => ({ signInWithEmailAndPassword: vi.fn(), createUserWithEmailAndPassword: vi.fn(), signOut: vi.fn(), - onAuthStateChanged: vi.fn() + onAuthStateChanged: vi.fn(), + signInWithPopup: vi.fn(), + GoogleAuthProvider: vi.fn(), + GithubAuthProvider: vi.fn() })) describe('useFirebaseAuthStore', () => { @@ -272,4 +275,90 @@ describe('useFirebaseAuthStore', () => { expect(tokenAfterLogout).toBeNull() }) }) + + describe('social authentication', () => { + describe('loginWithGoogle', () => { + it('should sign in with Google', async () => { + const mockUserCredential = { user: mockUser } + vi.mocked(firebaseAuth.signInWithPopup).mockResolvedValue( + mockUserCredential as any + ) + + const result = await store.loginWithGoogle() + + expect(firebaseAuth.signInWithPopup).toHaveBeenCalledWith( + mockAuth, + expect.any(firebaseAuth.GoogleAuthProvider) + ) + expect(result).toEqual(mockUserCredential) + expect(store.loading).toBe(false) + expect(store.error).toBe(null) + }) + + it('should handle Google sign in errors', async () => { + const mockError = new Error('Google authentication failed') + vi.mocked(firebaseAuth.signInWithPopup).mockRejectedValue(mockError) + + await expect(store.loginWithGoogle()).rejects.toThrow( + 'Google authentication failed' + ) + + expect(firebaseAuth.signInWithPopup).toHaveBeenCalledWith( + mockAuth, + expect.any(firebaseAuth.GoogleAuthProvider) + ) + expect(store.loading).toBe(false) + expect(store.error).toBe('Google authentication failed') + }) + }) + + describe('loginWithGithub', () => { + it('should sign in with Github', async () => { + const mockUserCredential = { user: mockUser } + vi.mocked(firebaseAuth.signInWithPopup).mockResolvedValue( + mockUserCredential as any + ) + + const result = await store.loginWithGithub() + + expect(firebaseAuth.signInWithPopup).toHaveBeenCalledWith( + mockAuth, + expect.any(firebaseAuth.GithubAuthProvider) + ) + expect(result).toEqual(mockUserCredential) + expect(store.loading).toBe(false) + expect(store.error).toBe(null) + }) + + it('should handle Github sign in errors', async () => { + const mockError = new Error('Github authentication failed') + vi.mocked(firebaseAuth.signInWithPopup).mockRejectedValue(mockError) + + await expect(store.loginWithGithub()).rejects.toThrow( + 'Github authentication failed' + ) + + expect(firebaseAuth.signInWithPopup).toHaveBeenCalledWith( + mockAuth, + expect.any(firebaseAuth.GithubAuthProvider) + ) + expect(store.loading).toBe(false) + expect(store.error).toBe('Github authentication failed') + }) + }) + + it('should handle concurrent social login attempts correctly', async () => { + const mockUserCredential = { user: mockUser } + vi.mocked(firebaseAuth.signInWithPopup).mockResolvedValue( + mockUserCredential as any + ) + + const googleLoginPromise = store.loginWithGoogle() + const githubLoginPromise = store.loginWithGithub() + + await Promise.all([googleLoginPromise, githubLoginPromise]) + + expect(store.loading).toBe(false) + }) + }) })