Files
chore/frontend/vue-app/src/components/profile/UserProfile.vue
Ryan Kegel f14de28daa
All checks were successful
Gitea Actions Demo / build-and-push (push) Successful in 36s
feat: Implement user validation and ownership checks for image, reward, and task APIs
- Added `get_validated_user_id` utility function to validate user authentication across multiple APIs.
- Updated image upload, request, and listing endpoints to ensure user ownership and proper error handling.
- Enhanced reward management endpoints to include user validation and ownership checks.
- Modified task management endpoints to enforce user authentication and ownership verification.
- Updated models to include `user_id` for images, rewards, tasks, and children to track ownership.
- Implemented frontend changes to ensure UI reflects the ownership of tasks and rewards.
- Added a new feature specification to prevent deletion of system tasks and rewards.
2026-01-31 19:48:51 -05:00

258 lines
6.8 KiB
Vue

<template>
<div class="edit-view">
<EntityEditForm
entityLabel="User Profile"
:fields="fields"
:initialData="initialData"
:isEdit="true"
:loading="loading"
:error="errorMsg"
:title="'User Profile'"
@submit="handleSubmit"
@cancel="router.back"
@add-image="onAddImage"
>
<template #custom-field-email="{ modelValue }">
<div class="email-actions">
<input id="email" type="email" :value="modelValue" disabled class="readonly-input" />
<button
type="button"
class="btn-link align-start btn-link-space"
@click="goToChangeParentPin"
>
Change Parent Pin
</button>
<button
type="button"
class="btn-link align-start btn-link-space"
@click="resetPassword"
:disabled="resetting"
>
Change Password
</button>
</div>
</template>
</EntityEditForm>
<div v-if="errorMsg" class="error-message" aria-live="polite">{{ errorMsg }}</div>
<ModalDialog
v-if="showModal"
:title="modalTitle"
:subtitle="modalSubtitle"
@close="handlePasswordModalClose"
>
<div class="modal-message">{{ modalMessage }}</div>
<div class="modal-actions" v-if="!resetting">
<button class="btn btn-primary" @click="handlePasswordModalClose">OK</button>
</div>
</ModalDialog>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { useRouter } from 'vue-router'
import EntityEditForm from '../shared/EntityEditForm.vue'
import ModalDialog from '../shared/ModalDialog.vue'
import '@/assets/styles.css'
import '@/assets/colors.css'
const router = useRouter()
const loading = ref(false)
const errorMsg = ref('')
const successMsg = ref('')
const resetting = ref(false)
const localImageFile = ref<File | null>(null)
const showModal = ref(false)
const modalTitle = ref('')
const modalSubtitle = ref('')
const modalMessage = ref('')
const initialData = ref({
image_id: null,
first_name: '',
last_name: '',
email: '',
})
const fields = [
{ name: 'image_id', label: 'Image', type: 'image', imageType: 1 },
{ name: 'first_name', label: 'First Name', type: 'text', required: true, maxlength: 64 },
{ name: 'last_name', label: 'Last Name', type: 'text', required: true, maxlength: 64 },
{ name: 'email', label: 'Email Address', type: 'custom' },
]
onMounted(async () => {
loading.value = true
try {
const res = await fetch('/api/user/profile')
if (!res.ok) throw new Error('Failed to load profile')
const data = await res.json()
initialData.value = {
image_id: data.image_id || null,
first_name: data.first_name || '',
last_name: data.last_name || '',
email: data.email || '',
}
} catch {
errorMsg.value = 'Could not load user profile.'
} finally {
loading.value = false
}
})
function onAddImage({ id, file }: { id: string; file: File }) {
if (id === 'local-upload') {
localImageFile.value = file
} else {
localImageFile.value = null
initialData.value.image_id = id
updateAvatar(id)
}
}
async function updateAvatar(imageId: string) {
errorMsg.value = ''
successMsg.value = ''
//todo update avatar loading state
try {
const res = await fetch('/api/user/avatar', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image_id: imageId }),
})
if (!res.ok) throw new Error('Failed to update avatar')
initialData.value.image_id = imageId
successMsg.value = 'Avatar updated!'
} catch {
//errorMsg.value = 'Failed to update avatar.'
//todo update avatar error handling
errorMsg.value = ''
}
}
watch(localImageFile, async (file) => {
if (!file) return
errorMsg.value = ''
successMsg.value = ''
const formData = new FormData()
formData.append('file', file)
formData.append('type', '2')
formData.append('permanent', 'true')
try {
const resp = await fetch('/api/image/upload', {
method: 'POST',
body: formData,
})
if (!resp.ok) throw new Error('Image upload failed')
const data = await resp.json()
initialData.value.image_id = data.id
await updateAvatar(data.id)
} catch {
errorMsg.value = 'Failed to upload avatar image.'
}
})
function handleSubmit(form: {
image_id: string | null
first_name: string
last_name: string
email: string
}) {
errorMsg.value = ''
loading.value = true
fetch('/api/user/profile', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
first_name: form.first_name,
last_name: form.last_name,
image_id: form.image_id,
}),
})
.then(async (res) => {
if (!res.ok) throw new Error('Failed to update profile')
modalTitle.value = 'Profile Updated'
modalSubtitle.value = ''
modalMessage.value = 'Your profile was updated successfully.'
showModal.value = true
})
.catch(() => {
errorMsg.value = 'Failed to update profile.'
})
.finally(() => {
loading.value = false
})
}
async function handlePasswordModalClose() {
showModal.value = false
}
async function resetPassword() {
// Show modal immediately with loading message
modalTitle.value = 'Change Password'
modalMessage.value = 'Sending password change email...'
modalSubtitle.value = ''
showModal.value = true
resetting.value = true
errorMsg.value = ''
try {
const res = await fetch('/api/request-password-reset', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: initialData.value.email }),
})
if (!res.ok) throw new Error('Failed to send reset email')
modalTitle.value = 'Password Change Email Sent'
modalMessage.value =
'If this email is registered, you will receive a password change link shortly.'
} catch {
modalTitle.value = 'Password Change Failed'
modalMessage.value = 'Failed to send password change email.'
} finally {
resetting.value = false
}
}
function goToChangeParentPin() {
router.push({ name: 'ParentPinSetup' })
}
</script>
<style scoped>
/* ...existing styles... */
.email-actions {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.align-start {
align-self: flex-start;
margin-top: 0.1rem;
}
.btn-link-space {
margin-top: 1rem;
margin-bottom: 1rem;
}
.success-message {
color: var(--success, #16a34a);
font-size: 1rem;
}
.error-message {
color: var(--error, #e53e3e);
font-size: 0.98rem;
margin-top: 0.4rem;
}
.readonly-input {
width: 100%;
padding: 0.6rem;
border-radius: 7px;
border: 1px solid var(--form-input-border, #e6e6e6);
font-size: 1rem;
background: var(--form-input-bg, #f5f5f5);
color: var(--form-label, #888);
box-sizing: border-box;
}
</style>