Compare commits

...

18 Commits

29 changed files with 9164 additions and 79 deletions

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
node-linker=hoisted

View File

@@ -2,5 +2,8 @@ export default defineAppConfig({
ui: {
primary: 'sky',
gray: 'cool',
container: {
constrained: 'max-w-full',
},
},
})

BIN
bun.lockb

Binary file not shown.

View File

@@ -1,16 +1,12 @@
<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',
to: 'https://ui.nuxt.com/getting-started',
}, {
label: 'Pro',
icon: 'i-heroicons-square-3-stack-3d',
to: '/pro',
to: 'https://ui.nuxt.com/pro',
}, {
label: 'Releases',
icon: 'i-heroicons-rocket-launch',
@@ -27,12 +23,11 @@ const links = [{
<template #right>
<UColorModeButton />
<UButton to="https://github.com/nuxt/ui" target="_blank" icon="i-simple-icons-github" color="gray" variant="ghost" />
<UserDropdown />
</template>
<template #panel>
<UNavigationTree :links="mapContentNavigation(navigation)" />
<UNavigationTree :links="links" />
</template>
</UHeader>
</template>

View File

@@ -6,19 +6,19 @@ const links = [
children: [
{
label: 'Overview',
to: '/partner/overview',
to: '/login',
icon: 'i-heroicons-eye',
},
{
label: 'Add Company',
to: '/pro/components/docs/docs-search',
to: '/login',
icon: 'i-heroicons-plus-circle',
},
],
},
{
label: 'People',
to: '/pro/components/docs/docs-search-button',
to: '/login',
icon: 'i-heroicons-user-group',
},
]

View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
const links = [{
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

@@ -1,6 +1,17 @@
export interface User {
id: number
name: string
email: string
email_verified_at: string | null
password?: string
remember_token?: string | null
roles: Role[]
created_at: string | null
updated_at: string | null
}
export interface Role {
name: string
email?: string
}
export interface LoginCredentials {
@@ -44,7 +55,9 @@ export function useAuth<T = User>() {
}
async function login(credentials: LoginCredentials) {
if (isLoggedIn.value) { return }
if (isLoggedIn.value) {
return
}
await $larafetch('/login', { method: 'post', body: credentials })
await refresh()
@@ -65,11 +78,16 @@ export function useAuth<T = User>() {
}
async function logout() {
if (!isLoggedIn.value) { return }
if (!isLoggedIn.value) {
return
}
await $larafetch('/logout', { method: 'post' })
user.value = null
const csrf_cookie = useCookie('XSRF-TOKEN')
csrf_cookie.value = null
await router.push('/login')
}
@@ -105,8 +123,12 @@ export async function fetchCurrentUser<T = User>() {
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 }
if ([401, 419].includes(error?.response?.status)) {
return null
}
if (error?.response?.status === undefined) {
return null
}
throw error
}
}

11
composables/useRoles.ts Normal file
View File

@@ -0,0 +1,11 @@
export function useRoles() {
const user = useUser()
function hasRole(roleName: string) {
return user.value?.roles?.some(role => role.name === roleName) ?? false
}
return {
hasRole,
}
}

View File

@@ -44,7 +44,9 @@ export function useSubmit<T>(
validationErrors.value = []
}
if (e.response?.status !== 422) { throw e }
if (e.response?.status !== 422) {
throw e
}
}
finally {
inProgress.value = false

13
error.vue Normal file
View File

@@ -0,0 +1,13 @@
<script setup lang="ts">
import type { NuxtError } from '#app'
defineProps<{
error: NuxtError
}>()
</script>
<template>
<NuxtLayout name="auth">
<UPageError :error="error" />
</NuxtLayout>
</template>

View File

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

5
layouts/auth.vue Normal file
View File

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

View File

@@ -1,5 +1,17 @@
<template>
<div>
<slot />
<div>
<Header />
<UPage>
<template #left>
<UAside class="lg:static">
<Navigation />
</UAside>
</template>
<UPageBody>
<UContainer>
<slot />
</UContainer>
</UPageBody>
</UPage>
</div>
</template>
</template>

11
middleware/admin.ts Normal file
View File

@@ -0,0 +1,11 @@
export default defineNuxtRouteMiddleware(async () => {
const { hasRole } = useRoles()
const requiredRole = 'super-admin' // Define the role required for this route
if (!hasRole(requiredRole)) {
return abortNavigation({
message: 'You are not authorized to access this page',
statusCode: 403,
})
}
})

View File

@@ -1,4 +1,6 @@
export default defineNuxtRouteMiddleware(async () => {
const user = useUser();
if (!user.value) return navigateTo("/login", { replace: true });
});
const user = useUser()
if (!user.value) {
return navigateTo('/login', { replace: true })
}
})

View File

@@ -1,4 +1,6 @@
export default defineNuxtRouteMiddleware(async () => {
const user = useUser();
if (user.value) return navigateTo("/", { replace: true });
});
const user = useUser()
if (user.value) {
return navigateTo('/', { replace: true })
}
})

View File

@@ -1,9 +1,11 @@
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("/");
});
const user = useUser()
if (!user.value) {
return navigateTo('/login')
}
if (user.value.email_verified_at) {
return navigateTo('/')
}
})

View File

@@ -1,9 +1,11 @@
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");
});
const user = useUser()
if (!user.value) {
return navigateTo('/login')
}
if (!(user.value.email_verified_at)) {
return navigateTo('/verify-email')
}
})

View File

@@ -1,4 +1,4 @@
// https://nuxt.com/docs/api/configuration/nuxt-config
// https://nuxt.com/docs/api/configuration/nbun uxt-config
export default defineNuxtConfig({
extends: ['@nuxt/ui-pro'],
modules: ['@nuxt/ui'],

View File

@@ -12,19 +12,17 @@
"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",
"@antfu/eslint-config": "^2.8.0",
"@iconify-json/heroicons": "^1.1.20",
"@nuxt/devtools": "latest",
"@nuxt/ui-pro": "^0.4.2",
"eslint": "^8.53.0",
"eslint-plugin-tailwindcss": "^3.13.0",
"@nuxt/ui-pro": "^1.0.2",
"eslint": "^8.57.0",
"eslint-plugin-tailwindcss": "^3.14.2",
"laravel-echo": "^1.15.3",
"nuxt": "^3.8.1",
"nuxt": "^3.10.3",
"pusher-js": "^8.3.0",
"typescript": "^5.2.2",
"vue": "^3.3.8",
"typescript": "^5.3.3",
"vue": "^3.4.19",
"vue-router": "^4.2.5"
}
}

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
definePageMeta({ middleware: ['guest'] })
definePageMeta({ middleware: ['guest'], layout: 'auth' })
const router = useRouter()
const { forgotPassword } = useAuth()

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
definePageMeta({ middleware: ['auth'], layout: 'app' })
definePageMeta({ middleware: ['auth'] })
</script>
<template>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
definePageMeta({ middleware: ['guest'] })
definePageMeta({ middleware: ['guest'], layout: 'auth' })
const router = useRouter()
const route = useRoute()

8
pages/logout/index.vue Normal file
View File

@@ -0,0 +1,8 @@
<template>
</template>
<script setup lang="ts">
definePageMeta({ middleware: ['guest'], layout: 'auth' })
const { logout } = useAuth()
logout()
</script>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
definePageMeta({ middleware: ['guest'] })
definePageMeta({ middleware: ['guest'], layout: 'auth' })
const router = useRouter()
const route = useRoute()

View File

@@ -4,7 +4,9 @@ export default defineNuxtPlugin(async () => {
const user = useUser()
// Skip if already initialized on server
if (user.value !== undefined) { return }
if (user.value !== undefined) {
return
}
user.value = await fetchCurrentUser()
})

View File

@@ -2,7 +2,9 @@ import { FetchError } from 'ofetch'
export default defineNuxtPlugin(async (nuxtApp) => {
nuxtApp.hook('vue:error', (error) => {
if (!(error instanceof FetchError)) { throw error }
if (!(error instanceof FetchError)) {
throw error
}
const status = error.response?.status ?? -1

8980
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@ export const $larafetch = $fetch.create({
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
@@ -46,17 +47,23 @@ export const $larafetch = $fetch.create({
},
async onResponseError({ response }) {
const status = response.status
if (status === 419) {
await initCsrf(true)
}
if ([500].includes(status)) {
console.error('[Laravel Error]', response.statusText, response._data)
}
},
})
async function initCsrf() {
async function initCsrf(forceRefresh = false) {
const { backendUrl } = useRuntimeConfig().public
const existingToken = useCookie(CSRF_COOKIE).value
if (existingToken) { return existingToken }
if (existingToken && !forceRefresh) {
return existingToken
}
await $fetch('/sanctum/csrf-cookie', {
baseURL: backendUrl,