Commit bbce0111 authored by Emily Jensen's avatar Emily Jensen

Merge in dev

parents ae115fa4 c8b3081f
......@@ -124,7 +124,7 @@ deploy-firefox-ext:
- master
when: manual
script:
- yarn install
- yarn add @wext/shipit
- yarn run ext:publish_firefox
pages:
......@@ -134,7 +134,7 @@ pages:
- node_modules/
script:
- yarn install
- yarn run compodoc-gitlab
- yarn build-storybook -o public
artifacts:
paths:
- public
......
import "@storybook/addon-knobs/register";
import "@storybook/addon-actions/register";
import "@storybook/addon-knobs/register";
import { configure } from "@storybook/angular";
import "!style-loader!css-loader!sass-loader!../src/styles.scss";
import { configure } from '@storybook/angular';
// automatically import all files ending in *.stories.ts
const req = require.context("../src/stories", true, /.stories.ts$/);
const req = require.context('../src/stories', true, /.stories.ts$/);
function loadStories() {
req.keys().forEach(filename => req(filename));
}
......
FROM node:8-alpine
FROM node:10-alpine
RUN mkdir /dist
WORKDIR /dist
......
......@@ -26,9 +26,7 @@
"output": "/"
}
],
"styles": [
"src/styles.scss"
],
"styles": ["src/styles.scss"],
"scripts": []
},
"configurations": {
......@@ -150,9 +148,7 @@
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.spec.json",
"scripts": [],
"styles": [
"src/styles.css"
],
"styles": ["src/styles.scss"],
"assets": [
"src/assets",
"src/manifest.json",
......@@ -168,13 +164,8 @@
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"src/tsconfig.app.json",
"src/tsconfig.spec.json"
],
"exclude": [
"**/node_modules/**/*"
]
"tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json"],
"exclude": ["**/node_modules/**/*"]
}
}
}
......@@ -199,12 +190,8 @@
"lint": {
"builder": "@angular-devkit/build-angular:tslint",
"options": {
"tsConfig": [
"e2e/tsconfig.e2e.json"
],
"exclude": [
"**/node_modules/**/*"
]
"tsConfig": ["e2e/tsconfig.e2e.json"],
"exclude": ["**/node_modules/**/*"]
}
}
}
......
......@@ -18,10 +18,7 @@
"e2e": "ng e2e",
"e2e:docker": "yarn e2e --webdriver-update false",
"bundle-report": "webpack-bundle-analyzer dist/stats.json",
"compodoc": "./node_modules/.bin/compodoc -p tsconfig.json",
"compodoc-serve": "./node_modules/.bin/compodoc -p tsconfig.json -s",
"compodoc-gitlab": "./node_modules/.bin/compodoc -p tsconfig.json -d public/",
"storybook": "start-storybook -p 6006 -s ./src",
"storybook": "start-storybook -p 6006",
"prettier": "find src/ -type f -name \"*.ts\" | xargs ./node_modules/.bin/prettier --write",
"build-storybook": "build-storybook"
},
......@@ -31,91 +28,89 @@
"url": "git@gitlab.com:burke-software/passit-frontend.git"
},
"dependencies": {
"@angular/animations": "^6.0.0",
"@angular/common": "^6.0.0",
"@angular/compiler": "^6.0.0",
"@angular/core": "^6.0.0",
"@angular/forms": "^6.0.0",
"@angular/http": "^6.0.0",
"@angular/platform-browser": "^6.0.0",
"@angular/platform-browser-dynamic": "^6.0.0",
"@angular/pwa": "^0.7.5",
"@angular/router": "^6.0.0",
"@angular/service-worker": "^6.0.0",
"@angular/animations": "^7.0.4",
"@angular/common": "^7.0.4",
"@angular/compiler": "^7.0.4",
"@angular/core": "^7.0.4",
"@angular/forms": "^7.0.4",
"@angular/http": "^7.0.4",
"@angular/platform-browser": "^7.0.4",
"@angular/platform-browser-dynamic": "^7.0.4",
"@angular/pwa": "^0.10.6",
"@angular/router": "^7.0.4",
"@angular/service-worker": "^7.0.4",
"@braintree/sanitize-url": "~2.1.0",
"@ngrx/effects": "^6.0.1",
"@ngrx/entity": "^6.0.1",
"@ngrx/router-store": "^6.0.1",
"@ngrx/store": "^6.0.1",
"@ngrx/store-devtools": "^6.0.1",
"@types/file-saver": "^1.3.0",
"@types/jspdf": "^1.1.31",
"@types/papaparse": "~4.5.2",
"@types/qrcode": "^1.2.0",
"@ngrx/effects": "^6.1.2",
"@ngrx/entity": "^6.1.2",
"@ngrx/router-store": "^6.1.2",
"@ngrx/store": "^6.1.2",
"@ngrx/store-devtools": "^6.1.2",
"@types/file-saver": "^2.0.0",
"@types/jspdf": "^1.2.1",
"@types/papaparse": "~4.5.5",
"@types/qrcode": "^1.3.0",
"@types/storybook__addon-actions": "^3.4.1",
"@types/storybook__addon-knobs": "^3.4.1",
"angular2-hotkeys": "^2.1.2",
"angular2-hotkeys": "^2.1.4",
"babel-loader": "^8.0.2",
"canvg": "^1.5.3",
"core-js": "^2.5.1",
"file-saver": "~1.3.3",
"file-saver": "~2.0.0",
"global": "^4.3.2",
"jasmine-marbles": "^0.3.1",
"jasmine-marbles": "^0.4.0",
"jspdf": "^1.4.1",
"jsqr": "^1.1.1",
"ng-inline-svg": "~8.0.0",
"ng-select": "~1.0.0",
"ngrx-forms": "^3.0.2",
"ngrx-store-localstorage": "~5.0.0",
"ngx-clipboard": "^11.0.0",
"ngx-tooltip": "~0.0.9",
"karma-webdriver-launcher": "^1.0.5",
"ng-inline-svg": "~8.1.0",
"ng-select": "~1.0.1",
"ngrx-forms": "^3.1.0",
"ngrx-store-localstorage": "~5.1.0",
"ngx-clipboard": "^11.1.9",
"ngx-tooltip": "^0.0.9",
"normalize-scss": "~7.0.0",
"papaparse": "~4.6.0",
"passit-sdk-js": "2.3.0",
"qrcode": "^1.2.2",
"raven-js": "^3.26.4",
"rxjs": "~6.1.0",
"rxjs-compat": "^6.1.0",
"passit-sdk-js": "2.3.2",
"qrcode": "^1.3.2",
"raven-js": "^3.27.0",
"rxjs": "^6.3.3",
"rxjs-tslint": "^0.1.5",
"stream": "^0.0.2",
"susy": "^2.2.12",
"url-parse": "^1.2.0",
"webextension-polyfill": "^0.3.0",
"url-parse": "^1.4.4",
"webextension-polyfill": "^0.3.1",
"zone.js": "~0.8.18"
},
"devDependencies": {
"@angular-devkit/build-angular": "~0.8.3",
"@angular-devkit/core": "0.8.3",
"@angular/cli": "^6.1.2",
"@angular/compiler-cli": "^6.0.0",
"@angular-devkit/build-angular": "~0.10.0",
"@angular/cli": "^7.0.6",
"@angular/compiler-cli": "^7.0.4",
"@babel/core": "^7.1.0",
"@compodoc/compodoc": "~1.1.3",
"@storybook/addon-actions": "^4.0.0-alpha.22",
"@storybook/addon-knobs": "^4.0.0-alpha.22",
"@storybook/addons": "^4.0.0-alpha.22",
"@storybook/angular": "^4.0.0-alpha.22",
"@storybook/addon-actions": "^4.0.7",
"@storybook/addon-knobs": "^4.0.7",
"@storybook/addon-links": "^4.0.7",
"@storybook/addon-notes": "^4.0.7",
"@storybook/addons": "^4.0.7",
"@storybook/angular": "^4.0.7",
"@types/jasmine": "~2.8.2",
"@types/jasminewd2": "~2.0.3",
"@types/node": "~8.5.2",
"@wext/shipit": "^0.1.3",
"@types/node": "~10.12.8",
"breakpoint-sass": "~2.7.1",
"codelyzer": "~4.4.3",
"jasmine-core": "~2.8.0",
"codelyzer": "~4.5.0",
"jasmine-core": "^2.99.0",
"jasmine-spec-reporter": "~4.2.1",
"karma": "~1.7.0",
"karma": "~3.1.1",
"karma-chrome-launcher": "~2.2.0",
"karma-cli": "~1.0.1",
"karma-coverage-istanbul-reporter": "~1.3.0",
"karma-coverage-istanbul-reporter": "~2.0.4",
"karma-jasmine": "~1.1.0",
"karma-jasmine-html-reporter": "~0.2.2",
"karma-webdriver-launcher": "^1.0.5",
"prettier": "^1.10.2",
"protractor": "~5.4.0",
"ts-node": "~7.0.0",
"tslint": "~5.11.0",
"typescript": "~2.9.2",
"web-ext": "^2.4.0",
"web-ext-types": "^2.1.0",
"typescript": "~3.1.6",
"web-ext": "^2.9.2",
"web-ext-types": "^3.0.0",
"webpack-bundle-analyzer": "^3.0.2"
}
}
import { tap, withLatestFrom, map, exhaustMap } from "rxjs/operators";
import { of as observableOf } from "rxjs";
import {
tap,
withLatestFrom,
map,
exhaustMap,
catchError
} from "rxjs/operators";
import { of, from } from "rxjs";
import { Injectable } from "@angular/core";
import { Router } from "@angular/router";
......@@ -21,33 +27,36 @@ import * as fromAccount from "./account.reducer";
import { IS_EXTENSION } from "../constants";
import { UserService } from "./user";
import { Store } from "@ngrx/store";
import { Store, select } from "@ngrx/store";
import { IState } from "../app.reducers";
import { HttpErrorResponse } from "@angular/common/http";
import { SetPasswordActionTypes } from "./reset-password/set-password/set-password.actions";
@Injectable()
export class LoginEffects {
@Effect()
login$ = this.actions$.pipe(
ofType<LoginAction>(AccountActionTypes.LOGIN),
withLatestFrom(this.store.select(fromAccount.getLoginForm)),
withLatestFrom(this.store.pipe(select(fromAccount.getLoginForm))),
map(([action, form]) => form.value),
exhaustMap(auth => {
const callLogin = () => {
return this.userService
.login(
return from(
this.userService.login(
auth.email,
auth.password,
auth.rememberMe ? auth.rememberMe : false
)
.then(resp => new LoginSuccessAction(resp))
.catch(err => new LoginFailureAction(err));
).pipe(
map(resp => new LoginSuccessAction(resp)),
catchError(err => of(new LoginFailureAction(err)))
);
};
const callCheckAndSetUrl = (url: string) => {
return this.userService
.checkAndSetUrl(url)
.then(() => callLogin())
.catch(err => new LoginFailureAction(err));
return this.userService.checkAndSetUrl(url).pipe(
exhaustMap(() => callLogin()),
catchError(err => of(new LoginFailureAction(err)))
);
};
if (auth.url) {
......@@ -59,7 +68,11 @@ export class LoginEffects {
);
@Effect({ dispatch: false })
loginSuccess$ = this.actions$.ofType(AccountActionTypes.LOGIN_SUCCESS).pipe(
loginSuccess$ = this.actions$.pipe(
ofType(
AccountActionTypes.LOGIN_SUCCESS,
SetPasswordActionTypes.SET_PASSWORD_SUCCESS
),
tap(() => {
if (IS_EXTENSION) {
this.router.navigate(["/popup"]);
......@@ -70,19 +83,19 @@ export class LoginEffects {
);
@Effect({ dispatch: false })
loginRedirect$ = this.actions$
.ofType(
loginRedirect$ = this.actions$.pipe(
ofType(
AccountActionTypes.LOGIN_REDIRECT,
AccountActionTypes.LOGOUT_SUCCESS
)
.pipe(
tap(() => {
this.router.navigate(["/login"], { replaceUrl: true });
})
);
),
tap(() => {
this.router.navigate(["/login"], { replaceUrl: true });
})
);
@Effect()
logout$ = this.actions$.ofType(AccountActionTypes.LOGOUT).pipe(
logout$ = this.actions$.pipe(
ofType(AccountActionTypes.LOGOUT),
exhaustMap(() =>
this.userService
.logout()
......@@ -92,48 +105,49 @@ export class LoginEffects {
);
@Effect({ dispatch: false })
logoutSuccess$ = this.actions$
.ofType(AccountActionTypes.LOGOUT_SUCCESS)
.pipe(tap(() => localStorage.clear()));
logoutSuccess$ = this.actions$.pipe(
ofType(AccountActionTypes.LOGOUT_SUCCESS),
tap(() => localStorage.clear())
);
@Effect()
handleAPIError$ = this.actions$
.ofType<HandleAPIErrorAction>(AccountActionTypes.HANDLE_API_ERROR)
.pipe(
map(action => action.payload),
exhaustMap(err => {
const res: HttpErrorResponse = err.res;
if (res) {
if (res.status === 0) {
// Not so bad, network is just down
return observableOf(new APIFailureNetworkDownAction());
}
if ([401, 403].includes(res.status)) {
if (
res.status === 403 &&
res.error &&
res.error.detail === "User's email is not confirmed."
) {
return observableOf(new UserMustConfirmEmailAction());
}
return observableOf(new LogoutAction());
handleAPIError$ = this.actions$.pipe(
ofType<HandleAPIErrorAction>(AccountActionTypes.HANDLE_API_ERROR),
map(action => action.payload),
map(err => {
const res: HttpErrorResponse = err.res;
if (res) {
if (res.status === 0) {
// Not so bad, network is just down
return new APIFailureNetworkDownAction();
}
if ([401, 403].includes(res.status)) {
if (
res.status === 403 &&
res.error &&
res.error.detail === "User's email is not confirmed."
) {
return new UserMustConfirmEmailAction();
}
return new LogoutAction();
}
}
if (err.name === "Passit SDK Authentication Error") {
return observableOf(new LogoutAction());
}
if (err.name === "Passit SDK Authentication Error") {
return new LogoutAction();
}
// This should never run
console.error("Unable to use api and unable to handle error", err);
return observableOf(new CriticalAPIErrorAction(err));
})
);
// This should never run
console.error("Unable to use api and unable to handle error", err);
return new CriticalAPIErrorAction(err);
})
);
@Effect({ dispatch: false })
userMustConfirmEmail$ = this.actions$
.ofType(AccountActionTypes.USER_MUST_CONFIRM_EMAIL)
.pipe(tap(() => this.router.navigate(["/confirm-email"])));
userMustConfirmEmail$ = this.actions$.pipe(
ofType(AccountActionTypes.USER_MUST_CONFIRM_EMAIL),
tap(() => this.router.navigate(["/confirm-email"]))
);
constructor(
private actions$: Actions,
......
import { take } from "rxjs/operators";
import { fakeAsync, inject, TestBed } from "@angular/core/testing";
import { Store, StoreModule } from "@ngrx/store";
import "rxjs/add/operator/take";
import { Store, StoreModule, select } from "@ngrx/store";
import { logout } from "../app.reducers";
import {
......@@ -60,7 +59,8 @@ describe("AccountReducer", () => {
userToken: "aaa",
url: fromAccount.initialState.url,
rememberMe: false,
optInErrorReporting: false
optInErrorReporting: false,
forceSetPassword: false
};
const result = fromAccount.authReducer(
......@@ -92,34 +92,33 @@ describe("AccountReducer", () => {
});
});
it(
"should logout a user",
fakeAsync(
inject([Store], (store: Store<any>) => {
const isLoggedInSelector = store.select(fromAccount.getIsLoggedIn);
it("should logout a user", fakeAsync(
inject([Store], (store: Store<any>) => {
const isLoggedInSelector = store.pipe(
select(fromAccount.getIsLoggedIn)
);
// Login first
const user: IAuthStore = {
email: "test@example.com",
privateKey: "fake",
publicKey: "fake",
userId: 1,
userToken: "aaa",
rememberMe: false,
optInErrorReporting: false
};
store.dispatch(new LoginSuccessAction(user));
isLoggedInSelector
.pipe(take(1))
.subscribe(isLoggedIn => expect(isLoggedIn).toBe(true));
// Login first
const user: IAuthStore = {
email: "test@example.com",
privateKey: "fake",
publicKey: "fake",
userId: 1,
userToken: "aaa",
rememberMe: false,
optInErrorReporting: false
};
store.dispatch(new LoginSuccessAction(user));
isLoggedInSelector
.pipe(take(1))
.subscribe(isLoggedIn => expect(isLoggedIn).toBe(true));
// Now logout
store.dispatch(new LogoutSuccessAction());
isLoggedInSelector
.pipe(take(1))
.subscribe(isLoggedIn => expect(isLoggedIn).toBe(false));
})
)
);
// Now logout
store.dispatch(new LogoutSuccessAction());
isLoggedInSelector
.pipe(take(1))
.subscribe(isLoggedIn => expect(isLoggedIn).toBe(false));
})
));
});
});
......@@ -19,12 +19,18 @@ import * as fromRegister from "./register/register.reducer";
import * as fromChangePassword from "./change-password/change-password.reducer";
import * as fromDeleteAccount from "./delete/delete.reducer";
import * as fromErrorReporting from "./error-reporting/error-reporting.reducer";
import * as fromForgotPassword from "./forgot-password/forgot-password.reducer";
import * as fromResetPasswordVerify from "./forgot-password/reset-password-verify/reset-password-verify.reducer";
import * as fromResetPassword from "./reset-password/reset-password.reducer";
import * as fromResetPasswordVerify from "./reset-password/reset-password-verify/reset-password-verify.reducer";
import * as fromSetPassword from "./reset-password/set-password/set-password.reducer";
import {
ResetPasswordVerifyActionTypes,
ResetPasswordVerifyActionsUnion
} from "./forgot-password/reset-password-verify/reset-password-verify.actions";
} from "./reset-password/reset-password-verify/reset-password-verify.actions";
import {
SetPasswordSuccess,
SetPasswordActionTypes,
ForceSetPassword
} from "./reset-password/set-password/set-password.actions";
export interface IAuthState {
email: string | null;
......@@ -35,6 +41,8 @@ export interface IAuthState {
publicKey: string | null;
rememberMe: boolean;
optInErrorReporting: boolean;
/** Always redirect logged in user to set password page */
forceSetPassword: boolean;
}
export const initialState: IAuthState = {
......@@ -45,7 +53,8 @@ export const initialState: IAuthState = {
privateKey: null,
publicKey: null,
rememberMe: false,
optInErrorReporting: false
optInErrorReporting: false,
forceSetPassword: false
};
export function authReducer(
......@@ -56,6 +65,8 @@ export function authReducer(
| RegisterSuccessAction
| ErrorReportingActionsUnion
| ResetPasswordVerifyActionsUnion
| SetPasswordSuccess
| ForceSetPassword
): IAuthState {
switch (action.type) {
case AccountActionTypes.LOGIN:
......@@ -78,9 +89,16 @@ export function authReducer(
};
case AccountActionTypes.SET_URL:
return Object.assign({}, state, {
return {
...state,
url: action.payload
});
};
case SetPasswordActionTypes.SET_PASSWORD_SUCCESS:
return {
...state,
forceSetPassword: false
};
case ErrorReportingTypes.SAVE_FORM_SUCCESS:
return {
......@@ -88,6 +106,12 @@ export function authReducer(
optInErrorReporting: action.payload.opt_in_error_reporting
};
case SetPasswordActionTypes.FORCE_SET_PASSWORD:
return {
...state,
forceSetPassword: true
};
default:
return state;
}
......@@ -101,8 +125,9 @@ export interface IAccountState {
changePassword: fromChangePassword.IChangePasswordState;
deleteAccount: fromDeleteAccount.IDeleteAccountState;
errorReporting: fromErrorReporting.IState;
forgotPassword: fromForgotPassword.IForgotPasswordState;
resetPassword: fromResetPassword.IResetPasswordState;
resetPasswordVerify: fromResetPasswordVerify.IResetPasswordVerifyState;
setPassword: fromSetPassword.ISetPasswordState;
}
export const reducers: ActionReducerMap<IAccountState> = {
......@@ -113,8 +138,9 @@ export const reducers: ActionReducerMap<IAccountState> = {
changePassword: fromChangePassword.reducer,
deleteAccount: fromDeleteAccount.reducer,
errorReporting: fromErrorReporting.reducer,
forgotPassword: fromForgotPassword.reducer,
resetPasswordVerify: fromResetPasswordVerify.reducer
resetPassword: fromResetPassword.reducer,
resetPasswordVerify: fromResetPasswordVerify.reducer,
setPassword: fromSetPassword.reducer
};
export const selectAccountState = createFeatureSelector<IAccountState>(
......@@ -154,6 +180,10 @@ export const getEmail = createSelector(
selectAuthState,
(state: IAuthState) => state.email
);
export const getForceUserSetPassword = createSelector(
selectAuthState,
(state: IAuthState) => state.forceSetPassword && state.userToken // Sanity check user is logged in
);
export const getUserId = createSelector(
selectAuthState,
(state: IAuthState) => state.userId
......@@ -294,25 +324,25 @@ export const getErrorReportingHasStarted = createSelector(
fromErrorReporting.getHasStarted
);
export const selectForgotPasswordState = createSelector(
export const selectResetPasswordState = createSelector(
selectAccountState,
(state: IAccountState) => state.forgotPassword
(state: IAccountState) => state.resetPassword
);
export const getForgotPasswordForm = createSelector(
selectForgotPasswordState,
fromForgotPassword.getForm
export const getResetPasswordForm = createSelector(
selectResetPasswordState,
fromResetPassword.getForm
);
export const getForgotPasswordHasStarted = createSelector(
selectForgotPasswordState,
fromForgotPassword.getHasStarted
export const getResetPasswordHasStarted = createSelector(
selectResetPasswordState,
fromResetPassword.getHasStarted
);
export const getForgotPasswordHasFinished = createSelector(
selectForgotPasswordState,
fromForgotPassword.getHasFinished
export const getResetPasswordHasFinished = createSelector(
selectResetPasswordState,
fromResetPassword.getHasFinished
);
export const getForgotPasswordErrorMessage = createSelector(
selectForgotPasswordState,