تعلمنا في الجزء الأول من هذا المقال كيفية بناء مدونة كتطبيق وحيد الصفحة باستخدام إطار العمل Angular للواجهة الأمامية، وقاعدة بيانات Firestore، وإضافة محرر للمدونة، ثم إضافة تدوينات جديدة وعرضها على الصفحة الرئيسية، ثم إضافة ميزات للتعديل على التدوينات وحذفها وتنسيق التدوينات في الصفحة الرئيسية وسنكمل في هذا الجزء العمل على المدونة وسنتعلم كيفية إضافة الاستيثاق authentication لتقييد وصول المستخدمين إلى المدونة وإعطاء كل مستخدم صلاحيات مناسبة لإضافة التدونيات والتعديل عليها.
هذا المقال جزء من سلسلة عن بناء مدونة باستخدام إطار العمل Angular وقاعدة بيانات Firestore
- مقدمة في بناء تطبيقات الويب باستخدام إطار العمل Angular وقاعدة بيانات Firestore.
- بناء مدونة باستخدام إطار العمل Angular وقاعدة بيانات Firestore - إضافة التدوينات وعرضها.
- بناء مدونة باستخدام إطار العمل Angular وقاعدة بيانات Firestore - تعديل التدوينات.
- بناء مدونة باستخدام إطار العمل Angular وقاعدة بيانات Firestore - إضافة الاستثيثاق.
- نشر مدونة مبنية عبر Angular على Firebase
إضافة استيثاق جوجل
سنضيف الآن ميزة التسجيل بحساب جوجل إلى التطبيق الخاص بنا، ونحتاج هنا إلى تفعيل خاصية الاستيثاق authentication من طرفية Firebase كما يلي:
- انتقل إلى صفحة Project Overview الخاصة بمشروع Firebase.
- في القائمة اليسرى، تحت تبويب Develop، اختر Authentication.
- انتقل إلى التبويب Sign-in method.
- اختر Google من القائمة التي تظهر تحته.
- انقر على زر Enable المقابل لخيار Google، ثم انقر على Save للحفظ.
سنهيئ التطبيق الآن ليستخدم استيثاق جوجل من 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 في الركن العلوي الأيمن من شريط التنقل.
إذا ضغطنا على ذلك الزر ستظهر نافذة منبثقة تطلب منا تسجيل الدخول باستخدام حساب جوجل:
ستحدِّث الصفحة نفسها بمجرد نجاح تصديق جوجل، ونرى أن الاتجاه الخاص بـ Add post قد ظهر على شريط التنقل، كما ظهر اسم المستخدم الذي سجل الدخول على الشريط، وسيظهر كل من زر Edit و Delete على بطاقة التدوينة، انظر:
تطبيق التصاريح
سنطبِّق الآن تصريحًا مبنيًا على قاعدة rule-based لموقعنا، وسنعرِّف دورًا -وهو دور المدير admin- بحيث يستطيع المستخدمون المدراء فقط أن يعدِّلوا ويحذفوا التدوينة، ثم سنضيف إمكانية نشر التعليقات على التدوينة لاحقًا، والتي يستطيع المدير حذفها هي أيضًا.
إعداد قاعدة بيانات Firebase لدور المدير
لقد عرَّفنا الخاصية البوليانية isAdmin
في الصنف AppUser
، وتُستخدم هذه الخاصية لتعريف دور المدير للمستخدم، وسنضيف -يدويًا- حقلًا جديدًا باسم isAdmin
للمستخدمين الذين نريد منحهم صلاحية المدير، وسننفذ التغييرات في تجميعة appusers
داخل قاعدة بيانات Firebase.
انتقل إلى قاعدة بيانات Firebase وافتح تجميعة appusers
التي تخزن سجلًا لجميع المستخدمين الذين سجلوا الدخول إلى تطبيقنا، انظر الصورة أدناه:
انقر على زر Add field لتظهر نافذة تطلب تعريف حقل جديد، فاجعل اسم الحقل isAdmin، ونوعه boolean، وقيمته True، ثم انقر على زر Add لإضافته، انظر الصورة أدناه:
سيضاف الحقل الجديد إلى التجميعة:
عند نجاح عملية تسجيل الدخول سنجلب بيانات المستخدم من تجميعة 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، وذلك يعني أن المستخدم يستطيع إضافة تدوينة جديدة لكنه لا يستطيع تعديل تدوينة موجودة أو أن يحذفها. انظر الصورة التالية:
انتقل إلى قاعدة بيانات Firebase وأضف حقل isAdmin جديد للمستخدم كما سبق شرحه، أو يمكنك تسجيل الخروج وإعادة تسجيل الدخول بمستخدم آخر يكون فيه حقل isAdmin يحمل القيمة true، وهنا تستطيع أن ترى زري Edit و Delete على بطاقة التدوينة. انظر الصورة التالية:
تحديث قواعد الأمان لقاعدة بيانات Firebase
لقد أنشأنا قاعدة البيانات في وضع الاختبار، وسنحدِّث القاعدة الأمنية لقاعدة البيانات الآن لتسمح بعملية الكتابة للمستخدم المصرَّح له فقط.
من القائمة التي على اليسار، اختر Database في قسم Develop، ثم اختر تبويب Rules، وحدِّث قواعد الأمان كما يلي:
service cloud.firestore { match /databases/{database}/documents { match /{document=**} { allow read; allow write: if request.auth.uid != null; } } }
ثم انقر على زر Publish لنشر القاعدة، انظر الصورة أدناه:
الخاتمة
تعلمنا في هذا الجزء من سلسلة بناء مدونة باستخدام إطار العمل Angular وقاعدة بيانات Firestore كيفية إضافة استيثاق جوجل للتأكد من تقييد وصول المستخدمين إلى مختلف أجزاء التطبيق وإعطاء كل مستخدم صلاحيات محددة بعد تسجيله في قاعدة بيانات التطبيق. أما في الجزء الخامس والأخير من هذه السلسلة، فسنكمل إضافة اللمسات النهائية على المدونة، مثل إضافة الملف الشخصي لكل كاتب ونشر التعليقات ومشاركة التدوينات على الشبكات الاجتماعية وغيرها ثم سنتعلم كيفية نشر المدونة على الإنترنت باستعمال Firebase.
ترجمة -وبتصرف- لفصول من كتاب Build a full stack web application using angular and firebase لصاحبه Ankit Sharma.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.