@cryptotaxi247 / CoPilot / commits / 5799dbfa

Add change password functionality with modal in customer portal (#536)

taylor_socfortress committed Nov 30, 2025 at 14:39 UTC 5799dbfa346a236a696eed7adfa3b11c7e4f3e7b
3 files changed +248 -1
backend/app/auth/routes/auth.py
+1 -1
@@ -332,7 +332,7 @@ async def reset_password_via_username(
332 "/reset-password/me",
333 status_code=200,
334 description="Reset user's password",
335 - dependencies=[Security(AuthHandler().require_any_scope("analyst", "admin"))],
335 + dependencies=[Security(AuthHandler().require_any_scope("analyst", "admin", "customer_user"))],
336 )
337 async def reset_password_me(
338 request: PasswordReset,
customer_portal/src/components/ChangePasswordModal.vue new
+225
@@ -0,0 +1,225 @@
1 +<template>
2 + <div v-if="isOpen" class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
3 + <div class="w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
4 + <div class="mb-4 flex items-center justify-between">
5 + <h3 class="text-xl font-bold text-gray-900">Change Password</h3>
6 + <button @click="closeModal" class="text-gray-400 hover:text-gray-600">
7 + <svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
8 + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
9 + </svg>
10 + </button>
11 + </div>
12 +
13 + <form @submit.prevent="handleSubmit" class="space-y-4">
14 + <!-- Error/Success Messages -->
15 + <div v-if="error" class="rounded-md border border-red-200 bg-red-50 p-3">
16 + <div class="text-sm text-red-700">{{ error }}</div>
17 + </div>
18 +
19 + <div v-if="success" class="rounded-md border border-green-200 bg-green-50 p-3">
20 + <div class="text-sm text-green-700">{{ success }}</div>
21 + </div>
22 +
23 + <!-- Current Password -->
24 + <div>
25 + <label for="currentPassword" class="mb-2 block text-sm font-medium text-gray-700">
26 + Current Password
27 + </label>
28 + <input
29 + id="currentPassword"
30 + v-model="currentPassword"
31 + type="password"
32 + required
33 + autocomplete="current-password"
34 + class="block w-full rounded-md border border-gray-300 px-3 py-2 text-base placeholder-gray-400 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
35 + placeholder="Enter current password"
36 + />
37 + </div>
38 +
39 + <!-- New Password -->
40 + <div>
41 + <label for="newPassword" class="mb-2 block text-sm font-medium text-gray-700">
42 + New Password
43 + </label>
44 + <input
45 + id="newPassword"
46 + v-model="newPassword"
47 + type="password"
48 + required
49 + autocomplete="new-password"
50 + class="block w-full rounded-md border border-gray-300 px-3 py-2 text-base placeholder-gray-400 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
51 + placeholder="Enter new password"
52 + />
53 + </div>
54 +
55 + <!-- Confirm New Password -->
56 + <div>
57 + <label for="confirmPassword" class="mb-2 block text-sm font-medium text-gray-700">
58 + Confirm New Password
59 + </label>
60 + <input
61 + id="confirmPassword"
62 + v-model="confirmPassword"
63 + type="password"
64 + required
65 + autocomplete="new-password"
66 + class="block w-full rounded-md border border-gray-300 px-3 py-2 text-base placeholder-gray-400 shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-2 focus:ring-indigo-500"
67 + placeholder="Confirm new password"
68 + />
69 + </div>
70 +
71 + <!-- Buttons -->
72 + <div class="flex justify-end gap-3 pt-4">
73 + <button
74 + type="button"
75 + @click="closeModal"
76 + :disabled="loading"
77 + class="rounded-md border border-gray-300 bg-white px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
78 + >
79 + Cancel
80 + </button>
81 + <button
82 + type="submit"
83 + :disabled="loading || !isFormValid"
84 + class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
85 + >
86 + <span v-if="loading" class="flex items-center">
87 + <svg
88 + class="mr-2 -ml-1 h-4 w-4 animate-spin text-white"
89 + xmlns="http://www.w3.org/2000/svg"
90 + fill="none"
91 + viewBox="0 0 24 24"
92 + >
93 + <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
94 + <path
95 + class="opacity-75"
96 + fill="currentColor"
97 + d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
98 + ></path>
99 + </svg>
100 + Changing...
101 + </span>
102 + <span v-else>Change Password</span>
103 + </button>
104 + </div>
105 + </form>
106 + </div>
107 + </div>
108 +</template>
109 +
110 +<script setup lang="ts">
111 +import { ref, computed } from "vue"
112 +import axios from "axios"
113 +
114 +const props = defineProps<{
115 + isOpen: boolean
116 +}>()
117 +
118 +const emit = defineEmits<{
119 + (e: "close"): void
120 + (e: "success"): void
121 +}>()
122 +
123 +const currentPassword = ref("")
124 +const newPassword = ref("")
125 +const confirmPassword = ref("")
126 +const loading = ref(false)
127 +const error = ref("")
128 +const success = ref("")
129 +
130 +const isFormValid = computed(() => {
131 + return (
132 + currentPassword.value.length > 0 &&
133 + newPassword.value.length >= 8 &&
134 + newPassword.value === confirmPassword.value
135 + )
136 +})
137 +
138 +const closeModal = () => {
139 + if (!loading.value) {
140 + resetForm()
141 + emit("close")
142 + }
143 +}
144 +
145 +const resetForm = () => {
146 + currentPassword.value = ""
147 + newPassword.value = ""
148 + confirmPassword.value = ""
149 + error.value = ""
150 + success.value = ""
151 +}
152 +
153 +const handleSubmit = async () => {
154 + error.value = ""
155 + success.value = ""
156 +
157 + // Validate passwords match
158 + if (newPassword.value !== confirmPassword.value) {
159 + error.value = "New passwords do not match"
160 + return
161 + }
162 +
163 + // Validate password length
164 + if (newPassword.value.length < 8) {
165 + error.value = "Password must be at least 8 characters long"
166 + return
167 + }
168 +
169 + loading.value = true
170 +
171 + try {
172 + // Get token and username from localStorage
173 + const token = localStorage.getItem("customer-portal-auth-token")
174 + const userStr = localStorage.getItem("customer-portal-user")
175 +
176 + if (!token || !userStr) {
177 + error.value = "Authentication required. Please log in again."
178 + return
179 + }
180 +
181 + const user = JSON.parse(userStr)
182 + const username = user.username
183 +
184 + // Call the reset-password/me endpoint
185 + const response = await axios.post(
186 + "/api/auth/reset-password/me",
187 + {
188 + username: username,
189 + new_password: newPassword.value
190 + },
191 + {
192 + headers: {
193 + Authorization: `Bearer ${token}`
194 + }
195 + }
196 + )
197 +
198 + if (response.data.success) {
199 + success.value = "Password changed successfully!"
200 +
201 + // Close modal after 1.5 seconds
202 + setTimeout(() => {
203 + emit("success")
204 + closeModal()
205 + }, 1500)
206 + } else {
207 + error.value = response.data.message || "Failed to change password"
208 + }
209 + } catch (err: any) {
210 + console.error("Change password error:", err)
211 +
212 + if (err.response?.data?.detail) {
213 + error.value = err.response.data.detail
214 + } else if (err.response?.data?.message) {
215 + error.value = err.response.data.message
216 + } else if (err.message) {
217 + error.value = err.message
218 + } else {
219 + error.value = "Failed to change password. Please try again."
220 + }
221 + } finally {
222 + loading.value = false
223 + }
224 +}
225 +</script>
customer_portal/src/views/OverviewPage.vue
+22
@@ -47,6 +47,12 @@
47 Welcome,
48 <span class="font-medium">{{ username }}</span>
49 </div>
50 + <button
51 + @click="openChangePasswordModal"
52 + class="rounded-md bg-indigo-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-indigo-700"
53 + >
54 + Change Password
55 + </button>
56 <button
57 @click="logout"
58 class="rounded-md bg-red-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-red-700"
@@ -511,6 +517,7 @@
517 </div>
518 </div>
519 </main>
520 + <ChangePasswordModal :is-open="showChangePasswordModal" @close="closeChangePasswordModal" @success="onPasswordChanged" />
521 </div>
522 </template>
523
@@ -521,6 +528,7 @@ import { usePortalSettingsStore } from "@/stores/portalSettings"
528 import AlertsAPI, { type Alert } from "@/api/alerts"
529 import CasesAPI, { type Case } from "@/api/cases"
530 import AgentsAPI from "@/api/agents"
531 +import ChangePasswordModal from "@/components/ChangePasswordModal.vue"
532
533 interface Stats {
534 totalAlerts: number
@@ -555,6 +563,7 @@ const portalSettingsStore = usePortalSettingsStore()
563 const loading = ref(true)
564 const error = ref("")
565 const showLogo = ref(true)
566 +const showChangePasswordModal = ref(false)
567 const stats = ref<Stats>({
568 totalAlerts: 0,
569 criticalAlerts: 0,
@@ -721,6 +730,19 @@ const goToAgents = () => {
730 router.push("/agents")
731 }
732
733 +const openChangePasswordModal = () => {
734 + showChangePasswordModal.value = true
735 +}
736 +
737 +const closeChangePasswordModal = () => {
738 + showChangePasswordModal.value = false
739 +}
740 +
741 +const onPasswordChanged = () => {
742 + // Optionally show a success message or perform other actions
743 + console.log("Password changed successfully")
744 +}
745 +
746 const logout = () => {
747 localStorage.removeItem("customer-portal-auth-token")
748 localStorage.removeItem("customer-portal-user")