保誠-保戶業務員媒合平台
wayne
2021-11-24 75fd8a02c058f6a51e3c5ea36d997c157c070793
Merge remote-tracking branch 'origin/master'
修改18個檔案
新增14個檔案
1326 ■■■■ 已變更過的檔案
PAMapp/.env.dev 1 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/.env.uat 1 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/scss/vendors/_elementUI.scss 1 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/scss/vendors/elementUI/_button.scss 3 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/scss/vendors/elementUI/_messageBox.scss 28 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/ts/api/consultant.ts 67 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/ts/api/share.ts 48 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/ts/models/ConsultantLoginInfo.ts 5 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/assets/ts/models/enum/Role.ts 5 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/BackActionBar.vue 7 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/components/NavBar.vue 21 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/nuxt.config.js 7 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/package.json 7 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/consultantLogin/index.vue 100 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/login/index.vue 444 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/pages/myAppointmentList.vue 1 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/store/localStorage.ts 4 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/doc/註冊API/註冊API.txt 13 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/domain/Customer.java 15 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/domain/OtpTmp.java 92 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/enums/CustomerDetailEnum.java 3 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/enums/OtpTmpStatusEnum.java 6 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/repository/OtpTmpRepository.java 15 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/security/provider/OtpAuthenticationProvider.java 29 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/CustomerAuthService.java 34 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/CustomerService.java 47 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/OtpTmpService.java 39 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/UserService.java 110 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/dto/CustomerRegisterDTO.java 53 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/service/mapper/CustomerDTOMapper.java 17 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/web/rest/AccountResource.java 47 ●●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
pamapi/src/main/java/com/pollex/pam/web/rest/OtpResource.java 56 ●●●● 修補檔 | 檢視 | 原始 | 究查 | 歷程
PAMapp/.env.dev
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1 @@
BASE_URL='http://localhost:8080/api'
PAMapp/.env.uat
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1 @@
BASE_URL='https://vtwlifeopensysuat.pru.intranet.asia/pamapi/api'
PAMapp/assets/scss/vendors/_elementUI.scss
@@ -10,3 +10,4 @@
@import './elementUI/tag';
@import './elementUI/pagination';
@import './elementUI/dialog';
@import './elementUI/messageBox';
PAMapp/assets/scss/vendors/elementUI/_button.scss
@@ -24,6 +24,7 @@
    &:hover{
      background-color: $MID_GREY;
      border-color: $MID_GREY;
      color: $PRIMARY_WHITE;
    }
  }
}
@@ -106,7 +107,7 @@
          border:none;
          font-size: 20px;
      }
  }
  .radio-btn{
      display: flex;
PAMapp/assets/scss/vendors/elementUI/_messageBox.scss
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,28 @@
.el-message-box__wrapper{
    display: flex;
    align-items: center;
    justify-content: center;
    .pam-message-box.el-message-box{
        display: flex;
        flex-direction: column;
        justify-content: space-between;
        width:300px;
        height: 150px;
        .el-message-box__content{
            padding: 15px;
            .el-message-box__message p{
                text-align: center;
                color: $PRIMARY_BLACK;
                letter-spacing: 2px;
                font-size: 20px;
            }
        }
        .el-message-box__btns{
            text-align: center;
        }
        .el-button.el-button--primary{
            color: $PRIMARY_WHITE;
        }
    }
}
PAMapp/assets/ts/api/consultant.ts
@@ -1,10 +1,26 @@
import { service } from '~/assets/ts/api/share';
import { AxiosResponse } from 'axios';
import { AppointmentDetail } from '../models/AppointmentDetail';
import { ConsultantLoginInfo } from '../models/ConsultantLoginInfo';
// é¡§å®¢ç™»å…¥(TODO: OTP認證開發前 æš«æ™‚使用)
export function login(user: any) {
    return service.post('/authenticate', user)
}
// é¡§å®¢ç™»å…¥-發送OTP
export function sendOtp(loginInfo: LoginRequest) {
    return service.post<OtpInfo>('/otp/sendOtp', loginInfo).then(res => res.data)
}
// é¡§å®¢ç™»å…¥-驗證otp並登入
export function loginVerify(loginVerify: LoginVerify) {
    return service.post('/otp/verify', loginVerify)
}
// é¡§å®¢è¨»å†Š
export function register(registerInfo: RegisterInfo) {
    return service.post('/otp/register', registerInfo)
}
// æŽ¨è–¦ä¿éšªé¡§å•
@@ -76,13 +92,8 @@
}
// é¡§å•ç™»å…¥
export function logInToConsultant(consultantDto:ConsultantLoginInfo):Promise<boolean>{
    console.log('consultantDto',consultantDto);
    return new Promise((resolve, reject)=>{
        setTimeout(()=>{
            resolve(true);
        },1000)
    })
export function logInToConsultant(consultantDto:ConsultantLoginInfo):Promise<AxiosResponse<RequestOfLoginSuccess>>{
    return service.post('/eService/authenticate',consultantDto);
}
// å–得預約單細節
@@ -91,11 +102,6 @@
        Authorization: 'Bearer ' + localStorage.getItem('id_token')
    }
    return service.get('/appointment/getDetail/'+apointmentId, {headers})
}
export interface ConsultantLoginInfo{
    account:string,
    password:string,
    verificationCode:string,
}
export interface Consultants {
    agentNo: string,
@@ -163,4 +169,41 @@
    seniority:     string;
    new:           boolean;
}
export interface RequestOfLoginSuccess{
    id_token:string;
}
export interface LoginRequest {
    /** "SMS"=手機,"EMAIL"=email */
    loginType: string,
    /** è‹¥loginTypeå¡«SMS則該欄帶入手機、EMAIL則帶入郵件信箱 */
    account: string,
}
export interface OtpInfo {
    /** ç”¨æ–¼å¸¶å…¥otp認證時 */
    indexKey: string,
    /** Otp是否有成功發送 */
    success: boolean,
    failCode: string,
    failReason: string,
}
export interface LoginVerify {
    /** å¯å¸¶å…¥æ‰‹æ©Ÿæˆ–email */
    account: string,
    /** ç”±otp的api回的index key */
    indexKey: string,
    /** ç”±æ‰‹æ©Ÿæˆ–信箱收到的認證碼 */
    otpCode: string
}
export interface RegisterInfo {
    phone?: string,
    email?: string,
    indexKey: string,
    otpCode: string,
    name: string,
    /** "SMS":Otp發送手機,"EMAIL":Otp發email */
    contactType: string
}
PAMapp/assets/ts/api/share.ts
@@ -1,31 +1,49 @@
import axios from 'axios';
import { AxiosRequestConfig, AxiosError, AxiosResponse } from 'axios';
import { AxiosRequestConfig, AxiosError, AxiosResponse } from 'axios';
import { MessageBox } from 'element-ui';
export const service = axios.create({
    baseURL: process.env.BASE_URL
})
service.interceptors.request.use(function (config: AxiosRequestConfig) {
  loadingStart();
service.interceptors.request.use(
  (config:AxiosRequestConfig)=>{
    loadingStart();
    return config;
}, function (error: AxiosError) {
  loadingFinish();
    return Promise.reject(error);
});
  }
);
service.interceptors.response.use(function (response: AxiosResponse) {
  loadingFinish();
    return response;
}, function (error: AxiosError) {
  loadingFinish();
service.interceptors.response.use(
  (response:AxiosResponse)=>{
    loadingFinish();
    return response;  // maybe can use response.data
  },
  (error:AxiosError)=>{
    loadingFinish();
    if (error.config.url !== '/otp/verify') {
      openErrorMessage();
    }
    return Promise.reject(error);
});
  }
);
function loadingStart(): void {
  setTimeout(() => {
    window.$nuxt.$loading.start();
  });
}
function loadingFinish(): void {
    window.$nuxt.$loading.finish();
}
function openErrorMessage():void{
  MessageBox({
    message: '系統發生錯誤',
    showClose:false,
    showConfirmButton:true,
    confirmButtonText:'確認',
    customClass:'pam-message-box',
    closeOnClickModal:false,
  });
}
PAMapp/assets/ts/models/ConsultantLoginInfo.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,5 @@
export interface ConsultantLoginInfo {
    username: string;
    password: string;
    verificationCode: string;
}
PAMapp/assets/ts/models/enum/Role.ts
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,5 @@
export enum Role{
    ADMIN = 'admin',
    USER = 'user',
    NOT_LOGIN = ''
}
PAMapp/components/BackActionBar.vue
@@ -9,12 +9,13 @@
<script lang="ts">
import { namespace } from 'nuxt-property-decorator';
import { Vue, Component,} from 'vue-property-decorator';
import { Role } from '~/assets/ts/models/enum/Role';
import * as _ from 'lodash';
import { Role } from './NavBar.vue';
const localStorage = namespace('localStorage');
const roleStorage = namespace('localStorage');
@Component
export default class UiCarousel extends Vue {
  @localStorage.Getter currentRole!:string;
  @roleStorage.Getter currentRole!:string;
  get label(): string {
    if (this.$route.name) {
      const routeName = this.$route.name.split('-')[0];
PAMapp/components/NavBar.vue
@@ -9,7 +9,7 @@
    <div class="pam-header__action-bar">
      <i class="icon-bell text--dark-blue cursor--pointer fix-chrome-click--issue"
        @click="$router.push('/notification')"></i>
        <el-dropdown :class="{'is-open':isOpenDropdown}"
        <el-dropdown :class="{'is-open':isOpenDropdown}"
          ref="dropdown"
          trigger="click"
          @command="routerNavigateTo">
@@ -34,13 +34,15 @@
<script lang="ts">
  import { Vue, Component } from 'vue-property-decorator';
  import { namespace } from 'nuxt-property-decorator';
  import { Role } from '~/assets/ts/models/enum/Role';
  import * as _ from 'lodash';
  const localStorage = namespace('localStorage');
  const roleStorage = namespace('localStorage');
  @Component
  export default class NavBar extends Vue {
    @localStorage.Mutation storageClear!: () => void;
    @localStorage.Getter idToken!: string | null;
    @localStorage.Getter currentRole!: string | null;
    @roleStorage.Mutation storageClear!: () => void;
    @roleStorage.Getter idToken!: string | null;
    @roleStorage.Getter currentRole!: string | null;
    navBarList = [{
        authorityOfRoleList: [Role.NOT_LOGIN],
@@ -99,11 +101,6 @@
      this.storageClear();
      _.isEqual(this.$route.name, 'index') ? location.reload() : this.$router.push('/');
    }
  }
  export enum Role {
    USER = 'user',
    ADMIN = 'admin',
    NOT_LOGIN = '',
  }
</script>
@@ -187,7 +184,7 @@
        color: $PRIMARY_WHITE;
      }
    }
  }
  }
  @include desktop {
    .pam-header {
@@ -205,7 +202,7 @@
      .pam-header__title {
        display: flex;
        justify-content: start;
        justify-content: flex-start;
        align-items: center;
        border: none;
        padding-left: 30px;
PAMapp/nuxt.config.js
@@ -50,7 +50,9 @@
    // https://go.nuxtjs.dev/typescript
    '@nuxt/typescript-build',
    '@nuxtjs/axios',
    '@nuxtjs/style-resources'
    '@nuxtjs/style-resources',
    ['@nuxtjs/dotenv', { filename: '.env.' + process.env.ENV }],
  ],
  // Modules: https://go.nuxtjs.dev/config-modules
@@ -74,8 +76,5 @@
        document.documentElement.scrollTop = 0;
      }
    }
  },
  env: {
    BASE_URL: 'http://localhost:8080/api'
  }
}
PAMapp/package.json
@@ -3,10 +3,13 @@
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "nuxt",
    "dev": "cross-env ENV=dev nuxt",
    "uat": "cross-env ENV=uat nuxt",
    "build": "nuxt build",
    "start": "nuxt start",
    "generate": "nuxt generate",
    "generate.dev": "cross-env ENV=dev nuxt generate",
    "generate.uat": "cross-env ENV=uat nuxt generate",
    "test": "jest"
  },
  "dependencies": {
@@ -26,10 +29,12 @@
  "devDependencies": {
    "@nuxt/types": "^2.15.8",
    "@nuxt/typescript-build": "^2.1.0",
    "@nuxtjs/dotenv": "^1.4.1",
    "@types/lodash": "^4.14.176",
    "@vue/test-utils": "^1.2.2",
    "babel-core": "7.0.0-bridge.0",
    "babel-jest": "^27.3.0",
    "cross-env": "^7.0.3",
    "fibers": "^5.0.0",
    "jest": "^27.3.0",
    "sass": "1.43.2",
PAMapp/pages/consultantLogin/index.vue
@@ -6,11 +6,11 @@
        <div class="pam-consultant-login__title">帳號</div>
        <div class="position-r mt-10">
          <input type="text"
            v-model="consultantDto.account"
            v-model="consultantDto.username"
            class="pam-consultant-login__input"
            placeholder="輸入eService帳號">
          <div class="pam-consultant-login__inputIcon text--primary cursor--pointer"
            @click="recordAccount">
          <div class="pam-consultant-login__inputIcon text--primary cursor--pointer fix-chrome-click--issue"
            @click="isRemember = !isRemember">
            <i :class="[isRemember ? 'icon-checkbox-1' : 'icon-checkbox','pr-5']"></i>
            è¨˜ä½
          </div>
@@ -19,7 +19,7 @@
      <div class="pam-paragraph">
        <div class="pam-consultant-login__title ">
          <div>密碼</div>
          <a class="pam-consultant-login__forgot-password cursor--pointer"
          <a class="pam-consultant-login__forgot-password cursor--pointer fix-chrome-click--issue"
            :href="forgotPasswordLink" 
            target="_blank" 
            rel="保誠人壽">
@@ -31,16 +31,16 @@
            v-model="consultantDto.password"
            class="pam-consultant-login__input"
            placeholder="輸入eService密碼">
          <div class="pam-consultant-login__inputIcon cursor--pointer"
          <div class="pam-consultant-login__inputIcon cursor--pointer fix-chrome-click--issue"
            @click="isShowPassword = !isShowPassword">
            <i :class="[isShowPassword ? 'icon-eye-1 fs-25' : 'icon-eye' , 'text--primary']"></i>
            <i :class="[isShowPassword ? 'icon-eye':'icon-eye-1 fs-25', 'text--primary']"></i>
          </div>
        </div>
      </div>
      <div class="pam-paragraph">
        <div class="pam-consultant-login__title">
          <div>驗證碼</div>
          <div class="text--dark-blue fs-16 cursor--pointer"
          <div class="text--dark-blue fs-16 cursor--pointer fix-chrome-click--issue"
            @click="regenerateCode">重新產生</div>
        </div>
        <div class="pam-consultant-login__verifyBlock mt-10">
@@ -50,13 +50,14 @@
              class="pam-consultant-login__input">
          </div>
          <div class="pam-consultant-login__verifyImg">
            <img src="~/assets/images/logo.png" alt="驗證碼">
            <img src="~/assets/images/logo.png"
              alt="驗證碼">
          </div>
        </div>
      </div>
      <div class="pam-consultant-login__confirmBlock pam-paragraph">
        <button class="pam-consultant-login__confirm cursor--pointer"
          @click="fakeLogin">送出</button>
        <button class="pam-consultant-login__confirm cursor--pointer fix-chrome-click--issue"
          @click="loginWithConsultant">送出</button>
      </div>
    </div>
  </div>
@@ -64,70 +65,73 @@
<script lang="ts">
  import { namespace } from 'nuxt-property-decorator';
  import { Vue, Component} from 'vue-property-decorator';
  import { getForgotPasswordLink , getVerificationCodeImg , login } from '~/assets/ts/api/consultant';
  import { Role } from '../../components/NavBar.vue';
  import { Role } from '~/assets/ts/models/enum/Role';
  import { Vue, Component } from 'vue-property-decorator';
  import { getForgotPasswordLink, getVerificationCodeImg, logInToConsultant } from '~/assets/ts/api/consultant';
  const localStorage = namespace('localStorage');
  const roleStorage = namespace('localStorage');
  @Component({
    layout: 'home'
  })
  export default class ConsultantLogin extends Vue {
    @localStorage.Mutation storageIdToken!: (token:string) => void;
    @localStorage.Mutation storageRole!: (role:string) => void;
    @roleStorage.Mutation storageIdToken!: (token: string) => void;
    @roleStorage.Mutation storageRole!: (role: string) => void;
    isRemember = false;
    isShowPassword = false;
    forgotPasswordLink = ''; // å°šæœª
    imgOfVerificationCode = ''; // å°šæœª
    consultantDto = {
      account: '',
      username: '',
      password: '',
      verificationCode: '',
    }
    forgotPasswordLink = '';
    imgOfVerificationCode='';
    mounted() {
      this.getRememberUserName();
      this.regenerateCode();
      this.getLinkOfForgotPassword();
    };
    private getLinkOfForgotPassword():void{
      getForgotPasswordLink().then(link=>{
        console.log('link',link);
    private getRememberUserName(): void {
      const username = localStorage.getItem('consultantUserName')
      if (username) {
        this.consultantDto.username = username;
        this.isRemember = true;
      }
    }
    private getLinkOfForgotPassword(): void {
      getForgotPasswordLink().then(link => {
        this.forgotPasswordLink = link;
      });
    };
    recordAccount(): void {
      this.isRemember = !this.isRemember;
      if (this.isRemember) {
        console.log('sotre account');
      }
    };
    regenerateCode(): void {
      getVerificationCodeImg().then((imgOfbase64:any)=>{
    public regenerateCode(): void {
      getVerificationCodeImg().then(imgOfbase64 => {
        this.imgOfVerificationCode = imgOfbase64;
      });
    };
    // loginWithConsultant():void{
    //   console.log('consultantDto',this.consultantDto);
    //   logInToConsultant(this.consultantDto).then(res=>{
    //     localStorage.setItem('roleOfState',Role.CONSULTANT);
    //     this.$router.push('/myAppointmentList/appointmentList');
    //   });
    // }
    fakeLogin(): void {
        const user = {
            username: 'admin',
            password: 'admin'
        }
        login(user).then((res) => {
    public loginWithConsultant(): void {
      this.recordAccount();
      logInToConsultant(this.consultantDto).then(res => {
            this.storageIdToken(res.data.id_token);
            this.storageRole(Role.ADMIN);
            this.$router.push('/myAppointmentList/appointmentList');
        })
      }, (error) => {
        this.consultantDto.password = '';
        this.consultantDto.verificationCode = '';
      });
    }
    private recordAccount(): void {
      localStorage.setItem('consultantUserName', this.isRemember ? this.consultantDto.username : '');
    };
  };
</script>
<style lang="scss"
  scoped>
  .mt-20 {
@@ -187,11 +191,13 @@
        right: 15px;
      }
    }
    &__forgot-password{
    &__forgot-password {
        color: $PRIMARY_RED;
        text-decoration:none;
      text-decoration: none;
        font-size: 16px; 
    }
    &__verifyBlock {
      display: flex;
      justify-content: space-between;
@@ -201,7 +207,7 @@
      width: 126px;
      border: 1px black solid;
      height: 50px;
      img{
      img {
        width: 100%;
        height: 100%;
      }
PAMapp/pages/login/index.vue
@@ -1,105 +1,129 @@
<template>
    <div class="pam-login-page">
      <div class="text--middle">登入</div>
        <div class='mb-30'>
          <div class="mdTxt">登入方式</div>
          <div class="pam-field-title__hint mt-5 mb-10"
          >顧問將會以此{{connectDevice === 'MOBILE' ? '手機號碼' : 'Email'}}為主要預約諮詢聯繫方式</div>
      <div class="pam-paragraph">
        <div class="mdTxt">
        ç¶å®šæ–¹å¼
        </div>
        <div class="pam-tags">
          <el-row type="flex" class="pt-10">
            <el-button
              :class="{ 'active': connectDevice === 'MOBILE'}"
              @click="connectDevice = 'MOBILE'">手機號碼</el-button>
            <el-button
              :class="{ 'active': connectDevice === 'EMAIL'}"
              @click="connectDevice = 'EMAIL'">Email</el-button>
          </el-row>
        </div>
          <el-row type="flex" class="pt-10" v-show="connectDevice === 'MOBILE'">
            <input
              class="pam-input"
              :class="{
                'is-invalid': !phoneNumber
              }"
              v-model="phoneNumber"
              placeholder="請輸入手機號碼"
              >
          </el-row>
          <el-row class="pt-10" v-show="connectDevice === 'EMAIL'">
            <input
              class="pam-input"
              :class="{
                'is-invalid': !phoneNumber
              }"
              v-model="email"
              placeholder="請輸入 Email åœ°å€"
              >
          </el-row>
          <div class="pt-30" v-show="showPhoneOtpCodeField">
            <el-row type="flex" justify="space-between">
                <div class="mdTxt">輸入驗證碼</div>
                <div class="otp-count-timer">
                  13:50
                </div>
          <div class="pam-tags">
            <el-row type="flex" class="pt-30">
              <el-button
                :class="{ 'active': connectDevice === 'MOBILE'}"
                @click="connectDevice = 'MOBILE'">手機號碼</el-button>
              <el-button
                :class="{ 'active': connectDevice === 'EMAIL'}"
                @click="connectDevice = 'EMAIL'">Email</el-button>
            </el-row>
          </div>
            <el-row class="pt-10">
          <div class="pam-inputs mb-10">
            <div class="pt-10" v-show="connectDevice === 'MOBILE'">
              <input
                  class="pam-input"
                  :class="{
                    'is-invalid': !phoneValid
                  }"
                  v-model="phoneNumber"
                  placeholder="請輸入手機號碼">
              <div class="error mt-5 mb-5">
                  <span v-show="!phoneValid">手機號碼格式有誤</span>
              </div>
            </div>
            <div class="pt-10" v-show="connectDevice === 'EMAIL'">
              <input
                class="pam-input"
                :class="{
                  'is-invalid': !otpCode
                  'is-invalid': !emailValid
                }"
                v-model="otpCode"
                placeholder="請輸入驗證碼"
                v-model="email"
                placeholder="請輸入 Email åœ°å€"
              >
              <div class="error mt-5 mb-5">
                  <span v-show="!emailValid">Email格式有誤</span>
              </div>
            </div>
          </div>
          <!-- mobile é©—證碼 -->
          <template v-if="connectDevice === 'MOBILE'">
            <div v-show="showPhoneOtpCodeField">
              <el-row type="flex" justify="space-between">
                  <div class="mdTxt">輸入驗證碼</div>
                  <div class="otp-count-timer">
                    {{otpCounter}}
                  </div>
              </el-row>
              <el-row class="mb-30">
                <input
                  class="pam-input mt-10"
                  :class="{
                    'is-invalid': !otpCode
                  }"
                  v-model="otpCode"
                  placeholder="請輸入驗證碼"
                  >
              </el-row>
              <el-row>
                <el-button
                  :disabled="!phoneNumber || otpResendCounter !== 0 || !phoneValid"
                  @click="resentOtp('MOBILE')"
                  icon="icon-arrow"
                >
                  é‡ç™¼é©—證碼<span class="pam-field-title__hint pl-5">({{ otpResendCounter }})</span>
                </el-button>
              </el-row>
            </div>
            <el-row>
              <el-button
                  v-if="onPhoneVerifyStep === 'APPLY_OTP'"
                  :disabled="!phoneNumber || !phoneValid"
                  @click="applyOtpVerification('MOBILE')"
                  icon="icon-arrow"
                >
                  ç™¼é€é©—證碼
                </el-button>
            </el-row>
            <el-row class="pt-10">
              <button
                class="pam-otp-resend-btn"
                :class="{'disabled': true}">
                <i class="icon-arrow"></i>
                é‡ç™¼é©—證碼({{ otpResendCounter }})
              </button>
            </el-row>
          </div>
          </template>
          <div v-show="showEmailVerifyField">
            <el-row class="pt-10">
              <button
                class="pam-otp-resend-btn"
                :class="{'disabled': onEmailVerifyResendStatus === 'CANNOT_RESEND'}">
                <i class="icon-arrow"></i>
                é‡ç™¼é©—證碼({{ emailResendCounter }})
              </button>
          <!-- email é©—證碼 -->
          <template v-if="connectDevice === 'EMAIL'">
            <el-row v-show="showEmailVerifyField">
              <el-button
                :disabled="!email || emailResendCounter !== 0 || !emailValid"
                icon="icon-arrow"
                @click="resentOtp('EMAIL')"
              >
                é‡ç™¼é©—證碼<span class="pam-field-title__hint pl-5">({{ emailResendCounter }})</span>
              </el-button>
              <div class="mt-10 smTxt_bold text--primary">! è«‹ç¨ç­‰ï¼Œæ–°çš„Email待啟用</div>
            </el-row>
          </div>
            <el-row v-show="!showEmailVerifyField">
              <el-button
                  :disabled="!email || !emailValid"
                  @click="applyOtpVerification('EMAIL')"
                  icon="icon-arrow"
                >
                  ç™¼é€é©—證碼
                </el-button>
            </el-row>
          </template>
      </div>
      <el-row type="flex" justify="center" class="pam-login-page__action-bar">
        <div v-if="connectDevice === 'MOBILE'">
          <el-button
            type="primary"
            v-if="onPhoneVerifyStep === 'APPLY_OTP'"
            :disabled="!phoneNumber"
            @click="applyOtpVerification">
            ç™¼é€é©—證碼
          </el-button>
          <el-button
            type="primary"
            v-if="connectDevice === 'MOBILE' && onPhoneVerifyStep === 'INPUT_OTP'"
            :disabled="!otpCode"
            @click="registerDialogVisable = true">
            é€å‡º
          </el-button>
        </div>
      <el-row type="flex" justify="center" class="pam-login-page__action-bar mt-30">
        <el-button
          type="primary"
          v-if="connectDevice === 'MOBILE' && onPhoneVerifyStep === 'INPUT_OTP'"
          :disabled="!otpCode || !phoneNumber || !phoneValid"
          @click="phoneLogin">
          é€å‡º
        </el-button>
      </el-row>
      <el-dialog
@@ -213,41 +237,98 @@
        </span>
      </el-dialog>
      <PopUpFrame class="pam-popUpFrame"
        :isOpen.sync="emailOtpConfirmVisable">
        <div class="pam-popUp-title text--center">已將驗證訊息發送至</div>
        <div class="pam-popUp-title text--center">{{email}}</div>
        <div class="pam-popUp-title text--center">請查看電子郵件並完成驗證流程</div>
        <div class="pam-popUp-confirm-bolck pam-paragraph">
          <div class="text--center">
            <el-button
                type="primary"
                @click="emailOtpConfirmVisable = false"
            >我知道了</el-button>
          </div>
        </div>
      </PopUpFrame>
      <PopUpFrame class="pam-popUpFrame"
        :isOpen.sync="registerSuccessConfirmVisable">
          <div class="pam-popUp-title text--center">
            æ­¡è¿Žæ‚¨ç™»å…¥æˆåŠŸï¼Œå¦‚æ‚¨é ç´„è«®è©¢ï¼Œé¡§å•æœƒä»¥æ‚¨ç•™ä¸‹çš„{{ connectDevice === 'MOBILE' ? '手機號碼' : 'Email'}}與您聯繫
          </div>
          <div class="pam-popUp-confirm-bolck pam-paragraph">
            <div class="text--center">
              <el-button
                  type="primary"
                  @click="confirmApplySuccess"
              >我知道了</el-button>
            </div>
          </div>
      </PopUpFrame>
      <el-button class="mt-30" @click="fakeLogin">客戶登入</el-button>
      <PopUpFrame class="pam-popUpFrame"
        :isOpen.sync="phoneSuccessConfirmVisable">
          <div class="pam-popUp-title text--center mb-50"
          >歡迎您登入成功</div>
          <div class="pam-popUp-confirm-bolck pam-paragraph">
            <div class="text--center">
              <el-button
                  type="primary"
                  @click="confirmApplySuccess"
              >我知道了</el-button>
            </div>
          </div>
      </PopUpFrame>
    </div>
</template>
<script lang="ts">
import { namespace } from 'nuxt-property-decorator';
import { Vue, Component } from 'vue-property-decorator';
import { login } from '~/assets/ts/api/consultant';
import { Role } from '../../components/NavBar.vue';
const localStorage = namespace('localStorage');
import { LoginRequest, loginVerify, OtpInfo, register, RegisterInfo, sendOtp } from '~/assets/ts/api/consultant';
import { Role } from '~/assets/ts/models/enum/Role';
const roleStorage = namespace('localStorage');
@Component
export default class Login extends Vue {
  @localStorage.Mutation storageIdToken!: (token:string) => void;
  @localStorage.Mutation storageRole!: (role:string) => void;
  @roleStorage.Mutation storageIdToken!: (token:string) => void;
  @roleStorage.Mutation storageRole!: (role:string) => void;
  connectDevice: 'MOBILE' | 'EMAIL' = 'MOBILE';
  phoneNumber = '';
  otpCode = '';
  onPhoneVerifyStep: 'APPLY_OTP' | 'INPUT_OTP' | 'SUBMIT_OTP' = 'APPLY_OTP';
  otpCounter = '15:00';
  otpResendCounter = 30;
  otpInterval: any;
  phoneOtpInfo!: OtpInfo;
  email = '';
  onEmailVerifyResendStatus: 'CAN_RESEND' | 'CANNOT_RESEND' = 'CANNOT_RESEND';
  onEmailVerifyResendStatus: 'APPLY_OTP' | 'CAN_RESEND' = 'APPLY_OTP';
  emailResendCounter = 30;
  emailResendInterval: any;
  emailOtpInfo!: OtpInfo;
  registerDialogVisable = false;
  name = '';
  agreeControct = false;
  isReadContract = false;
  phoneSuccessConfirmVisable = false;
  emailOtpConfirmVisable = false;
  registerDialogVisable = false;
  registerSuccessConfirmVisable = false;
  detectContructReadStatus(event: any): void {
    this.isReadContract = Math.round(event.target.scrollTop) === (event.target.scrollHeight - event.target.clientHeight);
    const scrollTop = Math.round(event.target.scrollTop);
    const height = event.target.scrollHeight - event.target.clientHeight;
    if (Math.floor(scrollTop/10) === (Math.floor(height/10))) {
      this.isReadContract = true;
    }
  };
  get showPhoneOtpCodeField(): boolean {
@@ -255,29 +336,141 @@
  };
  get showEmailVerifyField(): boolean {
    return this.connectDevice === 'EMAIL';
    return this.connectDevice === 'EMAIL' && this.onEmailVerifyResendStatus !== 'APPLY_OTP';
  };
  applyOtpVerification(): void {
    this.onPhoneVerifyStep = 'INPUT_OTP';
  get phoneValid() {
    const rule = /^09[0-9]{8}$/;
    return this.phoneNumber ? rule.test(this.phoneNumber) : true;
  }
  get emailValid() {
    const rule = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/;
    return this.email ? rule.test(this.email) : true;
  }
  applyOtpVerification(type: string): void {
    const isMobile = this.connectDevice === 'MOBILE';
    const loginInfo: LoginRequest = {
      loginType: isMobile ? 'SMS' : 'EMAIL',
      account: isMobile ? this.phoneNumber : this.email,
    }
    sendOtp(loginInfo).then(otpInfo => {
      if (otpInfo.success) {
        this.startOtpCount(type, otpInfo);
      }
    });
  };
  resentOtp(type: string) {
    if (type === 'MOBILE') {
      clearInterval(this.otpInterval);
      this.otpResendCounter = 30;
      this.otpCounter = '15:00';
      this.startPhoneCounter();
    } else {
      this.emailResendCounter = 30;
      this.startEmailCounter();
      this.emailOtpConfirmVisable = true;
    }
  }
  private startOtpCount(type: string, otpInfo) {
    type === 'MOBILE' ? this.phoneOtpInfo = otpInfo : this.emailOtpInfo = otpInfo;
    if (type === 'MOBILE') {
        this.onPhoneVerifyStep = 'INPUT_OTP';
        this.startPhoneCounter();
      } else {
        this.onEmailVerifyResendStatus = 'CAN_RESEND';
        this.startEmailCounter();
        this.emailOtpConfirmVisable = true;
      }
  }
  private startEmailCounter() {
    this.emailResendInterval = setInterval(() => {
      this.emailResendCounter -= 1;
      if (this.emailResendCounter === 0) {
        clearInterval(this.emailResendInterval)
      }
    }, 1000)
  }
  private startPhoneCounter() {
    const minCount = this.otpCounter.split(':');
    let secCount = (+minCount[0] * 60) + (+minCount[1]);
    let min = 0;
    let sec = 0;
    this.otpInterval = setInterval(() => {
      secCount -= 1;
      min = Math.floor(secCount/60);
      sec = Math.floor(secCount%60);
      this.otpCounter = `${min < 10 ? '0' + min : min}:${sec < 10 ? '0' + sec : sec}`;
      if (this.otpResendCounter !== 0) {
        this.otpResendCounter -= 1;
      }
      if (secCount === 0) {
        clearInterval(this.otpInterval)
      }
    }, 1000)
  }
  private setRegisterInfo(): RegisterInfo {
    return this.connectDevice === 'MOBILE'
      ? {
          phone: this.phoneNumber,
          indexKey: this.phoneOtpInfo.indexKey,
          otpCode: this.otpCode,
          name: this.name,
          contactType: 'SMS'
        }
      : {
          email: this.email,
          indexKey: this.emailOtpInfo.indexKey,
          otpCode: this.otpCode,
          name: this.name,
          contactType: 'EMAIL'
        }
  }
  applyAccount(): void {
    console.log('apply new account!')
    const registerInfo = this.setRegisterInfo();
    register(registerInfo).then(res => {
      this.storageIdToken(res.data.id_token);
      this.storageRole(Role.USER);
      this.registerSuccessConfirmVisable = true;
    })
  };
  // TODO: åƒ…OTP認證開發前 æš«æ™‚使用
   fakeLogin() {
        const user = {
            username: 'user',
            password: 'user',
        }
        login(user).then((res) => {
            this.storageIdToken(res.data.id_token);
            this.storageRole(Role.USER);
            this.$router.go(-1);
        })
  };
  confirmApplySuccess(): void {
    this.phoneSuccessConfirmVisable = false;
    this.registerSuccessConfirmVisable = false;
    this.$router.go(-1);
  }
  phoneLogin() {
    const login = {
      account: this.phoneNumber,
      indexKey: this.phoneOtpInfo.indexKey,
      otpCode: this.otpCode
    }
    loginVerify(login).then(res => {
      this.storageIdToken(res.data.id_token);
      this.storageRole(Role.USER);
      this.phoneSuccessConfirmVisable = true;
    }).catch(error => {
      if (error.response.status === 401) {
        this.registerDialogVisable = true;
      }
    })
  }
  destroyed() {
    clearInterval(this.otpInterval);
    clearInterval(this.emailResendInterval);
  }
}
</script>
@@ -299,8 +492,8 @@
  width: calc(100% - 36px);
  border-radius: 10px !important;
  padding: 12px 18px !important;
  border-width: 1px;
  outline: none;
  border:1px solid #CCCCCC;
  outline: 0;
  @extend .text--middle;
  &::placeholder {
    color: $PRUDENTIAL_GREY;
@@ -308,19 +501,6 @@
  &.is-invalid {
    border: 1px solid $PRIMARY_RED !important;
    border-radius: 20px;
  }
}
.pam-otp-resend-btn {
  background: transparent;
  border: none;
  color: $PRIMARY_RED;
  font-weight: bold;
  i {
    margin-right: 10px;
  }
  &.disabled {
    color: $LIGHT_GREY;
  }
}
@@ -376,4 +556,18 @@
  }
}
.pam-field-title__hint {
  @extend .smTxt_bold;
  color: #68737A;
}
.error {
  @extend .smTxt_bold;
  @extend .text--primary;
  height: 16px;
}
.pam-popUp-title {
  line-height: 24px;
}
</style>
PAMapp/pages/myAppointmentList.vue
@@ -45,7 +45,6 @@
     this.storeMyAppointmentList();
     if (this.$route.name) {
         console.log('mounted route')
         this.activeTabName = this.$route.name.split('-')[1]
     }
    }
PAMapp/store/localStorage.ts
@@ -2,8 +2,8 @@
@Module
export default class LocalStorage extends VuexModule {
  id_token:string|null = null;
  role_State:string|null = null;
  id_token = localStorage.getItem('id_token');
  role_State= localStorage.getItem('current_role');
  get idToken(): string|null {
    return this.id_token;
pamapi/src/doc/µù¥UAPI/µù¥UAPI.txt
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,13 @@
post :
http://localhost:8080/api/otp/register
request body:
{
    "phone": "0973000003",
    "email":"email@pollex.com.tw",
    "indexKey": "3485a742",
    "otpCode": "123",
    "name":"Jack",
    "contactType":"SMS" // "SMS":Otp發送手機,"EMAIL":Otp發email
}
pamapi/src/main/java/com/pollex/pam/domain/Customer.java
@@ -5,6 +5,8 @@
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@@ -14,6 +16,7 @@
import org.springframework.data.annotation.LastModifiedDate;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.pollex.pam.enums.OtpLoginTypeEnum;
@Entity
@Table(name = "customer")
@@ -37,6 +40,10 @@
    
    @Column(name = "email")
    private String email;
    @Enumerated(value = EnumType.STRING)
    @Column(name = "contact_type")
    private OtpLoginTypeEnum contactType;
    
    @CreatedDate
    @Column(name = "created_date", updatable = false)
@@ -95,6 +102,14 @@
    public void setLastModifiedDate(Instant lastModifiedDate) {
        this.lastModifiedDate = lastModifiedDate;
    }
    public OtpLoginTypeEnum getContactType() {
        return contactType;
    }
    public void setContactType(OtpLoginTypeEnum contactType) {
        this.contactType = contactType;
    }
    
    
pamapi/src/main/java/com/pollex/pam/domain/OtpTmp.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,92 @@
package com.pollex.pam.domain;
import java.io.Serializable;
import java.time.Instant;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import org.springframework.data.annotation.CreatedDate;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.pollex.pam.enums.OtpLoginTypeEnum;
import com.pollex.pam.enums.OtpTmpStatusEnum;
@Entity
@Table(name = "otp_tmp")
public class OtpTmp implements Serializable{
    /**
     *
     */
    private static final long serialVersionUID = 1L;
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "account")
    private String account;
    @Column(name = "index_key")
    private String indexKey;
    @Enumerated(value = EnumType.STRING)
    @Column(name = "status")
    private OtpTmpStatusEnum status;
    @CreatedDate
    @Column(name = "created_date", updatable = false)
    @JsonIgnore
    private Instant createdDate = Instant.now();
    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getIndexKey() {
        return indexKey;
    }
    public void setIndexKey(String indexKey) {
        this.indexKey = indexKey;
    }
    public OtpTmpStatusEnum getStatus() {
        return status;
    }
    public void setStatus(OtpTmpStatusEnum status) {
        this.status = status;
    }
    public Instant getCreatedDate() {
        return createdDate;
    }
    public void setCreatedDate(Instant createdDate) {
        this.createdDate = createdDate;
    }
    public String getAccount() {
        return account;
    }
    public void setAccount(String account) {
        this.account = account;
    }
}
pamapi/src/main/java/com/pollex/pam/enums/CustomerDetailEnum.java
@@ -3,7 +3,8 @@
public enum CustomerDetailEnum {
    ID("CustomerId"),
    NAME("CustomerName"),
    ACCOUNT("CustomerAccount");
    ACCOUNT("CustomerAccount"),
    CONTACT_TYPE("ContactType");
    private final String value;
pamapi/src/main/java/com/pollex/pam/enums/OtpTmpStatusEnum.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,6 @@
package com.pollex.pam.enums;
public enum OtpTmpStatusEnum {
    UNVERIFIED,
    VERRIFIED
}
pamapi/src/main/java/com/pollex/pam/repository/OtpTmpRepository.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,15 @@
package com.pollex.pam.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.pollex.pam.domain.OtpTmp;
@Repository
public interface OtpTmpRepository extends JpaRepository<OtpTmp, Long>{
    OtpTmp findByAccountAndIndexKey(String account, String indexKey);
    OtpTmp findByAccount(String account);
}
pamapi/src/main/java/com/pollex/pam/security/provider/OtpAuthenticationProvider.java
@@ -2,9 +2,13 @@
import com.pollex.pam.config.ApplicationProperties;
import com.pollex.pam.domain.Customer;
import com.pollex.pam.domain.OtpTmp;
import com.pollex.pam.enums.CustomerDetailEnum;
import com.pollex.pam.enums.OtpLoginTypeEnum;
import com.pollex.pam.enums.OtpTmpStatusEnum;
import com.pollex.pam.repository.CustomerRepository;
import com.pollex.pam.security.token.OtpAuthenticationToken;
import com.pollex.pam.service.OtpTmpService;
import com.pollex.pam.service.OtpWebService;
import com.pollex.pam.service.dto.OtpResponseDTO;
import com.pollex.pam.web.rest.vm.OtpAccount;
@@ -38,6 +42,9 @@
    @Autowired
    CustomerRepository customerRepository;
    @Autowired
    OtpTmpService otpTmpService;
    public Authentication authenticate(OtpAuthenticationToken otpAuthenticationToken) throws AuthenticationException {
        OtpAccount otpAccount = otpAuthenticationToken.getPrincipal();
@@ -46,13 +53,15 @@
        String otpCode = otpAuthenticationToken.getCredentials();
        if(applicationProperty.isMockLogin()){
            return getCustomerToken(account, otpCode);
            setVerrifiedOtpTmp(account, indexKey);
            return getCustomerToken(account, otpCode, indexKey);
        }
        try {
            OtpResponseDTO otpResponseDTO = otpWebService.verifyOTP(indexKey, otpCode);
            if(otpResponseDTO.isSuccess()) {
                return getCustomerToken(account, otpCode);
                setVerrifiedOtpTmp(account, indexKey);
                return getCustomerToken(account, otpCode, indexKey);
            }
        } catch (Exception e) {
            log.error("Exception: ", e);
@@ -62,9 +71,18 @@
        throw new AuthenticationCredentialsNotFoundException("");
    }
    private UsernamePasswordAuthenticationToken getCustomerToken(String account, String otpCode) {
        // todo æœªå­˜åœ¨æ–¼DB所屬正常現象,需用特殊message告知前端可進行註冊
        Customer customer = customerRepository.findOneByEmailEqualsOrPhoneEquals(account, account).orElseThrow(() -> new UsernameNotFoundException("this customer is not in db, account = " + account));
    private void setVerrifiedOtpTmp(String account, String indexKey) {
        OtpTmp otpTmp = otpTmpService.findByAccountAndIndexKey(account, indexKey);
        otpTmp.setStatus(OtpTmpStatusEnum.VERRIFIED);
        otpTmpService.save(otpTmp);
    }
    private UsernamePasswordAuthenticationToken getCustomerToken(String account
            , String otpCode, String indexKey) {
        // todo æœªå­˜åœ¨æ–¼DB所屬正常現象,需用特殊message告知前端可進行註冊
        Customer customer = customerRepository.findOneByEmailEqualsOrPhoneEquals(account, account).orElseThrow(() -> new UsernameNotFoundException("this customer is not in register, account = " + account));
        List<GrantedAuthority> grantedAuths = Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(account, otpCode, grantedAuths);
@@ -73,6 +91,7 @@
        details.put(CustomerDetailEnum.ID.getValue(), customer.getId().toString());
        details.put(CustomerDetailEnum.NAME.getValue(), customer.getName());
        details.put(CustomerDetailEnum.ACCOUNT.getValue(), account);
//        details.put(CustomerDetailEnum.CONTACT_TYPE.getValue(), customer.getContactType());
        authenticationToken.setDetails(details);
        return authenticationToken;
pamapi/src/main/java/com/pollex/pam/service/CustomerAuthService.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,34 @@
package com.pollex.pam.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import com.pollex.pam.security.jwt.TokenProvider;
import com.pollex.pam.security.token.OtpAuthenticationToken;
import com.pollex.pam.web.rest.vm.OtpAccount;
@Service
public class CustomerAuthService {
    @Autowired
    AuthenticationManagerBuilder authenticationManagerBuilder;
    @Autowired
    TokenProvider tokenProvider;
    public String authorize(String account, String indexKey, String otpCode) {
        OtpAccount otpAccount = new OtpAccount(account, indexKey);
        OtpAuthenticationToken authenticationToken = new OtpAuthenticationToken(
            otpAccount,
            otpCode
        );
        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        String jwt = tokenProvider.createToken(authentication, false);
        return jwt;
    }
}
pamapi/src/main/java/com/pollex/pam/service/CustomerService.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,47 @@
package com.pollex.pam.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.pollex.pam.domain.Customer;
import com.pollex.pam.domain.OtpTmp;
import com.pollex.pam.enums.OtpLoginTypeEnum;
import com.pollex.pam.enums.OtpTmpStatusEnum;
import com.pollex.pam.repository.CustomerRepository;
import com.pollex.pam.service.dto.CustomerRegisterDTO;
import com.pollex.pam.service.mapper.CustomerDTOMapper;
@Service
public class CustomerService {
    @Autowired
    CustomerRepository customerRepository;
    @Autowired
    CustomerDTOMapper customerDTOMapper;
    @Autowired
    CustomerAuthService customerAuthService;
    @Autowired
    OtpTmpService otpTmpService;
    public Customer save(Customer customer) {
        return customerRepository.save(customer);
    }
    public String registerCustomer(CustomerRegisterDTO registDTO) {
        String account = registDTO.getContactType() == OtpLoginTypeEnum.EMAIL?registDTO.getEmail():registDTO.getPhone();
        OtpTmp otpTmp = otpTmpService.findByAccountAndIndexKey(account, registDTO.getIndexKey());
        if(otpTmp.getStatus() == OtpTmpStatusEnum.VERRIFIED) {
            Customer customer = customerDTOMapper.toCustomer(registDTO);
            save(customer);
            String jwt = customerAuthService.authorize(account, registDTO.getIndexKey(), registDTO.getOtpCode());
            return jwt;
        }else {
            throw new UsernameNotFoundException("Otp record not found");
        }
    }
}
pamapi/src/main/java/com/pollex/pam/service/OtpTmpService.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,39 @@
package com.pollex.pam.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.pollex.pam.domain.OtpTmp;
import com.pollex.pam.enums.OtpLoginTypeEnum;
import com.pollex.pam.enums.OtpTmpStatusEnum;
import com.pollex.pam.repository.OtpTmpRepository;
@Service
public class OtpTmpService {
    @Autowired
    OtpTmpRepository otpTmpRepository;
    public OtpTmp createOtpTmp(String account, String indexKey) {
        OtpTmp oldTmp = otpTmpRepository.findByAccount(account);
        if(oldTmp==null) {
            OtpTmp otpTmp = new OtpTmp();
            otpTmp.setIndexKey(indexKey);
            otpTmp.setAccount(account);
            otpTmp.setStatus(OtpTmpStatusEnum.UNVERIFIED);
            return otpTmpRepository.save(otpTmp);
        }else {
            oldTmp.setIndexKey(indexKey);
            oldTmp.setStatus(OtpTmpStatusEnum.UNVERIFIED);
            return otpTmpRepository.save(oldTmp);
        }
    }
    public OtpTmp findByAccountAndIndexKey(String account, String indexKey) {
        return otpTmpRepository.findByAccountAndIndexKey(account, indexKey);
    }
    public OtpTmp save(OtpTmp otpTmp) {
        return otpTmpRepository.save(otpTmp);
    }
}
pamapi/src/main/java/com/pollex/pam/service/UserService.java
@@ -1,18 +1,13 @@
package com.pollex.pam.service;
import com.pollex.pam.config.Constants;
import com.pollex.pam.domain.Authority;
import com.pollex.pam.domain.User;
import com.pollex.pam.repository.AuthorityRepository;
import com.pollex.pam.repository.UserRepository;
import com.pollex.pam.security.AuthoritiesConstants;
import com.pollex.pam.security.SecurityUtils;
import com.pollex.pam.service.dto.AdminUserDTO;
import com.pollex.pam.service.dto.UserDTO;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.CacheManager;
@@ -22,6 +17,16 @@
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.pollex.pam.config.Constants;
import com.pollex.pam.domain.Authority;
import com.pollex.pam.domain.User;
import com.pollex.pam.repository.AuthorityRepository;
import com.pollex.pam.repository.UserRepository;
import com.pollex.pam.security.SecurityUtils;
import com.pollex.pam.service.dto.AdminUserDTO;
import com.pollex.pam.service.dto.UserDTO;
import tech.jhipster.security.RandomUtil;
/**
@@ -40,6 +45,7 @@
    private final AuthorityRepository authorityRepository;
    private final CacheManager cacheManager;
    public UserService(
        UserRepository userRepository,
@@ -93,47 +99,47 @@
            });
    }
    public User registerUser(AdminUserDTO userDTO, String password) {
        userRepository
            .findOneByLogin(userDTO.getLogin().toLowerCase())
            .ifPresent(existingUser -> {
                boolean removed = removeNonActivatedUser(existingUser);
                if (!removed) {
                    throw new UsernameAlreadyUsedException();
                }
            });
        userRepository
            .findOneByEmailIgnoreCase(userDTO.getEmail())
            .ifPresent(existingUser -> {
                boolean removed = removeNonActivatedUser(existingUser);
                if (!removed) {
                    throw new EmailAlreadyUsedException();
                }
            });
        User newUser = new User();
        String encryptedPassword = passwordEncoder.encode(password);
        newUser.setLogin(userDTO.getLogin().toLowerCase());
        // new user gets initially a generated password
        newUser.setPassword(encryptedPassword);
        newUser.setFirstName(userDTO.getFirstName());
        newUser.setLastName(userDTO.getLastName());
        if (userDTO.getEmail() != null) {
            newUser.setEmail(userDTO.getEmail().toLowerCase());
        }
        newUser.setImageUrl(userDTO.getImageUrl());
        newUser.setLangKey(userDTO.getLangKey());
        // new user is not active
        newUser.setActivated(false);
        // new user gets registration key
        newUser.setActivationKey(RandomUtil.generateActivationKey());
        Set<Authority> authorities = new HashSet<>();
        authorityRepository.findById(AuthoritiesConstants.USER).ifPresent(authorities::add);
        newUser.setAuthorities(authorities);
        userRepository.save(newUser);
        this.clearUserCaches(newUser);
        log.debug("Created Information for User: {}", newUser);
        return newUser;
    }
//    public User registerUser(AdminUserDTO userDTO, String password) {
//        userRepository
//            .findOneByLogin(userDTO.getLogin().toLowerCase())
//            .ifPresent(existingUser -> {
//                boolean removed = removeNonActivatedUser(existingUser);
//                if (!removed) {
//                    throw new UsernameAlreadyUsedException();
//                }
//            });
//        userRepository
//            .findOneByEmailIgnoreCase(userDTO.getEmail())
//            .ifPresent(existingUser -> {
//                boolean removed = removeNonActivatedUser(existingUser);
//                if (!removed) {
//                    throw new EmailAlreadyUsedException();
//                }
//            });
//        User newUser = new User();
//        String encryptedPassword = passwordEncoder.encode(password);
//        newUser.setLogin(userDTO.getLogin().toLowerCase());
//        // new user gets initially a generated password
//        newUser.setPassword(encryptedPassword);
//        newUser.setFirstName(userDTO.getFirstName());
//        newUser.setLastName(userDTO.getLastName());
//        if (userDTO.getEmail() != null) {
//            newUser.setEmail(userDTO.getEmail().toLowerCase());
//        }
//        newUser.setImageUrl(userDTO.getImageUrl());
//        newUser.setLangKey(userDTO.getLangKey());
//        // new user is not active
//        newUser.setActivated(false);
//        // new user gets registration key
//        newUser.setActivationKey(RandomUtil.generateActivationKey());
//        Set<Authority> authorities = new HashSet<>();
//        authorityRepository.findById(AuthoritiesConstants.USER).ifPresent(authorities::add);
//        newUser.setAuthorities(authorities);
//        userRepository.save(newUser);
//        this.clearUserCaches(newUser);
//        log.debug("Created Information for User: {}", newUser);
//        return newUser;
//    }
    private boolean removeNonActivatedUser(User existingUser) {
        if (existingUser.isActivated()) {
@@ -322,4 +328,6 @@
            Objects.requireNonNull(cacheManager.getCache(UserRepository.USERS_BY_EMAIL_CACHE)).evict(user.getEmail());
        }
    }
}
pamapi/src/main/java/com/pollex/pam/service/dto/CustomerRegisterDTO.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,53 @@
package com.pollex.pam.service.dto;
import com.pollex.pam.enums.OtpLoginTypeEnum;
public class CustomerRegisterDTO {
    private String name;
    private String phone;
    private String email;
    private OtpLoginTypeEnum contactType;
    private String indexKey;
    private String otpCode;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getPhone() {
        return phone;
    }
    public void setPhone(String phone) {
        this.phone = phone;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public OtpLoginTypeEnum getContactType() {
        return contactType;
    }
    public void setContactType(OtpLoginTypeEnum contactType) {
        this.contactType = contactType;
    }
    public String getIndexKey() {
        return indexKey;
    }
    public void setIndexKey(String indexKey) {
        this.indexKey = indexKey;
    }
    public String getOtpCode() {
        return otpCode;
    }
    public void setOtpCode(String otpCode) {
        this.otpCode = otpCode;
    }
}
pamapi/src/main/java/com/pollex/pam/service/mapper/CustomerDTOMapper.java
¤ñ¹ï·sÀÉ®×
@@ -0,0 +1,17 @@
package com.pollex.pam.service.mapper;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import com.pollex.pam.domain.Customer;
import com.pollex.pam.service.dto.CustomerRegisterDTO;
@Service
public class CustomerDTOMapper {
    public Customer toCustomer(CustomerRegisterDTO source) {
        Customer target = new Customer();
        BeanUtils.copyProperties(source, target);
        return target;
    }
}
pamapi/src/main/java/com/pollex/pam/web/rest/AccountResource.java
@@ -3,10 +3,15 @@
import com.pollex.pam.domain.User;
import com.pollex.pam.repository.UserRepository;
import com.pollex.pam.security.SecurityUtils;
import com.pollex.pam.security.jwt.JWTFilter;
import com.pollex.pam.service.CustomerAuthService;
import com.pollex.pam.service.CustomerService;
import com.pollex.pam.service.MailService;
import com.pollex.pam.service.UserService;
import com.pollex.pam.service.dto.AdminUserDTO;
import com.pollex.pam.service.dto.CustomerRegisterDTO;
import com.pollex.pam.service.dto.PasswordChangeDTO;
import com.pollex.pam.web.rest.UserJWTController.JWTToken;
import com.pollex.pam.web.rest.errors.*;
import com.pollex.pam.web.rest.vm.KeyAndPasswordVM;
import com.pollex.pam.web.rest.vm.ManagedUserVM;
@@ -16,7 +21,10 @@
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
/**
@@ -40,6 +48,9 @@
    private final UserService userService;
    private final MailService mailService;
    @Autowired
    CustomerService customerService;
    public AccountResource(UserRepository userRepository, UserService userService, MailService mailService) {
        this.userRepository = userRepository;
@@ -47,23 +58,25 @@
        this.mailService = mailService;
    }
    /**
     * {@code POST  /register} : register the user.
     *
     * @param managedUserVM the managed user View Model.
     * @throws InvalidPasswordException {@code 400 (Bad Request)} if the password is incorrect.
     * @throws EmailAlreadyUsedException {@code 400 (Bad Request)} if the email is already used.
     * @throws LoginAlreadyUsedException {@code 400 (Bad Request)} if the login is already used.
     */
    @PostMapping("/register")
    @ResponseStatus(HttpStatus.CREATED)
    public void registerAccount(@Valid @RequestBody ManagedUserVM managedUserVM) {
        if (isPasswordLengthInvalid(managedUserVM.getPassword())) {
            throw new InvalidPasswordException();
        }
        User user = userService.registerUser(managedUserVM, managedUserVM.getPassword());
        mailService.sendActivationEmail(user);
    }
//    /**
//     * {@code POST  /register} : register the user.
//     *
//     * @param managedUserVM the managed user View Model.
//     * @throws InvalidPasswordException {@code 400 (Bad Request)} if the password is incorrect.
//     * @throws EmailAlreadyUsedException {@code 400 (Bad Request)} if the email is already used.
//     * @throws LoginAlreadyUsedException {@code 400 (Bad Request)} if the login is already used.
//     */
//    @PostMapping("/register")
//    @ResponseStatus(HttpStatus.CREATED)
//    public void registerAccount(@Valid @RequestBody ManagedUserVM managedUserVM) {
//        if (isPasswordLengthInvalid(managedUserVM.getPassword())) {
//            throw new InvalidPasswordException();
//        }
//        User user = userService.registerUser(managedUserVM, managedUserVM.getPassword());
//        mailService.sendActivationEmail(user);
//    }
    /**
     * {@code GET  /activate} : activate the registered user.
pamapi/src/main/java/com/pollex/pam/web/rest/OtpResource.java
@@ -5,7 +5,11 @@
import com.pollex.pam.security.jwt.JWTFilter;
import com.pollex.pam.security.jwt.TokenProvider;
import com.pollex.pam.security.token.OtpAuthenticationToken;
import com.pollex.pam.service.CustomerAuthService;
import com.pollex.pam.service.CustomerService;
import com.pollex.pam.service.OtpTmpService;
import com.pollex.pam.service.OtpWebService;
import com.pollex.pam.service.dto.CustomerRegisterDTO;
import com.pollex.pam.service.dto.OtpResponseDTO;
import com.pollex.pam.web.rest.vm.*;
import org.slf4j.Logger;
@@ -41,15 +45,35 @@
    @Autowired
    TokenProvider tokenProvider;
    @Autowired
    CustomerAuthService customerAuthService;
    @Autowired
    OtpTmpService otpTmpService;
    @Autowired
    CustomerService customerService;
    @PostMapping("/sendOtp")
    public ResponseEntity<Object> sendOtp(@RequestBody OtpLoginVM login) {
        if(applicationProperty.isMockLogin()) {
            return new ResponseEntity<>(getMockSendOtpResponse(), HttpStatus.OK);
        }
        if(login.getLoginType() == OtpLoginTypeEnum.SMS) {
            return new ResponseEntity<>(otpWebService.sendByPhone(login.getAccount()), HttpStatus.OK);
        OtpResponseDTO otpResponse;
        try {
            if(applicationProperty.isMockLogin()) {
                otpResponse = getMockSendOtpResponse();
            }else if(login.getLoginType() == OtpLoginTypeEnum.SMS) {
                otpResponse = otpWebService.sendByPhone(login.getAccount());
            }
            else if(login.getLoginType() == OtpLoginTypeEnum.EMAIL) {
                otpResponse = otpWebService.sendByEmail(login.getAccount());
            }else {
                return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("can not support this login type, loginType = " + login.getLoginType().name());
            }
            otpTmpService.createOtpTmp(login.getAccount(), otpResponse.getIndexKey());
            return new ResponseEntity<>(otpResponse, HttpStatus.OK);
        } catch (ServiceException | RemoteException e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("connecting otp web service error");
        }
        else if(login.getLoginType() == OtpLoginTypeEnum.EMAIL) {
            return new ResponseEntity<>(otpWebService.sendByEmail(login.getAccount()), HttpStatus.OK);
@@ -60,22 +84,22 @@
    @PostMapping("/verify")
    public ResponseEntity<UserJWTController.JWTToken> verifyOtp(@RequestBody VerifyOtpVM verifyOtpParam) {
        OtpAccount otpAccount = new OtpAccount(verifyOtpParam.getAccount(), verifyOtpParam.getIndexKey());
        OtpAuthenticationToken authenticationToken = new OtpAuthenticationToken(
            otpAccount,
            verifyOtpParam.getOtpCode()
        );
        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        String jwt = tokenProvider.createToken(authentication, false);
        String jwt = customerAuthService.authorize(verifyOtpParam.getAccount(), verifyOtpParam.getIndexKey(), verifyOtpParam.getOtpCode());
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.add(JWTFilter.AUTHORIZATION_HEADER, "Bearer" + jwt);
        return new ResponseEntity<>(new UserJWTController.JWTToken(jwt), httpHeaders, HttpStatus.OK);
    }
    private OtpResponseDTO getMockSendOtpResponse() {
        String indexKey = UUID.randomUUID().toString().substring(0, 8);
        return new OtpResponseDTO(Arrays.asList(indexKey, "0", "", ""));
    }
    @PostMapping("/register")
    public ResponseEntity<UserJWTController.JWTToken> registerAccount(@RequestBody CustomerRegisterDTO registDTO) {
        String jwt = customerService.registerCustomer(registDTO);
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.add(JWTFilter.AUTHORIZATION_HEADER, "Bearer" + jwt);
        return new ResponseEntity<>(new UserJWTController.JWTToken(jwt), httpHeaders, HttpStatus.OK);
    }
}