fix: user gets logged out when their token expires

Closes #230
This commit is contained in:
Thea Schöbl
2024-11-28 18:04:08 +00:00
committed by Jovan Krunić
parent 07e5c80223
commit 580ebee362
6 changed files with 67 additions and 64 deletions

View File

@@ -86,6 +86,8 @@ export abstract class AuthService implements IAuthService {
private _authenticatedSubject = new BehaviorSubject<boolean>(false);
private _loggedInSubject = new BehaviorSubject<boolean>(false);
private _initComplete = new BehaviorSubject<boolean>(false);
protected tokenHandler: TokenRequestHandler;
@@ -133,6 +135,13 @@ export abstract class AuthService implements IAuthService {
return this._authenticatedSubject.asObservable();
}
/**
* Similar to isAuthenticated$, but will also return true if the token is expired
*/
get isLoggedIn$(): Observable<boolean> {
return this._loggedInSubject.asObservable();
}
get initComplete$(): Observable<boolean> {
return this._initComplete.asObservable();
}
@@ -183,19 +192,20 @@ export abstract class AuthService implements IAuthService {
protected notifyActionListers(action: IAuthAction) {
/* eslint-disable unicorn/no-useless-undefined */
switch (action.action) {
case AuthActions.RefreshFailed:
case AuthActions.SignInFailed:
case AuthActions.SignOutSuccess:
case AuthActions.SignOutFailed: {
this._tokenSubject.next(undefined);
this._userSubject.next(undefined);
this._authenticatedSubject.next(false);
this._loggedInSubject.next(false);
break;
}
case AuthActions.LoadTokenFromStorageFailed: {
this._tokenSubject.next(undefined);
this._userSubject.next(undefined);
this._authenticatedSubject.next(false);
this._loggedInSubject.next(false);
this._initComplete.next(true);
break;
}
@@ -203,11 +213,13 @@ export abstract class AuthService implements IAuthService {
case AuthActions.RefreshSuccess: {
this._tokenSubject.next(action.tokenResponse);
this._authenticatedSubject.next(true);
this._loggedInSubject.next(true);
break;
}
case AuthActions.LoadTokenFromStorageSuccess: {
this._tokenSubject.next(action.tokenResponse);
this._authenticatedSubject.next((action.tokenResponse as TokenResponse).isValid(0));
this._loggedInSubject.next(true);
this._initComplete.next(true);
break;
}
@@ -442,7 +454,6 @@ export abstract class AuthService implements IAuthService {
public async refreshToken() {
await this.requestTokenRefresh().catch(error => {
this.storage.removeItem(TOKEN_RESPONSE_KEY);
this.notifyActionListers(AuthActionBuilder.RefreshFailed(error));
});
}

View File

@@ -81,6 +81,8 @@ export class PAIAAuthService {
private _authenticatedSubject = new BehaviorSubject<boolean>(false);
private _loggedIn = new BehaviorSubject<boolean>(false);
private _initComplete = new BehaviorSubject<boolean>(false);
protected tokenHandler: PAIATokenRequestHandler;
@@ -118,6 +120,13 @@ export class PAIAAuthService {
return this._authenticatedSubject.asObservable();
}
/**
* Similar to isAuthenticated$, but will also return true if the token is expired
*/
get isLoggedIn$(): Observable<boolean> {
return this._loggedIn.asObservable();
}
get initComplete$(): Observable<boolean> {
return this._initComplete.asObservable();
}
@@ -170,23 +179,27 @@ export class PAIAAuthService {
this._tokenSubject.next(undefined);
this._userSubject.next(undefined);
this._authenticatedSubject.next(false);
this._loggedIn.next(false);
break;
}
case AuthActions.LoadTokenFromStorageFailed: {
this._tokenSubject.next(undefined);
this._userSubject.next(undefined);
this._authenticatedSubject.next(false);
this._loggedIn.next(false);
this._initComplete.next(true);
break;
}
case AuthActions.SignInSuccess: {
this._tokenSubject.next(action.tokenResponse);
this._authenticatedSubject.next(true);
this._loggedIn.next(true);
break;
}
case AuthActions.LoadTokenFromStorageSuccess: {
this._tokenSubject.next(action.tokenResponse);
this._authenticatedSubject.next((action.tokenResponse as TokenResponse).isValid(0));
this._loggedIn.next(true);
this._initComplete.next(true);
break;
}

View File

@@ -23,9 +23,9 @@ export class IdCardsProvider {
this.encryptedStorageProvider.get<SCIdCard[]>('id-cards') as Promise<SCIdCard[]>,
).pipe(filter(it => it !== undefined));
return auth.isAuthenticated$.pipe(
mergeMap(isAuthenticated =>
isAuthenticated
return auth.isLoggedIn$.pipe(
mergeMap(isLoggedIn =>
isLoggedIn
? feature
? storedIdCards.pipe(
concatWith(

View File

@@ -7,7 +7,7 @@ import {SCAuthorizationProviderType} from '@openstapps/core';
import {EncryptedStorageProvider} from '../storage/encrypted-storage.provider';
class FakeAuth {
isAuthenticated$ = new BehaviorSubject(false);
isLoggedIn$ = new BehaviorSubject(false);
// eslint-disable-next-line @typescript-eslint/no-empty-function
getValidToken() {}
@@ -42,7 +42,7 @@ describe('IdCards', () => {
});
it('should emit network result when logged in', async () => {
fakeAuth.isAuthenticated$.next(true);
fakeAuth.isLoggedIn$.next(true);
httpClient.get = jasmine.createSpy().and.returnValue(of(['abc']));
fakeAuth.getValidToken = jasmine.createSpy().and.resolveTo({accessToken: 'fake-token'});
const provider = new IdCardsProvider(authHelper, configProvider, httpClient, encryptedStorageProvider);
@@ -63,7 +63,7 @@ describe('IdCards', () => {
expect(await firstValueFrom(observable)).toEqual([]);
httpClient.get = jasmine.createSpy().and.returnValue(of(['abc']));
fakeAuth.getValidToken = jasmine.createSpy().and.resolveTo({accessToken: 'fake-token'});
fakeAuth.isAuthenticated$.next(true);
fakeAuth.isLoggedIn$.next(true);
// this is counter-intuitive, but because we unsubscribed above the first value
// will now contain the network result.
expect(await firstValueFrom(observable)).toEqual(['abc' as never]);

View File

@@ -6,74 +6,56 @@
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* FITNESS FOR A PARTICULAr purpose. see the gnu general public license for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
* you should have received a copy of the gnu general public license along with
* this program. if not, see <https://www.gnu.org/licenses/>.
*/
import {Component, DestroyRef, inject, Input, OnInit} from '@angular/core';
import {Component, inject, input} from '@angular/core';
import {SCSection} from '../../../../config/profile-page-sections';
import {AuthHelperService} from '../../auth/auth-helper.service';
import {Observable} from 'rxjs';
import {SCAuthorizationProviderType} from '@openstapps/core';
import {mergeMap, of} from 'rxjs';
import Swiper from 'swiper';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {toObservable, toSignal} from '@angular/core/rxjs-interop';
@Component({
selector: 'stapps-profile-page-section',
templateUrl: 'profile-page-section.html',
styleUrls: ['profile-page-section.scss'],
})
export class ProfilePageSectionComponent implements OnInit {
@Input() item: SCSection;
export class ProfilePageSectionComponent {
item = input.required<SCSection>();
@Input() minSlideWidth = 110;
minSlideWidth = input(110);
isLoggedIn: boolean;
authHelper = inject(AuthHelperService);
loggedIn = toSignal(
toObservable(this.item).pipe(
mergeMap(item =>
item.authProvider ? this.authHelper.getProvider(item.authProvider).isLoggedIn$ : of(false),
),
),
);
isEnd = false;
isBeginning = true;
slidesPerView: number;
slidesPerView?: number;
slidesFillScreen = false;
data: {
[key in SCAuthorizationProviderType]: {loggedIn$: Observable<boolean>};
} = {
default: {
loggedIn$: this.authHelper.getProvider('default').isAuthenticated$,
},
paia: {
loggedIn$: this.authHelper.getProvider('paia').isAuthenticated$,
},
};
destroy$ = inject(DestroyRef);
constructor(private authHelper: AuthHelperService) {}
ngOnInit() {
if (this.item.authProvider) {
this.data[this.item.authProvider].loggedIn$
.pipe(takeUntilDestroyed(this.destroy$))
.subscribe(loggedIn => {
this.isLoggedIn = loggedIn;
});
}
}
activeIndexChange(swiper: Swiper) {
this.isBeginning = swiper.isBeginning;
this.isEnd = swiper.isEnd;
this.slidesFillScreen = this.slidesPerView >= swiper.slides.length;
this.slidesFillScreen = this.slidesPerView! >= swiper.slides.length;
}
resizeSwiper(resizeEvent: ResizeObserverEntry, swiper: Swiper) {
const slidesPerView =
Math.floor((resizeEvent.contentRect.width - this.minSlideWidth / 2) / this.minSlideWidth) + 0.5;
Math.floor((resizeEvent.contentRect.width - this.minSlideWidth() / 2) / this.minSlideWidth()) + 0.5;
if (slidesPerView > 1 && slidesPerView !== this.slidesPerView) {
this.slidesPerView = slidesPerView;
@@ -84,16 +66,13 @@ export class ProfilePageSectionComponent implements OnInit {
}
async toggleLogIn() {
if (!this.item.authProvider) return;
await (this.isLoggedIn ? this.signOut(this.item.authProvider) : this.signIn(this.item.authProvider));
}
async signIn(providerType: SCAuthorizationProviderType) {
await this.authHelper.getProvider(providerType).signIn();
}
async signOut(providerType: SCAuthorizationProviderType) {
await this.authHelper.getProvider(providerType).signOut();
await this.authHelper.endBrowserSession(providerType);
const providerType = this.item().authProvider;
if (!providerType) return;
if (this.loggedIn()) {
await this.authHelper.getProvider(providerType).signOut();
await this.authHelper.endBrowserSession(providerType);
} else {
await this.authHelper.getProvider(providerType).signIn();
}
}
}

View File

@@ -13,24 +13,24 @@
~ this program. If not, see <https://www.gnu.org/licenses/>.
-->
<stapps-section [title]="'name' | translateSimple: item">
@if (item.authProvider) {
<stapps-section [title]="'name' | translateSimple: item()">
@if (item().authProvider) {
<ion-button slot="button-end" fill="clear" color="dark" (click)="toggleLogIn()">
@if (isLoggedIn) {
@if (loggedIn()) {
<ion-icon slot="end" name="logout"></ion-icon>
} @else {
<ion-icon slot="end" name="login"></ion-icon>
}
<ion-label>{{ 'profile.buttons.default.log_' + (isLoggedIn ? 'out' : 'in') | translate }}</ion-label>
<ion-label>{{ 'profile.buttons.default.log_' + (loggedIn() ? 'out' : 'in') | translate }}</ion-label>
</ion-button>
}
<simple-swiper>
@for (link of item.links; track link) {
@for (link of item().links; track link) {
<ion-item
lines="none"
[routerLink]="link.link"
[disabled]="link.needsAuth && !isLoggedIn"
[disabled]="link.needsAuth && !loggedIn()"
[detail]="false"
>
<div>