feat(bookings): restrict management to assigned PIC or super admin
Secure API endpoints with requireBookingManager authorization check Update confirmation page to prompt for login if unauthorized Add safe redirect handling to login and guest middleware
This commit is contained in:
@@ -18,6 +18,7 @@ import { formatDateTime } from '../../utils/formatters'
|
||||
const route = useRoute()
|
||||
const toast = useToast()
|
||||
const apiClient = useApiClient()
|
||||
const auth = useAuth()
|
||||
const { locale, t } = useLocale()
|
||||
|
||||
const token = String(route.params.token || '')
|
||||
@@ -27,6 +28,8 @@ const savingPayment = ref(false)
|
||||
const uploadingTransactionDocument = ref(false)
|
||||
const deletingTransactionDocument = ref(false)
|
||||
|
||||
await auth.fetchSession()
|
||||
|
||||
let initialBooking: PublicBooking
|
||||
|
||||
try {
|
||||
@@ -50,6 +53,12 @@ const statusColor = computed(() => booking.value.status === 'confirmed' ? 'succe
|
||||
const ticketLabel = computed(() => booking.value.ticketLabel || booking.value.ticketType.toUpperCase())
|
||||
const totalFormatted = computed(() => formatBookingCurrency(booking.value.totalPrice, locale.value))
|
||||
const receiptPath = computed(() => `/receipt/${booking.value.receiptToken}`)
|
||||
const signInPath = computed(() => `/login?redirect=${encodeURIComponent(route.fullPath)}`)
|
||||
const canManageBooking = computed(() => {
|
||||
const user = auth.user.value
|
||||
|
||||
return Boolean(user && (user.role === 'super_admin' || user.id === booking.value.personInChargeId))
|
||||
})
|
||||
const transactionDocumentLimit = 10 * 1024 * 1024
|
||||
const transactionDocumentAccept = '.pdf,.jpg,.jpeg,.png,.webp,.heic,.heif,application/pdf,image/jpeg,image/png,image/webp,image/heic,image/heif'
|
||||
const selectedTransactionDocumentName = computed(() => transactionDocumentFile.value?.name || '')
|
||||
@@ -149,6 +158,10 @@ function formatFileSize(size: number) {
|
||||
}
|
||||
|
||||
function onTransactionDocumentChange(event: Event) {
|
||||
if (!canManageBooking.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const input = event.target as HTMLInputElement
|
||||
transactionDocumentFile.value = input.files?.[0] ?? null
|
||||
}
|
||||
@@ -159,7 +172,7 @@ function applyBooking(nextBooking: PublicBooking) {
|
||||
}
|
||||
|
||||
async function savePaymentDetails() {
|
||||
if (savingPayment.value || booking.value.status !== 'pending') {
|
||||
if (!canManageBooking.value || savingPayment.value || booking.value.status !== 'pending') {
|
||||
return booking.value
|
||||
}
|
||||
|
||||
@@ -185,7 +198,7 @@ async function savePaymentDetails() {
|
||||
}
|
||||
|
||||
async function uploadTransactionDocument(options: { silent?: boolean } = {}) {
|
||||
if (!transactionDocumentFile.value || uploadingTransactionDocument.value || booking.value.status !== 'pending') {
|
||||
if (!canManageBooking.value || !transactionDocumentFile.value || uploadingTransactionDocument.value || booking.value.status !== 'pending') {
|
||||
return booking.value
|
||||
}
|
||||
|
||||
@@ -249,7 +262,7 @@ async function uploadTransactionDocument(options: { silent?: boolean } = {}) {
|
||||
}
|
||||
|
||||
async function deleteTransactionDocument() {
|
||||
if (!booking.value.transactionDocument || deletingTransactionDocument.value || booking.value.status !== 'pending') {
|
||||
if (!canManageBooking.value || !booking.value.transactionDocument || deletingTransactionDocument.value || booking.value.status !== 'pending') {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -290,7 +303,7 @@ async function deleteTransactionDocument() {
|
||||
}
|
||||
|
||||
async function confirmBooking() {
|
||||
if (booking.value.status === 'confirmed') {
|
||||
if (!canManageBooking.value || booking.value.status === 'confirmed') {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -334,7 +347,7 @@ async function confirmBooking() {
|
||||
}
|
||||
|
||||
async function cancelBookingConfirmation() {
|
||||
if (booking.value.status !== 'confirmed') {
|
||||
if (!canManageBooking.value || booking.value.status !== 'confirmed') {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -413,6 +426,13 @@ async function cancelBookingConfirmation() {
|
||||
icon="i-lucide-badge-check"
|
||||
/>
|
||||
|
||||
<UAlert
|
||||
v-if="!canManageBooking"
|
||||
:title="auth.user.value ? t('confirm.managementRestricted') : t('confirm.signInToManage')"
|
||||
color="warning"
|
||||
icon="i-lucide-lock-keyhole"
|
||||
/>
|
||||
|
||||
<div class="overflow-hidden rounded-lg border border-default">
|
||||
<div
|
||||
v-for="row in detailRows"
|
||||
@@ -437,7 +457,7 @@ async function cancelBookingConfirmation() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3 rounded-lg border border-default p-4">
|
||||
<div v-if="canManageBooking" class="space-y-3 rounded-lg border border-default p-4">
|
||||
<div class="flex flex-col gap-1 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold text-highlighted">
|
||||
@@ -540,7 +560,7 @@ async function cancelBookingConfirmation() {
|
||||
/>
|
||||
|
||||
<UButton
|
||||
v-if="booking.status === 'confirmed'"
|
||||
v-if="canManageBooking && booking.status === 'confirmed'"
|
||||
:to="receiptPath"
|
||||
:label="t('confirm.openReceipt')"
|
||||
color="neutral"
|
||||
@@ -550,7 +570,7 @@ async function cancelBookingConfirmation() {
|
||||
/>
|
||||
|
||||
<UButton
|
||||
v-if="booking.status === 'pending'"
|
||||
v-if="canManageBooking && booking.status === 'pending'"
|
||||
:label="t('confirm.confirmBooking')"
|
||||
icon="i-lucide-check-check"
|
||||
class="justify-center"
|
||||
@@ -560,7 +580,7 @@ async function cancelBookingConfirmation() {
|
||||
/>
|
||||
|
||||
<UButton
|
||||
v-else
|
||||
v-else-if="canManageBooking"
|
||||
:label="t('confirm.cancelConfirmation')"
|
||||
color="error"
|
||||
variant="outline"
|
||||
@@ -570,6 +590,16 @@ async function cancelBookingConfirmation() {
|
||||
:disabled="savingPayment || uploadingTransactionDocument || deletingTransactionDocument"
|
||||
@click="cancelBookingConfirmation"
|
||||
/>
|
||||
|
||||
<UButton
|
||||
v-else-if="!auth.user.value"
|
||||
:to="signInPath"
|
||||
:label="t('confirm.signInToManage')"
|
||||
color="primary"
|
||||
variant="outline"
|
||||
icon="i-lucide-log-in"
|
||||
class="justify-center"
|
||||
/>
|
||||
</div>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
@@ -128,6 +128,7 @@ useSeoMeta({
|
||||
})
|
||||
|
||||
const toast = useToast()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = useAuth()
|
||||
const apiClient = useApiClient()
|
||||
@@ -141,6 +142,14 @@ const form = reactive({
|
||||
const passwordPending = ref(false)
|
||||
const passkeyPending = ref(false)
|
||||
|
||||
function getSafeRedirectPath(value: unknown) {
|
||||
const redirect = Array.isArray(value) ? value[0] : value
|
||||
|
||||
return typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//')
|
||||
? redirect
|
||||
: null
|
||||
}
|
||||
|
||||
function validateLogin(state: typeof form): FormError[] {
|
||||
const errors: FormError[] = []
|
||||
|
||||
@@ -160,7 +169,7 @@ async function finishLogin(user: Awaited<ReturnType<typeof auth.fetchSession>>)
|
||||
return
|
||||
}
|
||||
|
||||
await router.push(getDefaultAuthenticatedPath(user))
|
||||
await router.push(getSafeRedirectPath(route.query.redirect) || getDefaultAuthenticatedPath(user))
|
||||
}
|
||||
|
||||
async function onSubmit(event: FormSubmitEvent<typeof form>) {
|
||||
|
||||
Reference in New Issue
Block a user