@sneat/auth-core 0.1.3 → 0.1.6
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.
- package/esm2022/index.js +8 -0
- package/esm2022/index.js.map +1 -0
- package/esm2022/lib/login-required-service.service.js +20 -0
- package/esm2022/lib/login-required-service.service.js.map +1 -0
- package/esm2022/lib/private-token-store.service.js +36 -0
- package/esm2022/lib/private-token-store.service.js.map +1 -0
- package/esm2022/lib/sneat-auth-guard.js +79 -0
- package/esm2022/lib/sneat-auth-guard.js.map +1 -0
- package/esm2022/lib/sneat-auth-state-service.js +267 -0
- package/esm2022/lib/sneat-auth-state-service.js.map +1 -0
- package/{src/lib/sneat-auth.interface.ts → esm2022/lib/sneat-auth.interface.js} +1 -5
- package/esm2022/lib/sneat-auth.interface.js.map +1 -0
- package/esm2022/lib/telegram-auth.service.js +39 -0
- package/esm2022/lib/telegram-auth.service.js.map +1 -0
- package/esm2022/lib/user/index.js +3 -0
- package/esm2022/lib/user/index.js.map +1 -0
- package/esm2022/lib/user/sneat-user.service.js +171 -0
- package/esm2022/lib/user/sneat-user.service.js.map +1 -0
- package/esm2022/lib/user/user-record.service.js +30 -0
- package/esm2022/lib/user/user-record.service.js.map +1 -0
- package/esm2022/sneat-auth-core.js +5 -0
- package/esm2022/sneat-auth-core.js.map +1 -0
- package/lib/login-required-service.service.d.ts +6 -0
- package/lib/private-token-store.service.d.ts +8 -0
- package/lib/sneat-auth-guard.d.ts +26 -0
- package/lib/sneat-auth-state-service.d.ts +51 -0
- package/lib/sneat-auth.interface.d.ts +5 -0
- package/lib/telegram-auth.service.d.ts +9 -0
- package/lib/user/sneat-user.service.d.ts +34 -0
- package/lib/user/user-record.service.d.ts +25 -0
- package/package.json +14 -2
- package/sneat-auth-core.d.ts +5 -0
- package/tsconfig.lib.prod.tsbuildinfo +1 -0
- package/eslint.config.js +0 -7
- package/ng-package.json +0 -7
- package/project.json +0 -38
- package/src/lib/login-required-service.service.spec.ts +0 -39
- package/src/lib/login-required-service.service.ts +0 -14
- package/src/lib/private-token-store.service.spec.ts +0 -75
- package/src/lib/private-token-store.service.ts +0 -36
- package/src/lib/sneat-auth-guard.spec.ts +0 -124
- package/src/lib/sneat-auth-guard.ts +0 -107
- package/src/lib/sneat-auth-state-service.spec.ts +0 -332
- package/src/lib/sneat-auth-state-service.ts +0 -387
- package/src/lib/sneat-auth.interface.spec.ts +0 -39
- package/src/lib/telegram-auth.service.spec.ts +0 -186
- package/src/lib/telegram-auth.service.ts +0 -49
- package/src/lib/user/sneat-user.service.spec.ts +0 -151
- package/src/lib/user/sneat-user.service.ts +0 -266
- package/src/lib/user/user-record.service.spec.ts +0 -145
- package/src/lib/user/user-record.service.ts +0 -38
- package/src/test-setup.ts +0 -3
- package/tsconfig.json +0 -13
- package/tsconfig.lib.json +0 -19
- package/tsconfig.lib.prod.json +0 -7
- package/tsconfig.spec.json +0 -31
- package/vite.config.mts +0 -10
- /package/{src/index.ts → index.d.ts} +0 -0
- /package/{src/lib/user/index.ts → lib/user/index.d.ts} +0 -0
|
@@ -1,332 +0,0 @@
|
|
|
1
|
-
import { TestBed } from '@angular/core/testing';
|
|
2
|
-
import { Auth, UserCredential } from '@angular/fire/auth';
|
|
3
|
-
import { AnalyticsService, ErrorLogger } from '@sneat/core';
|
|
4
|
-
import {
|
|
5
|
-
SneatAuthStateService,
|
|
6
|
-
AuthStatuses,
|
|
7
|
-
} from './sneat-auth-state-service';
|
|
8
|
-
import { describe, it, expect, beforeEach, vi, Mock } from 'vitest';
|
|
9
|
-
import { firstValueFrom, Observer } from 'rxjs';
|
|
10
|
-
import { User } from '@angular/fire/auth';
|
|
11
|
-
|
|
12
|
-
// Mock Capacitor
|
|
13
|
-
vi.mock('@capacitor/core', () => ({
|
|
14
|
-
Capacitor: {
|
|
15
|
-
isNativePlatform: vi.fn().mockReturnValue(false),
|
|
16
|
-
},
|
|
17
|
-
}));
|
|
18
|
-
|
|
19
|
-
// Mock Firebase Authentication
|
|
20
|
-
vi.mock('@capacitor-firebase/authentication', () => ({
|
|
21
|
-
FirebaseAuthentication: {
|
|
22
|
-
signInWithGoogle: vi.fn(),
|
|
23
|
-
signInWithApple: vi.fn(),
|
|
24
|
-
signInWithFacebook: vi.fn(),
|
|
25
|
-
signInWithMicrosoft: vi.fn(),
|
|
26
|
-
},
|
|
27
|
-
}));
|
|
28
|
-
|
|
29
|
-
// Mock firebase/auth with proper constructor mocks
|
|
30
|
-
vi.mock('firebase/auth', () => {
|
|
31
|
-
class MockGoogleAuthProvider {}
|
|
32
|
-
class MockOAuthProvider {
|
|
33
|
-
credential = vi.fn();
|
|
34
|
-
}
|
|
35
|
-
class MockGithubAuthProvider {
|
|
36
|
-
addScope = vi.fn();
|
|
37
|
-
}
|
|
38
|
-
class MockFacebookAuthProvider {
|
|
39
|
-
addScope = vi.fn();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
GoogleAuthProvider: MockGoogleAuthProvider,
|
|
44
|
-
OAuthProvider: MockOAuthProvider,
|
|
45
|
-
GithubAuthProvider: MockGithubAuthProvider,
|
|
46
|
-
FacebookAuthProvider: MockFacebookAuthProvider,
|
|
47
|
-
signInWithEmailLink: vi.fn().mockResolvedValue({ user: { uid: 'test' } }),
|
|
48
|
-
signInWithCustomToken: vi.fn().mockResolvedValue({ user: { uid: 'test' } }),
|
|
49
|
-
signInWithPopup: vi.fn().mockResolvedValue({ user: { uid: 'test' } }),
|
|
50
|
-
linkWithPopup: vi.fn().mockResolvedValue({ user: { uid: 'test' } }),
|
|
51
|
-
unlink: vi.fn().mockResolvedValue({ uid: 'test', providerData: [] }),
|
|
52
|
-
signInWithCredential: vi.fn().mockResolvedValue({ user: { uid: 'test' } }),
|
|
53
|
-
getAuth: vi.fn().mockReturnValue({}),
|
|
54
|
-
};
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe('SneatAuthStateService', () => {
|
|
58
|
-
let service: SneatAuthStateService;
|
|
59
|
-
let authMock: {
|
|
60
|
-
onIdTokenChanged: Mock;
|
|
61
|
-
onAuthStateChanged: Mock;
|
|
62
|
-
signOut: Mock;
|
|
63
|
-
currentUser: User | null;
|
|
64
|
-
};
|
|
65
|
-
let onAuthStateChangedCallback: Observer<User | null>;
|
|
66
|
-
let onIdTokenChangedCallback: Observer<User | null>;
|
|
67
|
-
|
|
68
|
-
beforeEach(() => {
|
|
69
|
-
authMock = {
|
|
70
|
-
onIdTokenChanged: vi.fn().mockImplementation((obs) => {
|
|
71
|
-
onIdTokenChangedCallback = obs;
|
|
72
|
-
}),
|
|
73
|
-
onAuthStateChanged: vi.fn().mockImplementation((obs) => {
|
|
74
|
-
onAuthStateChangedCallback = obs;
|
|
75
|
-
}),
|
|
76
|
-
signOut: vi.fn().mockResolvedValue(undefined),
|
|
77
|
-
currentUser: null,
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
TestBed.configureTestingModule({
|
|
81
|
-
providers: [
|
|
82
|
-
SneatAuthStateService,
|
|
83
|
-
{
|
|
84
|
-
provide: ErrorLogger,
|
|
85
|
-
useValue: { logError: vi.fn(), logErrorHandler: () => vi.fn() },
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
provide: AnalyticsService,
|
|
89
|
-
useValue: {
|
|
90
|
-
identify: vi.fn(),
|
|
91
|
-
logEvent: vi.fn(),
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
provide: Auth,
|
|
96
|
-
useValue: authMock,
|
|
97
|
-
},
|
|
98
|
-
],
|
|
99
|
-
});
|
|
100
|
-
service = TestBed.inject(SneatAuthStateService);
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('should be created', () => {
|
|
104
|
-
expect(service).toBeTruthy();
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('should update auth status when onAuthStateChanged triggers', async () => {
|
|
108
|
-
const fbUser = {
|
|
109
|
-
uid: 'u1',
|
|
110
|
-
isAnonymous: false,
|
|
111
|
-
emailVerified: true,
|
|
112
|
-
email: 'test@example.com',
|
|
113
|
-
providerId: 'password',
|
|
114
|
-
providerData: [],
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
onAuthStateChangedCallback.next(fbUser as unknown as User);
|
|
118
|
-
|
|
119
|
-
const status = await firstValueFrom(service.authStatus);
|
|
120
|
-
expect(status).toBe(AuthStatuses.authenticated);
|
|
121
|
-
|
|
122
|
-
const user = await firstValueFrom(service.authUser);
|
|
123
|
-
expect(user?.uid).toBe('u1');
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('should call fbAuth.signOut when signOut is called', async () => {
|
|
127
|
-
await service.signOut();
|
|
128
|
-
expect(authMock.signOut).toHaveBeenCalled();
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('should handle null user in onAuthStateChanged', async () => {
|
|
132
|
-
onAuthStateChangedCallback.next(null);
|
|
133
|
-
|
|
134
|
-
const status = await firstValueFrom(service.authStatus);
|
|
135
|
-
expect(status).toBe(AuthStatuses.notAuthenticated);
|
|
136
|
-
|
|
137
|
-
const user = await firstValueFrom(service.authUser);
|
|
138
|
-
expect(user).toBeNull();
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('should handle user with multiple providerData', async () => {
|
|
142
|
-
const fbUser = {
|
|
143
|
-
uid: 'u2',
|
|
144
|
-
isAnonymous: false,
|
|
145
|
-
emailVerified: true,
|
|
146
|
-
email: 'multi@example.com',
|
|
147
|
-
providerId: 'firebase',
|
|
148
|
-
providerData: [
|
|
149
|
-
{ providerId: 'google.com', uid: 'g123' },
|
|
150
|
-
{ providerId: 'facebook.com', uid: 'f456' },
|
|
151
|
-
],
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
onAuthStateChangedCallback.next(fbUser as unknown as User);
|
|
155
|
-
|
|
156
|
-
const user = await firstValueFrom(service.authUser);
|
|
157
|
-
expect(user?.uid).toBe('u2');
|
|
158
|
-
expect(user?.providerId).toBe('firebase');
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('should handle user with single providerData', async () => {
|
|
162
|
-
const fbUser = {
|
|
163
|
-
uid: 'u3',
|
|
164
|
-
isAnonymous: false,
|
|
165
|
-
emailVerified: true,
|
|
166
|
-
email: 'single@example.com',
|
|
167
|
-
providerId: 'firebase',
|
|
168
|
-
providerData: [{ providerId: 'google.com', uid: 'g789' }],
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
onAuthStateChangedCallback.next(fbUser as unknown as User);
|
|
172
|
-
|
|
173
|
-
const user = await firstValueFrom(service.authUser);
|
|
174
|
-
expect(user?.providerId).toBe('google.com');
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('should handle onIdTokenChanged with authenticated user', async () => {
|
|
178
|
-
const fbUser = {
|
|
179
|
-
uid: 'u4',
|
|
180
|
-
isAnonymous: false,
|
|
181
|
-
emailVerified: true,
|
|
182
|
-
email: 'token@example.com',
|
|
183
|
-
providerId: 'google.com',
|
|
184
|
-
providerData: [],
|
|
185
|
-
getIdToken: vi.fn().mockResolvedValue('mock-token-123'),
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
// First set auth user
|
|
189
|
-
onAuthStateChangedCallback.next(fbUser as unknown as User);
|
|
190
|
-
|
|
191
|
-
// Then trigger token changed
|
|
192
|
-
onIdTokenChangedCallback.next(fbUser as unknown as User);
|
|
193
|
-
|
|
194
|
-
// Wait for async operations
|
|
195
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
196
|
-
|
|
197
|
-
const state = await firstValueFrom(service.authState);
|
|
198
|
-
expect(state.status).toBe(AuthStatuses.authenticated);
|
|
199
|
-
expect(state.token).toBe('mock-token-123');
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('should handle error in getIdToken', async () => {
|
|
203
|
-
const errorLogger = TestBed.inject(ErrorLogger);
|
|
204
|
-
const fbUser = {
|
|
205
|
-
uid: 'u5',
|
|
206
|
-
isAnonymous: false,
|
|
207
|
-
emailVerified: true,
|
|
208
|
-
email: 'error@example.com',
|
|
209
|
-
providerId: 'google.com',
|
|
210
|
-
providerData: [],
|
|
211
|
-
getIdToken: vi.fn().mockRejectedValue(new Error('Token error')),
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
onAuthStateChangedCallback.next(fbUser as unknown as User);
|
|
215
|
-
onIdTokenChangedCallback.next(fbUser as unknown as User);
|
|
216
|
-
|
|
217
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
218
|
-
|
|
219
|
-
expect(errorLogger.logError).toHaveBeenCalled();
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('should handle error in onAuthStateChanged', () => {
|
|
223
|
-
const errorLogger = TestBed.inject(ErrorLogger);
|
|
224
|
-
const error = new Error('Auth state error');
|
|
225
|
-
|
|
226
|
-
onAuthStateChangedCallback.error(error);
|
|
227
|
-
|
|
228
|
-
expect(errorLogger.logError).toHaveBeenCalledWith(
|
|
229
|
-
error,
|
|
230
|
-
'failed to retrieve Firebase auth user information',
|
|
231
|
-
);
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it('should handle error in onIdTokenChanged', () => {
|
|
235
|
-
const errorLogger = TestBed.inject(ErrorLogger);
|
|
236
|
-
const error = new Error('Token changed error');
|
|
237
|
-
|
|
238
|
-
onIdTokenChangedCallback.error(error);
|
|
239
|
-
|
|
240
|
-
expect(errorLogger.logError).toHaveBeenCalledWith(
|
|
241
|
-
error,
|
|
242
|
-
'failed in fbAuth.onIdTokenChanged',
|
|
243
|
-
);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
describe('signInWithToken', () => {
|
|
247
|
-
it('should call signInWithCustomToken', async () => {
|
|
248
|
-
const { signInWithCustomToken } = await import('firebase/auth');
|
|
249
|
-
const mockCredential = { user: { uid: 'test' } } as UserCredential;
|
|
250
|
-
(signInWithCustomToken as Mock).mockResolvedValue(mockCredential);
|
|
251
|
-
|
|
252
|
-
const result = await service.signInWithToken('custom-token');
|
|
253
|
-
|
|
254
|
-
expect(signInWithCustomToken).toHaveBeenCalledWith(
|
|
255
|
-
authMock,
|
|
256
|
-
'custom-token',
|
|
257
|
-
);
|
|
258
|
-
expect(result).toBe(mockCredential);
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
describe('signInWithEmailLink', () => {
|
|
263
|
-
it('should return observable that calls signInWithEmailLink', async () => {
|
|
264
|
-
const { signInWithEmailLink } = await import('firebase/auth');
|
|
265
|
-
|
|
266
|
-
const result = await firstValueFrom(
|
|
267
|
-
service.signInWithEmailLink('test@example.com'),
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
expect(signInWithEmailLink).toHaveBeenCalled();
|
|
271
|
-
expect(result.user.uid).toBe('test');
|
|
272
|
-
});
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
describe('linkWith', () => {
|
|
276
|
-
it('should reject if no current user', async () => {
|
|
277
|
-
authMock.currentUser = null;
|
|
278
|
-
|
|
279
|
-
await expect(service.linkWith('google.com')).rejects.toBe(
|
|
280
|
-
'no current user',
|
|
281
|
-
);
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it('should link provider to current user', async () => {
|
|
285
|
-
const { linkWithPopup } = await import('firebase/auth');
|
|
286
|
-
const mockUser = { uid: 'current-user' } as User;
|
|
287
|
-
authMock.currentUser = mockUser;
|
|
288
|
-
|
|
289
|
-
const result = await service.linkWith('google.com');
|
|
290
|
-
|
|
291
|
-
expect(linkWithPopup).toHaveBeenCalled();
|
|
292
|
-
expect(result?.user.uid).toBe('test');
|
|
293
|
-
});
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
describe('unlinkAuthProvider', () => {
|
|
297
|
-
it('should reject if no current user', async () => {
|
|
298
|
-
authMock.currentUser = null;
|
|
299
|
-
|
|
300
|
-
await expect(service.unlinkAuthProvider('google.com')).rejects.toBe(
|
|
301
|
-
'No user is currently signed in.',
|
|
302
|
-
);
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it('should unlink auth provider from current user', async () => {
|
|
306
|
-
const { unlink } = await import('firebase/auth');
|
|
307
|
-
const mockUser = {
|
|
308
|
-
uid: 'current-user',
|
|
309
|
-
isAnonymous: false,
|
|
310
|
-
emailVerified: true,
|
|
311
|
-
providerData: [],
|
|
312
|
-
} as unknown as User;
|
|
313
|
-
authMock.currentUser = mockUser;
|
|
314
|
-
|
|
315
|
-
await service.unlinkAuthProvider('google.com');
|
|
316
|
-
|
|
317
|
-
expect(unlink).toHaveBeenCalledWith(mockUser, 'google.com');
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
it('should handle unlink error', async () => {
|
|
321
|
-
const { unlink } = await import('firebase/auth');
|
|
322
|
-
const mockUser = { uid: 'current-user' } as User;
|
|
323
|
-
authMock.currentUser = mockUser;
|
|
324
|
-
const error = new Error('Unlink failed');
|
|
325
|
-
(unlink as Mock).mockRejectedValueOnce(error);
|
|
326
|
-
|
|
327
|
-
await expect(service.unlinkAuthProvider('google.com')).rejects.toMatch(
|
|
328
|
-
/Failed to unlink google.com account/,
|
|
329
|
-
);
|
|
330
|
-
});
|
|
331
|
-
});
|
|
332
|
-
});
|
|
@@ -1,387 +0,0 @@
|
|
|
1
|
-
import { SignInWithOAuthOptions } from '@capacitor-firebase/authentication/dist/esm/definitions';
|
|
2
|
-
import { Capacitor } from '@capacitor/core';
|
|
3
|
-
import {
|
|
4
|
-
FirebaseAuthentication,
|
|
5
|
-
SignInResult,
|
|
6
|
-
} from '@capacitor-firebase/authentication';
|
|
7
|
-
import {
|
|
8
|
-
AnalyticsService,
|
|
9
|
-
EnumAsUnionOfKeys,
|
|
10
|
-
IAnalyticsService,
|
|
11
|
-
} from '@sneat/core';
|
|
12
|
-
import { BehaviorSubject, from, Observable } from 'rxjs';
|
|
13
|
-
import { Injectable, inject } from '@angular/core';
|
|
14
|
-
import { ErrorLogger, IErrorLogger } from '@sneat/core';
|
|
15
|
-
import { distinctUntilChanged, shareReplay } from 'rxjs/operators';
|
|
16
|
-
import {
|
|
17
|
-
Auth,
|
|
18
|
-
AuthProvider,
|
|
19
|
-
getAuth,
|
|
20
|
-
signInWithCredential,
|
|
21
|
-
User,
|
|
22
|
-
UserCredential,
|
|
23
|
-
UserInfo,
|
|
24
|
-
} from '@angular/fire/auth';
|
|
25
|
-
|
|
26
|
-
import {
|
|
27
|
-
GoogleAuthProvider,
|
|
28
|
-
OAuthProvider,
|
|
29
|
-
GithubAuthProvider,
|
|
30
|
-
FacebookAuthProvider,
|
|
31
|
-
signInWithEmailLink,
|
|
32
|
-
signInWithCustomToken,
|
|
33
|
-
signInWithPopup,
|
|
34
|
-
linkWithPopup,
|
|
35
|
-
unlink,
|
|
36
|
-
} from 'firebase/auth';
|
|
37
|
-
|
|
38
|
-
// TODO: fix & remove this eslint hint @nrwl/nx/enforce-module-boundaries
|
|
39
|
-
|
|
40
|
-
import { newRandomId } from '@sneat/random';
|
|
41
|
-
|
|
42
|
-
export enum AuthStatuses {
|
|
43
|
-
authenticating = 'authenticating',
|
|
44
|
-
authenticated = 'authenticated',
|
|
45
|
-
notAuthenticated = 'notAuthenticated',
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export type AuthStatus = EnumAsUnionOfKeys<typeof AuthStatuses>;
|
|
49
|
-
|
|
50
|
-
export interface ISneatAuthUser extends UserInfo {
|
|
51
|
-
readonly isAnonymous: boolean;
|
|
52
|
-
readonly emailVerified: boolean;
|
|
53
|
-
readonly providerData: readonly UserInfo[];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface ISneatAuthState {
|
|
57
|
-
readonly status: AuthStatus;
|
|
58
|
-
readonly token?: string | null;
|
|
59
|
-
readonly user?: ISneatAuthUser | null;
|
|
60
|
-
readonly err?: unknown;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const initialAuthStatus = AuthStatuses.authenticating;
|
|
64
|
-
export const initialSneatAuthState = { status: initialAuthStatus };
|
|
65
|
-
|
|
66
|
-
@Injectable({ providedIn: 'root' })
|
|
67
|
-
export class SneatAuthStateService {
|
|
68
|
-
private readonly errorLogger = inject<IErrorLogger>(ErrorLogger);
|
|
69
|
-
private readonly analyticsService =
|
|
70
|
-
inject<IAnalyticsService>(AnalyticsService);
|
|
71
|
-
readonly fbAuth = inject(Auth);
|
|
72
|
-
|
|
73
|
-
private readonly id = newRandomId({ len: 5 });
|
|
74
|
-
|
|
75
|
-
private readonly authStatus$ = new BehaviorSubject<AuthStatus>(
|
|
76
|
-
initialAuthStatus,
|
|
77
|
-
);
|
|
78
|
-
public readonly authStatus = this.authStatus$
|
|
79
|
-
.asObservable()
|
|
80
|
-
.pipe(distinctUntilChanged());
|
|
81
|
-
|
|
82
|
-
private readonly authUser$ = new BehaviorSubject<
|
|
83
|
-
ISneatAuthUser | null | undefined
|
|
84
|
-
>(undefined);
|
|
85
|
-
public readonly authUser = this.authUser$.asObservable();
|
|
86
|
-
|
|
87
|
-
private readonly authState$ = new BehaviorSubject<ISneatAuthState>(
|
|
88
|
-
initialSneatAuthState,
|
|
89
|
-
);
|
|
90
|
-
public readonly authState = this.authState$.asObservable().pipe(
|
|
91
|
-
// tap(v => console.log('SneatAuthStateService => SneatAuthState:', v)),
|
|
92
|
-
shareReplay(1),
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
// private readonly fbAuth: Auth;
|
|
96
|
-
|
|
97
|
-
constructor() {
|
|
98
|
-
const errorLogger = this.errorLogger;
|
|
99
|
-
this.fbAuth.onIdTokenChanged({
|
|
100
|
-
next: (firebaseUser) => {
|
|
101
|
-
const status: AuthStatus = firebaseUser
|
|
102
|
-
? AuthStatuses.authenticated
|
|
103
|
-
: AuthStatuses.notAuthenticated;
|
|
104
|
-
if (
|
|
105
|
-
firebaseUser &&
|
|
106
|
-
this.authState$.value?.user?.uid !== firebaseUser?.uid
|
|
107
|
-
) {
|
|
108
|
-
this.analyticsService.identify(firebaseUser.uid);
|
|
109
|
-
}
|
|
110
|
-
firebaseUser
|
|
111
|
-
?.getIdToken()
|
|
112
|
-
.then((token) => {
|
|
113
|
-
const current = this.authState$.value || {};
|
|
114
|
-
this.authState$.next({
|
|
115
|
-
...current,
|
|
116
|
-
status,
|
|
117
|
-
token,
|
|
118
|
-
user: this.authUser$.value,
|
|
119
|
-
});
|
|
120
|
-
this.authStatus$.next(status); // Should be after authState$
|
|
121
|
-
})
|
|
122
|
-
.catch((err) => {
|
|
123
|
-
const current = this.authState$.value || {};
|
|
124
|
-
this.authState$.next({
|
|
125
|
-
...current,
|
|
126
|
-
err: `fbUser.getIdToken() failed: ${err}`,
|
|
127
|
-
});
|
|
128
|
-
this.errorLogger.logError(err, 'Failed in fbUser.getIdToken()');
|
|
129
|
-
});
|
|
130
|
-
},
|
|
131
|
-
error: (err) => {
|
|
132
|
-
const current = this.authState$.value || {};
|
|
133
|
-
this.authState$.next({
|
|
134
|
-
...current,
|
|
135
|
-
err: `fbAuth.onIdTokenChanged() failed: ${err}`,
|
|
136
|
-
});
|
|
137
|
-
errorLogger.logError(err, 'failed in fbAuth.onIdTokenChanged');
|
|
138
|
-
},
|
|
139
|
-
complete: () => void 0,
|
|
140
|
-
});
|
|
141
|
-
this.fbAuth.onAuthStateChanged({
|
|
142
|
-
complete: () => void 0,
|
|
143
|
-
next: (fbUser) => {
|
|
144
|
-
// console.log(
|
|
145
|
-
// `SneatAuthStateService => authStatus: ${this.authStatus$.value}; fbUser`,
|
|
146
|
-
// fbUser,
|
|
147
|
-
// );
|
|
148
|
-
|
|
149
|
-
const authUser = createSneatAuthUserFromFbUser(fbUser);
|
|
150
|
-
|
|
151
|
-
const status = authUser
|
|
152
|
-
? AuthStatuses.authenticated
|
|
153
|
-
: AuthStatuses.notAuthenticated;
|
|
154
|
-
this.authStatus$.next(status);
|
|
155
|
-
this.authUser$.next(authUser);
|
|
156
|
-
this.authState$.next({
|
|
157
|
-
...this.authState$.value,
|
|
158
|
-
user: authUser,
|
|
159
|
-
status,
|
|
160
|
-
});
|
|
161
|
-
},
|
|
162
|
-
error: (err) => {
|
|
163
|
-
this.errorLogger.logError(
|
|
164
|
-
err,
|
|
165
|
-
'failed to retrieve Firebase auth user information',
|
|
166
|
-
);
|
|
167
|
-
const current = this.authState$.value || {};
|
|
168
|
-
this.authState$.next({
|
|
169
|
-
...current,
|
|
170
|
-
err: `fbAuth.onAuthStateChanged() failed: ${err}`,
|
|
171
|
-
});
|
|
172
|
-
},
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
public signOut(): Promise<void> {
|
|
177
|
-
return this.fbAuth.signOut();
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
public signInWithToken(token: string): Promise<UserCredential> {
|
|
181
|
-
return signInWithCustomToken(this.fbAuth, token);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
public signInWithEmailLink(email: string): Observable<UserCredential> {
|
|
185
|
-
return from(signInWithEmailLink(this.fbAuth, email));
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
private isSigningInWith?: AuthProviderName;
|
|
189
|
-
|
|
190
|
-
private async signInOnNativeLayer(
|
|
191
|
-
authProviderID: AuthProviderID,
|
|
192
|
-
): Promise<UserCredential> {
|
|
193
|
-
let signInResult: SignInResult | undefined;
|
|
194
|
-
|
|
195
|
-
const o: SignInWithOAuthOptions = { skipNativeAuth: true };
|
|
196
|
-
|
|
197
|
-
switch (authProviderID) {
|
|
198
|
-
case 'google.com':
|
|
199
|
-
signInResult = await FirebaseAuthentication.signInWithGoogle(o);
|
|
200
|
-
break;
|
|
201
|
-
case 'apple.com':
|
|
202
|
-
signInResult = await FirebaseAuthentication.signInWithApple(o);
|
|
203
|
-
break;
|
|
204
|
-
case 'facebook.com':
|
|
205
|
-
signInResult = await FirebaseAuthentication.signInWithFacebook(o);
|
|
206
|
-
break;
|
|
207
|
-
case 'microsoft.com':
|
|
208
|
-
signInResult = await FirebaseAuthentication.signInWithMicrosoft(o);
|
|
209
|
-
break;
|
|
210
|
-
default:
|
|
211
|
-
return Promise.reject('unsupported auth provider: ' + authProviderID);
|
|
212
|
-
}
|
|
213
|
-
// console.log(
|
|
214
|
-
// `SneatAuthStateService.signInWith(${authProviderID}) => signed in on native layer, authenticating in webview...`,
|
|
215
|
-
// signInResult,
|
|
216
|
-
// );
|
|
217
|
-
// we need to authenticate on webview layer using the id token and nonce from signInResult
|
|
218
|
-
const userCredential = await this.authenticateOnWebviewLayer(
|
|
219
|
-
authProviderID,
|
|
220
|
-
signInResult,
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
return Promise.resolve(userCredential);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
private async authenticateOnWebviewLayer(
|
|
227
|
-
authProviderID: AuthProviderID,
|
|
228
|
-
signInResult: SignInResult,
|
|
229
|
-
): Promise<UserCredential> {
|
|
230
|
-
const oauthProvider = new OAuthProvider(authProviderID);
|
|
231
|
-
const credential = oauthProvider.credential({
|
|
232
|
-
idToken: signInResult.credential?.idToken,
|
|
233
|
-
accessToken: signInResult.credential?.accessToken,
|
|
234
|
-
rawNonce: signInResult.credential?.nonce,
|
|
235
|
-
});
|
|
236
|
-
const auth = getAuth();
|
|
237
|
-
const userCredential = await signInWithCredential(auth, credential);
|
|
238
|
-
|
|
239
|
-
// Get a valid Firebase ID token that has a 'kid' header
|
|
240
|
-
const _firebaseIdToken = await userCredential.user.getIdToken();
|
|
241
|
-
|
|
242
|
-
return Promise.resolve(userCredential);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
private async signInWithWebSDK(
|
|
246
|
-
authProviderID: AuthProviderID,
|
|
247
|
-
): Promise<UserCredential> {
|
|
248
|
-
const authProvider = getAuthProvider(authProviderID);
|
|
249
|
-
const userCredential = await signInWithPopup(this.fbAuth, authProvider);
|
|
250
|
-
return Promise.resolve(userCredential);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
public async linkWith(
|
|
254
|
-
authProviderID: AuthProviderID,
|
|
255
|
-
): Promise<UserCredential | undefined> {
|
|
256
|
-
const authProvider = getAuthProvider(authProviderID);
|
|
257
|
-
if (!this.fbAuth.currentUser) {
|
|
258
|
-
return Promise.reject('no current user');
|
|
259
|
-
}
|
|
260
|
-
const userCredential = await linkWithPopup(
|
|
261
|
-
this.fbAuth.currentUser,
|
|
262
|
-
authProvider,
|
|
263
|
-
);
|
|
264
|
-
return Promise.resolve(userCredential);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
public async unlinkAuthProvider(authProviderID: string): Promise<void> {
|
|
268
|
-
const currentUser = this.fbAuth.currentUser;
|
|
269
|
-
if (currentUser) {
|
|
270
|
-
try {
|
|
271
|
-
const updatedUser = await unlink(currentUser, authProviderID);
|
|
272
|
-
const authUser = createSneatAuthUserFromFbUser(updatedUser);
|
|
273
|
-
this.authUser$.next(authUser);
|
|
274
|
-
return Promise.resolve();
|
|
275
|
-
} catch (error) {
|
|
276
|
-
return Promise.reject(
|
|
277
|
-
`Failed to unlink ${authProviderID} account:` + error,
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
} else {
|
|
281
|
-
return Promise.reject('No user is currently signed in.');
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
public async signInWith(
|
|
286
|
-
authProviderID: AuthProviderID,
|
|
287
|
-
): Promise<UserCredential | undefined> {
|
|
288
|
-
// console.log(
|
|
289
|
-
// `SneatAuthStateService.signInWith(${authProviderID}), isSigningInWith=${this.isSigningInWith}, location.protocol=${location.protocol}`,
|
|
290
|
-
// );
|
|
291
|
-
this.analyticsService.logEvent('signingInWith', {
|
|
292
|
-
provider: authProviderID,
|
|
293
|
-
});
|
|
294
|
-
try {
|
|
295
|
-
if (this.isSigningInWith) {
|
|
296
|
-
return Promise.reject(
|
|
297
|
-
new Error(
|
|
298
|
-
`a repeated call to SneatAuthStateService.signInWith(${authProviderID}) white previous sign in with ${this.isSigningInWith} is in progress`,
|
|
299
|
-
),
|
|
300
|
-
);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
let userCredential: UserCredential | undefined = undefined;
|
|
304
|
-
|
|
305
|
-
if (Capacitor.isNativePlatform()) {
|
|
306
|
-
userCredential = await this.signInOnNativeLayer(authProviderID);
|
|
307
|
-
} else {
|
|
308
|
-
userCredential = await this.signInWithWebSDK(authProviderID);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
this.isSigningInWith = undefined;
|
|
312
|
-
this.analyticsService.logEvent('signedInWith', {
|
|
313
|
-
provider: authProviderID,
|
|
314
|
-
});
|
|
315
|
-
return Promise.resolve(userCredential);
|
|
316
|
-
} catch (e) {
|
|
317
|
-
this.isSigningInWith = undefined;
|
|
318
|
-
return Promise.reject(e);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
export type AuthProviderID =
|
|
324
|
-
| 'apple.com'
|
|
325
|
-
| 'google.com'
|
|
326
|
-
| 'microsoft.com'
|
|
327
|
-
| 'facebook.com'
|
|
328
|
-
| 'github.com'
|
|
329
|
-
| 'phone';
|
|
330
|
-
|
|
331
|
-
export type AuthProviderName =
|
|
332
|
-
| 'Google'
|
|
333
|
-
| 'Apple'
|
|
334
|
-
| 'Microsoft'
|
|
335
|
-
| 'Facebook'
|
|
336
|
-
| 'GitHub';
|
|
337
|
-
|
|
338
|
-
function getAuthProvider(authProviderID: AuthProviderID): AuthProvider {
|
|
339
|
-
switch (authProviderID) {
|
|
340
|
-
case 'google.com':
|
|
341
|
-
return new GoogleAuthProvider();
|
|
342
|
-
case 'apple.com':
|
|
343
|
-
return new OAuthProvider('apple.com');
|
|
344
|
-
//
|
|
345
|
-
// https://developer.apple.com/documentation/sign_in_with_apple/incorporating-sign-in-with-apple-into-other-platforms
|
|
346
|
-
// (authProvider as OAuthProvider).setCustomParameters({
|
|
347
|
-
// // // Localize the Apple authentication screen in current app locale.
|
|
348
|
-
// locale: 'en', // TODO: set locale
|
|
349
|
-
// });
|
|
350
|
-
case 'microsoft.com':
|
|
351
|
-
return new OAuthProvider('microsoft.com');
|
|
352
|
-
case 'facebook.com': {
|
|
353
|
-
const facebookAuthProvider = new FacebookAuthProvider();
|
|
354
|
-
facebookAuthProvider.addScope('email');
|
|
355
|
-
return facebookAuthProvider;
|
|
356
|
-
}
|
|
357
|
-
case 'github.com': {
|
|
358
|
-
const githubAuthProvider = new GithubAuthProvider();
|
|
359
|
-
githubAuthProvider.addScope('read:user');
|
|
360
|
-
githubAuthProvider.addScope('user:email');
|
|
361
|
-
return githubAuthProvider;
|
|
362
|
-
}
|
|
363
|
-
default:
|
|
364
|
-
throw new Error('unsupported auth provider: ' + authProviderID);
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function createSneatAuthUserFromFbUser(
|
|
369
|
-
fbUser: User | null,
|
|
370
|
-
): ISneatAuthUser | null {
|
|
371
|
-
return (
|
|
372
|
-
fbUser && {
|
|
373
|
-
isAnonymous: fbUser.isAnonymous,
|
|
374
|
-
emailVerified: fbUser.emailVerified,
|
|
375
|
-
email: fbUser.email,
|
|
376
|
-
uid: fbUser.uid,
|
|
377
|
-
displayName: fbUser.displayName,
|
|
378
|
-
phoneNumber: fbUser.phoneNumber,
|
|
379
|
-
photoURL: fbUser.photoURL,
|
|
380
|
-
providerId:
|
|
381
|
-
fbUser.providerData?.length === 1 && fbUser.providerData[0]
|
|
382
|
-
? fbUser.providerData[0].providerId
|
|
383
|
-
: fbUser.providerId,
|
|
384
|
-
providerData: fbUser.providerData,
|
|
385
|
-
}
|
|
386
|
-
);
|
|
387
|
-
}
|