Commit 28978369 authored by David Burke's avatar David Burke

qr code reading for reset password verify

parent 58e0b272
Pipeline #31527090 failed with stage
in 11 minutes and 30 seconds
......@@ -313,3 +313,19 @@ export const selectResetPasswordVerifyState = createSelector(
selectAccountState,
(state: IAccountState) => state.resetPasswordVerify
);
export const getResetPasswordVerifyForm = createSelector(
selectResetPasswordVerifyState,
fromResetPasswordVerify.getForm
);
export const getResetPasswordVerifyHasStarted = createSelector(
selectResetPasswordVerifyState,
fromResetPasswordVerify.getHasStarted
);
export const getResetPasswordVerifyErrorMessage = createSelector(
selectResetPasswordVerifyState,
fromResetPasswordVerify.getErrorMessage
);
export const getResetPasswordVerifyEmailAndCode = createSelector(
selectResetPasswordVerifyState,
fromResetPasswordVerify.getEmailAndCode
);
export const BACKUP_CODE_LENGTH = 32;
export const BACKUP_CODE_CHARS = "BCDFGHJKMQRTVWXY2346789";
import { Action } from "@ngrx/store";
export enum ResetPasswordVerifyActionTypes {
INIT = "[Reset Password Verify] Init",
VERIFY_AND_LOGIN = "[Reset Password Verify] Verify and Login",
SET_ERROR = "[Reset Password Verify] Set error"
}
export class Init implements Action {
readonly type = ResetPasswordVerifyActionTypes.INIT;
constructor(public payload: { email: string | null; code: string | null }) {}
}
export class VerifyAndLogin implements Action {
readonly type = ResetPasswordVerifyActionTypes.VERIFY_AND_LOGIN;
constructor(public payload: string) {}
}
export class SetError implements Action {
readonly type = ResetPasswordVerifyActionTypes.SET_ERROR;
constructor(public payload: string) {}
}
export type ResetPasswordVerifyActionsUnion = Init | VerifyAndLogin;
......@@ -9,8 +9,36 @@
</div>
</div>
<p>Next we need your backup code. Passit cannot be recovered without either your original password or recovery code/QR code</p>
<app-non-field-messages *ngIf="errorMessage || localErrorMessage" [messages]="[errorMessage, localErrorMessage]"></app-non-field-messages>
<button id="scan-qr-button" (click)="toggleScan()">Scan QR Code</button>
<div>
<video id="scan-qr-video"></video>
</div>
<form [ngrxFormState]="form" (submit)="onSubmit()" class="auth-form__form">
<div>
<label [for]="form.controls.code.id">
Or Type Code
</label>
<input
type="email"
[ngrxFormControlState]="form.controls.code"
autofocus
class="form-field__input"
[class.form-field__input--invalid]="form.errors._code && form.isSubmitted"
[class.form-field__input--valid]="!form.errors._code"
/>
</div>
<button
id="loginSubmit"
class="button button--large button--green"
[disabled]="hasStarted"
type="submit">
Next
</button>
</form>
</div>
</app-marketing-frame>
\ No newline at end of file
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { Component, Input, OnInit, EventEmitter, Output } from "@angular/core";
import { FormGroupState } from "ngrx-forms";
import { IResetPasswordVerifyForm } from "./reset-password-verify.reducer";
import * as Instascan from "instascan";
@Component({
selector: 'app-reset-password-verify',
templateUrl: './reset-password-verify.component.html',
styleUrls: ['./reset-password-verify.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
selector: "app-reset-password-verify",
templateUrl: "./reset-password-verify.component.html",
styleUrls: ["./reset-password-verify.component.scss"]
})
export class ResetPasswordVerifyComponent implements OnInit {
@Input()
form: FormGroupState<IResetPasswordVerifyForm>;
@Input()
hasStarted: boolean;
@Input()
errorMessage: string;
constructor() { }
@Output()
verify = new EventEmitter<string>();
ngOnInit() {
showScanner = false;
scanner: any; // InstaScan.Scanner
localErrorMessage: string | null;
constructor() {}
toggleScan() {
this.showScanner = !this.showScanner;
if (this.showScanner) {
this.startScan();
} else {
this.stopScan();
}
}
startScan = () => {
this.localErrorMessage = null;
const scanner = this.scanner;
Instascan.Camera.getCameras()
.then(function(cameras: object[]) {
if (cameras.length > 0) {
scanner.start(cameras[0]);
} else {
this.localErrorMessage = "No cameras found.";
}
})
.catch(function(e: any) {
console.error(e);
});
};
stopScan() {
this.scanner.stop();
}
handleQrFound = (content: string) => {
this.verify.emit(content);
};
onSubmit() {
if (this.form.isValid) {
this.verify.emit(this.form.value.code);
}
}
ngOnInit() {
this.scanner = new Instascan.Scanner({
video: document.getElementById("scan-qr-video")
});
this.scanner.addListener("scan", this.handleQrFound);
}
}
import { Component, ChangeDetectionStrategy } from "@angular/core";
import { Component, ChangeDetectionStrategy, OnInit } from "@angular/core";
import * as fromAccount from "../../account.reducer";
import { Store } from "@ngrx/store";
import {} from "../forgot-password.actions";
import { IS_EXTENSION } from "../../../constants";
import {
Init,
VerifyAndLogin,
SetError
} from "./reset-password-verify.actions";
import { ActivatedRoute } from "@angular/router";
import { BACKUP_CODE_CHARS, BACKUP_CODE_LENGTH } from "../constants";
@Component({
template: `
<app-reset-password-verify
[form]="form$ | async"
[hasStarted]="hasStarted$ | async"
[errorMessage]="errorMessage$ | async"
(verify)="verify($event)"
></app-reset-password-verify>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ResetPasswordVerifyContainer {
form$ = this.store.select(fromAccount.getForgotPasswordForm);
hasStarted$ = this.store.select(fromAccount.getForgotPasswordHasStarted);
hasFinished$ = this.store.select(fromAccount.getForgotPasswordHasFinished);
errorMessage$ = this.store.select(fromAccount.getForgotPasswordErrorMessage);
isExtension = IS_EXTENSION;
export class ResetPasswordVerifyContainer implements OnInit {
form$ = this.store.select(fromAccount.getResetPasswordVerifyForm);
hasStarted$ = this.store.select(fromAccount.getResetPasswordVerifyHasStarted);
// hasFinished$ = this.store.select(fromAccount.getForgotPasswordHasFinished);
errorMessage$ = this.store.select(
fromAccount.getResetPasswordVerifyErrorMessage
);
constructor(private store: Store<fromAccount.IAuthState>) {}
constructor(
private store: Store<fromAccount.IAuthState>,
private route: ActivatedRoute
) {}
ngOnInit() {
const payload = {
code: this.route.snapshot.queryParamMap.get("code"),
email: this.route.snapshot.queryParamMap.get("email")
};
this.store.dispatch(new Init(payload));
}
verify(backupCode: string) {
backupCode = backupCode.replace(" ", "").toUpperCase();
if (
backupCode.length === BACKUP_CODE_LENGTH &&
!backupCode.match(new RegExp(`[^${BACKUP_CODE_CHARS}\-]`))
) {
this.store.dispatch(new VerifyAndLogin(backupCode));
} else {
this.store.dispatch(new SetError("Invalid backup code"));
}
}
}
import { Injectable } from "@angular/core";
import { Effect, Actions } from "@ngrx/effects";
import { withLatestFrom, map, exhaustMap } from "rxjs/operators";
import { Store } from "@ngrx/store";
import { IState } from "../../../app.reducers";
import { getResetPasswordVerifyEmailAndCode } from "../../account.reducer";
// import { UserService } from "../../user";
import {
VerifyAndLogin,
ResetPasswordVerifyActionTypes
} from "./reset-password-verify.actions";
@Injectable()
export class ResetPasswordVerifyEffects {
@Effect()
verifyAndLogin$ = this.actions$
.ofType<VerifyAndLogin>(ResetPasswordVerifyActionTypes.VERIFY_AND_LOGIN)
.pipe(
map(action => action.payload),
withLatestFrom(this.store.select(getResetPasswordVerifyEmailAndCode)),
exhaustMap(([backupCode, emailAndCode]) => {
return backupCode;
})
);
constructor(
private actions$: Actions,
private store: Store<IState> // private userService: UserService
) {}
}
......@@ -7,6 +7,10 @@ import {
} from "ngrx-forms";
import { minLength } from "ngrx-forms/validation";
import { IBaseFormState } from "../../../utils/interfaces";
import {
ResetPasswordVerifyActionTypes,
ResetPasswordVerifyActionsUnion
} from "./reset-password-verify.actions";
const FORM_ID = "Forgot Password Form";
......@@ -29,6 +33,7 @@ export const initialResetPasswordVerifyFormState = validateAndUpdateResetPasswor
export interface IResetPasswordVerifyState extends IBaseFormState {
form: FormGroupState<IResetPasswordVerifyForm>;
errorMessage: string | null;
email: string | null;
emailCode: string | null;
}
......@@ -37,6 +42,7 @@ const initialState: IResetPasswordVerifyState = {
hasStarted: false,
hasFinished: false,
errorMessage: null,
email: null,
emailCode: null
};
......@@ -46,9 +52,42 @@ export const formReducer = createFormStateReducerWithUpdate<
export function reducer(
state = initialState,
action: any
action: ResetPasswordVerifyActionsUnion
): IResetPasswordVerifyState {
const form = formReducer(state.form, action);
state = { ...state, form };
switch (action.type) {
case ResetPasswordVerifyActionTypes.INIT:
if (action.payload.email && action.payload.code) {
return {
...state,
email: action.payload.email,
emailCode: action.payload.code,
errorMessage: null
};
} else {
return {
...state,
errorMessage: "Invalid reset password link"
};
}
case ResetPasswordVerifyActionTypes.VERIFY_AND_LOGIN:
return {
...state,
hasStarted: true
};
}
return state;
}
export const getForm = (state: IResetPasswordVerifyState) => state.form;
export const getHasStarted = (state: IResetPasswordVerifyState) =>
state.hasStarted;
export const getErrorMessage = (state: IResetPasswordVerifyState) =>
state.errorMessage;
export const getEmailAndCode = (state: IResetPasswordVerifyState) => {
return { email: state.email, emailCode: state.emailCode };
};
......@@ -3,3 +3,4 @@ declare module "@braintree/sanitize-url" {
}
declare module "canvg";
declare module "instascan";
(window as any).global = window;
export const NOOP = 0;
......@@ -13,6 +13,7 @@ import { SharedModule } from "../app/shared";
import { ProgressIndicatorModule } from "../app/progress-indicator";
import * as fromForgotPassword from "../app/account/forgot-password/forgot-password.reducer";
import * as fromLogin from "../app/account/login/login.reducer";
import { RouterTestingModule } from "@angular/router/testing";
storiesOf("Account", module)
.addDecorator(withKnobs)
......@@ -23,6 +24,7 @@ storiesOf("Account", module)
HttpClientModule,
NgrxFormsModule,
SharedModule,
RouterTestingModule,
ProgressIndicatorModule,
StoreModule.forRoot({})
],
......
......@@ -22,7 +22,11 @@
"lib": [
"es2016",
"dom"
]
],
"paths": {
"fs": [ "./global-shims" ],
"crypto": [ "./global-shims" ]
}
},
"angularCompilerOptions": {
"preserveWhitespaces": false
......
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment