change password is implemented

This commit is contained in:
병준 박 2019-11-28 18:12:20 +09:00
parent c5d61417f3
commit b72e2adc8b
16 changed files with 479 additions and 59 deletions

View File

@ -1 +1,21 @@
<ucap-account-change-password></ucap-account-change-password>
<mat-card class="confirm-card mat-elevation-z setting-frame">
<mat-card-header cdkDrag cdkDragRootElement=".cdk-overlay-pane" cdkDragHandle>
<mat-card-title>패스워드 변경</mat-card-title>
</mat-card-header>
<mat-card-content>
<div fxFlex class="setting-tab">
<ucap-account-change-password
(changePassword)="onChangePassword($event)"
></ucap-account-change-password>
</div>
</mat-card-content>
<mat-card-actions class="button-farm flex-row">
<button
mat-stroked-button
(click)="onClickChoice(false)"
class="mat-primary"
>
취소
</button>
</mat-card-actions>
</mat-card>

View File

@ -0,0 +1,35 @@
::ng-deep .setting-frame {
padding: 16px;
height: 100%;
min-width: 400px;
position: relative;
.mat-dialog-container {
position: relative;
}
.mat-card-header {
position: relative;
width: 100%;
border-bottom: 1px solid #dddddd;
margin-bottom: 12px;
}
.mat-card-content {
flex: 0 0 auto;
display: flex;
align-items: flex-start;
height: calc(100% - 100px);
border-bottom: 1px solid #dddddd;
}
.button-farm {
text-align: right;
position: absolute;
width: 100%;
bottom: 10px;
.mat-primary {
margin-left: 4px;
}
}
}

View File

@ -6,15 +6,14 @@ import { SessionStorageService } from '@ucap-webmessenger/web-storage';
import { Store } from '@ngrx/store';
import { UserInfo } from '@ucap-webmessenger/protocol-sync';
import {
UserInfoSS,
UserInfoF,
UserInfoDN
} from '@ucap-webmessenger/protocol-query';
import { DialogService } from '@ucap-webmessenger/ui';
import { VersionInfo2Response } from '@ucap-webmessenger/api-public';
import { LoginResponse } from '@ucap-webmessenger/protocol-authentication';
import { AppAuthenticationService } from '@app/services/authentication.service';
import { StringUtil, CharactorType } from '@ucap-webmessenger/core';
import * as ServiceStore from '@app/store/messenger/service';
import CryptoJS from 'crypto-js';
export interface ChangePasswordDialogData {}
@ -40,6 +39,7 @@ export class ChangePasswordDialogComponent implements OnInit, OnDestroy {
@Inject(MAT_DIALOG_DATA) public data: ChangePasswordDialogData,
private dialogService: DialogService,
private sessionStorageService: SessionStorageService,
private appAuthenticationService: AppAuthenticationService,
private store: Store<any>
) {
this.sessionVerinfo = this.sessionStorageService.get<VersionInfo2Response>(
@ -53,4 +53,147 @@ export class ChangePasswordDialogComponent implements OnInit, OnDestroy {
ngOnInit() {}
ngOnDestroy(): void {}
onChangePassword(info: {
currentLoginPw: string;
newLoginPw: string;
newConfirmLoginPw: string;
notValid: () => void;
}): void {
if (!info.currentLoginPw || '' === info.currentLoginPw.trim()) {
// 현재 비밀번호를 입력해주세요
info.notValid();
return;
}
if (!this.appAuthenticationService.isSameForPassword(info.currentLoginPw)) {
// "현재 비밀번호가 다릅니다"
info.notValid();
return;
}
if (!info.newLoginPw || '' === info.newLoginPw.trim()) {
// 신규 비밀번호를 입력해주세요
info.notValid();
return;
}
if (-1 < info.newLoginPw.indexOf(this.loginRes.userId)) {
// "사용자 ID를 비밀번호에 포함할 수 없습니다."
info.notValid();
return;
}
const phoneNumberOnly = !!this.loginRes.userInfo.hpNumber
? this.loginRes.userInfo.hpNumber.trim().replace(new RegExp('-', 'g'), '')
: '';
switch (phoneNumberOnly.length) {
case 11:
if (-1 < info.newLoginPw.indexOf(phoneNumberOnly.substr(3, 4))) {
// "사용자 휴대폰번호를 비밀번호에 포함할 수 없습니다."
info.notValid();
return;
}
if (-1 < info.newLoginPw.indexOf(phoneNumberOnly.substr(7, 4))) {
// "사용자 휴대폰번호를 비밀번호에 포함할 수 없습니다."
info.notValid();
return;
}
break;
case 10:
if (-1 < info.newLoginPw.indexOf(phoneNumberOnly.substr(3, 3))) {
// "사용자 휴대폰번호를 비밀번호에 포함할 수 없습니다."
info.notValid();
return;
}
if (-1 < info.newLoginPw.indexOf(phoneNumberOnly.substr(6, 4))) {
// "사용자 휴대폰번호를 비밀번호에 포함할 수 없습니다."
info.notValid();
return;
}
break;
default:
break;
}
if (!info.newConfirmLoginPw || '' === info.newConfirmLoginPw.trim()) {
// "신규 비밀번호 확인을 입력해주세요"
info.notValid();
return;
}
if (info.newLoginPw !== info.newConfirmLoginPw.trim()) {
// "신규 비밀번호와 신규 비밀번호 확인이 다릅니다"
info.notValid();
return;
}
if (info.currentLoginPw === info.newLoginPw.trim()) {
// "현재 비밀번호와 새로운 비밀번호가 같습니다."
info.notValid();
return;
}
if (0 < StringUtil.includes(info.newLoginPw, CharactorType.Space)) {
// "비밀번호에는 공백을 입력할 수 없습니다."
info.notValid();
return;
}
if (StringUtil.isRepeat(info.newLoginPw, 3)) {
// "숫자나 문자를 3번이상 반복적으로 사용할 수 없습니다."
info.notValid();
return;
}
if (
StringUtil.isIncrements(info.newLoginPw, 3) ||
StringUtil.isDecrements(info.newLoginPw, 3)
) {
// "연속되는 숫자나 문자를 3번이상 사용할 수 없습니다."
info.notValid();
return;
}
let combinationCount = 0;
if (0 < StringUtil.includes(info.newLoginPw, CharactorType.Special)) {
combinationCount++;
}
if (0 < StringUtil.includes(info.newLoginPw, CharactorType.Number)) {
combinationCount++;
}
if (0 < StringUtil.includes(info.newLoginPw, CharactorType.Charactor)) {
combinationCount++;
}
let minPwLen = 0;
if (2 < combinationCount) {
minPwLen = 8;
} else if (2 === combinationCount) {
minPwLen = 10;
} else if (2 > combinationCount) {
// "2종류 이상 조합을 해야 합니다."
info.notValid();
return;
}
if (info.newLoginPw.length < minPwLen) {
// "비밀번호는 %d가지가 조합된 경우 %d자를 넘어야 합니다."
info.notValid();
return;
}
this.store.dispatch(
ServiceStore.userPasswordSet({
req: {
loginId: this.loginRes.userId,
companyCode: this.loginRes.companyCode,
oldLoginPw: CryptoJS.enc.Hex.stringify(
CryptoJS.SHA256(info.currentLoginPw)
),
newLoginPw: CryptoJS.enc.Hex.stringify(
CryptoJS.SHA256(info.newLoginPw)
)
}
})
);
this.dialogRef.close({ choice: true });
}
onClickChoice(choice: boolean): void {
this.dialogRef.close({ choice });
}
}

View File

@ -51,4 +51,11 @@ export class AppAuthenticationService {
this.sessionStorageService.remove(KEY_VER_INFO);
this.sessionStorageService.remove(KEY_LOGIN_INFO);
}
isSameForPassword(pw: string): boolean {
const loginInfo = this.sessionStorageService.get<LoginInfo>(KEY_LOGIN_INFO);
return (
loginInfo.loginPw === CryptoJS.enc.Hex.stringify(CryptoJS.SHA256(pw))
);
}
}

View File

@ -96,20 +96,3 @@ export const privacyAgreeFailure = createAction(
export const privacyDisagree = createAction(
'[Account::Authentication] Privacy Disagree'
);
export const changePassword = createAction(
'[Account::Authentication] Change Password',
props<{ req: UserPasswordSetRequest }>()
);
export const changePasswordSuccess = createAction(
'[Account::Authentication] Change Password Success',
props<{
res: UserPasswordSetResponse;
}>()
);
export const changePasswordFailure = createAction(
'[Account::Authentication] Change Password Failure',
props<{ error: any }>()
);

View File

@ -32,9 +32,6 @@ import {
privacyDisagree,
privacyAgreeFailure,
privacyAgreeSuccess,
changePassword,
changePasswordFailure,
changePasswordSuccess,
increaseLoginFailCount,
initialLoginFailCount,
logoutInitialize
@ -49,10 +46,7 @@ import { AppAuthenticationService } from '@app/services/authentication.service';
import { NGXLogger } from 'ngx-logger';
import { Store } from '@ngrx/store';
import { SessionStorageService } from '@ucap-webmessenger/web-storage';
import {
ServiceProtocolService,
UserPasswordSetResponse
} from '@ucap-webmessenger/protocol-service';
import {
AuthenticationProtocolService,
LoginResponse
@ -330,23 +324,6 @@ export class Effects {
)
);
changePassword$ = createEffect(() =>
this.actions$.pipe(
ofType(changePassword),
map(action => action.req),
exhaustMap(req =>
this.serviceProtocolService.userPasswordSet(req).pipe(
map((res: UserPasswordSetResponse) => {
return changePasswordSuccess({
res
});
}),
catchError(error => of(changePasswordFailure({ error })))
)
)
)
);
constructor(
private actions$: Actions,
private ngZone: NgZone,
@ -357,7 +334,7 @@ export class Effects {
private appAuthenticationService: AppAuthenticationService,
private protocolService: ProtocolService,
private authenticationProtocolService: AuthenticationProtocolService,
private serviceProtocolService: ServiceProtocolService,
private dialogService: DialogService,
@Inject(UCAP_NATIVE_SERVICE) private nativeService: NativeService,
private logger: NGXLogger

View File

@ -6,6 +6,7 @@ import * as EventStore from './event';
import * as OptionStore from './option';
import * as QueryStore from './query';
import * as RoomStore from './room';
import * as ServiceStore from './service';
import * as StatusStore from './status';
import * as SyncStore from './sync';
import * as SettingsStore from './settings';
@ -16,6 +17,7 @@ export interface State {
option: OptionStore.State;
query: QueryStore.State;
room: RoomStore.State;
service: ServiceStore.State;
status: StatusStore.State;
sync: SyncStore.State;
settings: SettingsStore.State;
@ -27,6 +29,7 @@ export const effects: Type<any>[] = [
OptionStore.Effects,
QueryStore.Effects,
RoomStore.Effects,
ServiceStore.Effects,
StatusStore.Effects,
SyncStore.Effects,
SettingsStore.Effects
@ -39,6 +42,7 @@ export function reducers(state: State | undefined, action: Action) {
option: OptionStore.reducer,
query: QueryStore.reducer,
room: RoomStore.reducer,
service: ServiceStore.reducer,
status: StatusStore.reducer,
sync: SyncStore.reducer,
settings: SettingsStore.reducer
@ -62,6 +66,9 @@ export function selectors<S>(selector: Selector<any, State>) {
QuerySelector: QueryStore.selectors(
createSelector(selector, (state: State) => state.query)
),
ServiceSelector: ServiceStore.selectors(
createSelector(selector, (state: State) => state.service)
),
StatusSelector: StatusStore.selectors(
createSelector(selector, (state: State) => state.status)
),

View File

@ -0,0 +1,23 @@
import { createAction, props } from '@ngrx/store';
import {
UserPasswordSetRequest,
UserPasswordSetResponse
} from '@ucap-webmessenger/protocol-service';
export const userPasswordSet = createAction(
'[Account::Authentication] User Password Set',
props<{ req: UserPasswordSetRequest }>()
);
export const userPasswordSetSuccess = createAction(
'[Account::Authentication] User Password Set Success',
props<{
res: UserPasswordSetResponse;
}>()
);
export const userPasswordSetFailure = createAction(
'[Account::Authentication] User Password Set Failure',
props<{ error: any }>()
);

View File

@ -0,0 +1,47 @@
import { Injectable } from '@angular/core';
import { Actions, ofType, createEffect } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { NGXLogger } from 'ngx-logger';
import { catchError, exhaustMap, map } from 'rxjs/operators';
import { of } from 'rxjs';
import {
userPasswordSet,
userPasswordSetSuccess,
userPasswordSetFailure
} from './actions';
import {
ServiceProtocolService,
UserPasswordSetResponse
} from '@ucap-webmessenger/protocol-service';
@Injectable()
export class Effects {
userPasswordSet$ = createEffect(() =>
this.actions$.pipe(
ofType(userPasswordSet),
map(action => action.req),
exhaustMap(req =>
this.serviceProtocolService.userPasswordSet(req).pipe(
map((res: UserPasswordSetResponse) => {
return userPasswordSetSuccess({
res
});
}),
catchError(error => of(userPasswordSetFailure({ error })))
)
)
)
);
constructor(
private actions$: Actions,
private serviceProtocolService: ServiceProtocolService,
private store: Store<any>,
private logger: NGXLogger
) {}
}

View File

@ -0,0 +1,4 @@
export * from './actions';
export * from './effects';
export * from './reducers';
export * from './state';

View File

@ -0,0 +1,4 @@
import { createReducer, on } from '@ngrx/store';
import { initialState } from './state';
export const reducer = createReducer(initialState);

View File

@ -0,0 +1,9 @@
import { Selector, createSelector } from '@ngrx/store';
export interface State {}
export const initialState: State = {};
export function selectors<S>(selector: Selector<any, State>) {
return {};
}

View File

@ -1,3 +1,13 @@
export enum CharactorType {
Number = 'Number',
Charactor = 'Charactor',
Space = 'Space',
Special = 'Special'
}
const CharCodeZero = '0'.charCodeAt(0);
const CharCodeNine = '9'.charCodeAt(0);
export class StringUtil {
/**
* prefix zero fill
@ -15,4 +25,160 @@ export class StringUtil {
return StringUtil.zeroFill(str.toString(), len);
}
}
/**
* includes
*/
public static includes(target: string, charactorType: CharactorType): number {
let nNum = 0;
let nChar = 0;
let nSpace = 0;
let nElse = 0;
for (let i = 0; i < target.length; i++) {
const char = target.charCodeAt(i);
// 숫자
if (char >= 48 && char <= 57) {
nNum++;
} else if (char >= 65 && char <= 90) {
// 영어(대문자)
nChar++;
} else if (char >= 97 && char <= 122) {
// 영어(소문자)
nChar++;
} else if (target.charAt(i) === ' ') {
// 공백
nSpace++;
} else {
// 특수기호
nElse++;
}
}
switch (charactorType) {
case CharactorType.Number:
return nNum;
case CharactorType.Charactor:
return nChar;
case CharactorType.Space:
return nSpace;
case CharactorType.Special:
return nElse;
default:
return -1;
}
}
/**
* isRepeat
*/
public static isRepeat(target: string, repeatTime: number) {
let old = '';
let rptCnt = 1;
for (let i = 0; i < target.length; i++) {
const s = target.charAt(i);
if (old === s) {
rptCnt++;
if (rptCnt === repeatTime) {
return true;
}
} else {
old = s;
rptCnt = 1;
}
}
return false;
}
/**
* isIncrements
*/
public static isIncrements(target: string, repeatTime: number) {
for (let i = 0; i < target.length - (repeatTime - 1); i++) {
let notNeed = false;
const ary: number[] = [];
for (let j = 0; j < repeatTime; j++) {
const char = target.charCodeAt(i + j);
if (!StringUtil.isAlphaNumeric(char)) {
notNeed = true;
break;
}
ary.push(char);
}
if (notNeed) {
continue;
}
for (let k = 1; k < ary.length; k++) {
if (ary[0] !== ary[k] - k) {
notNeed = true;
break;
}
}
if (notNeed) {
continue;
}
return true;
}
return false;
}
/**
* isIncrements
*/
public static isDecrements(target: string, repeatTime: number) {
for (let i = 0; i < target.length - (repeatTime - 1); i++) {
let notNeed = false;
const ary: number[] = [];
for (let j = 0; j < repeatTime; j++) {
const char = target.charCodeAt(i + j);
if (!StringUtil.isAlphaNumeric(char)) {
notNeed = true;
break;
}
ary.push(char);
}
if (notNeed) {
continue;
}
for (let k = 1; k < ary.length; k++) {
if (ary[0] !== ary[k] + k) {
notNeed = true;
break;
}
}
if (notNeed) {
continue;
}
return true;
}
return false;
}
/**
* isAlphabet
*/
public static isAlphabet(char: number): boolean {
return (char >= 65 && char <= 90) || (char >= 97 && char <= 122);
}
/**
* isNumeric
*/
public static isNumeric(char: number): boolean {
return char >= 48 && char <= 57;
}
/**
* isAlphaNumeric
*/
public static isAlphaNumeric(char: number): boolean {
return StringUtil.isAlphabet(char) || StringUtil.isNumeric(char);
}
}

View File

@ -1,6 +1,4 @@
<div class="change-password-form">
<div class="mat-title">비밀번호 변경</div>
<form name="changePasswordForm" [formGroup]="changePasswordForm" novalidate>
<mat-form-field>
<mat-label>현재 패스워드</mat-label>
@ -44,7 +42,7 @@
class="submit-button bg-accent-dark"
aria-label="패스워드 변경"
[disabled]="changePasswordForm.invalid || !changePasswordBtnEnable"
(click)="onClickLogin()"
(click)="onClickChangePassword()"
>
패스워드 변경
</button>

View File

@ -1,12 +1,9 @@
.change-password-form {
position: relative;
width: 384px;
max-width: 384px;
padding: 50px;
width: 100%;
min-width: 380px;
padding: 0px;
text-align: center;
background-color: rgba(255, 255, 255, 0.8);
border-radius: 0px;
box-shadow: 4px 4px 0px rgba(0, 0, 0, 0.1);
.mat-title {
margin: 16px 0 32px 0;

View File

@ -54,7 +54,7 @@ export class ChangePasswordComponent implements OnInit {
});
}
onClickLogin() {
onClickChangePassword() {
this.changePassword.emit({
currentLoginPw: this.changePasswordForm.get('currentLoginPw').value,
newLoginPw: this.changePasswordForm.get('newLoginPw').value,