اذهب إلى المحتوى

بناء مدونة باستخدام إطار العمل Angular وقاعدة بيانات Firestore - إضافة الاستثيثاق


أسامة دمراني

تعلمنا في الجزء الأول من هذا المقال كيفية بناء مدونة كتطبيق وحيد الصفحة باستخدام إطار العمل Angular للواجهة الأمامية، وقاعدة بيانات Firestore، وإضافة محرر للمدونة، ثم إضافة تدوينات جديدة وعرضها على الصفحة الرئيسية، ثم إضافة ميزات للتعديل على التدوينات وحذفها وتنسيق التدوينات في الصفحة الرئيسية وسنكمل في هذا الجزء العمل على المدونة وسنتعلم كيفية إضافة الاستيثاق authentication لتقييد وصول المستخدمين إلى المدونة وإعطاء كل مستخدم صلاحيات مناسبة لإضافة التدونيات والتعديل عليها.

هذا المقال جزء من سلسلة عن بناء مدونة باستخدام إطار العمل Angular وقاعدة بيانات Firestore

إضافة استيثاق جوجل

سنضيف الآن ميزة التسجيل بحساب جوجل إلى التطبيق الخاص بنا، ونحتاج هنا إلى تفعيل خاصية الاستيثاق authentication من طرفية Firebase كما يلي:

  1. انتقل إلى صفحة Project Overview الخاصة بمشروع Firebase.
  2. في القائمة اليسرى، تحت تبويب Develop، اختر Authentication.
  3. انتقل إلى التبويب Sign-in method.
  4. اختر Google من القائمة التي تظهر تحته.
  5. انقر على زر Enable المقابل لخيار Google، ثم انقر على Save للحفظ.

gglauth.png

سنهيئ التطبيق الآن ليستخدم استيثاق جوجل من Firebase.

إنشاء نموذج AppUser

أنشئ الملف appuser.ts وضع الشيفرة التالية فيه:

export class AppUser {
 name: string;
 email: string;
 isAdmin: boolean;
 photoURL: string;
}

إنشاء خدمة الاستيثاق

لإنشاء خدمة تعالج عملية الاستيثاق، نكتب الأمر التالي:

ng g s services/auth

افتح الملف auth.service.ts وضع تعريفات الاستيراد التالية فيه:

import { AppUser } from '../models/appuser';
import { Observable, of } from 'rxjs';
import { AngularFireAuth } from '@angular/fire/auth';
import { ActivatedRoute, Router } from '@angular/router';
import { AngularFirestore } from '@angular/fire/firestore';
import { switchMap } from 'rxjs/operators';
import * as firebase from 'firebase/app';

صرِّح عن الكائن الملاحَظ observable الذي من النوع AppUser في الصنف AuthService كما يلي:

appUser$: Observable<AppUser>;

ثم احقن الخدمات في المنشئ:

constructor(
 public afAuth: AngularFireAuth,
 private route: ActivatedRoute,
 private router: Router,
 private db: AngularFirestore
)

يحصل الكائن الملاحَظ appUser$‎ على حالة الاستيثاق للمستخدم، فإذا كان قد سجل دخوله سيجلب بيانات المستخدم من قاعدة بيانات Firebase، وإلا يعيد قيمة غير معرَّفة null:

this.appUser$ = this.afAuth.authState.pipe(
 switchMap(user => {
 if (user) {
   return this.db.doc<AppUser>(`appusers/${user.uid}`).valueChanges();
 } else {
   return of(null);
   }
 })
);

كذلك، سنضيف التابع updateUserData لحفظ بيانات المستخدم في قاعدة بياناتنا عند نجاح تسجيل الدخول، فنخزن الاسم وعنوان البريد ورابط الصورة التي في حساب جوجل لكل مستخدم في قاعدة البيانات.

نضيف التابع updateUserData كما يلي:

private updateUserData(user) {
 const userRef = this.db.doc(`appusers/${user.uid}`);
 const data = {
  name: user.displayName,
  email: user.email,
  photoURL: user.photoURL
 };
 return userRef.set(data, { merge: true });
}

ونحن نخزن هذه البيانات لسببين:

  • معرفة المستخدمين الذين سجلوا الدخول إلى تطبيقنا.
  • عرض اسم المستخدم وصورته في شريط التنقل في حالة تسجيل الدخول الناجح.

أضف تعريف التابع التالي لتسجيل الدخول والخروج في صنف AuthService كما يلي:

async login() {
 const returnUrl = this.route.snapshot.queryParamMap.get('returnUrl')
|| this.router.url;
 localStorage.setItem('returnUrl', returnUrl);

 const credential = await this.afAuth.auth.signInWithPopup(new
firebase.auth.GoogleAuthProvider());
 return this.updateUserData(credential.user);
}

async logout() {
 await this.afAuth.auth.signOut().then(() => {
  this.router.navigate(['/']);
 });
}

سنحصل على الرابط returnUrl من الموجه route ثم نخزن قيمته في الذاكرة المحلية، وذلك لضمان عدم فقد القيمة أثناء إعادة توجيه الصفحة أو تحديثها.

سيستوثق التابع signInWithPopup من عميل Firebase باستخدام تدفق توثيق OAuth منبثق -popup-based OAuth authentication flow-، فإذا نجحت عملية تسجيل الدخول سيعيد مستخدمًا مسجَّلًا دخوله مع اعتماديات المزود، أما إذا فشل فسيعيد كائن خطأ يحتوي على معلومات حول هذا الخطأ، وسينفذ التابع logout عملية تسجيل الخروج للمستخدم الحالي ويوجهه إلى الصفحة الرئيسية.

تحديث AppComponent

افتح الملف app.component.ts وأضف تعريفات الاستيراد التالية في بدايته:

import { AuthService } from './services/auth.service';
import { Router } from '@angular/router';

ثم احقن الخدمات في المنشئ كما يلي:

constructor(
 private authService: AuthService,
 private router: Router
) { }

سنشترك في الكائن الملاحَظ appUser$‎ داخل التابع ngOnInit الخاص بالصنف AppComponent كما يلي:

this.authService.appUser$.subscribe(user => {
 if (!user) {
   return;
 } else {
   const returnUrl = localStorage.getItem('returnUrl');
   if (!returnUrl) {
     return;
   }
   localStorage.removeItem('returnUrl');
   this.router.navigateByUrl(returnUrl);
   }
});

إذا كان المستخدم قد سجل دخوله فسنجلب قيمة الرابط المعاد من الذاكرة المحلية، فإذا كان ذلك الرابط متاحًا يُوجَّه المستخدم إليه، ثم نحذف الرابط من الذاكرة المحلية.

إضافة زر تسجيل الدخول في شريط التنقل

افتح الملف nav-bar.component.ts وأضف تعريفات الاستيراد التالية في بدايته:

import { AuthService } from 'src/app/services/auth.service';
import { AppUser } from 'src/app/models/appuser';

سنصرِّح عن خاصية تحتفظ ببيانات المستخدم، كما سنحقن AuthService في المنشئ، انظر الشيفرة التالية:

appUser: AppUser;

constructor(private authService: AuthService) {}

كما سنشترك في الكائن appUser$‎ الخاص بالخدمة AuthService ونعيِّن الخاصية appUser، فنفِّذ الواجهة OnInit على صنف NavBarComponent، وأضف السطر التالي إلى التابع ngOnInit:

this.authService.appUser$.subscribe(appUser => this.appUser = appUser);

كما سنضيف التابع من أجل معالجة تسجيل الدخول والخروج من تطبيقنا داخل الصنف NavBarComponent كما يلي:

login() {
 this.authService.login();
}

logout() {
 this.authService.logout();
}

سنحدِّث الآن قالب شريط التنقل، فافتح الملف الملف nav-bar.component.html واستبدل الشيفرة التالية بالموجود فيه:

<mat-toolbar class="nav-bar mat-elevation-z2">
 <button mat-button [routerLink]='["/"]'> My blog </button>
 <ng-container *ngIf="appUser">
  <button mat-button [routerLinkActive]='["link-active"]'
[routerLink]='["/addpost"]'>
   Add Post
  </button>
 </ng-container>
 <span class="spacer"></span>

 <ng-template #anonymousUser>
  <button mat-button (click)="login()">Login with Google</button>
 </ng-template>
 <ng-container *ngIf="appUser; else anonymousUser">
  <img mat-card-avatar class="user-avatar" src={{appUser.photoURL}}>
  <button mat-button [matMenuTriggerFor]="menu">
 {{appUser.name}}<mat-icon>arrow_drop_down</mat-icon>
  </button>
  <mat-menu #menu="matMenu">
   <button mat-menu-item (click)=logout()>Logout</button>
  </mat-menu>
 </ng-container>
</mat-toolbar>

إذا كان المستخدم قد سجل الدخول سنعرض كلا من الاسم والصورة اللذين في حساب جوجل له، أما إذا لم يسجل الدخول فسنعرض زر Login with Google على شريط التنقل.

سنضيف تعريف التنسيق التالي في الملف nav-bar.component.scss:

.user-avatar {
 height: 40px;
 width: 40px;
 border-radius: 50%;
 flex-shrink: 0;
}

تحديث نموذج التطبيق

استورد AngularFireAuthModule إلى الملف app.module.ts كما يلي:

import { AngularFireAuthModule } from '@angular/fire/auth';

@NgModule({
  ...
  imports: [
   ...
   AngularFireAuthModule,
  ],
})

المستخدم ذو صلاحيات التحرير والحذف

افتح الملف blog-card.component.ts وأضف تعريفي الاستيراد التاليين:

import { AppUser } from 'src/app/models/appuser';
import { AuthService } from 'src/app/services/auth.service';

سنصرِّح عن خاصية في صنف BlogCardComponent -كما في مكون شريط التنقل-، ونحقن AuthService في المنشئ، انظر الشيفرة التالية:

appUser: AppUser;

constructor(
 // خدمات أخرى
 private authService: AuthService) {}

سنشترك في الكائن الملاحَظ appUser$‎ من AuthService ونعيّن الخاصية appUser، فأضف السطر التالي إلى التابع ngOnInit الخاص بالصنف BlogCardComponent.

this.authService.appUser$.subscribe(appUser => this.appUser = appUser);

ثم افتح الملف blog-card.components.html وأضف توجيه ngIf إلى الوسم <ng-container>، وهو يحتوي أزرار Edit و Delete، وذلك لقصر استخدام هذين الزرين على المستخدمين الذين سجلوا دخولهم.

<ng-container *ngIf="appUser">

نحن الآن نسمح بخاصية التعديل والحذف لأي مستخدم قد سجل دخوله، لكن في القسم التالي سنضيف خاصية الترخيص أو الإذن authorization، ثم نحدِّث الشيفرة لتقصر تلك المزايا على المستخدمين المدراء فقط.

الحصول على اسم الكاتب

بما أننا لا نسمح بإضافة تدوينات من المستخدمين الذين لم يسجلوا دخولهم، فنستطيع التقاط اسم الكاتب لكل تدوينة، ويكون هو نفس اسمه الذي في حساب جوجل الخاص به، فسنعرض هذا الاسم على صفحة التدوينة إلى جانب تاريخ إنشائها.

أضف تعريفات الاستيراد التالية إلى الملف blog-card.components.ts:

import { AuthService } from 'src/app/services/auth.service';
import { AppUser } from 'src/app/models/appuser';

سنصرِّح عن خاصية لتحتوي بيانات المستخدم، ثم نحقن AuthService في المنشئ الخاص بالمكون BlogEditorComponent. كذلك، سنشترك في الكائن الملاحَظ appUser$‎ من AuthService، ونعيّن الخاصية appUser، انظر الشيفرة أدناه:

appUser: AppUser;

constructor(
 // حقن آخر للخدمات
 private authService: AuthService) { }

 ngOnInit() {
 ...
 this.authService.appUser$.subscribe(appUser => this.appUser =
appUser);
}

نعيّن خاصية الكاتب للكائن postData أثناء حفظ التدوينة الجديدة، فأضف السطر التالي داخل قسم else من التابع saveBlogPost، مباشرة قبل استدعاء التابع createPost الخاص بالصنف BlogService.

this.postData.author = this.appUser.name;

أضف السطر التالي في الملف blog.component.html، بعد السطر الذي نربط فيه createdDate داخل الوسم <mat-card-subtitle>. يُستخدم هذا لعرض اسم الكاتب إلى جانب تاريخ إنشاء التدوينة.

<i class="fa fa-user" aria-hidden="true"></i> {{postData.author}}

تأمين الوجهات

سنضيف حاجز استيثاق auth guard إلى التطبيق لتقييد الوصول غير المصرح له إلى وجهات routes بعينها لا يجب على أي مستخدم الوصول لها خصوصًا المستخدم غير المسجل.

شغِّل الأمر التالي لإضافة حاجز جديد:

ng g g guards/auth --implements CanActivate

افتح الملف src/app/guards/auth.guard.ts وأضف تعريفات الاستيراد التالية:

import { Router } from '@angular/router';
import { AuthService } from '../services/auth.service';
import { map } from 'rxjs/operators';

أضف منشئًا في الصنف AuthGuard، واحقن Router و AuthService في المنشئ كما يلي:

constructor(
 private router: Router,
 private authService: AuthService
) { }

سنحدِّث التابع canActivate ليعالج الوصول غير المصدَّق له unauthenticated إلى الوجهات فأضف الشيفرة التالية إلى هذا التابع:

return this.authService.appUser$.pipe(map(user => {
 if (user) {
  return true;
 }
 this.router.navigate(['/'], { queryParams: { returnUrl: state.url }
});
 return false;
}));

سنشترك في الكائن appUser$‎ لجلب حالة الاستيثاق authentication state للمستخدم، فإذا سجل المستخدم الدخول نعيد true، أما إذا لم يسجل الدخول فتحدث ثلاثة أشياء:

  • نعيّن معامِل استعلام query parameter يسمى returnUrl على قيمة الرابط الحالي.
  • ننقل المستخدم إلى الصفحة الرئيسية.
  • نعيد false من التابع.

إضافة حواجز الوجهة route guards في نموذج التطبيق

إذا أردنا تفعيل حاجز الوجة لوجهة بعينها في التطبيق، نحتاج إلى إضافة الخاصية canActivate إلى الوجهة في الملف app.module.ts، وسنؤمن الوجهات الخاصة بإضافة تدوينة جديدة من خلال إضافة الخاصية canActivate.

أضف تعريف الاستيراد التالي في الملف app.module.ts:

import { AuthGuard } from './guards/auth.guard';

وحدِّث الاتجاه الخاص بالمكون addpost كما يلي:

{ path: 'addpost', component: BlogEditorComponent, canActivate:[AuthGuard] },

إذا حاول المستخدم أن يصل إلى الوجهة addpost فسيُستدعى الصنف AuthGuard، فإذا أعاد هذا الصنف true يُسمح للمستخدم أن يصل إلى تلك الوجهة لإضافة تدوينة جديدة، أما إذا أعاد false فلا يُسمح له بالوصول إلى تلك الوجهة ويُعاد إلى الصفحة الرئيسية.

اختبار عملية الاستيثاق

افتح المتصفح لترى أن بطاقة التدوينة لا تحتوي على زري التعديل والحذف، كذلك لا يظهر زر Add post في شريط التنقل، وذلك لأن المستخدم لم يسجل الدخول بعد، لهذا نرى زر Login with Google في الركن العلوي الأيمن من شريط التنقل.

إذا ضغطنا على ذلك الزر ستظهر نافذة منبثقة تطلب منا تسجيل الدخول باستخدام حساب جوجل:

chkpt7.png

ستحدِّث الصفحة نفسها بمجرد نجاح تصديق جوجل، ونرى أن الاتجاه الخاص بـ Add post قد ظهر على شريط التنقل، كما ظهر اسم المستخدم الذي سجل الدخول على الشريط، وسيظهر كل من زر Edit و Delete على بطاقة التدوينة، انظر:

loggedin.png

تطبيق التصاريح

سنطبِّق الآن تصريحًا مبنيًا على قاعدة rule-based لموقعنا، وسنعرِّف دورًا -وهو دور المدير admin- بحيث يستطيع المستخدمون المدراء فقط أن يعدِّلوا ويحذفوا التدوينة، ثم سنضيف إمكانية نشر التعليقات على التدوينة لاحقًا، والتي يستطيع المدير حذفها هي أيضًا.

إعداد قاعدة بيانات Firebase لدور المدير

لقد عرَّفنا الخاصية البوليانية isAdmin في الصنف AppUser، وتُستخدم هذه الخاصية لتعريف دور المدير للمستخدم، وسنضيف -يدويًا- حقلًا جديدًا باسم isAdmin للمستخدمين الذين نريد منحهم صلاحية المدير، وسننفذ التغييرات في تجميعة appusers داخل قاعدة بيانات Firebase.

انتقل إلى قاعدة بيانات Firebase وافتح تجميعة appusers التي تخزن سجلًا لجميع المستخدمين الذين سجلوا الدخول إلى تطبيقنا، انظر الصورة أدناه:

addfield.png

انقر على زر Add field لتظهر نافذة تطلب تعريف حقل جديد، فاجعل اسم الحقل isAdmin، ونوعه boolean، وقيمته True، ثم انقر على زر Add لإضافته، انظر الصورة أدناه:

isadmin.png

سيضاف الحقل الجديد إلى التجميعة:

isadmintrue.png

عند نجاح عملية تسجيل الدخول سنجلب بيانات المستخدم من تجميعة appusers ونربطها بكائن يكون نوعه هو الصنف AppUser، ثم نستخدم الخاصية isAdmin بعدها لتقييد الدخول للمستخدم.

وبما أننا ننشئ تطبيقًا للتوضيح والشرح فقط يحتوي على دور واحد، فإننا نعيّن خاصية isAdmin يدويًا، أما التطبيقات الكبيرة فلا يُنصح فيها بهذا إذ قد نحتاج إلى التعامل مع أدوار كثيرة مختلفة، ففي تلك الحالة ننشئ تطبيقًا منفصلًا لإدارة الأدوار لجميع المستخدمين.

إنشاء حاجز admin-auth

نفّذ الأمر التالي لإنشاء حاجز جديد باسم AdminAuthGuard:

ng g g guards/admin-auth --implements CanActivate 

ثم افتح الملف src/app/guards/admin-auth.guard.ts وأضف تعليمات الاستيراد التالية في أعلاه:

import { AuthService } from '../services/auth.service';
import { map } from 'rxjs/operators';
import { AppUser } from '../models/appuser';

أضف منشئًا واحقن الموجِّه Router والخدمة AuthService فيه كما يظهر في الشيفرة التالية:

constructor(
 private router: Router,
 private authService: AuthService) { }

سنحدِّث التابع canActivate لمعالجة الوصول غير المصرح له إلى الوجهات، وذلك بإضافة الشيفرة التالية:

return this.authService.appUser$.pipe(map((user: AppUser) => {
 if (user && user.isAdmin) {
  return true;
 }
 this.router.navigate(['/'], { queryParams: { returnUrl: state.url }
});
 return false;
 }));

سنشترك في الكائن appUser$‎ لجلب حالة التصديق/التصريح للمستخدم، فإذا كان قد سجل دخوله وكانت قيمة الخاصية isAdmin للمستخدم على القيمة true فإننا نعيد القيمة true أيضًا.

بالمثل في سلوك الصنف AuthGuard فإذا لم يكن المستخدم قد سجل دخوله، فستحدث هذه الأشياء الثلاثة:

  • نعين معامِل استعلام باسم returnUrl على قيمة الرابط الحالي.
  • ننقل المستخدم إلى الصفحة الرئيسية.
  • نعيد false من التابع.

إضافة صلاحيات مدير للتطبيق

لقد حدَّثنا المكون BlogCardComponent في القسم السابق ليقيد الوصول إلى أزرار Edit و Delete ويقصره على المستخدمين الذين سجلوا الدخول، أما الآن فسنحدِّث الشيفرة مرة أخرى لتقصر الوصول على المستخدمين المدراء فقط.

افتح الملف src/app/components/blog-card.component.html، وحدِّث التوجيه ngIf الذي في الوسم <ng-container>، والذي بدوره يحتوي على الزر Edit والزر Delete -انظر الشيفرة أدناه- وهذا سيقصر إمكانية التعديل والحذف على المستخدمين الذين سجلوا الدخول فقط.

<ng-container *ngIf="appUser?.isAdmin">

أضف تعليمة الاستيراد التالية في الملف app.module.ts:

import { AdminAuthGuard } from './guards/admin-auth.guard';

حدِّث الاتجاه الخاص بالمكون editpost كما يلي:

{ path: 'editpost/:id', component: BlogEditorComponent, canActivate:[AdminAuthGuard] },

لقد قصرنا إمكانية التعديل على التدوينة هنا لتكن للمستخدمين المدراء فقط، فحين يحاول المستخدم الوصول إلى اتجاه editpost، فسيُستدعى الصنف AdminAuthGuard، فإذا أعاد هذا الصنف true يُسمح للمستخدم بالوصول إلى الوجهة editpost لتعديل التدوينة، أما إذا أعاد false فلا يُسمح له بالذهاب لتلك الوجهة لتعديل التدوينة.

اختبار التصاريح

افتح المتصفح وسجل الدخول إلى التطبيق بمستخدم غير مدير، سترى رابط AddPost في شريط التنقل، لكن بطاقة التدوينة لن يكون فيها زري Edit و Delete، وذلك يعني أن المستخدم يستطيع إضافة تدوينة جديدة لكنه لا يستطيع تعديل تدوينة موجودة أو أن يحذفها. انظر الصورة التالية:

non-admin.png

انتقل إلى قاعدة بيانات Firebase وأضف حقل isAdmin جديد للمستخدم كما سبق شرحه، أو يمكنك تسجيل الخروج وإعادة تسجيل الدخول بمستخدم آخر يكون فيه حقل isAdmin يحمل القيمة true، وهنا تستطيع أن ترى زري Edit و Delete على بطاقة التدوينة. انظر الصورة التالية:

isadmin-edit.png

تحديث قواعد الأمان لقاعدة بيانات Firebase

لقد أنشأنا قاعدة البيانات في وضع الاختبار، وسنحدِّث القاعدة الأمنية لقاعدة البيانات الآن لتسمح بعملية الكتابة للمستخدم المصرَّح له فقط.

من القائمة التي على اليسار، اختر Database في قسم Develop، ثم اختر تبويب Rules، وحدِّث قواعد الأمان كما يلي:

service cloud.firestore {
 match /databases/{database}/documents {
  match /{document=**} {
  allow read;
  allow write: if request.auth.uid != null;
  }
 }
}

ثم انقر على زر Publish لنشر القاعدة، انظر الصورة أدناه:

rules.png

الخاتمة

تعلمنا في هذا الجزء من سلسلة بناء مدونة باستخدام إطار العمل Angular وقاعدة بيانات Firestore كيفية إضافة استيثاق جوجل للتأكد من تقييد وصول المستخدمين إلى مختلف أجزاء التطبيق وإعطاء كل مستخدم صلاحيات محددة بعد تسجيله في قاعدة بيانات التطبيق. أما في الجزء الخامس والأخير من هذه السلسلة، فسنكمل إضافة اللمسات النهائية على المدونة، مثل إضافة الملف الشخصي لكل كاتب ونشر التعليقات ومشاركة التدوينات على الشبكات الاجتماعية وغيرها ثم سنتعلم كيفية نشر المدونة على الإنترنت باستعمال Firebase.

ترجمة -وبتصرف- لفصول من كتاب Build a full stack web application using angular and firebase لصاحبه Ankit Sharma.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...