احراز هویت کاربران با یک کادر Pop-up در انگولار — از صفر تا صد

۱۰۹ بازدید
آخرین به‌روزرسانی: ۱۱ شهریور ۱۴۰۲
زمان مطالعه: ۹ دقیقه
احراز هویت کاربران با یک کادر Pop-up در انگولار — از صفر تا صد

در این مقاله با روش طراحی یک کادر محاوره‌ای Pop-up در انگولار برای احراز هویت کاربران آشنا می‌شویم و بدین ترتیب لزوم ریدایرکت کردن کاربران از میان برداشته می‌شود.

هر نوع اپلیکیشنی که قصد داشته باشید توسعه دهید، در هر حال باید یکی از روش‌های احراز هویت کاربران را پیاده‌سازی کنید. با این که ممکن است تصور کنید کاربران اهمیت کمی به جزییات پیاده‌سازی این قابلیت‌های اساسی یک اپلیکیشن می‌دهند، اما مطمئن باشید که این موارد، تأثیر زیادی روی طراحی اپلیکیشن دارند و از این رو باید همه نکات را پیش از کامیت یا پیاده‌سازی گزینه‌های خود مورد ارزیابی دقیق قرار دهید.

در این مقاله یک روش احراز هویت کاربران با استفاده از کادر محاوره‌ای modal را بررسی می‌کنیم. بدین ترتیب اگر کاربر تلاش کند تا عملی اجرا کند یا به یک صفحه مراجعه کند که نیازمند احراز هویت است، این کادر محاوره‌ای باز می‌شود و از لزوم ریدایرکت کردن کاربر به صفحه دیگر جلوگیری می‌کند.

آیتم‌های مورد نیاز

برای ساختن کادر محاوره‌ای احراز هویت به موارد زیر نیاز داریم:

  1. Auth: سرویس احراز هویت با استفاده از سرویس AngularFireAuth از AngularFire (+) پیاده سازی می‌شود.
  2. AuthGuard: یک سرویس کاربردی به عنوان محافظ مسیریابی استفاده خواهد شد که هر زمان کاربر تلاش کند به صفحه‌های خصوصی دست یابد، به کاربر هشدار می‌دهد که باید اطلاعات احراز هویت خود را وارد کند.
  3. LoginComponent: کامپوننت واقعی که فرم لاگین را تحقق می‌بخشد و به لطف سرویس MatDialog از Angular Material (+) به صورت یک کادر محاوره‌ای رندر می‌شود.

همه موارد فوق بخشی از یک دموی زنده هستند که می‌توانید در این آدرس (+) ملاحظه کنید و همه کارکردهای توصیف‌شده را به چشم خود بررسی کنید.

سرویس Auth

ما با پیاده‌سازی سرویس احراز هویت‌مان قادر خواهیم بود نیازهای اپلیکیشن خود را به نحو بهتری پاسخگو باشیم و از این رو خواندن و نگهداری کدبیس کلی آسان‌تر خواهد بود.

فایل auth.service.ts

1@Injectable()
2/** Wraps the AngularFireAuth service for extended functionalities */
3export class AuthService implements OnDestroy {
4
5  /** User object snapshot */
6  public user: User = null;
7  private sub: Subscription;
8  
9  /** User object observable */
10  get user$(): Observable<User|null> {
11    return this.fire.user;
12  }
13  
14  constructor(readonly fire: AngularFireAuth) {
15    // Keeps a snapshot of the current user object
16    this.sub = this.user$.subscribe( user => {
17      this.user = user;
18    });
19  }
20
21  ngOnDestroy() { this.sub.unsubscribe(); }
22
23  /** Returns true if user is logged in */
24  get authenticated(): boolean {
25    return !!this.user;
26  }
27
28  /** Returns the current user id, when authenticated */
29  get userId(): string {
30    return this.authenticated ? this.user.uid : '';
31  }
32
33...
34
35  /**
36   * Registers a new user by email and confirmPasswordReset
37   * @param email the email to register with
38   * @param password the secret password
39   * @param name (optional) the user name
40   */
41  public registerNew(email: string, password: string, name: string = ""): Promise<void> {
42    
43    console.log("Registering a new user: " + email);
44    // Create a new user with email and password
45    return this.fire.auth.createUserWithEmailAndPassword(email, password)
46      // Update the user info with the given name
47      .then( credential => credential.user.updateProfile({ displayName: name } as User));
48  }
49
50  /**
51   * Signs in with the given user email and password
52   * @param email the email to register with
53   * @param password the secret password 
54   * @returns the authenticated User object
55   */
56  public signIn(email: string, password: string): Promise<User>  {
57    
58    console.log("Signing in as: " + email);
59
60    return this.fire.auth.signInWithEmailAndPassword(email, password)
61      .then( credential => credential.user );
62  }
63...
64}

پیاده‌سازی فوق اساساً پوششی برای همه کارکردهای ارائه شده از سوی AngularFireAuth است و همزمان شیء User احراز هویت شده را نیز عرضه می‌کند. بدین ترتیب به لطف ()authenticated که هر زمان شیء کاربر معتبر باشد، مقدار true بازگشت می‌دهد، می‌توانیم وضعیت احراز هویت را در همه جای اپلیکیشن بدون نیاز به اشتراک در یک observable مشاهده کنیم. به طور مشابه ()userId اقدام به بازگرداندن id کاربر احراز هویت شده می‌کند.

چنان که مشاهده خواهید کرد، هر دو مشخصه کاملاً کارگشا هستند.

نکته: البته Firebase.Auth یک شیء User احراز هویت شده در متغیری به نام currentUser ارائه می‌کند و ما نیز در نهایت کدبیس خود را به این روش آپدیت کردیم.

محافظ Auth

انگولار از مسیریابی برای ناوبری از یک view به view دیگر بهره می‌گیرد و در این زمان کاربران وظایفی در اپلیکیشن اجرا می‌کنند.

می‌توان از مسیرها محافظت کرد تا کاربران نتوانند به صفحه‌هایی که اجازه ندارند، دسترسی یابند. این سازوکار یک کادر محاوره‌ای pop-up نمایش می‌دهد که از کاربر می‌خواهد هویت خود را احراز کند.

فایل auth-guard.service.ts

1@Injectable({
2  providedIn: 'root'
3})
4export class AuthGuard implements CanActivate {
5
6  constructor(readonly auth: AuthService, private router: Router, private dialog: MatDialog) {}
7
8  /** Returns true whenever the user is authenticated */
9  get authenticated() { return this.auth.authenticated; }
10
11  /** Returns the current authenticated user id */
12  get userId() { return this.auth.userId; }
13
14  /** Prompts the user for authentication */
15  public prompt(data: loginAction = 'signIn'): Promise<User> {
16
17    return this.dialog.open<LoginComponent,loginAction, User>(LoginComponent, { data })
18      .afterClosed().toPromise();
19  }
20
21  /** Performs the user authentication prompting the user when neeed or resolving to the current authenticated user otherwise */
22  public authenticate(action: loginAction = 'signIn'): Promise<User> {
23
24    return this.auth.user$.pipe(
25      
26      take(1),
27
28      switchMap( user => !user ? this.prompt(action) : of(user) )
29
30    ).toPromise();
31  }
32
33  /** Disconnects the user navigating to home */
34  public disconnect(jumpTo = '/'): Promise<boolean> {
35
36    return this.auth.signOut()
37      .then( () => this.router.navigateByUrl(jumpTo) );
38  }
39
40  // Implements single route user authentication guarding
41  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
42    // Gets the authorization mode when specified
43    const mode = route.queryParamMap.get('authMode') || 'signIn';
44    // Prompts the user for authentication 
45    return this.authenticate(mode as loginAction)
46      .then( user => !!user );
47  }
48}

AuthGuard که در کد فوق ارائه شده است، اینترفیس canActivate را پیاده‌سازی می‌کند تا به مسیریاب اطلاع دهد که در مسیر خود در موارد لزوم از مسیرهای محافظت‌شده بهره بگیرد. AuthGuard پوششی برای AuthService به عنوان یک متغیر فقط-خواندنی است و از این رو با تزریق AuthGuard در یک کامپوننت همواره دسترسی آسانی به canActivate نیز خواهیم داشت.

تابع ()canActivate تابعی است که یک Promise با مقدار بولی بازگشت می‌دهد و زمانی که true باشد مسیر فعال می‌شود و زمانی که false باشد مسیر مسدود خواهد بود. همه کارها در فراخوانی ()authenticate انجام می‌یابد و تابع اصلی کادر محاوره‌ای را نمایش می‌دهد و در صورت موفق بودن، شیء User احراز هویت شده بازگشت می‌یابد.

در مثال زیر از ظرفیت ()canActivate برای کار با اشیای ناهمگام استفاده می‌کنیم. به طوری که کادر محاوره‌ای pop-up نمایش می‌یابد و از کاربر می‌خواهد که احراز هویت کند و بر همین مبنا کار را ادامه می‌دهد یا ناوبری لغو می‌شود.

متد احراز هویت

فرض کنید اپلیکیشن شما یک صفحه عمومی دارد که همه کس می‌توانند آن را ببینند، اما تنها کاربران احراز هویت شده می‌توانند کاری روی آن انجام دهند. ()authenticate برای این نوع از موقعیت‌ها طراحی شده است:

فایل authenticate.ts

1/** Performs the user authentication prompting the user when neeed or resolving to the current authenticated user otherwise */
2public authenticate(action: loginAction = 'signIn'): Promise<User> {
3
4  return this.auth.user$.pipe(
5
6    take(1),
7
8    switchMap( user => !user ? this.prompt(action) : of(user) )
9
10  ).toPromise();
11}

این متد یک observable به نام user$ را resolve می‌کند تا مشخص شود که آیا کاربر جاری قبلاً احراز هویت شده است یا نه. take(1) این observable را درست پس از این که مقدار منفرد مورد نیاز را دریافت کرد تکمیل می‌کند، سپس در صورت تهی بودن شیء User در ادامه ()switchMap از کاربر می‌خواهد که احراز هویت کند و در غیر این صورت مستقیماً با شیء User جاری اقدام به resolve می‌کند.

متد Prompt

متد ()Prompt متدی است که کادر محاوره‌ای احراز هویت را به کمک سرویس MatDialog نمایش می‌دهد.

فایل prompt.ts

1/** Prompts the user for authentication */
2public prompt(data: loginAction = 'signIn'): Promise<User> {
3
4  return this.dialog.open<LoginComponent,loginAction, User>(LoginComponent, { data })
5    .afterClosed().toPromise();
6}

این سرویس به موارد زیر نیاز دارد:

  1. یک کامپوننت یا قالب برای رندر کردن به عنوان بدنه کادر محاوره‌ای که به وسیله LoginComponent انجام می‌یابد.
  2. یک شیء اختیاری DIALOG_DATA که روی سازنده تزریق‌شده LoginComponent برای وارد کردن اطلاعات در دسترس خواهد بود. ما در این جا از متغیر loginAction برای انتخاب حالت نمایش کادر محاوره‌ای استفاده می‌کنیم.
  3. یک شیء User که انتظار داریم از observable به نام ()afterClosed در کادر محاوره‌ای بازگشت یابد.

همه موارد فوق زمانی که متد ()open برای نمایش کار محاوره‌ای فراخوانی می‌شود، مورد استفاده قرار می‌گیرند و یک promise بازگشت می‌دهد که به صورت شیء User احراز هویت شده یا مقدار null خواهد بود.

کامپوننت Login

LoginComponent اساساً یک فرم است که در آن ایمیل و رمز عبور را برای وارد شدن به اکانت وارد می‌کنیم.

به علاوه این فرم به صورت دینامیک از یک حالت نمایشی به حالت دیگر تغییر می‌یابد. حالت‌های نمایشی ممکن به صورت زیر هستند:

  • signIn: جفت فیلدهای ایمیل و رمز عبور را به همراه آیکون‌های ارائه‌دهنده‌های مختلف سرویس‌های احراز هویت (گوگل، فیسبوک و غیره) نمایش می‌دهد تا به صورت یک کاربر ثبت شده موجود وارد شوید.
  • Register: یک نام، ایمیل، و رمز عبور برای ثبت کردن کاربر جدید می‌پرسد.
  • forgotPassword: یک ایمیل می‌خواهد تا لینک ریست کردن رمز عبور را به آن ارسال کند.
  • changePassword: یک رمز عبور جدید می‌خواهد و عملیات را با دریافت رمز عبور فعلی تأیید می‌کند.
  • changeEmail: یک ایمیل جدید می‌خواهد و عملیات با رمز عبور فعلی تأیید می‌شود.
  • delete: از کاربر می‌خواهد که حذف حساب را با رمز عبور فعلی تأیید کند.

ساخت فرم

با توجه به این که کد کامل این پروژه در انتهای این مقاله ارائه شده است، ما در این بخش روی بخش‌های کلیدی متمرکز می‌شویم:

فایل login-constructor.ts

1constructor(private auth: AuthService, private ref: MatDialogRef<LoginComponent>, @Inject(MAT_DIALOG_DATA) private action: loginAction) {
2
3  this.name = new FormControl(null, Validators.required);
4  this.email = new FormControl(null, [Validators.required, Validators.email]);
5  this.password = new FormControl(null, Validators.required);
6  this.newEmail = new FormControl(null, [Validators.required, Validators.email]);
7  this.newPassword = new FormControl(null, Validators.required);
8
9  this.form = new FormGroup({});
10
11  this.switchPage(this.page = action);
12}

زمانی که کادر محاوره‌ای رندر می‌شود، سازنده LoginComponent نیز فراخوانی می‌شود. در این بخش MatDialogRef، یک شیء ارجاع به کادر محاوره‌ای است و متغیر ورودی‌مان MAT_DIALOG_DATA را تزریق می‌کنیم که شامل حالت نمایشی است که می‌خواهیم فرم با آن آغاز به کار کند. فراموش نکنید که LoginComponent را در میان entryComponents ماژولش برای MatDialog لیست کنید تا آن را به طرز صحیحی رندر کند.

از آنجا که فرم بسته به حالت نمایشی، شامل فیلدهای مختلفی است، همه کنترل‌های فرم به طور مجزا ایجاد می‌شوند، در حالی که فرم به عنوان یک گروه خالی ساخته می‌شود و منتظر متد switchPage()‎ می‌ماند تا کنترل‌های مناسبی که باید اضافه شوند را انتخاب کند.

سوئیچ بین صفحات

این متد ابتدا با حذف همه کنترل‌ها از گروه فرم و سپس افزودن موارد مرتبط بسته به آرگومان page، میان حالت‌های نمایشی مختلف سوئیچ می‌کند.

فایل login-switch-page.ts

1get currentPage() { return this.pages[this.page || 'signIn']; }
2
3private switchPage(page: loginAction) {
4
5  // Removes all the controls from the form group
6  Object.keys(this.form.controls).forEach( control => {
7    this.form.removeControl(control);
8  });
9
10  // Add the relevant controls to the form according to selected page
11  switch(this.page = page) {
12
13    case 'register':
14    this.form.addControl('name', this.name);
15    this.form.addControl('email', this.email);
16    this.form.addControl('password', this.password);
17    break;
18
19    default:
20    case 'signIn':
21    this.form.addControl('email', this.email);
22    this.form.addControl('password', this.password);      
23    break;
24
25    case 'forgotPassword':
26    this.form.addControl('email', this.email);
27    break;
28
29    case 'changePassword':
30    this.form.addControl('password', this.password);
31    this.form.addControl('newPassword', this.newPassword);
32    break;
33
34    case 'changeEmail':
35    this.form.addControl('password', this.password);
36    this.form.addControl('newEmail', this.newEmail);
37    break;
38
39    case 'delete':
40    this.form.addControl('password', this.password);      
41    break;
42  }
43}

نمای کامپوننت بر همین اساس رندر می‌شود، چون قالب از متد ()contains گروه فرم برای تشخیص این که کدام فیلدها باید نمایش پیدا کنند استفاده می‌کند.

فایل login-template.html

1...
2
3<form [formGroup]="form" (ngSubmit)="activate(page)">
4
5  <!-- ERROR MESSAGE -->
6  <mat-error *ngIf="error" @inflate>{{ error }}</mat-error>
7
8  <!-- NAME -->
9  <mat-form-field appearance="legacy" *ngIf="form.contains('name')" @inflate>
10    <mat-label>Full name</mat-label>
11    <input matInput formControlName="name">
12    <mat-error *ngIf="form.controls.name.errors?.required">
13      Please specify your name here
14    </mat-error>
15  </mat-form-field>
16
17  <!-- EMAIL -->
18  <mat-form-field appearance="legacy" *ngIf="form.contains('email')" @inflate>
19    <mat-label>Email</mat-label>
20    <input matInput formControlName="email">
21    <mat-error *ngIf="form.controls.email.errors?.required">
22      Please specify an email address
23    </mat-error>
24    <mat-error *ngIf="form.controls.email.errors?.email">
25      Ooops! it looks like this is not a valid email
26    </mat-error>
27  </mat-form-field>
28
29  ...
30
31  <!-- ACTION BUTTON -->
32  <button mat-stroked-button color="primary" type="submit" [disabled]="!form.valid" class="btn">
33    {{ currentPage.caption }}
34  </button>
35
36  <mat-progress-bar *ngIf="progress" mode="indeterminate" @inflate></mat-progress-bar>
37
38</form>

از این رو در کد فوق می‌توانید ببینید که mat-input-field شامل "formControlName="name است و از یک دایرکتیو "*ngIf="form.contains('name') برای تصمیم‌گیری در مورد رندر کردن یا نکردن view بهره می‌گیرد.

اعتبارسنجی فرم

ما با استفاده از این تکنیک تغییر دادن دینامیک محتوای گروه فرم، می‌توانیم همواره طرز کار صحیحی از اعتبارسنجی Reactive Forms انگولار را بر اساس اعتبارسنج‌های کنترل فرم ذکر شده شاهد باشیم. در این مثال از Validators.required بری هر کنترل تعیین‌شده به صورت الزامی و از Validators.email برای کنترل‌های ایمیل استفاده کرده‌ایم تا بر اساس ساختار ایمیل مناسب مورد بررسی قرار گیرند.

تحویل فرم

به طور مشابه، زمانی که کاربر فرم را تحویل دهد، کامپوننت ()activate را فراخوانی می‌کند.

فایل login-activate.ts

1public activate(action: loginAction) {
2
3  this.progress = true;
4
5  switch(action) {
6
7    default:
8    case 'signIn':
9    this.signIn( this.email.value, this.password.value );
10    break;
11
12    case 'register':
13    this.registerNew( this.email.value, this.password.value, this.name.value );
14    break;
15
16    case 'forgotPassword':
17    this.forgotPassword( this.email.value );
18    break;
19
20    case 'changePassword':
21    this.updatePassword( this.password.value, this.newPassword.value );
22    break;
23
24    case 'changeEmail':
25    this.updateEmail( this.password.value, this.newEmail.value );
26    break;
27
28    case 'delete':
29    this.deleteAccount( this.password.value );
30    break;
31  }
32}

این کد به نوبه خود متد خاصی را برای حالت نمایشی فراخوانی می‌کند. از این رو در ادامه نگاهی به ()signIn به عنوان یک مثال ارزشمند خواهیم داشت:

فایل login-sign-in.ts

1private signIn(email: string, password: string) {
2  // Sign-in using email/password
3  this.auth.signIn(email, password)
4    // Closes the dialog returning the user
5    .then( user => this.ref.close(user) )
6    // Dispays the error code, eventually
7    .catch( error => this.showError(error.code) );
8}

متد ()signIn ابتدا متد مرتبط AuthService را فرا می‌خواند و سپس کادر محاوره‌ای را با استفاده از متد ()close که در MatDialogRef اشاره شده است می‌بندد و شیء USER احراز هویت شده را در بازگشت ارسال می‌کند.

سخن پایانی

در این مقاله دیدیم که متد ()authenticate در ابتدا و در طی حفاظت از مسیر از سوی ()canActivate فراخوانی می‌شود، مسیریاب بر اساس نتیجه این متد می‌تواند کار خود را ادامه داده یا ناوبری را لغو کند. بدیهی است که متد ()authenticate می‌تواند در غیر این صورت فراخوانی شود تا از کاربر درخواست ورود به حساب بکند و بدین ترتیب مطمئن می‌شویم که کاربر پیش از اجرای کاری که نیازمند احراز هویت روی صفحه‌ای عمومی است، احراز هویت شده است.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۰ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
wizdm-genesys
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *