diff --git a/app/composables/useLocale.ts b/app/composables/useLocale.ts
index a7da718..8c77923 100644
--- a/app/composables/useLocale.ts
+++ b/app/composables/useLocale.ts
@@ -109,6 +109,8 @@ const messages = {
'confirm.alreadyPendingDescription': 'This booking was already pending confirmation.',
'confirm.cancelledDescription': 'The booking has been returned to pending status.',
'confirm.cancelFailed': 'Cancellation failed',
+ 'confirm.signInToManage': 'Sign in to manage this booking',
+ 'confirm.managementRestricted': 'Only the assigned PIC or Super Admin can manage this booking.',
'receipt.badge': 'Ticket Receipt',
'receipt.mainQr': 'Main QR',
'receipt.seatList': 'Seat List',
@@ -277,6 +279,8 @@ const messages = {
'confirm.alreadyPendingDescription': '此预订已经处于待确认状态。',
'confirm.cancelledDescription': '预订已回到待确认状态。',
'confirm.cancelFailed': '取消失败',
+ 'confirm.signInToManage': '登录以管理此预订',
+ 'confirm.managementRestricted': '只有指定负责人或 Super Admin 可以管理此预订。',
'receipt.badge': '票券收据',
'receipt.mainQr': '主二维码',
'receipt.seatList': '座位列表',
diff --git a/app/middleware/guest.ts b/app/middleware/guest.ts
index fc1ebd5..dd32a9d 100644
--- a/app/middleware/guest.ts
+++ b/app/middleware/guest.ts
@@ -1,6 +1,14 @@
import { getDefaultAuthenticatedPath } from '~~/shared/auth'
-export default defineNuxtRouteMiddleware(async () => {
+function getSafeRedirectPath(value: unknown) {
+ const redirect = Array.isArray(value) ? value[0] : value
+
+ return typeof redirect === 'string' && redirect.startsWith('/') && !redirect.startsWith('//')
+ ? redirect
+ : null
+}
+
+export default defineNuxtRouteMiddleware(async (to) => {
const auth = useAuth()
await auth.fetchSession()
@@ -8,5 +16,5 @@ export default defineNuxtRouteMiddleware(async () => {
return
}
- return navigateTo(getDefaultAuthenticatedPath(auth.user.value))
+ return navigateTo(getSafeRedirectPath(to.query.redirect) || getDefaultAuthenticatedPath(auth.user.value))
})
diff --git a/app/pages/confirmation/[token].vue b/app/pages/confirmation/[token].vue
index 3f0bd83..e2eef38 100644
--- a/app/pages/confirmation/[token].vue
+++ b/app/pages/confirmation/[token].vue
@@ -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"
/>
+