Browse Source

fix: fix remember me functionality

pull/2610/head
mehmet-erim 7 years ago
parent
commit
189a77f4c4
  1. 8
      npm/ng-packs/packages/core/src/lib/actions/session.actions.ts
  2. 9
      npm/ng-packs/packages/core/src/lib/core.module.ts
  3. 7
      npm/ng-packs/packages/core/src/lib/models/session.ts
  4. 20
      npm/ng-packs/packages/core/src/lib/services/session-state.service.ts
  5. 106
      npm/ng-packs/packages/core/src/lib/states/session.state.ts
  6. 2
      npm/ng-packs/packages/core/src/lib/tests/config.plugin.spec.ts
  7. 6
      npm/ng-packs/packages/core/src/lib/tests/session-state.service.spec.ts
  8. 10
      npm/ng-packs/packages/core/src/lib/tests/session.state.spec.ts
  9. 2
      npm/ng-packs/packages/theme-shared/src/lib/tests/confirmation.service.spec.ts
  10. 56
      npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts
  11. 9
      npm/ng-packs/packages/theme-shared/src/lib/tests/toaster.service.spec.ts

8
npm/ng-packs/packages/core/src/lib/actions/session.actions.ts

@ -8,3 +8,11 @@ export class SetTenant {
static readonly type = '[Session] Set Tenant'; static readonly type = '[Session] Set Tenant';
constructor(public payload: ABP.BasicItem) {} 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) {}
}

9
npm/ng-packs/packages/core/src/lib/core.module.ts

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

7
npm/ng-packs/packages/core/src/lib/models/session.ts

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

20
npm/ng-packs/packages/core/src/lib/services/session-state.service.ts

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

106
npm/ng-packs/packages/core/src/lib/states/session.state.ts

@ -1,14 +1,29 @@
import { Action, Selector, State, StateContext } from '@ngxs/store'; import {
import { from } from 'rxjs'; Action,
import { switchMap } from 'rxjs/operators'; 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 { 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 { ABP, Session } from '../models';
import { LocalizationService } from '../services/localization.service'; import { LocalizationService } from '../services/localization.service';
import { OAuthService } from 'angular-oauth2-oidc';
@State<Session.State>({ @State<Session.State>({
name: 'SessionState', name: 'SessionState',
defaults: {} as Session.State, defaults: { sessionDetail: { openedTabCount: 0 } } as Session.State,
}) })
export class SessionState { export class SessionState {
@Selector() @Selector()
@ -21,7 +36,42 @@ export class SessionState {
return tenant; 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) @Action(SetLanguage)
setLanguage({ patchState, dispatch }: StateContext<Session.State>, { payload }: SetLanguage) { setLanguage({ patchState, dispatch }: StateContext<Session.State>, { payload }: SetLanguage) {
@ -40,4 +90,48 @@ export class SessionState {
tenant: payload, 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,
},
});
}
} }

2
npm/ng-packs/packages/core/src/lib/tests/config.plugin.spec.ts

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

6
npm/ng-packs/packages/core/src/lib/tests/session-state.service.spec.ts

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

10
npm/ng-packs/packages/core/src/lib/tests/session.state.spec.ts

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

2
npm/ng-packs/packages/theme-shared/src/lib/tests/confirmation.service.spec.ts

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

56
npm/ng-packs/packages/theme-shared/src/lib/tests/error.handler.spec.ts

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

9
npm/ng-packs/packages/theme-shared/src/lib/tests/toaster.service.spec.ts

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

Loading…
Cancel
Save