fix: fix remember me functionality

pull/2610/head
mehmet-erim 6 years ago
parent e8adc2bca9
commit 189a77f4c4

@ -8,3 +8,11 @@ export class SetTenant {
static readonly type = '[Session] Set Tenant';
constructor(public payload: ABP.BasicItem) {}
}
export class ModifyOpenedTabCount {
static readonly type = '[Session] Modify Opened Tab Count';
constructor(public operation: 'increase' | 'decrease') {}
}
export class SetRemember {
static readonly type = '[Session] Set Remember';
constructor(public payload: boolean) {}
}

@ -6,7 +6,7 @@ import { RouterModule } from '@angular/router';
import { NgxsRouterPluginModule } from '@ngxs/router-plugin';
import { NgxsStoragePluginModule } from '@ngxs/storage-plugin';
import { NgxsModule, NGXS_PLUGINS } from '@ngxs/store';
import { OAuthModule } from 'angular-oauth2-oidc';
import { OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';
import { AbstractNgModelComponent } from './abstracts/ng-model.component';
import { DynamicLayoutComponent } from './components/dynamic-layout.component';
import { RouterOutletComponent } from './components/router-outlet.component';
@ -34,12 +34,15 @@ import { ReplaceableComponentsState } from './states/replaceable-components.stat
import { InitDirective } from './directives/init.directive';
import { ReplaceableTemplateDirective } from './directives/replaceable-template.directive';
export function storageFactory(): OAuthStorage {
return localStorage;
}
@NgModule({
imports: [
NgxsModule.forFeature([ReplaceableComponentsState, ProfileState, SessionState, ConfigState]),
NgxsRouterPluginModule.forRoot(),
NgxsStoragePluginModule.forRoot({ key: ['SessionState'] }),
OAuthModule.forRoot(),
OAuthModule,
CommonModule,
HttpClientModule,
FormsModule,
@ -127,6 +130,8 @@ export class CoreModule {
deps: [Injector],
useFactory: localeInitializer,
},
...OAuthModule.forRoot().providers,
{ provide: OAuthStorage, useFactory: storageFactory },
],
};
}

@ -4,5 +4,12 @@ export namespace Session {
export interface State {
language: string;
tenant: ABP.BasicItem;
sessionDetail: SessionDetail;
}
export interface SessionDetail {
openedTabCount: number;
lastExitTime: number;
remember: boolean;
}
}

@ -1,8 +1,12 @@
import { Injectable } from '@angular/core';
import { Store } from '@ngxs/store';
import {
SetLanguage,
SetRemember,
SetTenant,
ModifyOpenedTabCount,
} from '../actions/session.actions';
import { SessionState } from '../states';
import { ABP } from '../models';
import { SetLanguage, SetTenant } from '../actions';
@Injectable({
providedIn: 'root',
@ -18,6 +22,10 @@ export class SessionStateService {
return this.store.selectSnapshot(SessionState.getTenant);
}
getSessionDetail() {
return this.store.selectSnapshot(SessionState.getSessionDetail);
}
dispatchSetLanguage(...args: ConstructorParameters<typeof SetLanguage>) {
return this.store.dispatch(new SetLanguage(...args));
}
@ -25,4 +33,12 @@ export class SessionStateService {
dispatchSetTenant(...args: ConstructorParameters<typeof SetTenant>) {
return this.store.dispatch(new SetTenant(...args));
}
dispatchSetRemember(...args: ConstructorParameters<typeof SetRemember>) {
return this.store.dispatch(new SetRemember(...args));
}
dispatchModifyOpenedTabCount(...args: ConstructorParameters<typeof ModifyOpenedTabCount>) {
return this.store.dispatch(new ModifyOpenedTabCount(...args));
}
}

@ -1,14 +1,29 @@
import { Action, Selector, State, StateContext } from '@ngxs/store';
import { from } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import {
Action,
Selector,
State,
StateContext,
Store,
NgxsOnInit,
Actions,
ofActionSuccessful,
} from '@ngxs/store';
import { from, fromEvent } from 'rxjs';
import { switchMap, take } from 'rxjs/operators';
import { GetAppConfiguration } from '../actions/config.actions';
import { SetLanguage, SetTenant } from '../actions/session.actions';
import {
SetLanguage,
SetTenant,
ModifyOpenedTabCount,
SetRemember,
} from '../actions/session.actions';
import { ABP, Session } from '../models';
import { LocalizationService } from '../services/localization.service';
import { OAuthService } from 'angular-oauth2-oidc';
@State<Session.State>({
name: 'SessionState',
defaults: {} as Session.State,
defaults: { sessionDetail: { openedTabCount: 0 } } as Session.State,
})
export class SessionState {
@Selector()
@ -21,7 +36,42 @@ export class SessionState {
return tenant;
}
constructor(private localizationService: LocalizationService) {}
@Selector()
static getSessionDetail({ sessionDetail }: Session.State): Session.SessionDetail {
return sessionDetail;
}
constructor(
private localizationService: LocalizationService,
private oAuthService: OAuthService,
private store: Store,
private actions: Actions,
) {
actions
.pipe(ofActionSuccessful(GetAppConfiguration))
.pipe(take(1))
.subscribe(() => {
const { sessionDetail } = this.store.selectSnapshot(SessionState) || { sessionDetail: {} };
const fiveMinutesBefore = new Date().valueOf() - 5 * 60 * 1000;
if (
sessionDetail.lastExitTime &&
sessionDetail.openedTabCount === 0 &&
this.oAuthService.hasValidAccessToken() &&
sessionDetail.remember === false &&
sessionDetail.lastExitTime < fiveMinutesBefore
) {
this.oAuthService.logOut();
}
this.store.dispatch(new ModifyOpenedTabCount('increase'));
fromEvent(window, 'beforeunload').subscribe(() => {
this.store.dispatch(new ModifyOpenedTabCount('decrease'));
});
});
}
@Action(SetLanguage)
setLanguage({ patchState, dispatch }: StateContext<Session.State>, { payload }: SetLanguage) {
@ -40,4 +90,48 @@ export class SessionState {
tenant: payload,
});
}
@Action(SetRemember)
setRemember(
{ getState, patchState }: StateContext<Session.State>,
{ payload: remember }: SetRemember,
) {
const { sessionDetail } = getState();
patchState({
sessionDetail: {
...sessionDetail,
remember,
},
});
}
@Action(ModifyOpenedTabCount)
modifyOpenedTabCount(
{ getState, patchState }: StateContext<Session.State>,
{ operation }: ModifyOpenedTabCount,
) {
// tslint:disable-next-line: prefer-const
let { openedTabCount, lastExitTime, ...detail } =
getState().sessionDetail || ({ openedTabCount: 0 } as Session.SessionDetail);
if (operation === 'increase') {
openedTabCount++;
} else if (operation === 'decrease') {
openedTabCount--;
lastExitTime = new Date().valueOf();
}
if (!openedTabCount || openedTabCount < 0) {
openedTabCount = 0;
}
patchState({
sessionDetail: {
openedTabCount,
lastExitTime,
...detail,
},
});
}
}

@ -10,6 +10,7 @@ import { ABP } from '../models';
import { ConfigPlugin, NGXS_CONFIG_PLUGIN_OPTIONS } from '../plugins';
import { ConfigState } from '../states';
import { addAbpRoutes } from '../utils';
import { OAuthModule } from 'angular-oauth2-oidc';
addAbpRoutes([
{
@ -323,6 +324,7 @@ describe('ConfigPlugin', () => {
service: ConfigPlugin,
imports: [
CoreModule,
OAuthModule.forRoot(),
NgxsModule.forRoot([]),
RouterTestingModule.withRoutes([
{

@ -3,13 +3,17 @@ import { SessionStateService } from '../services/session-state.service';
import { SessionState } from '../states/session.state';
import { Store } from '@ngxs/store';
import * as SessionActions from '../actions';
import { OAuthService } from 'angular-oauth2-oidc';
describe('SessionStateService', () => {
let service: SessionStateService;
let spectator: SpectatorService<SessionStateService>;
let store: SpyObject<Store>;
const createService = createServiceFactory({ service: SessionStateService, mocks: [Store] });
const createService = createServiceFactory({
service: SessionStateService,
mocks: [Store, OAuthService],
});
beforeEach(() => {
spectator = createService();
service = spectator.service;

@ -1,9 +1,11 @@
import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { Session } from '../models/session';
import { LocalizationService } from '../services';
import { LocalizationService, AuthService } from '../services';
import { SessionState } from '../states';
import { GetAppConfiguration } from '../actions/config.actions';
import { of } from 'rxjs';
import { of, Subject } from 'rxjs';
import { Store, Actions } from '@ngxs/store';
import { OAuthService } from 'angular-oauth2-oidc';
export class DummyClass {}
@ -18,12 +20,12 @@ describe('SessionState', () => {
const createService = createServiceFactory({
service: DummyClass,
mocks: [LocalizationService],
mocks: [LocalizationService, Store, Actions, OAuthService],
});
beforeEach(() => {
spectator = createService();
state = new SessionState(spectator.get(LocalizationService));
state = new SessionState(spectator.get(LocalizationService), null, null, new Subject());
});
describe('#getLanguage', () => {

@ -6,6 +6,7 @@ import { NgxsModule } from '@ngxs/store';
import { MessageService } from 'primeng/components/common/messageservice';
import { ConfirmationService } from '../services/confirmation.service';
import { ThemeSharedModule } from '../theme-shared.module';
import { OAuthModule, OAuthService } from 'angular-oauth2-oidc';
@Component({
selector: 'abp-dummy',
@ -24,6 +25,7 @@ describe('ConfirmationService', () => {
component: DummyComponent,
imports: [CoreModule, ThemeSharedModule.forRoot(), NgxsModule.forRoot(), RouterTestingModule],
providers: [MessageService],
mocks: [OAuthService],
});
beforeEach(() => {

@ -9,8 +9,12 @@ import { ThemeSharedModule } from '../theme-shared.module';
import { MessageService } from 'primeng/components/common/messageservice';
import { RouterError, RouterDataResolved } from '@ngxs/router-plugin';
import { NavigationError, ResolveEnd } from '@angular/router';
import { OAuthModule, OAuthService } from 'angular-oauth2-oidc';
@Component({ selector: 'abp-dummy', template: 'dummy works! <abp-confirmation></abp-confirmation>' })
@Component({
selector: 'abp-dummy',
template: 'dummy works! <abp-confirmation></abp-confirmation>',
})
class DummyComponent {
constructor(public errorHandler: ErrorHandler) {}
}
@ -21,6 +25,7 @@ describe('ErrorHandler', () => {
const createComponent = createRoutingFactory({
component: DummyComponent,
imports: [CoreModule, ThemeSharedModule.forRoot(), NgxsModule.forRoot([])],
mocks: [OAuthService],
stubsEnabled: false,
routes: [
{ path: '', component: DummyComponent },
@ -38,33 +43,53 @@ describe('ErrorHandler', () => {
it('should display the error component when server error occurs', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 500 })));
expect(document.querySelector('.error-template')).toHaveText(DEFAULT_ERROR_MESSAGES.defaultError500.title);
expect(document.querySelector('.error-details')).toHaveText(DEFAULT_ERROR_MESSAGES.defaultError500.details);
expect(document.querySelector('.error-template')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError500.title,
);
expect(document.querySelector('.error-details')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError500.details,
);
});
it('should display the error component when authorize error occurs', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 403 })));
expect(document.querySelector('.error-template')).toHaveText(DEFAULT_ERROR_MESSAGES.defaultError403.title);
expect(document.querySelector('.error-details')).toHaveText(DEFAULT_ERROR_MESSAGES.defaultError403.details);
expect(document.querySelector('.error-template')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError403.title,
);
expect(document.querySelector('.error-details')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError403.details,
);
});
it('should display the error component when unknown error occurs', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 0, statusText: 'Unknown Error' })));
expect(document.querySelector('.error-template')).toHaveText(DEFAULT_ERROR_MESSAGES.defaultError.title);
store.dispatch(
new RestOccurError(new HttpErrorResponse({ status: 0, statusText: 'Unknown Error' })),
);
expect(document.querySelector('.error-template')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError.title,
);
});
it('should display the confirmation when not found error occurs', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 404 })));
spectator.detectChanges();
expect(spectator.query('.abp-confirm-summary')).toHaveText(DEFAULT_ERROR_MESSAGES.defaultError404.title);
expect(spectator.query('.abp-confirm-body')).toHaveText(DEFAULT_ERROR_MESSAGES.defaultError404.details);
expect(spectator.query('.abp-confirm-summary')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError404.title,
);
expect(spectator.query('.abp-confirm-body')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError404.details,
);
});
it('should display the confirmation when default error occurs', () => {
store.dispatch(new RestOccurError(new HttpErrorResponse({ status: 412 })));
spectator.detectChanges();
expect(spectator.query('.abp-confirm-summary')).toHaveText(DEFAULT_ERROR_MESSAGES.defaultError.title);
expect(spectator.query('.abp-confirm-body')).toHaveText(DEFAULT_ERROR_MESSAGES.defaultError.details);
expect(spectator.query('.abp-confirm-summary')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError.title,
);
expect(spectator.query('.abp-confirm-body')).toHaveText(
DEFAULT_ERROR_MESSAGES.defaultError.details,
);
});
it('should display the confirmation when authenticated error occurs', async () => {
@ -110,7 +135,8 @@ describe('ErrorHandler', () => {
@Component({
selector: 'abp-dummy-error',
template: '<p>{{errorStatus}}</p><button id="close-dummy" (click)="destroy$.next()">Close</button>',
template:
'<p>{{errorStatus}}</p><button id="close-dummy" (click)="destroy$.next()">Close</button>',
})
class DummyErrorComponent {
errorStatus;
@ -130,12 +156,16 @@ describe('ErrorHandler with custom error component', () => {
imports: [
CoreModule,
ThemeSharedModule.forRoot({
httpErrorConfig: { errorScreen: { component: DummyErrorComponent, forWhichErrors: [401, 403, 404, 500] } },
httpErrorConfig: {
errorScreen: { component: DummyErrorComponent, forWhichErrors: [401, 403, 404, 500] },
},
}),
NgxsModule.forRoot([]),
ErrorModule,
],
mocks: [OAuthService],
stubsEnabled: false,
routes: [
{ path: '', component: DummyComponent },
{ path: 'account/login', component: RouterOutletComponent },

@ -6,6 +6,7 @@ import { NgxsModule } from '@ngxs/store';
import { MessageService } from 'primeng/components/common/messageservice';
import { ToasterService } from '../services/toaster.service';
import { ThemeSharedModule } from '../theme-shared.module';
import { OAuthService } from 'angular-oauth2-oidc';
@Component({
selector: 'abp-dummy',
@ -24,6 +25,7 @@ describe('ToasterService', () => {
component: DummyComponent,
imports: [CoreModule, ThemeSharedModule.forRoot(), NgxsModule.forRoot(), RouterTestingModule],
providers: [MessageService],
mocks: [OAuthService],
});
beforeEach(() => {
@ -67,10 +69,9 @@ describe('ToasterService', () => {
{ summary: 'summary2', detail: 'detail2' },
]);
spectator.detectChanges();
expect(spectator.queryAll('div.ui-toast-summary').map(node => node.textContent.trim())).toEqual([
'summary1',
'summary2',
]);
expect(
spectator.queryAll('div.ui-toast-summary').map(node => node.textContent.trim()),
).toEqual(['summary1', 'summary2']);
expect(spectator.queryAll('div.ui-toast-detail').map(node => node.textContent.trim())).toEqual([
'detail1',
'detail2',

Loading…
Cancel
Save