feat: implement auth system, passkeys, and user management
Add PostgreSQL and Redis integration for users and sessions Implement password and WebAuthn passkey login flows Add Docker stack, super-admin seeding, and protected routes
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
<script lang="ts" setup>
|
||||
import type { FormError, FormSubmitEvent } from '@nuxt/ui'
|
||||
|
||||
import { isValidPhoneNumber, type PublicContact } from '~~/shared/auth'
|
||||
|
||||
type BookingMode = 'table' | 'pax'
|
||||
type TicketType = 'vip' | 'supporter'
|
||||
|
||||
const toast = useToast()
|
||||
const apiClient = useApiClient()
|
||||
|
||||
const eventDetails = [
|
||||
{
|
||||
@@ -50,16 +53,13 @@ const ticketCatalog = [
|
||||
}
|
||||
] satisfies Array<{ value: TicketType, label: string, description: string, price: number }>
|
||||
|
||||
const personInCharge = [
|
||||
{
|
||||
label: 'Xiaomai',
|
||||
value: '601157753558'
|
||||
},
|
||||
{
|
||||
label: 'Lily',
|
||||
value: '60172661198'
|
||||
}
|
||||
]
|
||||
const contactsResponse = await apiClient<{ contacts: PublicContact[] }>('/api/public/contacts')
|
||||
const personInCharge = computed(() => {
|
||||
return contactsResponse.contacts.map((contact) => ({
|
||||
label: contact.fullName,
|
||||
value: contact.id
|
||||
}))
|
||||
})
|
||||
|
||||
const priceFormatter = new Intl.NumberFormat('en-MY', {
|
||||
style: 'currency',
|
||||
@@ -76,7 +76,11 @@ const form = reactive({
|
||||
ticketType: 'vip' as TicketType
|
||||
})
|
||||
|
||||
const selectedPersonInCharge = ref(personInCharge[0]?.value ?? '')
|
||||
const selectedPersonInCharge = ref(contactsResponse.contacts[0]?.id ?? '')
|
||||
|
||||
const selectedPersonInChargeRecord = computed(() => {
|
||||
return contactsResponse.contacts.find((contact) => contact.id === selectedPersonInCharge.value) ?? null
|
||||
})
|
||||
|
||||
const selectedTicket = computed(() => {
|
||||
return ticketCatalog.find((ticket) => ticket.value === form.ticketType) ?? ticketCatalog[0]
|
||||
@@ -101,7 +105,7 @@ function validateBooking(state: typeof form): FormError[] {
|
||||
|
||||
if (!state.phone.trim()) {
|
||||
errors.push({ name: 'phone', message: 'Please enter a contact number.' })
|
||||
} else if (!/^\+?[0-9\s-]{8,15}$/.test(state.phone.trim())) {
|
||||
} else if (!isValidPhoneNumber(state.phone.trim())) {
|
||||
errors.push({ name: 'phone', message: 'Use a valid phone number with 8 to 15 digits.' })
|
||||
}
|
||||
|
||||
@@ -129,9 +133,20 @@ function buildBookingMessage() {
|
||||
}
|
||||
|
||||
function bookTicket(event: FormSubmitEvent<typeof form>) {
|
||||
const selectedPic = personInCharge.find((item) => item.value === selectedPersonInCharge.value) ?? personInCharge[0]
|
||||
const selectedPic = selectedPersonInChargeRecord.value
|
||||
|
||||
if (!selectedPic) {
|
||||
toast.add({
|
||||
title: 'No person in charge available',
|
||||
description: 'Add a user with a phone number in the management page first.',
|
||||
color: 'error',
|
||||
icon: 'i-lucide-circle-alert'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const encodedMessage = encodeURIComponent(buildBookingMessage())
|
||||
const whatsappUrl = `https://wa.me/${selectedPic.value}?text=${encodedMessage}`
|
||||
const whatsappUrl = `https://wa.me/${selectedPic.phoneNumber}?text=${encodedMessage}`
|
||||
const bookingWindow = window.open(whatsappUrl, '_blank', 'noopener,noreferrer')
|
||||
|
||||
if (!bookingWindow) {
|
||||
@@ -146,7 +161,7 @@ function bookTicket(event: FormSubmitEvent<typeof form>) {
|
||||
|
||||
toast.add({
|
||||
title: 'WhatsApp booking draft opened',
|
||||
description: `Your reservation details were sent to ${selectedPic.label}.`,
|
||||
description: `Your reservation details were sent to ${selectedPic.fullName}.`,
|
||||
color: 'success',
|
||||
icon: 'i-lucide-check-circle-2'
|
||||
})
|
||||
@@ -217,11 +232,17 @@ function bookTicket(event: FormSubmitEvent<typeof form>) {
|
||||
</div>
|
||||
|
||||
<UFormField label="Person In Charge">
|
||||
<USelect v-model="selectedPersonInCharge" size="xl" class="w-full" :items="personInCharge" />
|
||||
<USelect
|
||||
v-model="selectedPersonInCharge"
|
||||
size="xl"
|
||||
class="w-full"
|
||||
:items="personInCharge"
|
||||
:disabled="!personInCharge.length"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UButton id="getTicketBtn" type="submit" label="Book Your Ticket Now" size="xl"
|
||||
class="w-full justify-center" />
|
||||
class="w-full justify-center" :disabled="!selectedPersonInCharge" />
|
||||
</UForm>
|
||||
</UCard>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user