Skip to content

Commit 555b69b

Browse files
committed
add passwordchangerequired in listUsers API response, it will be used in UI to render reset password form
1 parent ea92759 commit 555b69b

File tree

7 files changed

+132
-55
lines changed

7 files changed

+132
-55
lines changed

api/src/main/java/org/apache/cloudstack/api/response/UserResponse.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,10 @@ public class UserResponse extends BaseResponse implements SetResourceIconRespons
132132
@Param(description = "whether api key access is Enabled, Disabled or set to Inherit (it inherits the value from the parent)", since = "4.20.1.0")
133133
ApiConstants.ApiKeyAccess apiKeyAccess;
134134

135+
@SerializedName(value = ApiConstants.PASSWORD_CHANGE_REQUIRED)
136+
@Param(description = "Is User required to change password on next login.", since = "4.23.0")
137+
private Boolean passwordChangeRequired;
138+
135139
@Override
136140
public String getObjectId() {
137141
return this.getId();
@@ -317,4 +321,12 @@ public void set2FAmandated(Boolean is2FAmandated) {
317321
public void setApiKeyAccess(Boolean apiKeyAccess) {
318322
this.apiKeyAccess = ApiConstants.ApiKeyAccess.fromBoolean(apiKeyAccess);
319323
}
324+
325+
public Boolean isPasswordChangeRequired() {
326+
return passwordChangeRequired;
327+
}
328+
329+
public void setPasswordChangeRequired(Boolean passwordChangeRequired) {
330+
this.passwordChangeRequired = passwordChangeRequired;
331+
}
320332
}

engine/schema/src/main/resources/META-INF/db/views/cloud.user_view.sql

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ select
5353
async_job.uuid job_uuid,
5454
async_job.job_status job_status,
5555
async_job.account_id job_account_id,
56-
user.is_user_2fa_enabled is_user_2fa_enabled
56+
user.is_user_2fa_enabled is_user_2fa_enabled,
57+
`user_details`.`value` AS `password_change_required`
5758
from
5859
`cloud`.`user`
5960
inner join
@@ -63,4 +64,7 @@ from
6364
left join
6465
`cloud`.`async_job` ON async_job.instance_id = user.id
6566
and async_job.instance_type = 'User'
66-
and async_job.job_status = 0;
67+
and async_job.job_status = 0
68+
left join
69+
`cloud`.`user_details` AS `user_details` ON `user_details`.`user_id` = `user`.`id`
70+
and `user_details`.`name` = 'PasswordChangeRequired';

server/src/main/java/com/cloud/api/query/dao/UserAccountJoinDaoImpl.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ public UserResponse newUserResponse(ResponseView view, UserAccountJoinVO usr) {
7373
userResponse.setSecretKey(usr.getSecretKey());
7474
userResponse.setIsDefault(usr.isDefault());
7575
userResponse.set2FAenabled(usr.isUser2faEnabled());
76+
userResponse.setPasswordChangeRequired(usr.isPasswordChangeRequired());
7677
long domainId = usr.getDomainId();
7778
boolean is2FAmandated = Boolean.TRUE.equals(AccountManagerImpl.enableUserTwoFactorAuthentication.valueIn(domainId)) && Boolean.TRUE.equals(AccountManagerImpl.mandateUserTwoFactorAuthentication.valueIn(domainId));
7879
userResponse.set2FAmandated(is2FAmandated);

server/src/main/java/com/cloud/api/query/vo/UserAccountJoinVO.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ public class UserAccountJoinVO extends BaseViewVO implements InternalIdentity, I
136136
@Column(name = "api_key_access")
137137
Boolean apiKeyAccess;
138138

139+
@Column(name = "password_change_required")
140+
Boolean passwordChangeRequired;
141+
139142
public UserAccountJoinVO() {
140143
}
141144

@@ -288,4 +291,8 @@ public boolean isUser2faEnabled() {
288291
public Boolean getApiKeyAccess() {
289292
return apiKeyAccess;
290293
}
294+
295+
public Boolean isPasswordChangeRequired() {
296+
return passwordChangeRequired;
297+
}
291298
}

ui/public/locales/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3124,6 +3124,7 @@
31243124
"message.change.offering.for.volume.failed": "Change offering for the volume failed",
31253125
"message.change.offering.for.volume.processing": "Changing offering for the volume...",
31263126
"message.change.password": "Please change your password.",
3127+
"message.change.password.required": "You are required to change your password.",
31273128
"message.change.scope.failed": "Scope change failed",
31283129
"message.change.scope.processing": "Scope change in progress",
31293130
"message.change.service.offering.sharedfs.failed": "Failed to change service offering for the Shared FileSystem.",
@@ -3673,6 +3674,7 @@
36733674
"message.please.confirm.remove.user.data": "Please confirm that you want to remove this User Data",
36743675
"message.please.enter.valid.value": "Please enter a valid value.",
36753676
"message.please.enter.value": "Please enter values.",
3677+
"message.please.login.new.password": "Please log in again with your new password",
36763678
"message.please.wait.while.autoscale.vmgroup.is.being.created": "Please wait while your AutoScaling Group is being created; this may take a while...",
36773679
"message.please.wait.while.zone.is.being.created": "Please wait while your Zone is being created; this may take a while...",
36783680
"message.pod.dedicated": "Pod dedicated.",

ui/src/store/modules/user.js

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -332,13 +332,6 @@ const user = {
332332

333333
GetInfo ({ commit }, switchDomain) {
334334
return new Promise((resolve, reject) => {
335-
// A. Restore Lock State
336-
const loginSource = vueProps.$localStorage.get(LOGIN_SOURCE)
337-
const isPwdChangeRequired = vueProps.$localStorage.get(PASSWORD_CHANGE_REQUIRED) === 'true'
338-
// Only lock if source was password
339-
const isLocked = (loginSource === 'password' && isPwdChangeRequired)
340-
commit('SET_PASSWORD_CHANGE_REQUIRED', isLocked)
341-
342335
const cachedApis = switchDomain ? {} : vueProps.$localStorage.get(APIS, {})
343336
const cachedZones = vueProps.$localStorage.get(ZONES, [])
344337
const cachedTimezoneOffset = vueProps.$localStorage.get(TIMEZONE_OFFSET, 0.0)
@@ -355,28 +348,22 @@ const user = {
355348
commit('SET_DARK_MODE', darkMode)
356349
commit('SET_LATEST_VERSION', latestVersion)
357350

358-
if (isLocked) {
359-
console.log('Password change required. Fetching user info only.')
360-
361-
// We MUST fetch listUsers so the UI Header (Avatar/Name) works
351+
// This block is to enforce password change for first time login after admin resets password
352+
const loginSource = vueProps.$localStorage.get(LOGIN_SOURCE)
353+
const isPwdChangeRequired = vueProps.$localStorage.get(PASSWORD_CHANGE_REQUIRED) === 'true'
354+
const isPwdChangeRequiredForLogin = (loginSource === 'password' && isPwdChangeRequired)
355+
commit('SET_PASSWORD_CHANGE_REQUIRED', isPwdChangeRequiredForLogin)
356+
if (isPwdChangeRequiredForLogin) {
362357
getAPI('listUsers', { id: Cookies.get('userid') }).then(response => {
363358
const result = response.listusersresponse.user[0]
364-
365-
// Populate State
366359
commit('SET_INFO', result)
367360
commit('SET_NAME', result.firstname + ' ' + result.lastname)
368361
if (result.icon?.base64image) commit('SET_AVATAR', result.icon.base64image)
369-
370-
// DO NOT fetch Apis
371-
// DO NOT fetch Zones
372-
// DO NOT call GenerateRoutes
373-
374-
resolve({}) // Resolve empty to signal permission.js to proceed
362+
resolve({})
375363
}).catch(error => {
376364
reject(error)
377365
})
378-
379-
return // Stop execution
366+
return
380367
}
381368

382369
if (hasAuth) {

ui/src/views/iam/ForceChangePassword.vue

Lines changed: 96 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,38 @@
2424
<div style="text-align: center; font-size: 18px; font-weight: bold;">
2525
{{ $t('label.action.change.password') }}
2626
</div>
27-
<div style="text-align: center; font-size: 14px; color: #666; margin-top: 5px;">
28-
{{ $t('message.change.password') }}
27+
<div v-if="!isSubmitted" style="text-align: center; font-size: 14px; color: #666; margin-top: 5px;">
28+
{{ $t('message.change.password.required') }}
2929
</div>
3030
</template>
31+
<a-spin :spinning="loading">
32+
<div v-if="isSubmitted" class="success-state">
33+
<div class="success-icon">✓</div>
34+
<div class="success-text">
35+
{{ $t('message.success.change.password') }}
36+
</div>
37+
<div class="success-subtext">
38+
{{ $t('message.please.login.new.password') }}
39+
</div>
40+
<a-button
41+
type="primary"
42+
size="large"
43+
block
44+
@click="handleLogout"
45+
style="margin-top: 20px;"
46+
>
47+
{{ $t('label.login') }}
48+
</a-button>
49+
</div>
3150

3251
<a-form
52+
v-else
3353
:ref="formRef"
3454
:model="form"
3555
:rules="rules"
3656
layout="vertical"
3757
@finish="handleSubmit"
58+
v-ctrl-enter="handleSubmit"
3859
>
3960
<a-form-item name="currentpassword" :label="$t('label.currentpassword')">
4061
<a-input-password
@@ -77,6 +98,7 @@
7798
<a @click="handleLogout">{{ $t('label.logout') }}</a>
7899
</div>
79100
</a-form>
101+
</a-spin>
80102
</a-card>
81103
</div>
82104
</div>
@@ -87,33 +109,34 @@
87109
import { ref, reactive, toRaw } from 'vue'
88110
import { postAPI } from '@/api'
89111
import Cookies from 'js-cookie'
112+
import { PASSWORD_CHANGE_REQUIRED } from '@/store/mutation-types'
90113
91114
export default {
92115
name: 'ForceChangePassword',
93116
data () {
94117
return {
95-
loading: false
118+
loading: false,
119+
isSubmitted: false
96120
}
97121
},
98-
beforeCreate () {
99-
this.apiParams = this.$getApiParams('updateUser')
100-
},
101122
created () {
102-
this.initForm()
123+
this.formRef = ref()
124+
this.form = reactive({})
125+
this.isPasswordChangeRequired()
103126
},
104-
methods: {
105-
initForm () {
106-
this.formRef = ref()
107-
this.form = reactive({})
108-
this.rules = reactive({
109-
currentpassword: [{ required: true, message: this.$t('message.error.current.password') }],
110-
password: [{ required: true, message: this.$t('message.error.new.password') }],
127+
computed: {
128+
rules () {
129+
return {
130+
currentpassword: [{ required: true, message: this.$t('message.error.current.password') || 'Please enter current password' }],
131+
password: [{ required: true, message: this.$t('message.error.new.password') || 'Please enter new password' }],
111132
confirmpassword: [
112-
{ required: true, message: this.$t('message.error.confirm.password') },
113-
{ validator: this.validateTwoPassword }
133+
{ required: true, message: this.$t('message.error.confirm.password') || 'Please confirm new password' },
134+
{ validator: this.validateTwoPassword, trigger: 'change' }
114135
]
115-
})
116-
},
136+
}
137+
}
138+
},
139+
methods: {
117140
async validateTwoPassword (rule, value) {
118141
if (!value || value.length === 0) {
119142
return Promise.resolve()
@@ -130,9 +153,6 @@ export default {
130153
return Promise.resolve()
131154
}
132155
},
133-
isValidValueForKey (obj, key) {
134-
return key in obj && obj[key] != null
135-
},
136156
handleSubmit (e) {
137157
e.preventDefault()
138158
if (this.loading) return
@@ -147,9 +167,8 @@ export default {
147167
currentpassword: values.currentpassword
148168
}
149169
postAPI('updateUser', params).then(() => {
150-
this.$message.success(this.$t('message.success.change.password'), 5)
151-
console.log('Password changed successfully.')
152-
this.handleLogout()
170+
this.$message.success(this.$t('message.success.change.password'))
171+
this.isSubmitted = true
153172
}).catch(error => {
154173
console.error(error)
155174
this.$notification.error({
@@ -164,17 +183,39 @@ export default {
164183
console.log('Validation failed:', error)
165184
})
166185
},
167-
handleLogout () {
168-
this.$store.dispatch('Logout').then(() => {
186+
async handleLogout () {
187+
try {
188+
await this.$store.dispatch('Logout')
189+
} catch (e) {
190+
console.error('Logout failed:', e)
191+
} finally {
169192
Cookies.remove('userid')
170193
Cookies.remove('token')
194+
this.$localStorage.remove(PASSWORD_CHANGE_REQUIRED)
171195
this.$router.replace({ path: '/user/login' })
172-
}).catch(err => {
173-
this.$message.error({
174-
title: 'Failed to Logout',
175-
description: err.message
176-
})
177-
})
196+
}
197+
},
198+
async isPasswordChangeRequired () {
199+
try {
200+
this.loading = true
201+
const user = await this.getUserInfo()
202+
if (user && !user.passwordchangerequired) {
203+
this.isSubmitted = true
204+
this.$router.replace({ path: '/user/login' })
205+
}
206+
} catch (e) {
207+
console.error('Failed to resolve user info:', e)
208+
} finally {
209+
this.loading = false
210+
}
211+
},
212+
async getUserInfo () {
213+
const userInfo = this.$store.getters.userInfo
214+
if (userInfo && userInfo.id) {
215+
return userInfo
216+
}
217+
await this.$store.dispatch('GetInfo')
218+
return this.$store.getters.userInfo
178219
}
179220
}
180221
}
@@ -217,4 +258,27 @@ export default {
217258
}
218259
}
219260
}
261+
262+
.success-state {
263+
text-align: center;
264+
padding: 20px 0;
265+
266+
.success-icon {
267+
font-size: 48px;
268+
color: #52c41a;
269+
margin-bottom: 16px;
270+
}
271+
272+
.success-text {
273+
font-size: 20px;
274+
font-weight: 500;
275+
color: #333;
276+
margin-bottom: 8px;
277+
}
278+
279+
.success-subtext {
280+
font-size: 14px;
281+
color: #666;
282+
}
283+
}
220284
</style>

0 commit comments

Comments
 (0)