Init
commit
aac376bb56
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Nuxt dev/build outputs
|
||||||
|
.output
|
||||||
|
.data
|
||||||
|
.nuxt
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Node dependencies
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
.DS_Store
|
||||||
|
.fleet
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Local env files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
# Nuxt 3 Minimal Starter
|
||||||
|
|
||||||
|
Look at the [Nuxt 3 documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Make sure to install the dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Server
|
||||||
|
|
||||||
|
Start the development server on `http://localhost:3000`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm run dev
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn dev
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production
|
||||||
|
|
||||||
|
Build the application for production:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm run build
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn build
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Locally preview production build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm run preview
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm run preview
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn preview
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export default defineAppConfig({
|
||||||
|
ui: {
|
||||||
|
primary: 'sky',
|
||||||
|
gray: 'cool',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<NuxtLayout>
|
||||||
|
<NuxtPage />
|
||||||
|
</NuxtLayout>
|
||||||
|
|
||||||
|
<UNotifications />
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { NavItem } from '@nuxt/content/dist/runtime/types'
|
||||||
|
|
||||||
|
const navigation = inject<Ref<NavItem[]>>('navigation')
|
||||||
|
|
||||||
|
const links = [{
|
||||||
|
label: 'Documentation',
|
||||||
|
icon: 'i-heroicons-book-open',
|
||||||
|
to: '/getting-started',
|
||||||
|
}, {
|
||||||
|
label: 'Pro',
|
||||||
|
icon: 'i-heroicons-square-3-stack-3d',
|
||||||
|
to: '/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 />
|
||||||
|
|
||||||
|
<UButton to="https://github.com/nuxt/ui" target="_blank" icon="i-simple-icons-github" color="gray" variant="ghost" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #panel>
|
||||||
|
<UNavigationTree :links="mapContentNavigation(navigation)" />
|
||||||
|
</template>
|
||||||
|
</UHeader>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<span class="text-primary">Nuxt</span> Breeze
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
const links = [
|
||||||
|
{
|
||||||
|
label: 'Company',
|
||||||
|
icon: 'i-heroicons-building-office-2',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
label: 'Overview',
|
||||||
|
to: '/partner/overview',
|
||||||
|
icon: 'i-heroicons-eye',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Add Company',
|
||||||
|
to: '/pro/components/docs/docs-search',
|
||||||
|
icon: 'i-heroicons-plus-circle',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'People',
|
||||||
|
to: '/pro/components/docs/docs-search-button',
|
||||||
|
icon: 'i-heroicons-user-group',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UContainer>
|
||||||
|
<UNavigationTree :links="links" />
|
||||||
|
</UContainer>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,112 @@
|
||||||
|
export interface User {
|
||||||
|
name: string
|
||||||
|
email?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginCredentials {
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
remember?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisterCredentials {
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
password_confirmation: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResetPasswordCredentials {
|
||||||
|
email: string
|
||||||
|
password: string
|
||||||
|
password_confirmation: string
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value is initialized in: ~/plugins/auth.ts
|
||||||
|
export function useUser<T = User>() {
|
||||||
|
return useState<T | undefined | null>('user', () => undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAuth<T = User>() {
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const user = useUser<T>()
|
||||||
|
const isLoggedIn = computed(() => !!user.value)
|
||||||
|
|
||||||
|
async function refresh() {
|
||||||
|
try {
|
||||||
|
user.value = await fetchCurrentUser()
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
user.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function login(credentials: LoginCredentials) {
|
||||||
|
if (isLoggedIn.value) { return }
|
||||||
|
|
||||||
|
await $larafetch('/login', { method: 'post', body: credentials })
|
||||||
|
await refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function register(credentials: RegisterCredentials) {
|
||||||
|
await $larafetch('/register', { method: 'post', body: credentials })
|
||||||
|
await refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resendEmailVerification() {
|
||||||
|
return await $larafetch<{ status: string }>(
|
||||||
|
'/email/verification-notification',
|
||||||
|
{
|
||||||
|
method: 'post',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
if (!isLoggedIn.value) { return }
|
||||||
|
|
||||||
|
await $larafetch('/logout', { method: 'post' })
|
||||||
|
user.value = null
|
||||||
|
|
||||||
|
await router.push('/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function forgotPassword(email: string) {
|
||||||
|
return await $larafetch<{ status: string }>('/forgot-password', {
|
||||||
|
method: 'post',
|
||||||
|
body: { email },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetPassword(credentials: ResetPasswordCredentials) {
|
||||||
|
return await $larafetch<{ status: string }>('/reset-password', {
|
||||||
|
method: 'post',
|
||||||
|
body: credentials,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
isLoggedIn,
|
||||||
|
login,
|
||||||
|
register,
|
||||||
|
resendEmailVerification,
|
||||||
|
logout,
|
||||||
|
forgotPassword,
|
||||||
|
resetPassword,
|
||||||
|
refresh,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchCurrentUser<T = User>() {
|
||||||
|
try {
|
||||||
|
return await $larafetch<T>('/api/user')
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
if ([401, 419].includes(error?.response?.status)) { return null }
|
||||||
|
if (error?.response?.status === undefined) { return null }
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import type { UseFetchOptions } from 'nuxt/app'
|
||||||
|
|
||||||
|
export function useLarafetch<T>(
|
||||||
|
url: string | (() => string),
|
||||||
|
options: UseFetchOptions<T> = {},
|
||||||
|
) {
|
||||||
|
return useFetch(url, {
|
||||||
|
$fetch: $larafetch,
|
||||||
|
async onResponseError({ response }) {
|
||||||
|
const status = response.status
|
||||||
|
if ([500].includes(status)) {
|
||||||
|
console.error('[Laravel Error]', response.statusText, response._data)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([401, 419].includes(status)) {
|
||||||
|
navigateTo('/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([409].includes(status)) {
|
||||||
|
navigateTo('/verify-email')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
import type { FormError } from '@nuxt/ui/dist/runtime/types'
|
||||||
|
|
||||||
|
export interface UseSubmitOptions {
|
||||||
|
onSuccess?: (result: any) => any
|
||||||
|
onError?: (error: Error) => any
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSubmit<T>(
|
||||||
|
fetchable: () => Promise<T>,
|
||||||
|
options: UseSubmitOptions = {},
|
||||||
|
) {
|
||||||
|
const validationErrors = ref<FormError[]>([])
|
||||||
|
const error = ref<Error | null>(null)
|
||||||
|
const inProgress = ref(false)
|
||||||
|
const succeeded = ref<boolean | null>(null)
|
||||||
|
|
||||||
|
async function submit() {
|
||||||
|
validationErrors.value = []
|
||||||
|
error.value = null
|
||||||
|
inProgress.value = true
|
||||||
|
succeeded.value = null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await fetchable()
|
||||||
|
succeeded.value = true
|
||||||
|
options?.onSuccess?.(data)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
catch (e: any) {
|
||||||
|
error.value = e
|
||||||
|
succeeded.value = false
|
||||||
|
options?.onError?.(e)
|
||||||
|
if (e.data?.errors) {
|
||||||
|
const errorsArray: FormError[] = []
|
||||||
|
for (const path in e.data.errors) {
|
||||||
|
const messages = e.data.errors[path]
|
||||||
|
messages.forEach((message: string) => {
|
||||||
|
errorsArray.push({ path, message })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
validationErrors.value = errorsArray
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
validationErrors.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.response?.status !== 422) { throw e }
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
inProgress.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
submit,
|
||||||
|
inProgress,
|
||||||
|
succeeded,
|
||||||
|
validationErrors,
|
||||||
|
error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import antfu from '@antfu/eslint-config'
|
||||||
|
import { FlatCompat } from '@eslint/eslintrc'
|
||||||
|
|
||||||
|
const compat = new FlatCompat()
|
||||||
|
|
||||||
|
export default antfu({
|
||||||
|
rules: {
|
||||||
|
'curly': ['error', 'all'],
|
||||||
|
'node/prefer-global/process': ['error', 'always'],
|
||||||
|
},
|
||||||
|
}, ...compat.config({
|
||||||
|
extends: ['plugin:tailwindcss/recommended'],
|
||||||
|
rules: {
|
||||||
|
'tailwindcss/no-custom-classname': 'off',
|
||||||
|
'tailwindcss/migration-from-tailwind-2': 'off',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<Header />
|
||||||
|
<UPage>
|
||||||
|
<template #left>
|
||||||
|
<UAside class="lg:static">
|
||||||
|
<Navigation />
|
||||||
|
</UAside>
|
||||||
|
</template>
|
||||||
|
<UPageBody>
|
||||||
|
<UContainer>
|
||||||
|
<slot />
|
||||||
|
</UContainer>
|
||||||
|
</UPageBody>
|
||||||
|
</UPage>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
export default defineNuxtRouteMiddleware(async () => {
|
||||||
|
const user = useUser();
|
||||||
|
if (!user.value) return navigateTo("/login", { replace: true });
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
export default defineNuxtRouteMiddleware(async () => {
|
||||||
|
const user = useUser();
|
||||||
|
if (user.value) return navigateTo("/", { replace: true });
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
export default defineNuxtRouteMiddleware(() => {
|
||||||
|
const user = useUser();
|
||||||
|
|
||||||
|
if (!user.value) return navigateTo("/login");
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
if (user.value.email_verified_at || user.value.is_verified)
|
||||||
|
return navigateTo("/");
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
export default defineNuxtRouteMiddleware(() => {
|
||||||
|
const user = useUser();
|
||||||
|
|
||||||
|
if (!user.value) return navigateTo("/login");
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
if (!(user.value.email_verified_at || user.value.is_verified))
|
||||||
|
return navigateTo("/verify-email");
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
|
export default defineNuxtConfig({
|
||||||
|
extends: ['@nuxt/ui-pro'],
|
||||||
|
modules: ['@nuxt/ui'],
|
||||||
|
runtimeConfig: {
|
||||||
|
public: {
|
||||||
|
backendUrl: 'http://localhost',
|
||||||
|
frontendUrl: 'http://localhost:3000',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
imports: {
|
||||||
|
dirs: ['./utils'],
|
||||||
|
},
|
||||||
|
devtools: { enabled: true },
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"name": "nuxt-app",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "nuxt build",
|
||||||
|
"dev": "nuxt dev",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
|
"generate": "nuxt generate",
|
||||||
|
"preview": "nuxt preview",
|
||||||
|
"postinstall": "nuxt prepare"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@antfu/eslint-config": "^1.1.0",
|
||||||
|
"@iconify-json/heroicons": "^1.1.13",
|
||||||
|
"@iconify-json/logos": "^1.1.37",
|
||||||
|
"@iconify-json/simple-icons": "^1.1.76",
|
||||||
|
"@nuxt/devtools": "latest",
|
||||||
|
"@nuxt/ui-pro": "^0.4.2",
|
||||||
|
"eslint": "^8.53.0",
|
||||||
|
"eslint-plugin-tailwindcss": "^3.13.0",
|
||||||
|
"nuxt": "^3.8.1",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"vue": "^3.3.8",
|
||||||
|
"vue-router": "^4.2.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({ middleware: ['guest'] })
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const { forgotPassword } = useAuth()
|
||||||
|
|
||||||
|
const form = ref()
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const state = ref({
|
||||||
|
email: '' as string,
|
||||||
|
})
|
||||||
|
|
||||||
|
const status = ref('')
|
||||||
|
|
||||||
|
const {
|
||||||
|
submit,
|
||||||
|
inProgress,
|
||||||
|
validationErrors,
|
||||||
|
} = useSubmit(
|
||||||
|
() => {
|
||||||
|
status.value = ''
|
||||||
|
return forgotPassword(state.value.email)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => router.push('/login'),
|
||||||
|
onError: error => toast.add({ title: 'Error', description: error.message, color: 'red' }),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(validationErrors, async (errors) => {
|
||||||
|
if (errors.length > 0) {
|
||||||
|
await form.value?.setErrors(errors)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</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="submit">
|
||||||
|
<UFormGroup label="Email" name="email">
|
||||||
|
<UInput v-model="state.email" />
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<UButton block size="md" type="submit" :loading="inProgress" icon="i-heroicons-envelope">
|
||||||
|
Reset Password
|
||||||
|
</UButton>
|
||||||
|
</UForm>
|
||||||
|
|
||||||
|
<!-- <UDivider label="OR" class=" my-4"/> -->
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
</UPage>
|
||||||
|
</UMain>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({ middleware: ['auth'], layout: 'app' })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
This is the Page Content
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({ middleware: ['guest'] })
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const { login } = useAuth()
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const form = ref()
|
||||||
|
|
||||||
|
const state = ref<LoginCredentials>({
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
remember: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const status = ref(
|
||||||
|
(route.query.reset ?? '').length > 0 ? atob(route.query.reset as string) : '',
|
||||||
|
)
|
||||||
|
|
||||||
|
const {
|
||||||
|
submit,
|
||||||
|
inProgress,
|
||||||
|
validationErrors,
|
||||||
|
} = useSubmit(
|
||||||
|
() => {
|
||||||
|
status.value = ''
|
||||||
|
return login(state.value)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => router.push('/'),
|
||||||
|
onError: (error) => {
|
||||||
|
toast.add({ title: 'Error', description: error.message, color: 'red' })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(validationErrors, async (errors) => {
|
||||||
|
if (errors.length > 0) {
|
||||||
|
await form.value?.setErrors(errors)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</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="submit">
|
||||||
|
<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="inProgress" icon="i-heroicons-arrow-right-on-rectangle">
|
||||||
|
Login
|
||||||
|
</UButton>
|
||||||
|
</UForm>
|
||||||
|
|
||||||
|
<!-- <UDivider label="OR" class=" my-4"/> -->
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
</UPage>
|
||||||
|
</UMain>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
definePageMeta({ middleware: ['guest'] })
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
const { resetPassword } = useAuth()
|
||||||
|
|
||||||
|
if (!route.query.email) {
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
const toast = useToast()
|
||||||
|
|
||||||
|
const state = ref<ResetPasswordCredentials>({
|
||||||
|
email: route.query.email as string,
|
||||||
|
password: '',
|
||||||
|
password_confirmation: '',
|
||||||
|
token: route.params.token as string,
|
||||||
|
})
|
||||||
|
|
||||||
|
const status = ref(
|
||||||
|
(route.query.reset ?? '').length > 0 ? atob(route.query.reset as string) : '',
|
||||||
|
)
|
||||||
|
|
||||||
|
const form = ref()
|
||||||
|
|
||||||
|
const {
|
||||||
|
submit,
|
||||||
|
inProgress,
|
||||||
|
validationErrors,
|
||||||
|
} = useSubmit(
|
||||||
|
() => {
|
||||||
|
status.value = ''
|
||||||
|
return resetPassword(state.value)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: () => router.push('/'),
|
||||||
|
onError: (error) => {
|
||||||
|
toast.add({ title: 'Error', description: error.message, color: 'red' })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(validationErrors, async (errors) => {
|
||||||
|
if (errors.length > 0) {
|
||||||
|
await form.value?.setErrors(errors)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</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="submit">
|
||||||
|
<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="inProgress" icon="i-heroicon-lock-closed">
|
||||||
|
Change Password
|
||||||
|
</UButton>
|
||||||
|
</UForm>
|
||||||
|
|
||||||
|
<!-- <UDivider label="OR" class=" my-4"/> -->
|
||||||
|
</UCard>
|
||||||
|
</div>
|
||||||
|
</UPage>
|
||||||
|
</UMain>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { fetchCurrentUser, useUser } from '~/composables/useAuth'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin(async () => {
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
|
// Skip if already initialized on server
|
||||||
|
if (user.value !== undefined) { return }
|
||||||
|
|
||||||
|
user.value = await fetchCurrentUser()
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { FetchError } from 'ofetch'
|
||||||
|
|
||||||
|
export default defineNuxtPlugin(async (nuxtApp) => {
|
||||||
|
nuxtApp.hook('vue:error', (error) => {
|
||||||
|
if (!(error instanceof FetchError)) { throw error }
|
||||||
|
|
||||||
|
const status = error.response?.status ?? -1
|
||||||
|
|
||||||
|
if ([401, 419].includes(status)) {
|
||||||
|
navigateTo('/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([409].includes(status)) {
|
||||||
|
navigateTo('/verify-email')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"extends": "../.nuxt/tsconfig.server.json"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
|
"extends": "./.nuxt/tsconfig.json"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { $fetch } from 'ofetch'
|
||||||
|
import { parseCookies } from 'h3'
|
||||||
|
|
||||||
|
const CSRF_COOKIE = 'XSRF-TOKEN'
|
||||||
|
const CSRF_HEADER = 'X-XSRF-TOKEN'
|
||||||
|
|
||||||
|
export const $larafetch = $fetch.create({
|
||||||
|
credentials: 'include',
|
||||||
|
async onRequest({ options }) {
|
||||||
|
const { backendUrl, frontendUrl } = useRuntimeConfig().public
|
||||||
|
const event = process.nitro ? useEvent() : null
|
||||||
|
let token = event
|
||||||
|
? parseCookies(event)[CSRF_COOKIE]
|
||||||
|
: useCookie(CSRF_COOKIE).value
|
||||||
|
|
||||||
|
// on client initiate a csrf request and get it from the cookie set by laravel
|
||||||
|
if (
|
||||||
|
process.client
|
||||||
|
&& ['post', 'delete', 'put', 'patch'].includes(
|
||||||
|
options?.method?.toLowerCase() ?? '',
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
token = await initCsrf()
|
||||||
|
}
|
||||||
|
|
||||||
|
let headers: any = {
|
||||||
|
accept: 'application/json',
|
||||||
|
...options?.headers,
|
||||||
|
...(token && { [CSRF_HEADER]: token }),
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.server) {
|
||||||
|
const cookieString = event
|
||||||
|
? event.headers.get('cookie')
|
||||||
|
: useRequestHeaders(['cookie']).cookie
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
...headers,
|
||||||
|
...(cookieString && { cookie: cookieString }),
|
||||||
|
referer: frontendUrl,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
options.headers = headers
|
||||||
|
options.baseURL = backendUrl
|
||||||
|
},
|
||||||
|
async onResponseError({ response }) {
|
||||||
|
const status = response.status
|
||||||
|
if ([500].includes(status)) {
|
||||||
|
console.error('[Laravel Error]', response.statusText, response._data)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
async function initCsrf() {
|
||||||
|
const { backendUrl } = useRuntimeConfig().public
|
||||||
|
const existingToken = useCookie(CSRF_COOKIE).value
|
||||||
|
|
||||||
|
if (existingToken) { return existingToken }
|
||||||
|
|
||||||
|
await $fetch('/sanctum/csrf-cookie', {
|
||||||
|
baseURL: backendUrl,
|
||||||
|
credentials: 'include',
|
||||||
|
})
|
||||||
|
|
||||||
|
return useCookie(CSRF_COOKIE).value
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue