@sneat/auth-ui 0.1.0
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/eslint.config.cjs +7 -0
- package/ng-package.json +7 -0
- package/package.json +19 -0
- package/project.json +38 -0
- package/src/index.ts +3 -0
- package/src/lib/components/auth-menu-item/auth-menu-item.component.html +59 -0
- package/src/lib/components/auth-menu-item/auth-menu-item.component.spec.ts +5 -0
- package/src/lib/components/auth-menu-item/auth-menu-item.component.ts +116 -0
- package/src/lib/components/index.ts +3 -0
- package/src/lib/components/user-auth-providers/user-auth-accounts.component.html +127 -0
- package/src/lib/components/user-auth-providers/user-auth-accounts.component.spec.ts +47 -0
- package/src/lib/components/user-auth-providers/user-auth-accounts.component.ts +109 -0
- package/src/lib/components/user-auth-providers/user-auth-provider-status.html +42 -0
- package/src/lib/components/user-auth-providers/user-auth-provider-status.ts +172 -0
- package/src/lib/components/user-required-fields/index.ts +2 -0
- package/src/lib/components/user-required-fields/user-required-fields-modal.component.html +64 -0
- package/src/lib/components/user-required-fields/user-required-fields-modal.component.spec.ts +45 -0
- package/src/lib/components/user-required-fields/user-required-fields-modal.component.ts +141 -0
- package/src/lib/components/user-required-fields/user-required-fields.service.spec.ts +335 -0
- package/src/lib/components/user-required-fields/user-required-fields.service.ts +23 -0
- package/src/lib/pages/login-page/email-login-form/email-login-form.component.html +165 -0
- package/src/lib/pages/login-page/email-login-form/email-login-form.component.spec.ts +59 -0
- package/src/lib/pages/login-page/email-login-form/email-login-form.component.ts +356 -0
- package/src/lib/pages/login-page/index.ts +3 -0
- package/src/lib/pages/login-page/login-page.component.html +146 -0
- package/src/lib/pages/login-page/login-page.component.spec.ts +5 -0
- package/src/lib/pages/login-page/login-page.component.ts +209 -0
- package/src/lib/pages/login-page/login-with-telegram.component.spec.ts +42 -0
- package/src/lib/pages/login-page/login-with-telegram.component.ts +82 -0
- package/src/lib/pages/login-page/sneat-auth-with-telegram.service.spec.ts +31 -0
- package/src/lib/pages/login-page/sneat-auth-with-telegram.service.ts +56 -0
- package/src/lib/pages/sign-in-from-email-link/sign-in-from-email-link-page.component.html +35 -0
- package/src/lib/pages/sign-in-from-email-link/sign-in-from-email-link-page.component.spec.ts +43 -0
- package/src/lib/pages/sign-in-from-email-link/sign-in-from-email-link-page.component.ts +70 -0
- package/src/lib/pages/sneat-auth-routing.module.ts +25 -0
- package/src/lib/pipes/person-names.pipe.spec.ts +317 -0
- package/src/lib/pipes/person-names.pipe.ts +31 -0
- package/src/test-setup.ts +3 -0
- package/tsconfig.json +13 -0
- package/tsconfig.lib.json +19 -0
- package/tsconfig.lib.prod.json +7 -0
- package/tsconfig.spec.json +31 -0
- package/vite.config.mts +10 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { Component, signal, inject } from '@angular/core';
|
|
2
|
+
import { FormsModule } from '@angular/forms';
|
|
3
|
+
import { ActivatedRoute } from '@angular/router';
|
|
4
|
+
import { Capacitor } from '@capacitor/core';
|
|
5
|
+
import {
|
|
6
|
+
NavController,
|
|
7
|
+
IonBackButton,
|
|
8
|
+
IonButtons,
|
|
9
|
+
IonCard,
|
|
10
|
+
IonCardContent,
|
|
11
|
+
IonCol,
|
|
12
|
+
IonContent,
|
|
13
|
+
IonGrid,
|
|
14
|
+
IonHeader,
|
|
15
|
+
IonIcon,
|
|
16
|
+
IonItem,
|
|
17
|
+
IonItemDivider,
|
|
18
|
+
IonLabel,
|
|
19
|
+
IonList,
|
|
20
|
+
IonRow,
|
|
21
|
+
IonSpinner,
|
|
22
|
+
IonText,
|
|
23
|
+
IonTitle,
|
|
24
|
+
IonToolbar,
|
|
25
|
+
} from '@ionic/angular/standalone';
|
|
26
|
+
import {
|
|
27
|
+
AuthProviderID,
|
|
28
|
+
AuthStatuses,
|
|
29
|
+
ILoginEventsHandler,
|
|
30
|
+
ISneatAuthState,
|
|
31
|
+
LoginEventsHandler,
|
|
32
|
+
SneatAuthStateService,
|
|
33
|
+
} from '@sneat/auth-core';
|
|
34
|
+
import { SneatUserService } from '@sneat/auth-core';
|
|
35
|
+
import {
|
|
36
|
+
AnalyticsService,
|
|
37
|
+
APP_INFO,
|
|
38
|
+
IAnalyticsService,
|
|
39
|
+
IAppInfo,
|
|
40
|
+
} from '@sneat/core';
|
|
41
|
+
import { RandomIdService } from '@sneat/random';
|
|
42
|
+
import { ClassName, SneatBaseComponent } from '@sneat/ui';
|
|
43
|
+
import { Subject, takeUntil } from 'rxjs';
|
|
44
|
+
import {
|
|
45
|
+
EmailFormSigningWith,
|
|
46
|
+
EmailLoginFormComponent,
|
|
47
|
+
} from './email-login-form/email-login-form.component';
|
|
48
|
+
import { UserCredential } from 'firebase/auth';
|
|
49
|
+
import { LoginWithTelegramComponent } from './login-with-telegram.component';
|
|
50
|
+
|
|
51
|
+
type Action = 'join' | 'refuse'; // TODO: inject provider for action descriptions/messages.
|
|
52
|
+
|
|
53
|
+
@Component({
|
|
54
|
+
selector: 'sneat-login',
|
|
55
|
+
templateUrl: './login-page.component.html',
|
|
56
|
+
imports: [
|
|
57
|
+
FormsModule,
|
|
58
|
+
LoginWithTelegramComponent,
|
|
59
|
+
EmailLoginFormComponent,
|
|
60
|
+
IonHeader,
|
|
61
|
+
IonToolbar,
|
|
62
|
+
IonButtons,
|
|
63
|
+
IonBackButton,
|
|
64
|
+
IonTitle,
|
|
65
|
+
IonContent,
|
|
66
|
+
IonCardContent,
|
|
67
|
+
IonText,
|
|
68
|
+
IonCard,
|
|
69
|
+
IonItemDivider,
|
|
70
|
+
IonLabel,
|
|
71
|
+
IonRow,
|
|
72
|
+
IonCol,
|
|
73
|
+
IonItem,
|
|
74
|
+
IonSpinner,
|
|
75
|
+
IonIcon,
|
|
76
|
+
IonList,
|
|
77
|
+
IonGrid,
|
|
78
|
+
],
|
|
79
|
+
providers: [
|
|
80
|
+
{
|
|
81
|
+
provide: ClassName,
|
|
82
|
+
useValue: 'LoginPageComponent',
|
|
83
|
+
},
|
|
84
|
+
RandomIdService,
|
|
85
|
+
],
|
|
86
|
+
})
|
|
87
|
+
export class LoginPageComponent extends SneatBaseComponent {
|
|
88
|
+
private readonly analyticsService =
|
|
89
|
+
inject<IAnalyticsService>(AnalyticsService);
|
|
90
|
+
private readonly route = inject(ActivatedRoute);
|
|
91
|
+
private readonly navController = inject(NavController);
|
|
92
|
+
private readonly userService = inject(SneatUserService);
|
|
93
|
+
private readonly authStateService = inject(SneatAuthStateService);
|
|
94
|
+
private appInfo = inject<IAppInfo>(APP_INFO);
|
|
95
|
+
private readonly loginEventsHandler = inject<ILoginEventsHandler>(
|
|
96
|
+
LoginEventsHandler,
|
|
97
|
+
{ optional: true },
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
protected readonly signingWith = signal<AuthProviderID | undefined>(
|
|
101
|
+
undefined,
|
|
102
|
+
);
|
|
103
|
+
private readonly redirectTo?: string;
|
|
104
|
+
protected readonly to?: string;
|
|
105
|
+
protected readonly action?: Action; // TODO: document possible values?
|
|
106
|
+
|
|
107
|
+
protected readonly isNativePlatform = Capacitor.isNativePlatform();
|
|
108
|
+
|
|
109
|
+
protected readonly appTitle: string;
|
|
110
|
+
|
|
111
|
+
constructor() {
|
|
112
|
+
super();
|
|
113
|
+
const appInfo = this.appInfo;
|
|
114
|
+
this.appTitle = appInfo.appTitle || 'Sneat.app';
|
|
115
|
+
if (location.hash.startsWith('#/')) {
|
|
116
|
+
this.redirectTo = location.hash.substring(1);
|
|
117
|
+
}
|
|
118
|
+
this.to = this.route.snapshot.queryParams['to']; // should we subscribe? I believe no.
|
|
119
|
+
const action = location.hash.match(/[#&]action=(\w+)/);
|
|
120
|
+
this.action = action?.[1] as Action;
|
|
121
|
+
|
|
122
|
+
const userRecordLoaded = new Subject<void>();
|
|
123
|
+
this.userService.userState
|
|
124
|
+
.pipe(takeUntil(userRecordLoaded), this.takeUntilDestroyed())
|
|
125
|
+
.subscribe({
|
|
126
|
+
next: (userState) => {
|
|
127
|
+
if (userState.record) {
|
|
128
|
+
userRecordLoaded.next();
|
|
129
|
+
} else {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const redirectTo = this.redirectTo || '/'; // TODO: default one should be app specific.
|
|
133
|
+
this.navController
|
|
134
|
+
.navigateRoot(redirectTo)
|
|
135
|
+
.catch(
|
|
136
|
+
this.errorLogger.logErrorHandler(
|
|
137
|
+
'Failed to navigate back to ' + redirectTo,
|
|
138
|
+
),
|
|
139
|
+
);
|
|
140
|
+
},
|
|
141
|
+
error: this.errorHandler('Failed to get user state after login'),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
protected onEmailFormStatusChanged(signingWith?: EmailFormSigningWith): void {
|
|
146
|
+
this.signingWith.set(signingWith as AuthProviderID);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
protected async loginWith(provider: AuthProviderID) {
|
|
150
|
+
this.signingWith.set(provider);
|
|
151
|
+
try {
|
|
152
|
+
await this.authStateService.signInWith(provider);
|
|
153
|
+
// We do not reset this.signingWith in case of succesful sign in as we should redirect from login page
|
|
154
|
+
// and not to allow user to do a double sign-in.
|
|
155
|
+
} catch (e) {
|
|
156
|
+
const errMsg = (e as { errorMessage?: string }).errorMessage;
|
|
157
|
+
if (
|
|
158
|
+
errMsg !== 'The user canceled the sign-in flow.' &&
|
|
159
|
+
!errMsg?.includes(
|
|
160
|
+
'com.apple.AuthenticationServices.AuthorizationError error 1001.',
|
|
161
|
+
)
|
|
162
|
+
) {
|
|
163
|
+
this.errorLogger.logError(e, `Failed to sign-in with ${provider}`);
|
|
164
|
+
}
|
|
165
|
+
this.signingWith.set(undefined);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
protected onLoggedIn(userCredential: UserCredential): void {
|
|
170
|
+
this.signingWith.set(undefined);
|
|
171
|
+
if (!userCredential.user) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (userCredential.user.email) {
|
|
175
|
+
const prevEmail = localStorage.getItem('emailForSignIn') || '';
|
|
176
|
+
if (!prevEmail) {
|
|
177
|
+
localStorage.setItem('emailForSignIn', userCredential.user.email);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const authState: ISneatAuthState = {
|
|
181
|
+
status: AuthStatuses.authenticated,
|
|
182
|
+
user: userCredential.user,
|
|
183
|
+
};
|
|
184
|
+
this.userService.onUserSignedIn(authState);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private errorHandler(
|
|
188
|
+
m: string,
|
|
189
|
+
eventName?: string,
|
|
190
|
+
eventParams?: Record<string, string>,
|
|
191
|
+
): (err: unknown) => void {
|
|
192
|
+
return (err) => this.handleError(err, m, eventName, eventParams);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private handleError(
|
|
196
|
+
err: unknown,
|
|
197
|
+
m: string,
|
|
198
|
+
eventName?: string,
|
|
199
|
+
eventParams?: Record<string, string>,
|
|
200
|
+
): void {
|
|
201
|
+
if (eventName) {
|
|
202
|
+
this.analyticsService.logEvent(eventName, eventParams);
|
|
203
|
+
}
|
|
204
|
+
this.errorLogger.logError(err, m, {
|
|
205
|
+
report: !(err as { code: unknown }).code,
|
|
206
|
+
});
|
|
207
|
+
this.signingWith.set(undefined);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
|
2
|
+
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|
3
|
+
import { SneatApiService } from '@sneat/api';
|
|
4
|
+
import { SneatAuthStateService } from '@sneat/auth-core';
|
|
5
|
+
import { ErrorLogger } from '@sneat/core';
|
|
6
|
+
import { LoginWithTelegramComponent } from './login-with-telegram.component';
|
|
7
|
+
|
|
8
|
+
describe('LoginWithTelegramComponent', () => {
|
|
9
|
+
let component: LoginWithTelegramComponent;
|
|
10
|
+
let fixture: ComponentFixture<LoginWithTelegramComponent>;
|
|
11
|
+
|
|
12
|
+
beforeEach(waitForAsync(async () => {
|
|
13
|
+
await TestBed.configureTestingModule({
|
|
14
|
+
imports: [LoginWithTelegramComponent],
|
|
15
|
+
providers: [
|
|
16
|
+
{
|
|
17
|
+
provide: ErrorLogger,
|
|
18
|
+
useValue: { logError: vi.fn(), logErrorHandler: () => vi.fn() },
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
provide: SneatApiService,
|
|
22
|
+
useValue: { post: vi.fn(), postAsAnonymous: vi.fn() },
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
provide: SneatAuthStateService,
|
|
26
|
+
useValue: { signInWithToken: vi.fn() },
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
30
|
+
})
|
|
31
|
+
.overrideComponent(LoginWithTelegramComponent, {
|
|
32
|
+
set: { imports: [], schemas: [CUSTOM_ELEMENTS_SCHEMA] },
|
|
33
|
+
})
|
|
34
|
+
.compileComponents();
|
|
35
|
+
fixture = TestBed.createComponent(LoginWithTelegramComponent);
|
|
36
|
+
component = fixture.componentInstance;
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
it('should create', () => {
|
|
40
|
+
expect(component).toBeTruthy();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { DOCUMENT } from '@angular/common';
|
|
2
|
+
import { Component, ElementRef, Input, OnInit, inject } from '@angular/core';
|
|
3
|
+
import {
|
|
4
|
+
ITelegramAuthData,
|
|
5
|
+
SneatAuthWithTelegramService,
|
|
6
|
+
} from './sneat-auth-with-telegram.service';
|
|
7
|
+
|
|
8
|
+
let authWithTelegramService: SneatAuthWithTelegramService;
|
|
9
|
+
|
|
10
|
+
@Component({
|
|
11
|
+
providers: [SneatAuthWithTelegramService],
|
|
12
|
+
selector: 'sneat-login-with-telegram',
|
|
13
|
+
template: `
|
|
14
|
+
<!--
|
|
15
|
+
<script
|
|
16
|
+
async
|
|
17
|
+
src="https://telegram.org/js/telegram-widget.js?22"
|
|
18
|
+
data-telegram-login="SneatBot"
|
|
19
|
+
data-size="large"
|
|
20
|
+
data-onauth="onTelegramAuth(user)"
|
|
21
|
+
data-request-access="write"
|
|
22
|
+
></script>
|
|
23
|
+
-->
|
|
24
|
+
`,
|
|
25
|
+
})
|
|
26
|
+
export class LoginWithTelegramComponent implements OnInit {
|
|
27
|
+
private readonly el = inject(ElementRef);
|
|
28
|
+
private readonly document = inject<Document>(DOCUMENT);
|
|
29
|
+
readonly authWithTelegram = inject(SneatAuthWithTelegramService);
|
|
30
|
+
|
|
31
|
+
// TODO: Article about Telegram login
|
|
32
|
+
constructor() {
|
|
33
|
+
const authWithTelegram = this.authWithTelegram;
|
|
34
|
+
|
|
35
|
+
authWithTelegramService = authWithTelegram;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@Input() public isUserAuthenticated = false;
|
|
39
|
+
|
|
40
|
+
@Input() public botID: string = location.hostname.startsWith('local')
|
|
41
|
+
? 'AlextDevBot'
|
|
42
|
+
: 'SneatBot';
|
|
43
|
+
|
|
44
|
+
@Input() public size: 'small' | 'medium' | 'large' = 'large';
|
|
45
|
+
@Input() public requestAccess: 'write' | 'read' = 'write';
|
|
46
|
+
@Input() public userPic = true;
|
|
47
|
+
|
|
48
|
+
ngOnInit() {
|
|
49
|
+
const botID = this.botID;
|
|
50
|
+
if (botID) {
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
52
|
+
// @ts-ignore
|
|
53
|
+
window.onTelegramAuth = (tgAuthData: ITelegramAuthData) => {
|
|
54
|
+
// https://core.telegram.org/widgets/login#receiving-authorization-data
|
|
55
|
+
// After a successful authorization, the widget returns data
|
|
56
|
+
// by calling the callback function data-onauth with the JSON-object containing
|
|
57
|
+
// id, first_name, last_name, username, photo_url, auth_date and hash fields.
|
|
58
|
+
authWithTelegramService.loginWithTelegram(
|
|
59
|
+
botID,
|
|
60
|
+
tgAuthData,
|
|
61
|
+
this.isUserAuthenticated,
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const script = this.document.createElement('script');
|
|
66
|
+
|
|
67
|
+
script.src = 'https://telegram.org/js/telegram-widget.js?22';
|
|
68
|
+
script.setAttribute('data-telegram-login', botID);
|
|
69
|
+
script.setAttribute('data-request-access', this.requestAccess);
|
|
70
|
+
script.setAttribute('data-size', this.size);
|
|
71
|
+
if (!this.userPic) {
|
|
72
|
+
script.setAttribute('data-userpic', 'false');
|
|
73
|
+
}
|
|
74
|
+
// https://core.telegram.org/widgets/login#receiving-authorization-data
|
|
75
|
+
// After a successful authorization, the widget returns data
|
|
76
|
+
// by calling the callback function data-onauth with the JSON-object containing
|
|
77
|
+
// id, first_name, last_name, username, photo_url, auth_date and hash fields.
|
|
78
|
+
script.setAttribute('data-onauth', 'onTelegramAuth(user)');
|
|
79
|
+
this.el.nativeElement.appendChild(script);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { TestBed } from '@angular/core/testing';
|
|
2
|
+
import { SneatApiService } from '@sneat/api';
|
|
3
|
+
import { SneatAuthStateService } from '@sneat/auth-core';
|
|
4
|
+
import { ErrorLogger } from '@sneat/core';
|
|
5
|
+
import { SneatAuthWithTelegramService } from './sneat-auth-with-telegram.service';
|
|
6
|
+
|
|
7
|
+
describe('SneatAuthWithTelegramService', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
TestBed.configureTestingModule({
|
|
10
|
+
providers: [
|
|
11
|
+
SneatAuthWithTelegramService,
|
|
12
|
+
{
|
|
13
|
+
provide: ErrorLogger,
|
|
14
|
+
useValue: { logError: vi.fn(), logErrorHandler: () => vi.fn() },
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
provide: SneatApiService,
|
|
18
|
+
useValue: { post: vi.fn(), postAsAnonymous: vi.fn() },
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
provide: SneatAuthStateService,
|
|
22
|
+
useValue: { signInWithToken: vi.fn() },
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should be created', () => {
|
|
29
|
+
expect(TestBed.inject(SneatAuthWithTelegramService)).toBeTruthy();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Injectable, inject } from '@angular/core';
|
|
2
|
+
import { SneatApiService } from '@sneat/api';
|
|
3
|
+
import { SneatAuthStateService } from '@sneat/auth-core';
|
|
4
|
+
import { ErrorLogger, IErrorLogger } from '@sneat/core';
|
|
5
|
+
import { Observable } from 'rxjs';
|
|
6
|
+
|
|
7
|
+
export interface ITelegramAuthData {
|
|
8
|
+
id: number;
|
|
9
|
+
first_name: string;
|
|
10
|
+
last_name: string;
|
|
11
|
+
username?: string;
|
|
12
|
+
photo_url?: string;
|
|
13
|
+
auth_date: number;
|
|
14
|
+
hash: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface IResponse {
|
|
18
|
+
token: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@Injectable()
|
|
22
|
+
export class SneatAuthWithTelegramService {
|
|
23
|
+
private readonly errorLogger = inject<IErrorLogger>(ErrorLogger);
|
|
24
|
+
private readonly apiService = inject(SneatApiService);
|
|
25
|
+
private readonly authService = inject(SneatAuthStateService);
|
|
26
|
+
|
|
27
|
+
public loginWithTelegram(
|
|
28
|
+
botID: string,
|
|
29
|
+
tgAuthData: ITelegramAuthData,
|
|
30
|
+
isUserAuthenticated: boolean,
|
|
31
|
+
): void {
|
|
32
|
+
const postRequest: (
|
|
33
|
+
endpoint: string,
|
|
34
|
+
body: unknown,
|
|
35
|
+
) => Observable<IResponse> = isUserAuthenticated
|
|
36
|
+
? (endpoint: string, body: unknown) =>
|
|
37
|
+
this.apiService.post<IResponse>(endpoint, body)
|
|
38
|
+
: (endpoint: string, body: unknown) =>
|
|
39
|
+
this.apiService.postAsAnonymous<IResponse>(endpoint, body);
|
|
40
|
+
postRequest(
|
|
41
|
+
'auth/login-from-telegram-widget?botID=' + botID,
|
|
42
|
+
tgAuthData,
|
|
43
|
+
).subscribe({
|
|
44
|
+
next: (response) => {
|
|
45
|
+
this.authService
|
|
46
|
+
.signInWithToken(response.token)
|
|
47
|
+
.catch(
|
|
48
|
+
this.errorLogger.logErrorHandler(
|
|
49
|
+
'Failed to sign-in with custom token',
|
|
50
|
+
),
|
|
51
|
+
);
|
|
52
|
+
},
|
|
53
|
+
error: this.errorLogger.logErrorHandler('signInWithTelegram() error:'),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
<ion-header>
|
|
2
|
+
<ion-toolbar color="light">
|
|
3
|
+
<ion-title>Signing in with email link</ion-title>
|
|
4
|
+
</ion-toolbar>
|
|
5
|
+
</ion-header>
|
|
6
|
+
<ion-content>
|
|
7
|
+
<ion-card>
|
|
8
|
+
@if (!emailFromStorage) {
|
|
9
|
+
<p>
|
|
10
|
+
Welcome to our app! For your safety please re-type your email address to
|
|
11
|
+
sign-in.
|
|
12
|
+
</p>
|
|
13
|
+
}
|
|
14
|
+
<ion-item>
|
|
15
|
+
<ion-icon name="mail-outline" slot="start" />
|
|
16
|
+
<ion-input
|
|
17
|
+
label="Email"
|
|
18
|
+
[required]="true"
|
|
19
|
+
[readonly]="isSigning"
|
|
20
|
+
[value]="email"
|
|
21
|
+
[disabled]="emailFromStorage || isSigning"
|
|
22
|
+
/>
|
|
23
|
+
</ion-item>
|
|
24
|
+
|
|
25
|
+
<ion-button [disabled]="isSigning" (click)="signIn()">
|
|
26
|
+
<ion-label>
|
|
27
|
+
@if (isSigning) {
|
|
28
|
+
Signing in...
|
|
29
|
+
} @else {
|
|
30
|
+
Sign in
|
|
31
|
+
}
|
|
32
|
+
</ion-label>
|
|
33
|
+
</ion-button>
|
|
34
|
+
</ion-card>
|
|
35
|
+
</ion-content>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
|
2
|
+
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
|
3
|
+
import { NavController } from '@ionic/angular/standalone';
|
|
4
|
+
import { SneatAuthStateService } from '@sneat/auth-core';
|
|
5
|
+
import { ErrorLogger } from '@sneat/core';
|
|
6
|
+
import { of } from 'rxjs';
|
|
7
|
+
import { SignInFromEmailLinkPageComponent } from './sign-in-from-email-link-page.component';
|
|
8
|
+
|
|
9
|
+
describe('SignInFromEmailLinkPageComponent', () => {
|
|
10
|
+
let component: SignInFromEmailLinkPageComponent;
|
|
11
|
+
let fixture: ComponentFixture<SignInFromEmailLinkPageComponent>;
|
|
12
|
+
|
|
13
|
+
beforeEach(waitForAsync(async () => {
|
|
14
|
+
await TestBed.configureTestingModule({
|
|
15
|
+
imports: [SignInFromEmailLinkPageComponent],
|
|
16
|
+
providers: [
|
|
17
|
+
{
|
|
18
|
+
provide: ErrorLogger,
|
|
19
|
+
useValue: { logError: vi.fn(), logErrorHandler: () => vi.fn() },
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
provide: SneatAuthStateService,
|
|
23
|
+
useValue: { signInWithEmailLink: vi.fn(() => of({})) },
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
provide: NavController,
|
|
27
|
+
useValue: { navigateRoot: vi.fn(() => Promise.resolve()) },
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
31
|
+
})
|
|
32
|
+
.overrideComponent(SignInFromEmailLinkPageComponent, {
|
|
33
|
+
set: { imports: [], schemas: [CUSTOM_ELEMENTS_SCHEMA] },
|
|
34
|
+
})
|
|
35
|
+
.compileComponents();
|
|
36
|
+
fixture = TestBed.createComponent(SignInFromEmailLinkPageComponent);
|
|
37
|
+
component = fixture.componentInstance;
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
it('should create', () => {
|
|
41
|
+
expect(component).toBeTruthy();
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Component, inject } from '@angular/core';
|
|
2
|
+
import {
|
|
3
|
+
IonButton,
|
|
4
|
+
IonCard,
|
|
5
|
+
IonContent,
|
|
6
|
+
IonHeader,
|
|
7
|
+
IonIcon,
|
|
8
|
+
IonInput,
|
|
9
|
+
IonItem,
|
|
10
|
+
IonLabel,
|
|
11
|
+
IonTitle,
|
|
12
|
+
IonToolbar,
|
|
13
|
+
NavController,
|
|
14
|
+
} from '@ionic/angular/standalone';
|
|
15
|
+
import { SneatAuthStateService } from '@sneat/auth-core';
|
|
16
|
+
import { ErrorLogger, IErrorLogger } from '@sneat/core';
|
|
17
|
+
|
|
18
|
+
@Component({
|
|
19
|
+
selector: 'sneat-sign-in-from-email-link-page',
|
|
20
|
+
templateUrl: 'sign-in-from-email-link-page.component.html',
|
|
21
|
+
imports: [
|
|
22
|
+
IonHeader,
|
|
23
|
+
IonToolbar,
|
|
24
|
+
IonTitle,
|
|
25
|
+
IonContent,
|
|
26
|
+
IonCard,
|
|
27
|
+
IonItem,
|
|
28
|
+
IonIcon,
|
|
29
|
+
IonInput,
|
|
30
|
+
IonButton,
|
|
31
|
+
IonLabel,
|
|
32
|
+
],
|
|
33
|
+
})
|
|
34
|
+
export class SignInFromEmailLinkPageComponent {
|
|
35
|
+
private readonly errorLogger = inject<IErrorLogger>(ErrorLogger);
|
|
36
|
+
private readonly authStateService = inject(SneatAuthStateService);
|
|
37
|
+
private readonly navController = inject(NavController);
|
|
38
|
+
|
|
39
|
+
email: string;
|
|
40
|
+
emailFromStorage = false;
|
|
41
|
+
isSigning = false;
|
|
42
|
+
|
|
43
|
+
constructor() {
|
|
44
|
+
this.email = localStorage.getItem('emailForSignIn') || '';
|
|
45
|
+
this.emailFromStorage = !!this.email;
|
|
46
|
+
if (this.email) {
|
|
47
|
+
this.signIn();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public signIn(): void {
|
|
52
|
+
this.isSigning = true;
|
|
53
|
+
this.authStateService.signInWithEmailLink(this.email).subscribe({
|
|
54
|
+
next: () => {
|
|
55
|
+
this.navController
|
|
56
|
+
.navigateRoot('/')
|
|
57
|
+
.catch(
|
|
58
|
+
this.errorLogger.logErrorHandler(
|
|
59
|
+
'Failed to navigate to root page after signing in with email link',
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
},
|
|
63
|
+
error: (err) => {
|
|
64
|
+
this.isSigning = false;
|
|
65
|
+
this.emailFromStorage = false;
|
|
66
|
+
this.errorLogger.logError(err, 'Failed to sign in with email link');
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { NgModule } from '@angular/core';
|
|
2
|
+
import { RouterModule, Routes } from '@angular/router';
|
|
3
|
+
|
|
4
|
+
export const authRoutes: Routes = [
|
|
5
|
+
{
|
|
6
|
+
path: 'login',
|
|
7
|
+
loadComponent: () =>
|
|
8
|
+
import('./login-page/login-page.component').then(
|
|
9
|
+
(m) => m.LoginPageComponent,
|
|
10
|
+
),
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
path: 'sign-in-from-email-link',
|
|
14
|
+
loadComponent: () =>
|
|
15
|
+
import('./sign-in-from-email-link/sign-in-from-email-link-page.component').then(
|
|
16
|
+
(m) => m.SignInFromEmailLinkPageComponent,
|
|
17
|
+
),
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
@NgModule({
|
|
22
|
+
imports: [RouterModule.forChild(authRoutes)],
|
|
23
|
+
exports: [RouterModule],
|
|
24
|
+
})
|
|
25
|
+
export class SneatAuthRoutingModule {}
|