جافاسكريبت - AngularFirebaseAuth: استدعاء واجهة برمجة التطبيقات الخاصة بالخادم مباشرة بعد مصادقة Firebase؟
مصادقتي مبنية على أمرين:
- مصادقة Firebase (البريد الإلكتروني/كلمة المرور)
- استدعاء واجهة برمجة تطبيقات الخادم لاسترجاع الكيان الكامل للعميل من قاعدة البيانات ومن معرف Firebase (يجب أن يكون المستخدم موجودًا) لذا سيكون المستخدم "موثقًا" إذا تم استيفاء هذين الشرطين.
لدي أيضًا حراس مصادقة استنادًا إلى isAuthenticated() الذي يُرجع Observable (لأنه عند تحديث الصفحة، يجب على الحارس الانتظار حتى تكتمل المصادقة قبل إعادة توجيه المستخدم إلى أي مكان).
المشكلة: لا أستطيع أن أجد طريقة لجعل ذلك يعمل مع كل الفوضى/الجحيم الخاص بـ async و rxjs.. حاليًا هو يعمل، لكن في كل مرة يتم فيها استدعاء isAuthenticated، يتم استدعاء مصادقة serverAPI في كل مرة... كيف يمكنني إعادة هيكلة ذلك من أجل استدعاء الخادم مرة واحدة فقط وكل شيء async/إعادة التحميل لا يزال يعمل؟
خدمة المصادقة :
export class AuthService {
public userRole: UserBoRole;
public authState$: Observable<firebase.User>;
constructor(
private afAuth: AngularFireAuth,
private snackBar: SnackBarService,
private translate: TranslateService,
private router: Router,
private grpcService: GrpcService
) {
this.authState$ = this.afAuth.authState.pipe(
take(1),
mergeMap(user => {
if (!user) {
return of(user);
}
// User is successfully logged in,
// now we need to check if he has a correct role to access our app
// if an error occured, consider our user has not logged in, so we return null
return this.checkProfile().pipe(
take(1),
map(() => {
this.test = true;
return user;
}),
catchError(err => {
console.error(err);
return of(null);
})
);
})
);
// Subscribing to auth state change. (useless here because access logic is handled by the AuthGuard)
this.authState$.subscribe(user => {
console.log('authState$ changed :', user ? user.toJSON() : 'not logged in');
});
}
checkProfile() {
return this.callAuthApi().pipe(
map((customer) => {
if (!customer || customer.hasRole() === "anonymous") {
return Promise.reject(new Error(AuthService.AUTH_ERROR_ROLE));
}
this.userRole = customer.getRole();
})
);
}
isAuthenticated(): Observable<boolean> {
return this.authState$.pipe(map(authState => !!authState));
}
}
حارس المصادقة :
export class AuthGuard implements CanActivate, CanActivateChild {
constructor(private authService: AuthService, private router: Router) {}
check(): Observable<boolean> {
return this.authService.isAuthenticated().pipe(
catchError(err => {
// notifying UI of the error
this.authService.handleAuthError(err);
// signout user
this.authService.signOut();
// if an error occured, consider our user has not logged in
return of(false);
}),
tap(isAuthenticated => {
if (!isAuthenticated) {
// redirecting to login
this.router.navigate(['login']);
}
})
);
}
canActivateChild(): Observable<boolean> {
return this.check();
}
canActivate(): Observable<boolean> {
return this.check();
}
}
شكراً
الإجابة رقم 1
يمكنك تغيير دالة checkProfile() لإرجاع observable بدلاً من observable من طلب http أو promise في حالة وجود خطأ. أولاً ستتحقق مما إذا كان المستخدم قد تم التحقق من هويته بالفعل (افترضت أن userRole سيكون مناسباً بما أنك تحفظه بعد الاتصال بالخادم) وإذا كان نعم، فقم بإرجاع observable تم إنشاؤه حديثًا بدون الاتصال بالخادم، وإلا ستجري الطلب وتصدر observable الخاص بك استنادًا إلى نتيجة طلب http. مع المثال التالي، ستقوم بالاتصال مرة واحدة فقط:
checkProfile() {
return new Observable((observer) => {
if (this.userRole) {
observer.next();
observer.complete();
} else {
this.callAuthApi().pipe(
map((customer) => {
if (!customer || customer.hasRole() === "anonymous") {
observer.error(new Error(AuthService.AUTH_ERROR_ROLE));
observer.complete();
}
this.userRole = customer.getRole();
observer.next();
observer.complete();
})
);
}
});
}
الإجابة رقم 2
هاها، ReactiveX ليس سهلاً. لديه منحنى تعلم حاد إلى حد ما. لكنه قوي حقًا.
1. اتصل بالخادم مرة واحدة فقط
يمكنك استخدام shareReplay.
لفهم كيفية عمل shareReplay، ألقِ نظرة هنا https://ng-rxjs-share-replay.stackblitz.io
//shareReplay example
ngOnInit() {
const tods$ = this.getTodos();
tods$.subscribe(console.log);// 1st sub
tods$.subscribe(console.log);// 2st sub
}
getTodos(): Observable<Todo[]> {
return this.http.get<Todo[]>(this.url)
.pipe(
tap(() => console.log('Request')),
shareReplay(1) // compare with comment and uncomment
);
}
الإخراج مع shareReplay
Request
[Object, Object, Object]
[Object, Object, Object]
الإخراج بدون shareReplay
Request
[Object, Object, Object]
Request
[Object, Object, Object]
يمكنك استخدام shareReplay في كود خدمة المصادقة الخاصة بك.
//auth.services.ts
import { shareReplay } from 'rxjs/operators';
...
this.user$ = this.afAuth.authState.pipe(
tap(user => {
console.log('login user$ here', user)
}),
switchMap(user => {
if (user) {
//do something
return this.db.object(`users/${user.uid}`).valueChanges();
} else {
return of(null);
}
}),
shareReplay(1) //**** this will prevent unnecessary request****
);
2. async و await toPromise()
//auth.service.ts
...
getUser() {
return this.user$.pipe(first()).toPromise();
}
//auth.guard.ts
...
async canActivate(next: ActivatedRouteSnapshot
, state: RouterStateSnapshot
): Promise<boolean> {
const user = await this.auth.getUser();
//TODO your API code or other conditional authentication here
if (!user) {
this.router.navigate(['/login']);
}
return !!user;
}
آمل أن هذا سيساعدك.
