main
vue 143 lines 3.82 KB
Raw
1 <template>
2 <n-modal
3 v-model:show="isOpen"
4 preset="card"
5 display-directive="show"
6 class="w-[90vw]! max-w-133!"
7 segmented
8 title="Change Password"
9 >
10 <n-form :disabled="loading">
11 <n-form-item path="currentPassword" label="Current Password" required>
12 <n-input
13 v-model:value="currentPassword"
14 type="password"
15 size="large"
16 show-password-on="click"
17 :input-props="{ autocomplete: 'new-password' }"
18 placeholder="Enter current password"
19 />
20 </n-form-item>
21 <n-form-item path="newPassword" label="New Password" required>
22 <n-input
23 v-model:value="newPassword"
24 type="password"
25 size="large"
26 show-password-on="click"
27 :input-props="{ autocomplete: 'new-password' }"
28 placeholder="Enter new password"
29 />
30 </n-form-item>
31 <n-form-item path="confirmPassword" label="Confirm Password" required>
32 <n-input
33 v-model:value="confirmPassword"
34 type="password"
35 show-password-on="click"
36 placeholder="Enter confirm password"
37 :input-props="{ autocomplete: 'new-password' }"
38 size="large"
39 :disabled="!newPassword"
40 >
41 <template #prefix>
42 <Icon name="carbon:locked" class="text-tertiary! mr-1" />
43 </template>
44 </n-input>
45 </n-form-item>
46 </n-form>
47
48 <template #footer>
49 <div class="flex w-full justify-end gap-4">
50 <n-button secondary :disabled="loading" @click="closeModal">Cancel</n-button>
51 <n-button type="primary" :loading :disabled="!isValid" @click="handleSubmit()">
52 Change Password
53 </n-button>
54 </div>
55 </template>
56 </n-modal>
57 </template>
58
59 <script setup lang="ts">
60 import type { ApiError } from "@/types/common"
61 import { NButton, NForm, NFormItem, NInput, NModal, useMessage } from "naive-ui"
62 import { computed, ref } from "vue"
63 import Api from "@/api"
64 import Icon from "@/components/common/Icon.vue"
65 import { useAuthStore } from "@/stores/auth"
66 import { getApiErrorMessage } from "@/utils"
67
68 const emit = defineEmits<{
69 (e: "close"): void
70 (e: "success"): void
71 }>()
72
73 const isOpen = defineModel<boolean>("open", { default: false })
74 const loading = defineModel<boolean>("loading", { default: false })
75
76 const message = useMessage()
77 const authStore = useAuthStore()
78 const currentPassword = ref("")
79 const newPassword = ref("")
80 const confirmPassword = ref("")
81
82 const isValid = computed(() => {
83 return (
84 currentPassword.value.length > 0 && newPassword.value.length >= 8 && newPassword.value === confirmPassword.value
85 )
86 })
87
88 function closeModal() {
89 if (!loading.value) {
90 resetForm()
91 isOpen.value = false
92 emit("close")
93 }
94 }
95
96 function resetForm() {
97 currentPassword.value = ""
98 newPassword.value = ""
99 confirmPassword.value = ""
100 }
101
102 async function handleSubmit() {
103 // Validate passwords match
104 if (newPassword.value !== confirmPassword.value) {
105 message.error("New passwords do not match")
106 return
107 }
108
109 // Mirror the backend complexity requirements so the user gets a clear message
110 // instead of an opaque 422 from the API.
111 if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&#])[A-Za-z\d@$!%*?&#]{8,72}$/.test(newPassword.value)) {
112 message.error(
113 "Password must be 8-72 characters and include an uppercase letter, a lowercase letter, a number, and a special character (@$!%*?&#)"
114 )
115 return
116 }
117
118 const username = authStore.userName
119 if (!username) {
120 message.error("Authentication required. Please log in again.")
121 return
122 }
123
124 loading.value = true
125
126 try {
127 // Token is injected by the HTTP client from the auth store.
128 const response = await Api.auth.resetPassword(username, newPassword.value, currentPassword.value)
129
130 if (response.data.success) {
131 message.success("Password changed successfully!")
132 emit("success")
133 closeModal()
134 } else {
135 message.error(response.data.message || "Failed to change password")
136 }
137 } catch (err) {
138 message.error(getApiErrorMessage(err as ApiError))
139 } finally {
140 loading.value = false
141 }
142 }
143 </script>