This commit is contained in:
2024-03-17 12:30:19 +01:00
commit c805b5ebaa
133 changed files with 24036 additions and 0 deletions

15
nuxt/app.config.ts Normal file
View File

@@ -0,0 +1,15 @@
export default defineAppConfig({
name: 'Laravel/Nuxt Boilerplate',
ui: {
primary: 'sky',
gray: 'cool',
container: {
constrained: 'max-w-7xl w-full'
},
avatar: {
default: {
icon: 'i-heroicons-user',
}
}
}
})

10
nuxt/app.vue Normal file
View File

@@ -0,0 +1,10 @@
<script lang="ts" setup>
</script>
<template>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<UNotifications />
</template>

25
nuxt/assets/css/main.css Normal file
View File

@@ -0,0 +1,25 @@
@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;
}
}

View File

@@ -0,0 +1,33 @@
<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>

5
nuxt/components/Logo.vue Normal file
View File

@@ -0,0 +1,5 @@
<template>
<div>
<span class="text-primary">Nuxt</span> Breeze
</div>
</template>

View File

@@ -0,0 +1,31 @@
<script setup lang="ts">
const links = [
{
label: 'Company',
icon: 'i-heroicons-building-office-2',
children: [
{
label: 'Overview',
to: '/login',
icon: 'i-heroicons-eye',
},
{
label: 'Add Company',
to: '/login',
icon: 'i-heroicons-plus-circle',
},
],
},
{
label: 'People',
to: '/login',
icon: 'i-heroicons-user-group',
},
]
</script>
<template>
<UContainer>
<UNavigationTree :links="links" />
</UContainer>
</template>

View File

@@ -0,0 +1,28 @@
<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>

View File

@@ -0,0 +1,102 @@
<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>

View File

@@ -0,0 +1,99 @@
<script lang="ts" setup>
const form = ref();
const auth = useAuthStore();
const state = reactive({
...{
email: auth.user.email,
name: auth.user.name,
avatar: auth.user.avatar,
},
});
const { refresh: sendEmailVerification, status: resendEmailStatus } = useFetch<any>("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",
title: response._data.message,
color: "emerald",
});
}
}
});
const { refresh: onSubmit, status: accountUpdateStatus } = useFetch<any>("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;
}
}
});
</script>
<template>
<UForm ref="form" :state="state" @submit="onSubmit" class="space-y-4">
<UFormGroup label="" name="avatar" class="flex">
<InputUploadAvatar
v-model="state.avatar"
accept=".png, .jpg, .jpeg, .webp"
entity="avatars"
max-size="2"
/>
</UFormGroup>
<UFormGroup label="Name" name="name" required>
<UInput v-model="state.name" type="text" />
</UFormGroup>
<UFormGroup label="Email" name="email" required>
<UInput
v-model="state.email"
placeholder="you@example.com"
icon="i-heroicons-envelope"
trailing
type="email"
/>
</UFormGroup>
<UAlert
v-if="auth.user.must_verify_email"
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."
:actions="[
{
label: 'Resend verification email',
variant: 'solid',
color: 'gray',
loading: resendEmailStatus === 'pending',
click: sendEmailVerification,
},
]"
/>
<div class="pt-2">
<UButton type="submit" label="Save" :loading="accountUpdateStatus === 'pending'" />
</div>
</UForm>
</template>

View File

@@ -0,0 +1,115 @@
<script lang="ts" setup>
const props = defineProps(["modelValue", "entity", "accept", "maxSize"]);
const emit = defineEmits(["update:modelValue"]);
const { $storage } = useNuxtApp();
const value = computed({
get() {
return props.modelValue;
},
set(value) {
emit("update:modelValue", value);
},
});
const inputRef = ref();
const loading = ref(false);
const onSelect = async (e: any) => {
const file = e.target.files[0];
e.target.value = null;
if (file.size > props.maxSize * 1024 * 1024) {
return useToast().add({
title: "File is too large.",
color: "red",
icon: "i-heroicons-exclamation-circle-solid",
});
}
loading.value = true;
const formData = new FormData();
formData.append("image", file);
await $fetch<any>("upload", {
method: "POST",
body: formData,
params: {
entity: props.entity,
width: 300,
height: 300,
},
ignoreResponseError: true,
onResponse({ response }) {
if (response.status !== 200) {
useToast().add({
icon: 'i-heroicons-exclamation-circle-solid',
color: 'red',
title: response._data?.message ?? response.statusText ?? 'Something went wrong',
});
} else if (response._data?.ok) {
value.value = response._data?.path;
}
loading.value = false;
},
});
};
</script>
<template>
<div class="flex gap-6">
<div class="relative flex">
<UAvatar
:src="$storage(value)"
size="3xl"
img-class="object-cover"
:ui="{ rounded: 'rounded-lg' }"
/>
<UTooltip
text="Upload avatar"
class="absolute top-0 end-0 -m-2"
:popper="{ placement: 'right' }"
>
<UButton
type="button"
color="gray"
icon="i-heroicons-cloud-arrow-up"
size="2xs"
:ui="{ rounded: 'rounded-full' }"
:loading="loading"
@click="inputRef.click()"
/>
</UTooltip>
<UTooltip
text="Delete avatar"
class="absolute bottom-0 end-0 -m-2"
:popper="{ placement: 'right' }"
>
<UButton
type="button"
color="gray"
icon="i-heroicons-x-mark-20-solid"
size="2xs"
:ui="{ rounded: 'rounded-full' }"
:disabled="loading"
@click="value = ''"
/>
</UTooltip>
<input
ref="inputRef"
type="file"
class="hidden"
:accept="accept"
@change="onSelect"
/>
</div>
<div class="text-sm opacity-80">
<div>Max upload size: {{ maxSize }}Mb</div>
<div>Accepted formats: {{ accept }}</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,18 @@
<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>
</template>
<USkeleton class="w-full h-60" />
<template #footer>
<UButton label="Close" @click="modal.close" color="gray" />
</template>
</UCard>
</UModal>
</template>

27
nuxt/error.vue Normal file
View File

@@ -0,0 +1,27 @@
<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>

7
nuxt/index.d.ts vendored Normal file
View File

@@ -0,0 +1,7 @@
declare module '#app' {
interface NuxtApp {
$storage(msg: string): string
}
}
export { }

5
nuxt/layouts/auth.vue Normal file
View File

@@ -0,0 +1,5 @@
<template>
<div>
<slot />
</div>
</template>

17
nuxt/layouts/default.vue Normal file
View File

@@ -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>

8
nuxt/middleware/auth.ts Normal file
View File

@@ -0,0 +1,8 @@
export default defineNuxtRouteMiddleware((to, from) => {
const nuxtApp = useNuxtApp()
const auth = useAuthStore()
if (!auth.isLoggedIn) {
return nuxtApp.runWithContext(() => navigateTo('/login'))
}
})

8
nuxt/middleware/guest.ts Normal file
View File

@@ -0,0 +1,8 @@
export default defineNuxtRouteMiddleware((to, from) => {
const nuxtApp = useNuxtApp()
const auth = useAuthStore()
if (auth.isLoggedIn) {
return nuxtApp.runWithContext(() => navigateTo('/'))
}
})

View File

@@ -0,0 +1,16 @@
export default defineNuxtRouteMiddleware((to, from) => {
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",
});
return navigateTo('/')
})
}
})

View File

@@ -0,0 +1,16 @@
export default defineNuxtRouteMiddleware((to, from) => {
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",
});
return navigateTo('/')
})
}
})

View File

@@ -0,0 +1,16 @@
export default defineNuxtRouteMiddleware((to, from) => {
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",
});
return navigateTo('/account/general')
})
}
})

27
nuxt/pages/account.vue Normal file
View File

@@ -0,0 +1,27 @@
<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>

View File

@@ -0,0 +1,86 @@
<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>

View File

@@ -0,0 +1,24 @@
<script lang="ts" setup></script>
<template>
<UCard :ui="{ body: { base: '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-sm opacity-80">
Update your account's profile information and email address.
</div>
</div>
<div class="col-span-12 lg:col-span-8">
<AccountUpdateProfile />
</div>
<UDivider 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-sm opacity-80">
Ensure your account is using a long, random password to stay secure.
</div>
</div>
<div class="col-span-12 lg:col-span-8">
<AccountUpdatePassword />
</div>
</UCard>
</template>

View File

@@ -0,0 +1,6 @@
<script lang="ts" setup>
definePageMeta({
redirect: "/account/general",
});
</script>
<template></template>

View File

@@ -0,0 +1,62 @@
<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>

13
nuxt/pages/index.vue Normal file
View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
definePageMeta({ middleware: ['auth'] })
const modal = useModal();
const router = useRouter();
const auth = useAuthStore();
</script>
<template>
<div>
This is the Page Content
</div>
</template>

103
nuxt/pages/login/index.vue Normal file
View File

@@ -0,0 +1,103 @@
<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>

View File

@@ -0,0 +1,10 @@
<template>
<div>
</div>
</template>
<script setup lang="ts">
definePageMeta({ middleware: ['auth'], layout: 'auth' });
const auth = useAuthStore()
auth.logout()
</script>

View File

@@ -0,0 +1,78 @@
<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>

129
nuxt/plugins/app.ts Normal file
View File

@@ -0,0 +1,129 @@
import { ofetch } from 'ofetch'
import type { FetchOptions } from 'ofetch';
export default defineNuxtPlugin({
name: 'app',
enforce: 'default',
parallel: true,
async setup(nuxtApp) {
const config = useRuntimeConfig()
const auth = useAuthStore()
nuxtApp.provide('storage', (path: string): string => {
if (!path) return ''
return path.startsWith('http://') || path.startsWith('https://') ?
path
: config.public.storageBase + path
})
function buildHeaders(headers: HeadersInit = {}): HeadersInit {
// Initial headers with Accept
const initialHeaders = {
...headers,
'Accept': 'application/json',
};
// Conditionally add server-specific headers
if (process.server) {
const serverHeaders = {
'referer': useRequestURL().toString(),
...useRequestHeaders(['x-forwarded-for', 'user-agent', 'referer']),
};
Object.assign(initialHeaders, serverHeaders);
}
// Conditionally add authorization header if logged in
if (auth.isLoggedIn) {
const authHeaders = {
'Authorization': `Bearer ${auth.token}`,
};
Object.assign(initialHeaders, authHeaders);
}
return initialHeaders;
}
function buildBaseURL(baseURL: string): string {
if (baseURL) return baseURL;
return process.server ?
config.apiLocal + config.public.apiPrefix
: config.public.apiBase + config.public.apiPrefix;
}
function buildSecureMethod(options: FetchOptions): void {
if (process.server) return;
const method = options.method?.toLowerCase() ?? 'get'
if (options.body instanceof FormData && method === 'put') {
options.method = 'POST';
options.body.append('_method', 'PUT');
}
}
function isRequestWithAuth(baseURL: string, path: string): boolean {
return !baseURL
&& !path.startsWith('/_nuxt')
&& !path.startsWith('http://')
&& !path.startsWith('https://');
}
globalThis.$fetch = ofetch.create({
retry: false,
onRequest({ request, options }) {
if (!isRequestWithAuth(options.baseURL ?? '', request.toString())) return
options.credentials = 'include';
options.baseURL = buildBaseURL(options.baseURL ?? '');
options.headers = buildHeaders(options.headers);
buildSecureMethod(options);
},
onRequestError({ error }) {
if (process.server) return;
if (error.name === 'AbortError') return;
useToast().add({
icon: 'i-heroicons-exclamation-circle-solid',
color: 'red',
title: error.message ?? 'Something went wrong',
})
},
onResponseError({ response }) {
if (response.status === 401) {
if (auth.isLoggedIn) {
auth.token = ''
auth.user = {} as User
}
if (process.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) {
useToast().add({
icon: 'i-heroicons-exclamation-circle-solid',
color: 'red',
title: response._data?.message ?? response.statusText ?? 'Something went wrong',
})
}
}
}
} as FetchOptions)
if (auth.isLoggedIn) {
await auth.fetchUser();
}
},
})

BIN
nuxt/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

0
nuxt/public/sw.js Normal file
View File

View File

@@ -0,0 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
}

52
nuxt/stores/auth.ts Normal file
View File

@@ -0,0 +1,52 @@
import { defineStore } from 'pinia'
export type User = {
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 token = useCookie('token', {
path: '/',
sameSite: 'strict',
secure: config.public.apiBase.startsWith('https://'),
maxAge: 60 * 60 * 24 * 365
})
const isLoggedIn = computed(() => !!token.value)
const { refresh: logout } = useFetch<any>('logout', {
method: 'POST',
immediate: false,
onResponse({ response }) {
if (response.status === 200) {
token.value = ''
user.value = <User>{}
return nuxtApp.runWithContext(() => {
return navigateTo('/')
})
}
}
})
const { refresh: fetchUser } = useFetch<any>('user', {
immediate: false,
onResponse({ response }) {
if (response.status === 200) {
user.value = response._data.user
}
}
})
return { user, isLoggedIn, logout, fetchUser, token }
})