@libreapps/auth-firebase 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Firebase Authentication Service Implementation
3
+ * Implements the AuthService interface using Firebase as the auth provider.
4
+ */
5
+
6
+ import { makeAutoObservable, makeObservable, computed } from 'mobx'
7
+
8
+ import type { AuthService, AuthServiceConf, LibreAppsUserInfo, LibreAppsUserInfoValue } from '@libreapps/auth'
9
+
10
+ import {
11
+ auth as fbAuth,
12
+ signupWithEmailAndPassword,
13
+ loginWithCustomToken,
14
+ loginWithEmailAndPassword,
15
+ loginWithProvider,
16
+ logoutBackend,
17
+ isFirebaseConfigured
18
+ } from './firebase-support'
19
+ import { associateWalletAddressWithAccount, getAssociatedWalletAddress } from './wallet-support'
20
+
21
+ class LibreAppsUserInfoStore implements LibreAppsUserInfo {
22
+
23
+ constructor() {
24
+ makeAutoObservable(this)
25
+ }
26
+
27
+ _email: string = ''
28
+ _displayName: string | null = null
29
+ _walletAddress: string | null = null
30
+
31
+ get email(): string { return this._email}
32
+ get displayName(): string | null { return this._displayName}
33
+ get walletAddress(): string | null { return this._walletAddress}
34
+
35
+ clear():void {
36
+ this._email = ''
37
+ this._displayName = null
38
+ this._walletAddress = null
39
+ }
40
+
41
+ set(v: LibreAppsUserInfoValue):void {
42
+ this._email = v.email
43
+ this._displayName = v.displayName
44
+ this._walletAddress = v.walletAddress
45
+ }
46
+
47
+ get isValid(): boolean {
48
+ return (this._email.length > 0)
49
+ }
50
+ }
51
+
52
+ export class FirebaseAuthService implements AuthService {
53
+
54
+ private _hzUser = new LibreAppsUserInfoStore()
55
+
56
+ constructor(conf: AuthServiceConf, user: LibreAppsUserInfoValue | null) {
57
+ makeObservable(this, {
58
+ loggedIn: computed,
59
+ user: computed
60
+ })
61
+
62
+ if (user) {
63
+ this._hzUser.set(user)
64
+ }
65
+ }
66
+
67
+ static isConfigured(): boolean {
68
+ return isFirebaseConfigured()
69
+ }
70
+
71
+ get user(): LibreAppsUserInfo | null {
72
+ return this._hzUser.isValid ? this._hzUser : null
73
+ }
74
+
75
+ get loggedIn(): boolean {
76
+ return this._hzUser.isValid
77
+ }
78
+
79
+ signupEmailAndPassword = async (
80
+ email: string,
81
+ password: string
82
+ ): Promise<{success: boolean, userInfo: LibreAppsUserInfo | null, message?: string}> => {
83
+
84
+ try {
85
+ this._hzUser.clear()
86
+ const res = await signupWithEmailAndPassword(email, password)
87
+ if (res.success && res.user) {
88
+ const walletAddress = res.user.email ? await getAssociatedWalletAddress(res.user.email) : undefined
89
+ this._hzUser.set({
90
+ email: res.user.email ?? '',
91
+ displayName : res.user.displayName ?? null,
92
+ walletAddress : walletAddress?.result ?? null
93
+ })
94
+
95
+ return {
96
+ success: true,
97
+ userInfo: this._hzUser,
98
+ message: res.message
99
+ }
100
+ }
101
+ return {
102
+ success: false,
103
+ userInfo: null,
104
+ message: res.message
105
+ }
106
+ }
107
+ catch (e) {
108
+ console.error('Error signing in with Firebase auth', e)
109
+ return {success: false, userInfo: null, message: 'Error signing in with Firebase auth'}
110
+ }
111
+ }
112
+
113
+ loginEmailAndPassword = async (
114
+ email: string,
115
+ password: string
116
+ ): Promise<{success: boolean, userInfo: LibreAppsUserInfo | null, message?: string}> => {
117
+
118
+ try {
119
+ this._hzUser.clear()
120
+ const res = await loginWithEmailAndPassword(email, password)
121
+ if (res.success && res.user) {
122
+ const walletAddress = res.user.email ? await getAssociatedWalletAddress(res.user.email) : undefined
123
+ this._hzUser.set({
124
+ email: res.user.email ?? '',
125
+ displayName : res.user.displayName ?? null,
126
+ walletAddress : walletAddress?.result ?? null
127
+ })
128
+
129
+ return {
130
+ success: true,
131
+ userInfo: this._hzUser,
132
+ message: res.message
133
+ }
134
+ }
135
+ return {
136
+ success: false,
137
+ userInfo: null,
138
+ message: res.message
139
+ }
140
+ }
141
+ catch (e) {
142
+ console.error('Error signing in with Firebase auth', e)
143
+ return {success: false, userInfo: null, message: 'Error signing in with Firebase auth'}
144
+ }
145
+ }
146
+
147
+ loginWithProvider = async (
148
+ provider: 'google' | 'facebook' | 'github'
149
+ ): Promise<{success: boolean, userInfo: LibreAppsUserInfo | null}> => {
150
+
151
+ try {
152
+ this._hzUser.clear()
153
+ const res = await loginWithProvider(provider)
154
+ if (res.success && res.user) {
155
+ const walletAddress = res.user.email ? await getAssociatedWalletAddress(res.user.email) : undefined
156
+ this._hzUser.set({
157
+ email: res.user.email ?? '',
158
+ displayName : res.user.displayName ?? null,
159
+ walletAddress : walletAddress?.result ?? null
160
+ })
161
+
162
+ return {
163
+ success: true,
164
+ userInfo: this._hzUser
165
+ }
166
+ }
167
+ return {
168
+ success: false,
169
+ userInfo: null
170
+ }
171
+ }
172
+ catch (e) {
173
+ console.error('Error signing in with Firebase auth', e)
174
+ return {success: false, userInfo: null}
175
+ }
176
+ }
177
+
178
+ loginWithCustomToken = async (
179
+ token: string
180
+ ): Promise<{success: boolean, userInfo: LibreAppsUserInfo | null}> => {
181
+
182
+ try {
183
+ this._hzUser.clear()
184
+ const res = await loginWithCustomToken(token)
185
+ if (res.success && res.user) {
186
+ const walletAddress = res.user.email ? await getAssociatedWalletAddress(res.user.email) : undefined
187
+ this._hzUser.set({
188
+ email: res.user.email ?? '',
189
+ displayName : res.user.displayName ?? null,
190
+ walletAddress : walletAddress?.result ?? null
191
+ })
192
+
193
+ return {
194
+ success: true,
195
+ userInfo: this._hzUser
196
+ }
197
+ }
198
+ return {
199
+ success: false,
200
+ userInfo: null
201
+ }
202
+ }
203
+ catch (e) {
204
+ console.error('Error signing in with Firebase auth', e)
205
+ return {success: false, userInfo: null}
206
+ }
207
+ }
208
+
209
+ associateWallet = async (): Promise<void> => {
210
+ if (this._hzUser.isValid) {
211
+ const res = await associateWalletAddressWithAccount(this._hzUser.email)
212
+ if (!res.error) {
213
+ this._hzUser._walletAddress = res.result ?? null
214
+ }
215
+ }
216
+ }
217
+
218
+ logout = async (): Promise<{ success: boolean }> => {
219
+ if (fbAuth) {
220
+ await fbAuth.signOut()
221
+ }
222
+ this._hzUser.clear()
223
+ return await logoutBackend()
224
+ }
225
+
226
+ setServerSideUser = (user: LibreAppsUserInfoValue | null) => {
227
+ if (user) {
228
+ this._hzUser.set(user)
229
+ }
230
+ }
231
+ }
232
+
233
+ export default FirebaseAuthService
@@ -0,0 +1,234 @@
1
+ /**
2
+ * Firebase Client-side Authentication Support
3
+ * This module provides Firebase authentication methods for the client-side.
4
+ */
5
+
6
+ import {
7
+ FacebookAuthProvider,
8
+ GoogleAuthProvider,
9
+ GithubAuthProvider,
10
+ signInWithPopup,
11
+ createUserWithEmailAndPassword,
12
+ type User,
13
+ signInWithEmailAndPassword,
14
+ signInWithCustomToken,
15
+ type Auth,
16
+ } from 'firebase/auth'
17
+
18
+ import { initializeApp, getApps, FirebaseError, type FirebaseApp } from "firebase/app"
19
+ import { getAuth } from "firebase/auth"
20
+ import { getFirestore, type Firestore } from 'firebase/firestore'
21
+
22
+ import type { APIResponse } from '@libreapps/auth/types'
23
+
24
+ export const firebaseConfig = {
25
+ apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
26
+ authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
27
+ projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
28
+ storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
29
+ messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
30
+ appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
31
+ }
32
+
33
+ // Check if Firebase is configured
34
+ export const isFirebaseConfigured = () => {
35
+ return !!(firebaseConfig.apiKey && firebaseConfig.projectId)
36
+ }
37
+
38
+ // Initialize Firebase only if configured
39
+ let firebaseApp: FirebaseApp | null = null
40
+ let auth: Auth | null = null
41
+ let db: Firestore | null = null
42
+
43
+ if (typeof window !== 'undefined' && isFirebaseConfigured()) {
44
+ firebaseApp = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0]
45
+ auth = getAuth(firebaseApp)
46
+ db = getFirestore(firebaseApp, 'accounts')
47
+ }
48
+
49
+ export { auth, db }
50
+
51
+ export async function loginWithProvider(provider: string): Promise<{ success: boolean, user: User | null }> {
52
+ if (!auth) {
53
+ console.warn('Firebase auth not configured')
54
+ return { success: false, user: null }
55
+ }
56
+
57
+ const authProvider = (() => {
58
+ switch (provider) {
59
+ case 'google':
60
+ return new GoogleAuthProvider()
61
+ case 'facebook':
62
+ return new FacebookAuthProvider()
63
+ case 'github':
64
+ return new GithubAuthProvider()
65
+ default:
66
+ return null
67
+ }
68
+ })()
69
+
70
+ if (!authProvider) {
71
+ return { success: false, user: null }
72
+ }
73
+
74
+ try {
75
+ const userCreds = await signInWithPopup(auth, authProvider)
76
+ const idToken = await userCreds.user.getIdToken()
77
+
78
+ const response = await fetch('/api/auth/login', {
79
+ method: 'POST',
80
+ headers: { 'Content-Type': 'application/json' },
81
+ body: JSON.stringify({ idToken }),
82
+ })
83
+ const resBody = (await response.json()) as unknown as APIResponse<string>
84
+
85
+ if (response.ok && resBody.success) {
86
+ return { success: true, user: userCreds.user }
87
+ }
88
+ else {
89
+ return { success: false, user: null }
90
+ }
91
+ }
92
+ catch (error) {
93
+ console.error('Error signing in with provider', error)
94
+ return { success: false, user: null }
95
+ }
96
+ }
97
+
98
+ export async function signupWithEmailAndPassword(
99
+ email: string,
100
+ password: string
101
+ ): Promise<{ success: boolean, user?: User, message?: string }> {
102
+ if (!auth) {
103
+ return { success: false, message: 'Firebase auth not configured' }
104
+ }
105
+
106
+ let user: User | undefined = undefined
107
+ try {
108
+ const userCredential = await createUserWithEmailAndPassword(auth, email, password)
109
+ user = userCredential.user
110
+ }
111
+ catch (error) {
112
+ if (error instanceof FirebaseError) {
113
+ console.error(error.code)
114
+ return {success: false, message: error.code as string}
115
+ }
116
+ return {success: false, message: error as string}
117
+ }
118
+
119
+ try {
120
+ const idToken = await user.getIdToken()
121
+
122
+ const response = await fetch('/api/auth/login', {
123
+ method: 'POST',
124
+ headers: { 'Content-Type': 'application/json' },
125
+ body: JSON.stringify({ idToken }),
126
+ })
127
+ const resBody = (await response.json()) as unknown as APIResponse<string>
128
+
129
+ if (response.ok && resBody.success) {
130
+ return { success: true, user }
131
+ }
132
+ else {
133
+ return { success: false }
134
+ }
135
+ }
136
+ catch (error) {
137
+ console.error('Error signing in with Firebase auth', error)
138
+ return { success: false }
139
+ }
140
+ }
141
+
142
+ export async function loginWithEmailAndPassword(
143
+ email: string,
144
+ password: string
145
+ ): Promise<{ success: boolean, user?: User, message?: string }> {
146
+ if (!auth) {
147
+ return { success: false, message: 'Firebase auth not configured' }
148
+ }
149
+
150
+ let user: User | undefined = undefined
151
+ try {
152
+ const userCredential = await signInWithEmailAndPassword(auth, email, password)
153
+ user = userCredential.user
154
+ } catch (error) {
155
+ if (error instanceof FirebaseError) {
156
+ console.error(error.code)
157
+ return {success: false, message: error.code as string}
158
+ }
159
+ return {success: false, message: error as string}
160
+ }
161
+
162
+ try {
163
+ const idToken = await user.getIdToken()
164
+
165
+ const response = await fetch('/api/auth/login', {
166
+ method: 'POST',
167
+ headers: { 'Content-Type': 'application/json' },
168
+ body: JSON.stringify({ idToken }),
169
+ })
170
+ const resBody = (await response.json()) as unknown as APIResponse<string>
171
+
172
+ if (response.ok && resBody.success) {
173
+ return { success: true, user, message: "Login Successfully!" }
174
+ }
175
+ else {
176
+ return { success: false , message: "Login API Failed"}
177
+ }
178
+ }
179
+ catch (error) {
180
+ console.error('Error signing in with Firebase auth', error)
181
+ return { success: false, message: "Error signing in with Firebase auth" }
182
+ }
183
+ }
184
+
185
+ export async function loginWithCustomToken(
186
+ token: string,
187
+ ): Promise<{ success: boolean, user?: User }> {
188
+ if (!auth) {
189
+ return { success: false }
190
+ }
191
+
192
+ let user: User | undefined = undefined
193
+ const userCredential = await signInWithCustomToken(auth, token)
194
+ user = userCredential.user
195
+
196
+ try {
197
+ const idToken = await user.getIdToken()
198
+
199
+ const response = await fetch('/api/auth/login', {
200
+ method: 'POST',
201
+ headers: { 'Content-Type': 'application/json' },
202
+ body: JSON.stringify({ idToken }),
203
+ })
204
+ const resBody = (await response.json()) as unknown as APIResponse<string>
205
+
206
+ if (response.ok && resBody.success) {
207
+ return { success: true, user }
208
+ }
209
+ else {
210
+ return { success: false }
211
+ }
212
+ }
213
+ catch (error) {
214
+ console.error('Error signing in with Firebase auth', error)
215
+ return { success: false }
216
+ }
217
+ }
218
+
219
+ export async function logoutBackend(): Promise<{ success: boolean }> {
220
+ try {
221
+ const response = await fetch('/api/auth/logout', { headers: { 'Content-Type': 'application/json' } })
222
+ const resBody = (await response.json()) as unknown as APIResponse<string>
223
+ if (response.ok && resBody.success) {
224
+ return { success: true }
225
+ }
226
+ else {
227
+ return { success: false }
228
+ }
229
+ }
230
+ catch (error) {
231
+ console.error('Error logging out on server', error)
232
+ return { success: false }
233
+ }
234
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Firebase Auth Service exports
3
+ */
4
+
5
+ export { default as FirebaseAuthService } from './firebase-auth-service'
6
+ export {
7
+ auth,
8
+ db,
9
+ isFirebaseConfigured,
10
+ firebaseConfig,
11
+ loginWithProvider,
12
+ signupWithEmailAndPassword,
13
+ loginWithEmailAndPassword,
14
+ loginWithCustomToken,
15
+ logoutBackend
16
+ } from './firebase-support'
17
+ export {
18
+ associateWalletAddressWithAccount,
19
+ getAssociatedWalletAddress
20
+ } from './wallet-support'
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Wallet Support - Firebase Firestore integration for wallet addresses
3
+ */
4
+
5
+ import { collection, doc, getDoc, setDoc } from 'firebase/firestore'
6
+
7
+ import { auth, db } from './firebase-support'
8
+
9
+ const isObject = (obj: any): obj is Record<any, any> => typeof obj === 'object' && obj !== null
10
+ const isGlobalThisEthereum = (obj: any): obj is { ethereum: { request: <R = any>(payload: Record<any, any>) => Promise<R> } } => isObject(obj) && isObject(obj.ethereum) && typeof obj.ethereum.request === 'function'
11
+ const getEthereum = (obj: any) => isGlobalThisEthereum(obj) ? obj.ethereum : null
12
+
13
+ const ethereum = getEthereum(globalThis)
14
+
15
+ let signMessage = (opts: { siteName: string, address: string }) => `${opts.siteName} wants you to sign in with your Ethereum account:\n${opts.address}`
16
+
17
+ const USER_INFO_COLLECTION = 'HZ_USER_INFO'
18
+
19
+ async function connectWalletAddress(siteName?: string) {
20
+ if (!ethereum) {
21
+ throw new Error('No ethereum provider found')
22
+ }
23
+
24
+ if (!auth) {
25
+ throw new Error('Firebase auth not configured')
26
+ }
27
+
28
+ const [account] = await ethereum.request<string[]>({ method: 'eth_requestAccounts' })
29
+
30
+ if (!account) {
31
+ throw new Error('No account found')
32
+ }
33
+
34
+ const signed = await ethereum.request<string>({
35
+ method: 'personal_sign',
36
+ params: [
37
+ signMessage({
38
+ siteName: siteName ?? auth.app.options.projectId ?? globalThis.location.hostname,
39
+ address: account,
40
+ }),
41
+ account,
42
+ auth.app.options.appId,
43
+ ],
44
+ })
45
+
46
+ if (!signed) {
47
+ throw new Error('Not signed')
48
+ }
49
+
50
+ return {account, signed}
51
+ }
52
+
53
+ export async function associateWalletAddressWithAccount(userEmail: string, siteName?: string) {
54
+ const {account} = await connectWalletAddress(siteName)
55
+
56
+ if (!db) {
57
+ return { result: null, error: new Error('Firebase Firestore not configured') }
58
+ }
59
+
60
+ let result = null
61
+ let error = null
62
+ const accountsRef = collection(db, USER_INFO_COLLECTION)
63
+
64
+ try {
65
+ try {
66
+ await setDoc(doc(accountsRef, userEmail), { walletAddress: account })
67
+ result = account
68
+ } catch (e) {
69
+ console.error(e)
70
+ error = e
71
+ }
72
+ } catch (e) {
73
+ console.error(e)
74
+ error = e
75
+ }
76
+
77
+ return { result, error }
78
+ }
79
+
80
+ export async function getAssociatedWalletAddress(userEmail: string) : Promise<{error: any, result?: string}> {
81
+ if (!db) {
82
+ return { error: new Error('Firebase Firestore not configured'), result: undefined }
83
+ }
84
+
85
+ let result = undefined
86
+ let error = null
87
+ try {
88
+ try {
89
+ const docRef = await getDoc(doc(db, USER_INFO_COLLECTION, userEmail))
90
+ result = docRef.data() ? docRef.data()!.walletAddress as string : undefined
91
+ }
92
+ catch (e) {
93
+ console.error(e)
94
+ error = e
95
+ }
96
+ }
97
+ catch (e) {
98
+ console.error(e)
99
+ error = e
100
+ }
101
+
102
+ return { result, error }
103
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "extends": "../tsconfig.libreapps.base.json",
3
+ "include": [
4
+ "**/*.ts",
5
+ "**/*.tsx"
6
+ ],
7
+ "exclude": [
8
+ "node_modules",
9
+ "dist"
10
+ ],
11
+ "compilerOptions": {
12
+ "noEmit": false,
13
+ "declaration": true
14
+ }
15
+ }