Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import { SpaceConnector } from '@cloudforet/core-lib/space-connector';

import type { UserProfileConfirmEmailParameters } from '@/api-clients/identity/user-profile/schema/api-verbs/confirm-email';
import type { UserProfileConfirmMfaParameters } from '@/api-clients/identity/user-profile/schema/api-verbs/confirm-mfa';
import type { UserProfileDisableMfaParameters } from '@/api-clients/identity/user-profile/schema/api-verbs/disable-mfa';
import type { UserProfileEnableMfaParameters } from '@/api-clients/identity/user-profile/schema/api-verbs/enable-mfa';
import type { UserProfileGetWorkspacesParameters } from '@/api-clients/identity/user-profile/schema/api-verbs/get-workspaces';
import type { UserProfileResetPasswordParameters } from '@/api-clients/identity/user-profile/schema/api-verbs/reset-password';
import type { UserProfileUpdateParameters } from '@/api-clients/identity/user-profile/schema/api-verbs/update';
import type { UserProfileUpdatePasswordParameters } from '@/api-clients/identity/user-profile/schema/api-verbs/update-password';
import type { UserProfileVerifyEmailParameters } from '@/api-clients/identity/user-profile/schema/api-verbs/verify-email';
import type { UserModel } from '@/api-clients/identity/user/schema/model';



export const useUserProfileApi = () => {
Expand All @@ -16,9 +19,10 @@ export const useUserProfileApi = () => {
updatePassword: SpaceConnector.clientV2.identity.userProfile.updatePassword<UserProfileUpdatePasswordParameters, any>,
resetPassword: SpaceConnector.clientV2.identity.userProfile.resetPassword<UserProfileResetPasswordParameters, any>,
verifyEmail: SpaceConnector.clientV2.identity.userProfile.verifyEmail<UserProfileVerifyEmailParameters, any>,
confirmEmail: SpaceConnector.clientV2.identity.userProfile.confirmEmail<UserProfileConfirmEmailParameters, any>,
enableMfa: SpaceConnector.clientV2.identity.userProfile.enableMfa<UserProfileEnableMfaParameters, any>,
confirmMfa: SpaceConnector.clientV2.identity.userProfile.confirmMfa<UserProfileConfirmMfaParameters, any>,
confirmEmail: SpaceConnector.clientV2.identity.userProfile.confirmEmail<UserProfileConfirmEmailParameters, UserModel>,
disableMfa: SpaceConnector.clientV2.identity.userProfile.disableMfa<UserProfileDisableMfaParameters, UserModel>,
enableMfa: SpaceConnector.clientV2.identity.userProfile.enableMfa<UserProfileEnableMfaParameters, UserModel>,
confirmMfa: SpaceConnector.clientV2.identity.userProfile.confirmMfa<UserProfileConfirmMfaParameters, UserModel>,
getWorkspaces: SpaceConnector.clientV2.identity.userProfile.getWorkspaces<UserProfileGetWorkspacesParameters, any>,
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/* eslint-disable @typescript-eslint/no-empty-interface */
export interface UserProfileDisableMfaParameters {
// No parameters
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,8 @@ export const MULTI_FACTOR_AUTH_TYPE = {
OTP: 'OTP',
EMAIL: 'EMAIL',
} as const;

export const MFA_STATE = {
ENABLED: 'ENABLED',
DISABLED: 'DISABLED',
} as const;
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { MULTI_FACTOR_AUTH_TYPE } from '@/api-clients/identity/user-profile/schema/constant';
import type { MFA_STATE, MULTI_FACTOR_AUTH_TYPE } from '@/api-clients/identity/user-profile/schema/constant';

export type MultiFactorAuthType = typeof MULTI_FACTOR_AUTH_TYPE[keyof typeof MULTI_FACTOR_AUTH_TYPE];
export type MfaState = typeof MFA_STATE[keyof typeof MFA_STATE];
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Tags } from '@/api-clients/_common/schema/model';
import type { MfaState, MultiFactorAuthType } from '@/api-clients/identity/user-profile/schema/type';
import type { AuthType } from '@/api-clients/identity/user/schema/type';


Expand All @@ -12,4 +13,6 @@ export interface UserCreateParameters {
timezone?: string;
tags?: Tags;
reset_password?: boolean;
enforce_mfa_state?: MfaState;
enforce_mfa_type?: MultiFactorAuthType; // only when enforce_mfa_state is ENABLED, this field is required
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Tags } from '@/api-clients/_common/schema/model';
import type { MfaState, MultiFactorAuthType } from '@/api-clients/identity/user-profile/schema/type';


export interface UserUpdateParameters {
Expand All @@ -10,4 +11,6 @@ export interface UserUpdateParameters {
timezone?: string;
tags?: Tags;
reset_password?: boolean;
enforce_mfa_state?: MfaState;
enforce_mfa_type?: MultiFactorAuthType; // only when enforce_mfa_state is ENABLED, this field is required
}
10 changes: 6 additions & 4 deletions apps/web/src/api-clients/identity/user/schema/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface UserModel {
auth_type: AuthType; // backend
role_type: RoleType;
role_id?: string;
mfa: UserMfa;
mfa?: UserMfa;
language: string;
timezone: string;
api_key_count: number;
Expand All @@ -26,11 +26,13 @@ export interface UserModel {
}

export interface UserMfa {
state: UserMfaState,
mfa_type: MultiFactorAuthType,
options: {
state?: UserMfaState,
mfa_type?: MultiFactorAuthType,
options?: {
enforce?: boolean, // if true, mfa_type is required
email?: string,
user_secret_id?: string,
otp_qrcode_uri?: string, // response from 'enable-mfa' verb only
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Tags } from '@/api-clients/_common/schema/model';
import type { MfaState, MultiFactorAuthType } from '@/api-clients/identity/user-profile/schema/type';
import type { AuthType } from '@/api-clients/identity/user/schema/type';

export interface WorkspaceUserCreateParameters {
Expand All @@ -11,5 +12,7 @@ export interface WorkspaceUserCreateParameters {
timezone?: string;
tags?: Tags;
reset_password?: boolean;
enforce_mfa_state?: MfaState;
enforce_mfa_type?: MultiFactorAuthType;
role_id: string;
}
4 changes: 3 additions & 1 deletion apps/web/src/common/components/info-tooltip/InfoTooltip.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<script setup lang="ts">
import type { TranslateResult } from 'vue-i18n';

import { PTooltip, PI } from '@cloudforet/mirinae';
import type { TooltipPosition } from '@cloudforet/mirinae/types/data-display/tooltips/type';

interface Props {
tooltipContents: string;
tooltipContents: string|TranslateResult;
tooltipPosition?: TooltipPosition;
width?: string;
height?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import { postUserProfileDisableMfa, postEnableMfa } from '@/lib/helper/multi-fac

import { useProxyValue } from '@/common/composables/proxy-state';

const RE_SEND_CODE_COOLDOWN_TIME = 10000;

interface Props {
isDisabledModal: boolean
isReSyncModal: boolean
isSentCode: boolean
isDisabledModal?: boolean
isReSyncModal?: boolean
isSentCode?: boolean
}

const props = withDefaults(defineProps<Props>(), {
Expand All @@ -32,20 +34,38 @@ const storeState = reactive({
const state = reactive({
isCollapsed: true,
proxyIsSentCode: useProxyValue('is-sent-code', props, emit),
loading: false,
cooldown: false,
});

const startCooldown = () => {
state.cooldown = true;
setTimeout(() => {
state.cooldown = false;
}, RE_SEND_CODE_COOLDOWN_TIME);
};

const handleClickSendEmailButton = async () => {
if (props.isDisabledModal || props.isReSyncModal) {
await postUserProfileDisableMfa();
} else {
await postEnableMfa({
mfa_type: MULTI_FACTOR_AUTH_TYPE.EMAIL,
options: {
email: storeState.email,
},
});
if (state.cooldown) return;

state.loading = true;

try {
if (props.isDisabledModal || props.isReSyncModal) {
await postUserProfileDisableMfa();
} else {
await postEnableMfa({
mfa_type: MULTI_FACTOR_AUTH_TYPE.EMAIL,
options: {
email: storeState.email,
},
});
}
state.proxyIsSentCode = true;
startCooldown();
} finally {
state.loading = false;
}
state.proxyIsSentCode = true;
};
</script>

Expand All @@ -62,7 +82,7 @@ const handleClickSendEmailButton = async () => {
{{ $t('COMMON.MFA_MODAL.COLLAPSE_DESC') }}
<p-text-button class="send-code-button"
style-type="highlight"
:disabled="(props.isDisabledModal || props.isReSyncModal) ? !storeState.email : !props.isSentCode"
:disabled="((props.isDisabledModal || props.isReSyncModal) ? !storeState.email : !props.isSentCode) || state.loading || state.cooldown"
@click.prevent="handleClickSendEmailButton"
>
<span class="emphasis">{{ $t('COMMON.MFA_MODAL.SEND_NEW_CODE') }}</span>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,22 @@ import { emailValidator } from '@/lib/helper/user-validation-helper';
import { useFormValidator } from '@/common/composables/form-validator';
import { useProxyValue } from '@/common/composables/proxy-state';

import { useMultiFactorAuthStore } from '@/services/my-page/stores/multi-factor-auth-store';

interface Props {
isSentCode?: boolean
isSentCode?: boolean;
isForm?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
isSentCode: false,
isForm: false,
});

const multiFactorAuthStore = useMultiFactorAuthStore();
const multiFactorAuthState = multiFactorAuthStore.state;
const userStore = useUserStore();

const emit = defineEmits<{(e: 'update:is-sent-code'): void }>();

const storeState = reactive({
email: computed<string|undefined>(() => userStore.state.mfa?.options?.email),
isFormModal: computed(() => multiFactorAuthState.modalType === 'FORM'),
});
const state = reactive({
loading: false,
Expand All @@ -58,7 +55,7 @@ const {
const handleClickSendCodeButton = async () => {
state.loading = true;
try {
if (storeState.isFormModal) {
if (props.isForm) {
await postEnableMfa({
mfa_type: MULTI_FACTOR_AUTH_TYPE.EMAIL,
options: {
Expand All @@ -82,9 +79,9 @@ const handleClickSendCodeButton = async () => {

<template>
<div class="email-info-wrapper"
:class="{'form-modal': storeState.isFormModal}"
:class="{'form-modal': props.isForm}"
>
<div v-if="storeState.isFormModal"
<div v-if="props.isForm"
class="email-form-wrapper"
>
<p-field-group
Expand Down
40 changes: 40 additions & 0 deletions apps/web/src/common/components/mfa/components/OTPForm.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<script setup lang="ts">
import OTPQRInfo from '@/common/components/mfa/components/OTPQRInfo.vue';
import VerificationCodeForm from '@/common/components/mfa/components/VerificationCodeForm.vue';
import { useProxyValue } from '@/common/composables/proxy-state';

interface Props {
verificationCode: string;
verificationCodeInvalid: boolean;
invalidText: string;
}

interface Emits {
(e: 'update:verification-code', value: string): void;
(e: 'update:verification-code-invalid', value: boolean): void;
}

const props = withDefaults(defineProps<Props>(), {
verificationCode: '',
verificationCodeInvalid: false,
invalidText: '',
});

const emit = defineEmits<Emits>();

/* State */
const verificationCode = useProxyValue<string>('verificationCode', props, emit);
const verificationCodeInvalid = useProxyValue<boolean>('verificationCodeInvalid', props, emit);


</script>

<template>
<div class="o-t-p-form">
<o-t-p-q-r-info />
<verification-code-form :invalid.sync="verificationCodeInvalid"
:code-value.sync="verificationCode"
:invalid-text="props.invalidText"
/>
</div>
</template>
Loading
Loading