Refactor project structure and update dependencies
This commit is contained in:
10
nuxt/app.vue
10
nuxt/app.vue
@@ -1,10 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
|
||||
<UNotifications />
|
||||
</template>
|
||||
@@ -4,12 +4,12 @@ export default defineAppConfig({
|
||||
primary: 'sky',
|
||||
gray: 'cool',
|
||||
container: {
|
||||
constrained: 'max-w-7xl w-full'
|
||||
constrained: 'max-w-7xl w-full',
|
||||
},
|
||||
avatar: {
|
||||
default: {
|
||||
icon: 'i-heroicons-user',
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
10
nuxt/app/app.vue
Normal file
10
nuxt/app/app.vue
Normal file
@@ -0,0 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UApp>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</UApp>
|
||||
</template>
|
||||
7
nuxt/app/assets/css/main.css
Normal file
7
nuxt/app/assets/css/main.css
Normal file
@@ -0,0 +1,7 @@
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
|
||||
|
||||
@theme {
|
||||
|
||||
}
|
||||
36
nuxt/app/components/AppHeader.vue
Normal file
36
nuxt/app/components/AppHeader.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
const items = [{
|
||||
label: 'Documentation',
|
||||
icon: 'i-heroicons-book-open',
|
||||
to: 'https://ui.nuxt.com/getting-started',
|
||||
}, {
|
||||
label: 'Pro',
|
||||
icon: 'i-heroicons-square-3-stack-3d',
|
||||
to: 'https://ui.nuxt.com/pro',
|
||||
}, {
|
||||
label: 'Releases',
|
||||
icon: 'i-heroicons-rocket-launch',
|
||||
to: 'https://github.com/nuxt/ui/releases',
|
||||
target: '_blank',
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header
|
||||
class="bg-background/75 backdrop-blur -mb-px sticky top-0 z-50 border-b border-dashed border-gray-200/80 dark:border-gray-800/80"
|
||||
>
|
||||
<UContainer class="flex items-center justify-between gap-3 h-16 py-2">
|
||||
<AppLogo class="lg:flex-1" />
|
||||
|
||||
<UNavigationMenu
|
||||
orientation="horizontal"
|
||||
:items="items"
|
||||
class="hidden lg:block"
|
||||
/>
|
||||
|
||||
<div class="flex items-center justify-end gap-3 lg:flex-1">
|
||||
<UserDropdown />
|
||||
</div>
|
||||
</UContainer>
|
||||
</header>
|
||||
</template>
|
||||
8
nuxt/app/components/AppLogo.vue
Normal file
8
nuxt/app/components/AppLogo.vue
Normal file
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<NuxtLink
|
||||
to="/"
|
||||
class="font-bold text-xl"
|
||||
>
|
||||
<span class="text-primary-500">Nuxt</span> Breeze
|
||||
</NuxtLink>
|
||||
</template>
|
||||
@@ -25,7 +25,9 @@ const links = [
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UContainer>
|
||||
<UNavigationTree :links="links" />
|
||||
</UContainer>
|
||||
<UNavigationMenu
|
||||
class="data-[orientation=vertical]:w-48"
|
||||
orientation="vertical"
|
||||
:items="links"
|
||||
/>
|
||||
</template>
|
||||
35
nuxt/app/components/UserDropdown.vue
Normal file
35
nuxt/app/components/UserDropdown.vue
Normal file
@@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
const links = [
|
||||
{
|
||||
label: 'Account',
|
||||
to: '/account',
|
||||
icon: 'i-heroicons-user-solid',
|
||||
},
|
||||
{
|
||||
label: 'Logout',
|
||||
to: '/logout',
|
||||
icon: 'i-heroicons-arrow-left-on-rectangle',
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPopover>
|
||||
<UButton
|
||||
icon="i-heroicons-user-solid"
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
/>
|
||||
<template #content>
|
||||
<div class="p-4">
|
||||
<UNavigationMenu
|
||||
:items="links"
|
||||
:orientation="'vertical'"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
109
nuxt/app/components/account/AccountDeviceTable.vue
Normal file
109
nuxt/app/components/account/AccountDeviceTable.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import type { TableColumn } from '#ui/components/Table.vue'
|
||||
import type { Device, IDevicesResponse } from '~/types/device'
|
||||
|
||||
const UButton = resolveComponent('UButton')
|
||||
const UBadge = resolveComponent('UBadge')
|
||||
const UDropdownMenu = resolveComponent('UDropdownMenu')
|
||||
|
||||
const dayjs = useDayjs()
|
||||
const auth = useAuthStore()
|
||||
const loading = ref(false)
|
||||
const devices = ref<Device[]>([])
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true
|
||||
|
||||
const response = await $fetch<IDevicesResponse>('devices')
|
||||
|
||||
if (response.ok) {
|
||||
devices.value = response.devices
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const columns: TableColumn<Device>[] = [
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: 'Device',
|
||||
cell: ({ row }) => {
|
||||
return h('div', { class: 'font-semibold' }, [
|
||||
row.original.name,
|
||||
row.original.is_current && h(UBadge, {
|
||||
label: 'active',
|
||||
color: 'primary',
|
||||
variant: 'soft',
|
||||
size: 'sm',
|
||||
class: 'ms-1',
|
||||
}),
|
||||
h('div', { class: 'font-medium text-sm' }, `IP: ${row.original.ip}`),
|
||||
])
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'last_used_at',
|
||||
header: 'Last used at',
|
||||
cell: ({ row }) => {
|
||||
return dayjs(row.original.last_used_at).fromNow()
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'actions',
|
||||
header: 'Actions',
|
||||
cell: ({ row }) => {
|
||||
return h('div', { class: 'flex justify-end' },
|
||||
h(UDropdownMenu, {
|
||||
items: items(row.original),
|
||||
}, () => h(UButton, {
|
||||
icon: 'i-heroicons-ellipsis-vertical-20-solid',
|
||||
variant: 'ghost',
|
||||
color: 'neutral',
|
||||
})),
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const items = (row: Device) => [
|
||||
[
|
||||
{
|
||||
label: 'Delete',
|
||||
icon: 'i-heroicons-trash-20-solid',
|
||||
onSelect: async () => {
|
||||
await $fetch<never>('devices/disconnect', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
hash: row.hash,
|
||||
},
|
||||
async onResponse({ response }) {
|
||||
if (response._data?.ok) {
|
||||
await fetchData()
|
||||
await auth.fetchUser()
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
},
|
||||
],
|
||||
]
|
||||
|
||||
if (import.meta.client) {
|
||||
fetchData()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ClientOnly>
|
||||
<UTable
|
||||
:data="devices"
|
||||
:columns="columns"
|
||||
size="lg"
|
||||
:loading="loading"
|
||||
/>
|
||||
</ClientOnly>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
125
nuxt/app/components/account/AccountUpdatePassword.vue
Normal file
125
nuxt/app/components/account/AccountUpdatePassword.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<script lang="ts" setup>
|
||||
import type { IAccountChangePasswordResponse, IVerificationNotificationResponse } from '~/types/account'
|
||||
|
||||
const form = ref()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const state = reactive({
|
||||
current_password: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
})
|
||||
|
||||
const { refresh: onSubmit, status: accountPasswordStatus } = useFetch<IAccountChangePasswordResponse>('account/password', {
|
||||
method: 'POST',
|
||||
body: state,
|
||||
immediate: false,
|
||||
watch: false,
|
||||
async onResponse({ response }) {
|
||||
if (response?.status === 422) {
|
||||
form.value.setErrors(response._data?.errors)
|
||||
}
|
||||
else if (response._data?.ok) {
|
||||
useToast().add({
|
||||
icon: 'i-heroicons-check-circle-20-solid',
|
||||
title: 'The password was successfully updated.',
|
||||
color: 'primary',
|
||||
})
|
||||
|
||||
state.current_password = ''
|
||||
state.password = ''
|
||||
state.password_confirmation = ''
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const { refresh: sendResetPasswordEmail, status: resetPasswordEmailStatus } = useFetch<IVerificationNotificationResponse>('verification-notification', {
|
||||
method: 'POST',
|
||||
body: { email: auth.user.email },
|
||||
immediate: false,
|
||||
watch: false,
|
||||
onResponse({ response }) {
|
||||
if (response._data?.ok) {
|
||||
useToast().add({
|
||||
icon: 'i-heroicons-check-circle-20-solid',
|
||||
title: 'A link to reset your password has been sent to your email.',
|
||||
color: 'primary',
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UForm
|
||||
v-if="auth.user.has_password"
|
||||
ref="form"
|
||||
:state="state"
|
||||
class="space-y-4"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<UFormField
|
||||
label="Current Password"
|
||||
name="current_password"
|
||||
required
|
||||
>
|
||||
<UInput
|
||||
v-model="state.current_password"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField
|
||||
label="New Password"
|
||||
name="password"
|
||||
hint="min 8 characters"
|
||||
:ui="{ hint: 'text-xs text-gray-500 dark:text-gray-400' }"
|
||||
required
|
||||
>
|
||||
<UInput
|
||||
v-model="state.password"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField
|
||||
label="Repeat Password"
|
||||
name="password_confirmation"
|
||||
required
|
||||
>
|
||||
<UInput
|
||||
v-model="state.password_confirmation"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<div class="pt-2">
|
||||
<UButton
|
||||
type="submit"
|
||||
label="Save"
|
||||
:loading="accountPasswordStatus === 'pending'"
|
||||
/>
|
||||
</div>
|
||||
</UForm>
|
||||
|
||||
<UAlert
|
||||
v-else
|
||||
icon="i-heroicons-information-circle-20-solid"
|
||||
title="Send a link to your email to reset your password."
|
||||
description="To create a password for your account, you must go through the password recovery process."
|
||||
:actions="[
|
||||
{
|
||||
label: 'Send link to Email',
|
||||
variant: 'solid',
|
||||
color: 'neutral',
|
||||
loading: resetPasswordEmailStatus === 'pending',
|
||||
onClick: () => sendResetPasswordEmail(),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,6 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
const form = ref();
|
||||
const auth = useAuthStore();
|
||||
import type { IAccountUpdateResponse, IVerificationNotificationResponse } from '~/types/account'
|
||||
|
||||
const form = ref()
|
||||
const auth = useAuthStore()
|
||||
|
||||
const state = reactive({
|
||||
...{
|
||||
@@ -8,65 +10,86 @@ const state = reactive({
|
||||
name: auth.user.name,
|
||||
avatar: auth.user.avatar,
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const { refresh: sendEmailVerification, status: resendEmailStatus } = useFetch<any>("verification-notification", {
|
||||
method: "POST",
|
||||
const { refresh: sendEmailVerification, status: resendEmailStatus } = useFetch<IVerificationNotificationResponse>('verification-notification', {
|
||||
method: 'POST',
|
||||
body: { email: state.email },
|
||||
immediate: false,
|
||||
watch: false,
|
||||
onResponse({ response }) {
|
||||
if (response._data?.ok) {
|
||||
useToast().add({
|
||||
icon: "i-heroicons-check-circle-20-solid",
|
||||
icon: 'i-heroicons-check-circle-20-solid',
|
||||
title: response._data.message,
|
||||
color: "emerald",
|
||||
});
|
||||
color: 'primary',
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
})
|
||||
|
||||
const { refresh: onSubmit, status: accountUpdateStatus } = useFetch<any>("account/update", {
|
||||
method: "POST",
|
||||
const { refresh: onSubmit, status: accountUpdateStatus } = useFetch<IAccountUpdateResponse>('account/update', {
|
||||
method: 'POST',
|
||||
body: state,
|
||||
immediate: false,
|
||||
watch: false,
|
||||
async onResponse({ response }) {
|
||||
if (response?.status === 422) {
|
||||
form.value.setErrors(response._data?.errors);
|
||||
} else if (response._data?.ok) {
|
||||
useToast().add({
|
||||
icon: "i-heroicons-check-circle-20-solid",
|
||||
title: "Account details have been successfully updated.",
|
||||
color: "emerald",
|
||||
});
|
||||
|
||||
await auth.fetchUser();
|
||||
|
||||
state.name = auth.user.name;
|
||||
state.email = auth.user.email;
|
||||
state.avatar = auth.user.avatar;
|
||||
form.value.setErrors(response._data?.errors)
|
||||
}
|
||||
}
|
||||
});
|
||||
else if (response._data?.ok) {
|
||||
useToast().add({
|
||||
icon: 'i-heroicons-check-circle-20-solid',
|
||||
title: 'Account details have been successfully updated.',
|
||||
color: 'primary',
|
||||
})
|
||||
|
||||
await auth.fetchUser()
|
||||
|
||||
state.name = auth.user.name
|
||||
state.email = auth.user.email
|
||||
state.avatar = auth.user.avatar
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UForm ref="form" :state="state" @submit="onSubmit" class="space-y-4">
|
||||
<UFormGroup label="" name="avatar" class="flex">
|
||||
<UForm
|
||||
ref="form"
|
||||
:state="state"
|
||||
class="space-y-4"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<UFormField
|
||||
label=""
|
||||
name="avatar"
|
||||
class="flex"
|
||||
>
|
||||
<InputUploadAvatar
|
||||
v-model="state.avatar"
|
||||
accept=".png, .jpg, .jpeg, .webp"
|
||||
entity="avatars"
|
||||
max-size="2"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Name" name="name" required>
|
||||
<UInput v-model="state.name" type="text" />
|
||||
</UFormGroup>
|
||||
<UFormField
|
||||
label="Name"
|
||||
name="name"
|
||||
required
|
||||
>
|
||||
<UInput
|
||||
v-model="state.name"
|
||||
type="text"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormGroup label="Email" name="email" required>
|
||||
<UFormField
|
||||
label="Email"
|
||||
name="email"
|
||||
required
|
||||
>
|
||||
<UInput
|
||||
v-model="state.email"
|
||||
placeholder="you@example.com"
|
||||
@@ -74,10 +97,12 @@ const { refresh: onSubmit, status: accountUpdateStatus } = useFetch<any>("accoun
|
||||
trailing
|
||||
type="email"
|
||||
/>
|
||||
</UFormGroup>
|
||||
</UFormField>
|
||||
|
||||
<UAlert
|
||||
v-if="auth.user.must_verify_email"
|
||||
variant="subtle"
|
||||
color="warning"
|
||||
icon="i-heroicons-information-circle-20-solid"
|
||||
title="Please confirm your email address."
|
||||
description="A confirmation email has been sent to your email address. Please click on the confirmation link in the email to verify your email address."
|
||||
@@ -85,15 +110,19 @@ const { refresh: onSubmit, status: accountUpdateStatus } = useFetch<any>("accoun
|
||||
{
|
||||
label: 'Resend verification email',
|
||||
variant: 'solid',
|
||||
color: 'gray',
|
||||
color: 'neutral',
|
||||
loading: resendEmailStatus === 'pending',
|
||||
click: sendEmailVerification,
|
||||
onClick: () => sendEmailVerification(),
|
||||
},
|
||||
]"
|
||||
/>
|
||||
|
||||
<div class="pt-2">
|
||||
<UButton type="submit" label="Save" :loading="accountUpdateStatus === 'pending'" />
|
||||
<UButton
|
||||
type="submit"
|
||||
label="Save"
|
||||
:loading="accountUpdateStatus === 'pending'"
|
||||
/>
|
||||
</div>
|
||||
</UForm>
|
||||
</template>
|
||||
57
nuxt/app/components/auth/AuthForgotPasswordForm.vue
Normal file
57
nuxt/app/components/auth/AuthForgotPasswordForm.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
import type { IForgotPasswordResponse } from '~/types/account'
|
||||
|
||||
const form = useTemplateRef('form')
|
||||
|
||||
const state = reactive({
|
||||
email: '',
|
||||
})
|
||||
|
||||
const { refresh: onSubmit, status: forgotStatus } = useFetch<IForgotPasswordResponse>('forgot-password', {
|
||||
method: 'POST',
|
||||
body: state,
|
||||
immediate: false,
|
||||
watch: false,
|
||||
async onResponse({ response }) {
|
||||
if (response?.status === 422) {
|
||||
form.value.setErrors(response._data?.errors)
|
||||
}
|
||||
else if (response._data?.ok) {
|
||||
useToast().add({
|
||||
title: 'Success',
|
||||
description: response._data.message,
|
||||
color: 'primary',
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UForm
|
||||
ref="form"
|
||||
:state="state"
|
||||
class="space-y-8"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<UFormField
|
||||
label="Email"
|
||||
name="email"
|
||||
>
|
||||
<UInput
|
||||
v-model="state.email"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UButton
|
||||
block
|
||||
size="md"
|
||||
type="submit"
|
||||
:loading="forgotStatus === 'pending'"
|
||||
icon="i-heroicons-envelope"
|
||||
>
|
||||
Reset Password
|
||||
</UButton>
|
||||
</UForm>
|
||||
</template>
|
||||
173
nuxt/app/components/auth/AuthLoginForm.vue
Normal file
173
nuxt/app/components/auth/AuthLoginForm.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<script setup lang="ts">
|
||||
import type { IAccountLoginResponse, IAccountProvider, IAccountProviderData } from '~/types/account'
|
||||
import type { ButtonProps } from '#ui/components/Button.vue'
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
const router = useRouter()
|
||||
const auth = useAuthStore()
|
||||
const form = useTemplateRef('form')
|
||||
|
||||
const state = reactive({
|
||||
email: '',
|
||||
password: '',
|
||||
remember: false,
|
||||
})
|
||||
|
||||
const { refresh: onSubmit, status: loginStatus } = useFetch<IAccountLoginResponse>('login', {
|
||||
method: 'POST',
|
||||
body: state,
|
||||
immediate: false,
|
||||
watch: false,
|
||||
async onResponse({ response }) {
|
||||
if (response?.status === 422) {
|
||||
form?.value?.setErrors(response._data?.errors)
|
||||
}
|
||||
else if (response._data?.ok) {
|
||||
auth.token = response._data.token
|
||||
|
||||
await auth.fetchUser()
|
||||
await router.push('/')
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const providers = ref<{ [key: string]: IAccountProvider }>(
|
||||
Object.fromEntries(
|
||||
Object.entries(config.public.providers).map(([key, provider]) => [
|
||||
key,
|
||||
{
|
||||
...provider,
|
||||
color: provider.color as ButtonProps['color'],
|
||||
},
|
||||
]),
|
||||
),
|
||||
)
|
||||
|
||||
async function handleMessage(event: { data: IAccountProviderData }): Promise<void> {
|
||||
const provider = event.data.provider as string
|
||||
|
||||
if (Object.keys(providers.value).includes(provider) && event.data.token) {
|
||||
if (providers?.value[provider]?.loading) {
|
||||
providers.value[provider].loading = false
|
||||
}
|
||||
auth.token = event.data.token
|
||||
|
||||
await auth.fetchUser()
|
||||
await router.push('/')
|
||||
}
|
||||
else if (event.data.message) {
|
||||
useToast().add({
|
||||
icon: 'i-heroicons-exclamation-circle-solid',
|
||||
color: 'error',
|
||||
title: event.data.message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function loginVia(provider: string): void {
|
||||
providers.value[provider]!.loading = true
|
||||
|
||||
const width = 640
|
||||
const height = 660
|
||||
const left = window.screen.width / 2 - width / 2
|
||||
const top = window.screen.height / 2 - height / 2
|
||||
|
||||
const popup = window.open(
|
||||
`${config.public.apiBase}${config.public.apiPrefix}/login/${provider}/redirect`,
|
||||
'Sign In',
|
||||
`toolbar=no, location=no, directories=no, status=no, menubar=no, scollbars=no, resizable=no, copyhistory=no, width=${width},height=${height},top=${top},left=${left}`,
|
||||
)
|
||||
|
||||
const interval = setInterval(() => {
|
||||
if (!popup || popup.closed) {
|
||||
clearInterval(interval)
|
||||
providers.value[provider]!.loading = false
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
onMounted(() => window.addEventListener('message', handleMessage))
|
||||
onBeforeUnmount(() => window.removeEventListener('message', handleMessage))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UForm
|
||||
ref="form"
|
||||
:state="state"
|
||||
class="space-y-4"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<UFormField
|
||||
label="Email"
|
||||
name="email"
|
||||
>
|
||||
<UInput
|
||||
v-model="state.email"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField
|
||||
label="Password"
|
||||
name="password"
|
||||
>
|
||||
<UInput
|
||||
v-model="state.password"
|
||||
type="password"
|
||||
class="w-full"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<UCheckbox
|
||||
id="remember-me"
|
||||
v-model="state.remember"
|
||||
label="Remember me"
|
||||
name="remember-me"
|
||||
/>
|
||||
|
||||
<div class="text-sm leading-6">
|
||||
<NuxtLink
|
||||
to="/forgot-password"
|
||||
class="text-primary hover:text-primary-300 font-semibold"
|
||||
>
|
||||
Forgot
|
||||
password?
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
block
|
||||
size="md"
|
||||
type="submit"
|
||||
:loading="loginStatus === 'pending'"
|
||||
icon="i-heroicons-arrow-right-on-rectangle"
|
||||
>
|
||||
Login
|
||||
</UButton>
|
||||
</UForm>
|
||||
<USeparator
|
||||
v-if="Object.keys(providers).length > 0"
|
||||
color="neutral"
|
||||
label="Login with"
|
||||
class="my-4"
|
||||
/>
|
||||
<div class="flex gap-4">
|
||||
<UButton
|
||||
v-for="(provider, key) in providers"
|
||||
:key="key"
|
||||
:loading="provider.loading"
|
||||
:icon="provider.icon"
|
||||
:color="provider.color"
|
||||
:label="provider.name"
|
||||
size="lg"
|
||||
class="w-full flex items-center justify-center"
|
||||
@click="loginVia(key as string)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
91
nuxt/app/components/auth/AuthResetPasswordForm.vue
Normal file
91
nuxt/app/components/auth/AuthResetPasswordForm.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import type { IAccountResetPasswordResponse } from '~/types/account'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const auth = useAuthStore()
|
||||
const form = useTemplateRef('form')
|
||||
|
||||
const state = reactive({
|
||||
email: route.query.email as string,
|
||||
token: route.params.token,
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
})
|
||||
|
||||
const { refresh: onSubmit, status: resetStatus } = useFetch<IAccountResetPasswordResponse>('reset-password', {
|
||||
method: 'POST',
|
||||
body: state,
|
||||
immediate: false,
|
||||
watch: false,
|
||||
async onResponse({ response }) {
|
||||
if (response?.status === 422) {
|
||||
form?.value?.setErrors(response._data?.errors)
|
||||
}
|
||||
else if (response._data?.ok) {
|
||||
useToast().add({
|
||||
title: 'Success',
|
||||
description: response._data.message,
|
||||
color: 'primary',
|
||||
})
|
||||
|
||||
if (auth.isLoggedIn) {
|
||||
await auth.fetchUser()
|
||||
await router.push('/')
|
||||
}
|
||||
else {
|
||||
await router.push('/login')
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UForm
|
||||
ref="form"
|
||||
:state="state"
|
||||
class="space-y-4"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<UFormField
|
||||
label="Email"
|
||||
name="email"
|
||||
>
|
||||
<UInput
|
||||
v-model="state.email"
|
||||
disabled
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField
|
||||
label="Password"
|
||||
name="password"
|
||||
>
|
||||
<UInput
|
||||
v-model="state.password"
|
||||
type="password"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField
|
||||
label="Confirm Password"
|
||||
name="password_confirmation"
|
||||
>
|
||||
<UInput
|
||||
v-model="state.password_confirmation"
|
||||
type="password"
|
||||
/>
|
||||
</UFormField>
|
||||
|
||||
<UButton
|
||||
block
|
||||
size="md"
|
||||
type="submit"
|
||||
:loading="resetStatus === 'pending'"
|
||||
icon="i-heroicon-lock-closed"
|
||||
>
|
||||
Change Password
|
||||
</UButton>
|
||||
</UForm>
|
||||
</template>
|
||||
@@ -1,40 +1,54 @@
|
||||
<script lang="ts" setup>
|
||||
const props = defineProps(["modelValue", "entity", "accept", "maxSize"]);
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
import type { IUploadResponse } from '~/types/account'
|
||||
|
||||
const { $storage } = useNuxtApp();
|
||||
interface Props {
|
||||
modelValue: string
|
||||
entity: string
|
||||
accept: string
|
||||
maxSize: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
const { $storage } = useNuxtApp()
|
||||
|
||||
const value = computed({
|
||||
get() {
|
||||
return props.modelValue;
|
||||
get(): string {
|
||||
return props.modelValue
|
||||
},
|
||||
set(value) {
|
||||
emit("update:modelValue", value);
|
||||
set(value: string) {
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
const inputRef = ref();
|
||||
const loading = ref(false);
|
||||
const inputRef = ref<HTMLInputElement>()
|
||||
const loading = ref<boolean>(false)
|
||||
|
||||
const onSelect = async (e: any) => {
|
||||
const file = e.target.files[0];
|
||||
e.target.value = null;
|
||||
const onSelect = async (e: Event) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
const file = target.files?.[0]
|
||||
if (!file) return
|
||||
target.value = ''
|
||||
|
||||
if (file.size > props.maxSize * 1024 * 1024) {
|
||||
return useToast().add({
|
||||
title: "File is too large.",
|
||||
color: "red",
|
||||
icon: "i-heroicons-exclamation-circle-solid",
|
||||
});
|
||||
title: 'File is too large.',
|
||||
color: 'error',
|
||||
icon: 'i-heroicons-exclamation-circle-solid',
|
||||
})
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
loading.value = true
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("image", file);
|
||||
const formData = new FormData()
|
||||
formData.append('image', file)
|
||||
|
||||
await $fetch<any>("upload", {
|
||||
method: "POST",
|
||||
await $fetch<IUploadResponse>('upload', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
params: {
|
||||
entity: props.entity,
|
||||
@@ -46,17 +60,18 @@ const onSelect = async (e: any) => {
|
||||
if (response.status !== 200) {
|
||||
useToast().add({
|
||||
icon: 'i-heroicons-exclamation-circle-solid',
|
||||
color: 'red',
|
||||
color: 'error',
|
||||
title: response._data?.message ?? response.statusText ?? 'Something went wrong',
|
||||
});
|
||||
} else if (response._data?.ok) {
|
||||
value.value = response._data?.path;
|
||||
})
|
||||
}
|
||||
else if (response._data?.ok) {
|
||||
value.value = response._data?.path
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
loading.value = false
|
||||
},
|
||||
});
|
||||
};
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -66,7 +81,6 @@ const onSelect = async (e: any) => {
|
||||
:src="$storage(value)"
|
||||
size="3xl"
|
||||
img-class="object-cover"
|
||||
:ui="{ rounded: 'rounded-lg' }"
|
||||
/>
|
||||
|
||||
<UTooltip
|
||||
@@ -76,10 +90,11 @@ const onSelect = async (e: any) => {
|
||||
>
|
||||
<UButton
|
||||
type="button"
|
||||
color="gray"
|
||||
color="neutral"
|
||||
variant="link"
|
||||
icon="i-heroicons-cloud-arrow-up"
|
||||
size="2xs"
|
||||
:ui="{ rounded: 'rounded-full' }"
|
||||
size="xs"
|
||||
:ui="{ base: 'rounded-full' }"
|
||||
:loading="loading"
|
||||
@click="inputRef.click()"
|
||||
/>
|
||||
@@ -91,10 +106,11 @@ const onSelect = async (e: any) => {
|
||||
>
|
||||
<UButton
|
||||
type="button"
|
||||
color="gray"
|
||||
color="neutral"
|
||||
variant="link"
|
||||
icon="i-heroicons-x-mark-20-solid"
|
||||
size="2xs"
|
||||
:ui="{ rounded: 'rounded-full' }"
|
||||
size="xs"
|
||||
:ui="{ base: 'rounded-full' }"
|
||||
:disabled="loading"
|
||||
@click="value = ''"
|
||||
/>
|
||||
@@ -105,7 +121,7 @@ const onSelect = async (e: any) => {
|
||||
class="hidden"
|
||||
:accept="accept"
|
||||
@change="onSelect"
|
||||
/>
|
||||
>
|
||||
</div>
|
||||
<div class="text-sm opacity-80">
|
||||
<div>Max upload size: {{ maxSize }}Mb</div>
|
||||
@@ -1,17 +1,24 @@
|
||||
<script lang="ts" setup>
|
||||
const modal = useModal()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal>
|
||||
<UCard>
|
||||
<template #header>
|
||||
<div class="text-2xl leading-tight font-black">Welcome to LaravelNuxt</div>
|
||||
<div class="text-2xl leading-tight font-black">
|
||||
Welcome to LaravelNuxt
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<USkeleton class="w-full h-60" />
|
||||
|
||||
<template #footer>
|
||||
<UButton label="Close" @click="modal.close" color="gray" />
|
||||
<UButton
|
||||
label="Close"
|
||||
color="gray"
|
||||
@click="modal.close"
|
||||
/>
|
||||
</template>
|
||||
</UCard>
|
||||
</UModal>
|
||||
33
nuxt/app/error.vue
Normal file
33
nuxt/app/error.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import type { NuxtError } from '#app'
|
||||
|
||||
const props = defineProps<{ error: NuxtError }>()
|
||||
|
||||
const handleError = () => clearError({ redirect: '/' })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UContainer class="py-5 flex items-center justify-center">
|
||||
<AppLogo />
|
||||
</UContainer>
|
||||
<UContainer class="flex-grow flex flex-col items-center justify-center space-y-5">
|
||||
<h1 class="text-9xl font-bold">
|
||||
{{ props.error?.statusCode }}
|
||||
</h1>
|
||||
<div>{{ props.error?.message }}</div>
|
||||
<div
|
||||
v-if="props.error?.statusCode >= 500"
|
||||
>
|
||||
{{ error?.stack }}
|
||||
</div>
|
||||
<div>
|
||||
<UButton
|
||||
color="neutral"
|
||||
size="xl"
|
||||
variant="outline"
|
||||
label="Go back"
|
||||
@click="handleError"
|
||||
/>
|
||||
</div>
|
||||
</UContainer>
|
||||
</template>
|
||||
2
nuxt/index.d.ts → nuxt/app/index.d.ts
vendored
2
nuxt/index.d.ts → nuxt/app/index.d.ts
vendored
@@ -4,4 +4,4 @@ declare module '#app' {
|
||||
}
|
||||
}
|
||||
|
||||
export { }
|
||||
export { }
|
||||
17
nuxt/app/layouts/default.vue
Normal file
17
nuxt/app/layouts/default.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div>
|
||||
<AppHeader />
|
||||
<UContainer class="flex justify-start mt-4">
|
||||
<AppNavigation />
|
||||
<UContainer
|
||||
as="main"
|
||||
class="flex-grow py-4 sm:py-7 flex flex-col"
|
||||
>
|
||||
<slot />
|
||||
</UContainer>
|
||||
</UContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
@@ -1,4 +1,4 @@
|
||||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
export default defineNuxtRouteMiddleware(() => {
|
||||
const nuxtApp = useNuxtApp()
|
||||
const auth = useAuthStore()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
export default defineNuxtRouteMiddleware(() => {
|
||||
const nuxtApp = useNuxtApp()
|
||||
const auth = useAuthStore()
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
export default defineNuxtRouteMiddleware(() => {
|
||||
const nuxtApp = useNuxtApp()
|
||||
const auth = useAuthStore()
|
||||
|
||||
if (auth.isLoggedIn && !auth.user.roles.includes('admin')) {
|
||||
return nuxtApp.runWithContext(() => {
|
||||
useToast().add({
|
||||
icon: "i-heroicons-exclamation-circle-solid",
|
||||
title: "Access denied.",
|
||||
color: "red",
|
||||
});
|
||||
icon: 'i-heroicons-exclamation-circle-solid',
|
||||
title: 'Access denied.',
|
||||
color: 'error',
|
||||
})
|
||||
|
||||
return navigateTo('/')
|
||||
})
|
||||
@@ -1,14 +1,14 @@
|
||||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
export default defineNuxtRouteMiddleware(() => {
|
||||
const nuxtApp = useNuxtApp()
|
||||
const auth = useAuthStore()
|
||||
|
||||
if (auth.isLoggedIn && !auth.user.roles.includes('user')) {
|
||||
return nuxtApp.runWithContext(() => {
|
||||
useToast().add({
|
||||
icon: "i-heroicons-exclamation-circle-solid",
|
||||
title: "Access denied.",
|
||||
color: "red",
|
||||
});
|
||||
icon: 'i-heroicons-exclamation-circle-solid',
|
||||
title: 'Access denied.',
|
||||
color: 'error',
|
||||
})
|
||||
|
||||
return navigateTo('/')
|
||||
})
|
||||
@@ -1,14 +1,14 @@
|
||||
export default defineNuxtRouteMiddleware((to, from) => {
|
||||
export default defineNuxtRouteMiddleware(() => {
|
||||
const nuxtApp = useNuxtApp()
|
||||
const auth = useAuthStore()
|
||||
|
||||
if (auth.isLoggedIn && auth.user.must_verify_email) {
|
||||
return nuxtApp.runWithContext(() => {
|
||||
useToast().add({
|
||||
icon: "i-heroicons-exclamation-circle-solid",
|
||||
title: "Please confirm your email.",
|
||||
color: "red",
|
||||
});
|
||||
icon: 'i-heroicons-exclamation-circle-solid',
|
||||
title: 'Please confirm your email.',
|
||||
color: 'error',
|
||||
})
|
||||
|
||||
return navigateTo('/account/general')
|
||||
})
|
||||
30
nuxt/app/pages/account.vue
Normal file
30
nuxt/app/pages/account.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts" setup>
|
||||
definePageMeta({
|
||||
middleware: ['auth'],
|
||||
})
|
||||
|
||||
const links = [
|
||||
[
|
||||
{
|
||||
label: 'Account',
|
||||
icon: 'i-heroicons-user',
|
||||
to: '/account/general',
|
||||
},
|
||||
{
|
||||
label: 'Devices',
|
||||
icon: 'i-heroicons-device-phone-mobile',
|
||||
to: '/account/devices',
|
||||
},
|
||||
],
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UNavigationMenu
|
||||
:items="links"
|
||||
class="border-b border-gray-200 dark:border-gray-800 mb-4"
|
||||
/>
|
||||
<NuxtPage class="col-span-10 md:col-span-8" />
|
||||
</div>
|
||||
</template>
|
||||
5
nuxt/app/pages/account/devices.vue
Normal file
5
nuxt/app/pages/account/devices.vue
Normal file
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<UCard>
|
||||
<AccountDeviceTable />
|
||||
</UCard>
|
||||
</template>
|
||||
@@ -1,8 +1,11 @@
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<template>
|
||||
<UCard :ui="{ body: { base: 'grid grid-cols-12 gap-6 md:gap-8' } }">
|
||||
<UCard :ui="{ body: 'grid grid-cols-12 gap-6 md:gap-8' }">
|
||||
<div class="col-span-12 lg:col-span-4">
|
||||
<div class="text-lg font-semibold mb-2">Profile information</div>
|
||||
<div class="text-lg font-semibold mb-2">
|
||||
Profile information
|
||||
</div>
|
||||
<div class="text-sm opacity-80">
|
||||
Update your account's profile information and email address.
|
||||
</div>
|
||||
@@ -10,9 +13,11 @@
|
||||
<div class="col-span-12 lg:col-span-8">
|
||||
<AccountUpdateProfile />
|
||||
</div>
|
||||
<UDivider class="col-span-12" />
|
||||
<USeparator class="col-span-12" />
|
||||
<div class="col-span-12 lg:col-span-4">
|
||||
<div class="text-lg font-semibold mb-2">Update Password</div>
|
||||
<div class="text-lg font-semibold mb-2">
|
||||
Update Password
|
||||
</div>
|
||||
<div class="text-sm opacity-80">
|
||||
Ensure your account is using a long, random password to stay secure.
|
||||
</div>
|
||||
9
nuxt/app/pages/account/index.vue
Normal file
9
nuxt/app/pages/account/index.vue
Normal file
@@ -0,0 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
definePageMeta({
|
||||
redirect: '/account/general',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div />
|
||||
</template>
|
||||
29
nuxt/app/pages/forgot-password/index.vue
Normal file
29
nuxt/app/pages/forgot-password/index.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import AuthForgotPasswordForm from '~/components/auth/AuthForgotPasswordForm.vue'
|
||||
|
||||
definePageMeta({ middleware: ['guest'], layout: 'auth' })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto flex min-h-screen w-full items-center justify-center">
|
||||
<UCard class="w-96">
|
||||
<template #header>
|
||||
<div class="space-y-4 text-center ">
|
||||
<h1 class="text-2xl font-bold">
|
||||
Forgot Password
|
||||
</h1>
|
||||
<p class="text-sm">
|
||||
Remember your password? <NuxtLink
|
||||
to="/login"
|
||||
class="text-primary hover:text-primary-300 font-bold"
|
||||
>
|
||||
Login here
|
||||
</NuxtLink>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
<AuthForgotPasswordForm />
|
||||
<!-- <UDivider label="OR" class=" my-4"/> -->
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,9 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ middleware: ['auth'] })
|
||||
|
||||
const modal = useModal();
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
23
nuxt/app/pages/login/index.vue
Normal file
23
nuxt/app/pages/login/index.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import AuthLoginForm from '~/components/auth/AuthLoginForm.vue'
|
||||
|
||||
definePageMeta({ middleware: ['guest'], layout: 'auth' })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto flex min-h-screen w-full items-center justify-center">
|
||||
<UCard class="w-96">
|
||||
<template #header>
|
||||
<h1 class="text-center text-2xl font-bold">
|
||||
Login
|
||||
</h1>
|
||||
</template>
|
||||
|
||||
<AuthLoginForm />
|
||||
|
||||
<!-- <UDivider label="OR" class=" my-4"/> -->
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,10 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
<div />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ middleware: ['auth'], layout: 'auth' });
|
||||
definePageMeta({ middleware: ['auth'], layout: 'auth' })
|
||||
const auth = useAuthStore()
|
||||
auth.logout()
|
||||
</script>
|
||||
21
nuxt/app/pages/password-reset/[token].vue
Normal file
21
nuxt/app/pages/password-reset/[token].vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import AuthResetPasswordForm from '~/components/auth/AuthResetPasswordForm.vue'
|
||||
|
||||
definePageMeta({ middleware: ['guest'], layout: 'auth' })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mx-auto flex min-h-screen w-full items-center justify-center">
|
||||
<UCard class="w-96">
|
||||
<template #header>
|
||||
<h1 class="text-center text-2xl font-bold">
|
||||
Reset Password
|
||||
</h1>
|
||||
</template>
|
||||
<AuthResetPasswordForm />
|
||||
<!-- <UDivider label="OR" class=" my-4"/> -->
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ofetch } from 'ofetch'
|
||||
import type { FetchOptions } from 'ofetch';
|
||||
import type { FetchOptions } from 'ofetch'
|
||||
|
||||
export default defineNuxtPlugin({
|
||||
name: 'app',
|
||||
@@ -12,8 +12,8 @@ export default defineNuxtPlugin({
|
||||
nuxtApp.provide('storage', (path: string): string => {
|
||||
if (!path) return ''
|
||||
|
||||
return path.startsWith('http://') || path.startsWith('https://') ?
|
||||
path
|
||||
return path.startsWith('http://') || path.startsWith('https://')
|
||||
? path
|
||||
: config.public.storageBase + path
|
||||
})
|
||||
|
||||
@@ -21,45 +21,45 @@ export default defineNuxtPlugin({
|
||||
// Initial headers with Accept
|
||||
const initialHeaders = {
|
||||
...headers,
|
||||
'Accept': 'application/json',
|
||||
};
|
||||
Accept: 'application/json',
|
||||
}
|
||||
|
||||
// Conditionally add server-specific headers
|
||||
if (process.server) {
|
||||
if (import.meta.server) {
|
||||
const serverHeaders = {
|
||||
'referer': useRequestURL().toString(),
|
||||
referer: useRequestURL().toString(),
|
||||
...useRequestHeaders(['x-forwarded-for', 'user-agent', 'referer']),
|
||||
};
|
||||
Object.assign(initialHeaders, serverHeaders);
|
||||
}
|
||||
Object.assign(initialHeaders, serverHeaders)
|
||||
}
|
||||
|
||||
// Conditionally add authorization header if logged in
|
||||
if (auth.isLoggedIn) {
|
||||
const authHeaders = {
|
||||
'Authorization': `Bearer ${auth.token}`,
|
||||
};
|
||||
Object.assign(initialHeaders, authHeaders);
|
||||
Authorization: `Bearer ${auth.token}`,
|
||||
}
|
||||
Object.assign(initialHeaders, authHeaders)
|
||||
}
|
||||
|
||||
return initialHeaders;
|
||||
return initialHeaders
|
||||
}
|
||||
|
||||
function buildBaseURL(baseURL: string): string {
|
||||
if (baseURL) return baseURL;
|
||||
if (baseURL) return baseURL
|
||||
|
||||
return process.server ?
|
||||
config.apiLocal + config.public.apiPrefix
|
||||
: config.public.apiBase + config.public.apiPrefix;
|
||||
return import.meta.server
|
||||
? config.apiLocal + config.public.apiPrefix
|
||||
: config.public.apiBase + config.public.apiPrefix
|
||||
}
|
||||
|
||||
function buildSecureMethod(options: FetchOptions): void {
|
||||
if (process.server) return;
|
||||
if (import.meta.server) return
|
||||
|
||||
const method = options.method?.toLowerCase() ?? 'get'
|
||||
|
||||
if (options.body instanceof FormData && method === 'put') {
|
||||
options.method = 'POST';
|
||||
options.body.append('_method', 'PUT');
|
||||
options.method = 'POST'
|
||||
options.body.append('_method', 'PUT')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ export default defineNuxtPlugin({
|
||||
return !baseURL
|
||||
&& !path.startsWith('/_nuxt')
|
||||
&& !path.startsWith('http://')
|
||||
&& !path.startsWith('https://');
|
||||
&& !path.startsWith('https://')
|
||||
}
|
||||
|
||||
globalThis.$fetch = ofetch.create({
|
||||
@@ -76,18 +76,18 @@ export default defineNuxtPlugin({
|
||||
onRequest({ request, options }) {
|
||||
if (!isRequestWithAuth(options.baseURL ?? '', request.toString())) return
|
||||
|
||||
options.credentials = 'include';
|
||||
options.credentials = 'include'
|
||||
|
||||
options.baseURL = buildBaseURL(options.baseURL ?? '');
|
||||
options.headers = buildHeaders(options.headers);
|
||||
options.baseURL = buildBaseURL(options.baseURL ?? '')
|
||||
options.headers = buildHeaders(options.headers)
|
||||
|
||||
buildSecureMethod(options);
|
||||
buildSecureMethod(options)
|
||||
},
|
||||
|
||||
onRequestError({ error }) {
|
||||
if (process.server) return;
|
||||
if (import.meta.server) return
|
||||
|
||||
if (error.name === 'AbortError') return;
|
||||
if (error.name === 'AbortError') return
|
||||
|
||||
useToast().add({
|
||||
icon: 'i-heroicons-exclamation-circle-solid',
|
||||
@@ -103,15 +103,16 @@ export default defineNuxtPlugin({
|
||||
auth.user = {} as User
|
||||
}
|
||||
|
||||
if (process.client) {
|
||||
if (import.meta.client) {
|
||||
useToast().add({
|
||||
title: 'Please log in to continue',
|
||||
icon: 'i-heroicons-exclamation-circle-solid',
|
||||
color: 'primary',
|
||||
})
|
||||
}
|
||||
} else if (response.status !== 422) {
|
||||
if (process.client) {
|
||||
}
|
||||
else if (response.status !== 422) {
|
||||
if (import.meta.client) {
|
||||
useToast().add({
|
||||
icon: 'i-heroicons-exclamation-circle-solid',
|
||||
color: 'red',
|
||||
@@ -119,11 +120,11 @@ export default defineNuxtPlugin({
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
} as FetchOptions)
|
||||
|
||||
if (auth.isLoggedIn) {
|
||||
await auth.fetchUser();
|
||||
await auth.fetchUser()
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,30 +1,31 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import type { IAccountLogoutResponse } from '~/types/account'
|
||||
|
||||
export type User = {
|
||||
ulid: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatar: string;
|
||||
must_verify_email: boolean;
|
||||
has_password: boolean;
|
||||
roles: string[];
|
||||
providers: string[];
|
||||
ulid: string
|
||||
name: string
|
||||
email: string
|
||||
avatar: string
|
||||
must_verify_email: boolean
|
||||
has_password: boolean
|
||||
roles: string[]
|
||||
providers: string[]
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const config = useRuntimeConfig()
|
||||
const nuxtApp = useNuxtApp()
|
||||
|
||||
const user = ref(<User>{});
|
||||
const user = ref(<User>{})
|
||||
const token = useCookie('token', {
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: config.public.apiBase.startsWith('https://'),
|
||||
maxAge: 60 * 60 * 24 * 365
|
||||
maxAge: 60 * 60 * 24 * 365,
|
||||
})
|
||||
const isLoggedIn = computed(() => !!token.value)
|
||||
|
||||
const { refresh: logout } = useFetch<any>('logout', {
|
||||
const { refresh: logout } = useFetch<IAccountLogoutResponse>('logout', {
|
||||
method: 'POST',
|
||||
immediate: false,
|
||||
onResponse({ response }) {
|
||||
@@ -36,16 +37,16 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
return navigateTo('/')
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const { refresh: fetchUser } = useFetch<any>('user', {
|
||||
const { refresh: fetchUser } = useFetch<{ user: User }>('user', {
|
||||
immediate: false,
|
||||
onResponse({ response }) {
|
||||
if (response.status === 200) {
|
||||
user.value = response._data.user
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return { user, isLoggedIn, logout, fetchUser, token }
|
||||
51
nuxt/app/types/account.d.ts
vendored
Normal file
51
nuxt/app/types/account.d.ts
vendored
Normal file
@@ -0,0 +1,51 @@
|
||||
import type { ButtonProps } from '#ui/components/Button.vue'
|
||||
|
||||
export interface IAccountLoginResponse {
|
||||
ok: boolean
|
||||
token: string
|
||||
}
|
||||
|
||||
export interface IAccountLogoutResponse {
|
||||
ok: boolean
|
||||
}
|
||||
|
||||
export interface IAccountProviderData {
|
||||
provider: string
|
||||
token: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
export interface IAccountChangePasswordResponse {
|
||||
ok: boolean
|
||||
}
|
||||
|
||||
export interface IAccountResetPasswordResponse {
|
||||
ok: boolean
|
||||
}
|
||||
|
||||
export interface IVerificationNotificationResponse {
|
||||
ok: boolean
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface IAccountUpdateResponse {
|
||||
ok: boolean
|
||||
}
|
||||
|
||||
export interface IForgotPasswordResponse {
|
||||
ok: boolean
|
||||
message: string
|
||||
}
|
||||
|
||||
export interface IAccountProvider {
|
||||
name: string
|
||||
icon: string
|
||||
color: ButtonProps['color']
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
export interface IUploadResponse {
|
||||
ok: boolean
|
||||
path: string
|
||||
message?: string
|
||||
}
|
||||
12
nuxt/app/types/device.d.ts
vendored
Normal file
12
nuxt/app/types/device.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
export type Device = {
|
||||
name: string
|
||||
ip: string
|
||||
last_used_at: string
|
||||
is_current: boolean
|
||||
hash: string
|
||||
}
|
||||
|
||||
export interface IDevicesResponse {
|
||||
ok: boolean
|
||||
devices: Device[]
|
||||
}
|
||||
0
nuxt/app/types/responses.d.ts
vendored
Normal file
0
nuxt/app/types/responses.d.ts
vendored
Normal file
@@ -1,25 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
#__nuxt {
|
||||
@apply min-h-screen flex flex-col;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
html {
|
||||
@apply text-gray-800 dark:bg-gray-900;
|
||||
}
|
||||
|
||||
html.dark {
|
||||
@apply text-gray-50 bg-gray-900;
|
||||
}
|
||||
|
||||
button, a, [role="button"] {
|
||||
@apply transition-colors;
|
||||
}
|
||||
|
||||
input[type="checkbox"], input[type="radio"] {
|
||||
@apply transition-all;
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const links = [{
|
||||
label: 'Documentation',
|
||||
icon: 'i-heroicons-book-open',
|
||||
to: 'https://ui.nuxt.com/getting-started',
|
||||
}, {
|
||||
label: 'Pro',
|
||||
icon: 'i-heroicons-square-3-stack-3d',
|
||||
to: 'https://ui.nuxt.com/pro',
|
||||
}, {
|
||||
label: 'Releases',
|
||||
icon: 'i-heroicons-rocket-launch',
|
||||
to: 'https://github.com/nuxt/ui/releases',
|
||||
target: '_blank',
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UHeader :links="links">
|
||||
<template #logo>
|
||||
<Logo class="h-6 w-auto" />
|
||||
</template>
|
||||
|
||||
<template #right>
|
||||
<UColorModeButton />
|
||||
<UserDropdown />
|
||||
</template>
|
||||
|
||||
<template #panel>
|
||||
<UNavigationTree :links="links" />
|
||||
</template>
|
||||
</UHeader>
|
||||
</template>
|
||||
@@ -1,5 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<span class="text-primary">Nuxt</span> Breeze
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,28 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const links = [
|
||||
{
|
||||
label: 'Account',
|
||||
to: '/account',
|
||||
icon: 'i-heroicons-user-solid',
|
||||
},
|
||||
{
|
||||
label: 'Logout',
|
||||
to: '/logout',
|
||||
icon: 'i-heroicons-arrow-left-on-rectangle',
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPopover>
|
||||
<UButton icon="i-heroicons-user-solid" color="gray" variant="ghost" />
|
||||
<template #panel>
|
||||
<div class="p-4">
|
||||
<UNavigationLinks :links="links" />
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,102 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
const form = ref();
|
||||
const auth = useAuthStore();
|
||||
|
||||
const state = reactive({
|
||||
current_password: "",
|
||||
password: "",
|
||||
password_confirmation: "",
|
||||
});
|
||||
|
||||
const { refresh: onSubmit, status: accountPasswordStatus } = useFetch<any>("account/password", {
|
||||
method: "POST",
|
||||
body: state,
|
||||
immediate: false,
|
||||
watch: false,
|
||||
async onResponse({ response }) {
|
||||
if (response?.status === 422) {
|
||||
form.value.setErrors(response._data?.errors);
|
||||
} else if (response._data?.ok) {
|
||||
useToast().add({
|
||||
icon: "i-heroicons-check-circle-20-solid",
|
||||
title: "The password was successfully updated.",
|
||||
color: "emerald",
|
||||
});
|
||||
|
||||
state.current_password = "";
|
||||
state.password = "";
|
||||
state.password_confirmation = "";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const { refresh: sendResetPasswordEmail, status: resetPasswordEmailStatus } = useFetch<any>("verification-notification", {
|
||||
method: "POST",
|
||||
body: { email: auth.user.email },
|
||||
immediate: false,
|
||||
watch: false,
|
||||
onResponse({ response }) {
|
||||
if (response._data?.ok) {
|
||||
useToast().add({
|
||||
icon: "i-heroicons-check-circle-20-solid",
|
||||
title: "A link to reset your password has been sent to your email.",
|
||||
color: "emerald",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<UForm
|
||||
v-if="auth.user.has_password"
|
||||
ref="form"
|
||||
:state="state"
|
||||
@submit="onSubmit"
|
||||
class="space-y-4"
|
||||
>
|
||||
<UFormGroup label="Current Password" name="current_password" required>
|
||||
<UInput v-model="state.current_password" type="password" autocomplete="off" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup
|
||||
label="New Password"
|
||||
name="password"
|
||||
hint="min 8 characters"
|
||||
:ui="{ hint: 'text-xs text-gray-500 dark:text-gray-400' }"
|
||||
required
|
||||
>
|
||||
<UInput v-model="state.password" type="password" autocomplete="off" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Repeat Password" name="password_confirmation" required>
|
||||
<UInput
|
||||
v-model="state.password_confirmation"
|
||||
type="password"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</UFormGroup>
|
||||
|
||||
<div class="pt-2">
|
||||
<UButton type="submit" label="Save" :loading="accountPasswordStatus === 'pending'" />
|
||||
</div>
|
||||
</UForm>
|
||||
|
||||
<UAlert
|
||||
v-else
|
||||
icon="i-heroicons-information-circle-20-solid"
|
||||
title="Send a link to your email to reset your password."
|
||||
description="To create a password for your account, you must go through the password recovery process."
|
||||
:actions="[
|
||||
{
|
||||
label: 'Send link to Email',
|
||||
variant: 'solid',
|
||||
color: 'gray',
|
||||
loading: resetPasswordEmailStatus === 'pending',
|
||||
click: sendResetPasswordEmail,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,27 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
error: Object,
|
||||
});
|
||||
|
||||
const handleError = () => clearError({ redirect: "/" });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UContainer class="py-5 flex items-center justify-center">
|
||||
<AppLogo />
|
||||
</UContainer>
|
||||
<UContainer class="flex-grow flex flex-col items-center justify-center space-y-5">
|
||||
<h1 class="text-9xl font-bold">{{ error?.statusCode }}</h1>
|
||||
<div>{{ error?.message }}</div>
|
||||
<div v-if="error?.statusCode >= 500" v-html="error?.stack"></div>
|
||||
<div>
|
||||
<UButton
|
||||
@click="handleError"
|
||||
color="gray"
|
||||
size="xl"
|
||||
variant="ghost"
|
||||
label="Go back"
|
||||
/>
|
||||
</div>
|
||||
</UContainer>
|
||||
</template>
|
||||
@@ -1,17 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<Header />
|
||||
<UPage>
|
||||
<template #left>
|
||||
<UAside class="lg:static">
|
||||
<Navigation />
|
||||
</UAside>
|
||||
</template>
|
||||
<UPageBody>
|
||||
<UContainer>
|
||||
<slot />
|
||||
</UContainer>
|
||||
</UPageBody>
|
||||
</UPage>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,27 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
definePageMeta({
|
||||
middleware: ["auth"],
|
||||
});
|
||||
|
||||
const links = [
|
||||
[
|
||||
{
|
||||
label: "Account",
|
||||
icon: "i-heroicons-user",
|
||||
to: "/account/general",
|
||||
},
|
||||
{
|
||||
label: "Devices",
|
||||
icon: "i-heroicons-device-phone-mobile",
|
||||
to: "/account/devices",
|
||||
},
|
||||
],
|
||||
];
|
||||
</script>
|
||||
<template>
|
||||
<UHorizontalNavigation
|
||||
:links="links"
|
||||
class="border-b border-gray-200 dark:border-gray-800 mb-4"
|
||||
/>
|
||||
<NuxtPage class="col-span-10 md:col-span-8" />
|
||||
</template>
|
||||
@@ -1,86 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
const dayjs = useDayjs();
|
||||
const auth = useAuthStore();
|
||||
const loading = ref(false);
|
||||
const devices = ref([]);
|
||||
|
||||
async function fetchData() {
|
||||
loading.value = true;
|
||||
|
||||
const response = await $fetch<any>("devices");
|
||||
|
||||
if (response.ok) {
|
||||
devices.value = response.devices;
|
||||
}
|
||||
|
||||
loading.value = false;
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
key: "name",
|
||||
label: "Device",
|
||||
},
|
||||
{
|
||||
key: "last_used_at",
|
||||
label: "Last used at",
|
||||
class: "max-w-[9rem] w-[9rem] min-w-[9rem]",
|
||||
},
|
||||
{
|
||||
key: "actions",
|
||||
},
|
||||
];
|
||||
|
||||
const items = (row: any) => [
|
||||
[
|
||||
{
|
||||
label: "Delete",
|
||||
icon: "i-heroicons-trash-20-solid",
|
||||
click: async () => {
|
||||
await $fetch<any>("devices/disconnect", {
|
||||
method: "POST",
|
||||
body: {
|
||||
hash: row.hash,
|
||||
},
|
||||
async onResponse({ response }) {
|
||||
if (response._data?.ok) {
|
||||
await fetchData();
|
||||
await auth.fetchUser();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
if (process.client) {
|
||||
fetchData();
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<UCard :ui="{ body: { padding: 'p-0' } }">
|
||||
<ClientOnly>
|
||||
<UTable :rows="devices" :columns="columns" size="lg" :loading="loading">
|
||||
<template #name-data="{ row }">
|
||||
<div class="font-semibold">
|
||||
{{ row.name }}
|
||||
<UBadge v-if="row.is_current" label="active" color="emerald" variant="soft" size="xs" class="ms-1" />
|
||||
</div>
|
||||
<div class="font-medium text-sm">IP: {{ row.ip }}</div>
|
||||
</template>
|
||||
<template #last_used_at-data="{ row }">
|
||||
{{ dayjs(row.last_used_at).fromNow() }}
|
||||
</template>
|
||||
<template #actions-data="{ row }">
|
||||
<div class="flex justify-end">
|
||||
<UDropdown :items="items(row)">
|
||||
<UButton :disabled="row.is_current" color="gray" variant="ghost"
|
||||
icon="i-heroicons-ellipsis-horizontal-20-solid" />
|
||||
</UDropdown>
|
||||
</div>
|
||||
</template>
|
||||
</UTable>
|
||||
</ClientOnly>
|
||||
</UCard>
|
||||
</template>
|
||||
@@ -1,6 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
definePageMeta({
|
||||
redirect: "/account/general",
|
||||
});
|
||||
</script>
|
||||
<template></template>
|
||||
@@ -1,62 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ middleware: ['guest'], layout: 'auth' })
|
||||
|
||||
const form = ref();
|
||||
|
||||
const state = reactive({
|
||||
email: "",
|
||||
});
|
||||
|
||||
const { refresh: onSubmit, status: forgotStatus } = useFetch<any>("forgot-password", {
|
||||
method: "POST",
|
||||
body: state,
|
||||
immediate: false,
|
||||
watch: false,
|
||||
async onResponse({ response }) {
|
||||
if (response?.status === 422) {
|
||||
form.value.setErrors(response._data?.errors);
|
||||
} else if (response._data?.ok) {
|
||||
useToast().add({
|
||||
title: "Success",
|
||||
description: response._data.message,
|
||||
color: "emerald",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UMain>
|
||||
<UPage>
|
||||
<div class="mx-auto flex min-h-screen w-full items-center justify-center">
|
||||
<UCard class="w-96">
|
||||
<template #header>
|
||||
<div class="space-y-4 text-center ">
|
||||
<h1 class="text-2xl font-bold">
|
||||
Forgot Password
|
||||
</h1>
|
||||
<p class="text-sm">
|
||||
Remember your password? <NuxtLink to="/login" class="text-primary hover:text-primary-300 font-semibold">
|
||||
Login here
|
||||
</NuxtLink>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<UForm ref="form" :state="state" class="space-y-8" @submit="onSubmit">
|
||||
<UFormGroup label="Email" name="email">
|
||||
<UInput v-model="state.email" />
|
||||
</UFormGroup>
|
||||
|
||||
<UButton block size="md" type="submit" :loading="forgotStatus === 'pending'" icon="i-heroicons-envelope">
|
||||
Reset Password
|
||||
</UButton>
|
||||
</UForm>
|
||||
|
||||
<!-- <UDivider label="OR" class=" my-4"/> -->
|
||||
</UCard>
|
||||
</div>
|
||||
</UPage>
|
||||
</UMain>
|
||||
</template>
|
||||
@@ -1,103 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ middleware: ['guest'], layout: 'auth' })
|
||||
|
||||
const config = useRuntimeConfig();
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
const form = ref();
|
||||
|
||||
type Provider = {
|
||||
name: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
loading?: boolean;
|
||||
};
|
||||
|
||||
const state = reactive({
|
||||
email: "",
|
||||
password: "",
|
||||
remember: false,
|
||||
});
|
||||
|
||||
const { refresh: onSubmit, status: loginStatus } = useFetch<any>("login", {
|
||||
method: "POST",
|
||||
body: state,
|
||||
immediate: false,
|
||||
watch: false,
|
||||
async onResponse({ response }) {
|
||||
if (response?.status === 422) {
|
||||
form.value.setErrors(response._data?.errors);
|
||||
} else if (response._data?.ok) {
|
||||
auth.token = response._data.token;
|
||||
|
||||
await auth.fetchUser();
|
||||
await router.push("/");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const providers = ref<{ [key: string]: Provider }>(config.public.providers);
|
||||
|
||||
async function handleMessage(event: { data: any }): Promise<void> {
|
||||
const provider = event.data.provider as string;
|
||||
|
||||
if (Object.keys(providers.value).includes(provider) && event.data.token) {
|
||||
providers.value[provider].loading = false;
|
||||
auth.token = event.data.token;
|
||||
|
||||
await auth.fetchUser();
|
||||
await router.push("/");
|
||||
} else if (event.data.message) {
|
||||
useToast().add({
|
||||
icon: "i-heroicons-exclamation-circle-solid",
|
||||
color: "red",
|
||||
title: event.data.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UMain>
|
||||
<UPage>
|
||||
<div class="mx-auto flex min-h-screen w-full items-center justify-center">
|
||||
<UCard class="w-96">
|
||||
<template #header>
|
||||
<h1 class="text-center text-2xl font-bold">
|
||||
Login
|
||||
</h1>
|
||||
</template>
|
||||
|
||||
<UForm ref="form" :state="state" class="space-y-4" @submit="onSubmit">
|
||||
<UFormGroup label="Email" name="email">
|
||||
<UInput v-model="state.email" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Password" name="password">
|
||||
<UInput v-model="state.password" type="password" />
|
||||
</UFormGroup>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<UCheckbox id="remember-me" v-model="state.remember" label="Remember me" name="remember-me" />
|
||||
|
||||
<div class="text-sm leading-6">
|
||||
<NuxtLink to="/forgot-password" class="text-primary hover:text-primary-300 font-semibold">
|
||||
Forgot
|
||||
password?
|
||||
</NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UButton block size="md" type="submit" :loading="loginStatus === 'pending'" icon="i-heroicons-arrow-right-on-rectangle">
|
||||
Login
|
||||
</UButton>
|
||||
</UForm>
|
||||
|
||||
<!-- <UDivider label="OR" class=" my-4"/> -->
|
||||
</UCard>
|
||||
</div>
|
||||
</UPage>
|
||||
</UMain>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,78 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ middleware: ['guest'], layout: 'auth' })
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
const auth = useAuthStore();
|
||||
const form = ref();
|
||||
|
||||
const state = reactive({
|
||||
email: route.query.email as string,
|
||||
token: route.params.token,
|
||||
password: "",
|
||||
password_confirmation: "",
|
||||
});
|
||||
|
||||
const { refresh: onSubmit, status: resetStatus } = useFetch<any>("reset-password", {
|
||||
method: "POST",
|
||||
body: state,
|
||||
immediate: false,
|
||||
watch: false,
|
||||
async onResponse({ response }) {
|
||||
if (response?.status === 422) {
|
||||
form.value.setErrors(response._data?.errors);
|
||||
} else if (response._data?.ok) {
|
||||
useToast().add({
|
||||
title: "Success",
|
||||
description: response._data.message,
|
||||
color: "emerald",
|
||||
});
|
||||
|
||||
if (auth.isLoggedIn) {
|
||||
await auth.fetchUser();
|
||||
await router.push("/");
|
||||
} else {
|
||||
await router.push("/login");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UMain>
|
||||
<UPage>
|
||||
<div class="mx-auto flex min-h-screen w-full items-center justify-center">
|
||||
<UCard class="w-96">
|
||||
<template #header>
|
||||
<h1 class="text-center text-2xl font-bold">
|
||||
Reset Password
|
||||
</h1>
|
||||
</template>
|
||||
|
||||
<UForm ref="form" :state="state" class="space-y-4" @submit="onSubmit">
|
||||
<UFormGroup label="Email" name="email">
|
||||
<UInput v-model="state.email" disabled />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Password" name="password">
|
||||
<UInput v-model="state.password" type="password" />
|
||||
</UFormGroup>
|
||||
|
||||
<UFormGroup label="Confirm Password" name="password_confirmation">
|
||||
<UInput v-model="state.password_confirmation" type="password" />
|
||||
</UFormGroup>
|
||||
|
||||
<UButton block size="md" type="submit" :loading="resetStatus === 'pending'" icon="i-heroicon-lock-closed">
|
||||
Change Password
|
||||
</UButton>
|
||||
</UForm>
|
||||
|
||||
<!-- <UDivider label="OR" class=" my-4"/> -->
|
||||
</UCard>
|
||||
</div>
|
||||
</UPage>
|
||||
</UMain>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
Reference in New Issue
Block a user