Skip to content
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4577423
feat(user-page): add user-MFA bulk setting modals (#6033)
piggggggggy Jul 22, 2025
9607cbf
feat(mfa-disable): create New `modal-controller` func component & sep…
piggggggggy Jul 23, 2025
4736aa5
chore: separate and apply mfa-setting form components (#6039)
piggggggggy Jul 23, 2025
605c018
fix: fix api endpoint blocking by version
skdud4659 Jul 23, 2025
3a4ab99
chore: removed unnecessary code
skdud4659 Jul 23, 2025
0a34a33
fix: fix api endpoint blocking by version (#6042)
skdud4659 Jul 23, 2025
ead3a11
chore: version 2.0.dev378
admin-cloudforet Jul 23, 2025
dba9a0f
chore: merge master into develop after 2.0.dev378 version tagging
admin-cloudforet Jul 23, 2025
abc338b
feat(mfa): apply changed user-mfa flow and refactor mfa component (#6…
piggggggggy Jul 23, 2025
cb76f01
feat(mfa-enforced): create mfa-enforced scenario (after sign-in) (#6047)
piggggggggy Jul 23, 2025
fba4f65
refactor(user-page): refactor mfa-related models (user-page) (#6048)
piggggggggy Jul 23, 2025
0a519ed
chore: solve empty case
piggggggggy Jul 24, 2025
41cfc70
chore: MFA QA (#6058)
piggggggggy Jul 28, 2025
b94e7f6
chore: add missing scenario
piggggggggy Jul 28, 2025
537f93b
fix: fix tag column bug in user page (#6061)
yuda110 Jul 30, 2025
a8bc088
fix(action): update file path in storybook-deploy action (#6077)
sulmoJ Aug 1, 2025
7e142e6
chore: mfa minor QAs (#6085)
piggggggggy Aug 4, 2025
1390345
feat: apply password validation at user add/update form (#6114)
skdud4659 Aug 11, 2025
b3676e5
Merge branch 'develop' into feature-mfa-enhancement
piggggggggy Aug 11, 2025
3322d8f
feat(mfa): implementation new mfa feature
piggggggggy Aug 11, 2025
6a0a6e0
Merge branch 'develop' into feature-query-integration
seungyeoneeee Aug 14, 2025
f68961a
refactor: fix MFA related codes as vue query updated
seungyeoneeee Aug 14, 2025
8e60970
fix: bugs about MFA updated at mypage and admin user page
seungyeoneeee Aug 14, 2025
733cb1d
fix: major bug of not active of bulk MFA setting
seungyeoneeee Aug 14, 2025
067be56
fix: bug of selected local users getting info from false store
seungyeoneeee Aug 18, 2025
b20412e
chore: fix lint error (not exist on. type)
seungyeoneeee Aug 18, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/dispatch_storybook_release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ jobs:
aws-region: ap-northeast-2

- name: Deploy to s3
run: aws s3 sync apps/storybook/storybook-static/ s3://${{ secrets.STORYBOOK_S3_BUCKET }} --delete
run: aws s3 sync apps/mirinae-storybook/storybook-static/ s3://${{ secrets.STORYBOOK_S3_BUCKET }} --delete

- name: Invalidate CloudFront Cache
run: aws cloudfront create-invalidation --distribution-id ${{ secrets.CDN_DISTRIBUTION_ID }} --paths "/*"
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "web",
"version": "2.0.0-dev377",
"version": "2.0.0-dev378",
"private": true,
"description": "Cloudforet Console Web Application",
"author": "Cloudforet",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { SpaceConnector } from '@cloudforet/core-lib/space-connector';

import type { ListResponse } from '@/api-clients/_common/schema/api-verbs/list';
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 { WorkspaceGroupModel } from '@/api-clients/identity/workspace-group/schema/model';
import type { WorkspaceModel } from '@/api-clients/identity/workspace/schema/model';
import type { UserModel } from '@/api-clients/identity/user/schema/model';



export const useUserProfileApi = () => {
Expand All @@ -19,12 +19,11 @@ 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>,
getWorkspaces: SpaceConnector.clientV2.identity.userProfile.getWorkspaces<UserProfileGetWorkspacesParameters, ListResponse<WorkspaceModel>>,
getWorkspaceGroups: SpaceConnector.clientV2.identity.userProfile.getWorkspaceGroups<undefined, ListResponse<WorkspaceGroupModel>>,

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>,
};

return {
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;
Copy link
Preview

Copilot AI Aug 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Magic number for cooldown time should be extracted to a configuration file or constant. Consider making this configurable based on security requirements.

Copilot uses AI. Check for mistakes.


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