Skip to content

Commit

Permalink
Auth
Browse files Browse the repository at this point in the history
  • Loading branch information
DotNetTitan committed Sep 6, 2024
1 parent e1d3d50 commit d502493
Show file tree
Hide file tree
Showing 7 changed files with 377 additions and 42 deletions.
110 changes: 102 additions & 8 deletions src/components/Layout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,61 @@
<GlobalSearch />
</div>
<div class="hidden md:flex items-center space-x-4">
<RouterLink to="/login" class="flex items-center text-gray-300 hover:text-indigo-400 px-3 py-2 rounded-md text-sm font-medium transition duration-150 ease-in-out">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd" />
</svg>
Sign in
</RouterLink>
<template v-if="userStore.isAuthenticated && userStore.user">
<div class="relative">
<button
@click="toggleDropdown"
@blur="closeDropdown"
class="flex items-center space-x-2 text-gray-300 hover:text-indigo-400 focus:outline-none"
:disabled="isLoading"
>
<div class="w-8 h-8 rounded-full overflow-hidden bg-gray-200 dark:bg-gray-700">
<img
:src="userAvatar"
:alt="`${userStore.user.firstName}'s avatar`"
class="w-full h-full object-cover"
>
</div>
<span class="font-medium">{{ isLoading ? 'Signing out...' : userStore.user.firstName }}</span>
<svg v-if="!isLoading" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
<div
v-show="isDropdownOpen && !isLoading"
class="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg py-1 z-10"
>
<RouterLink
to="/profile"
class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700"
@click="closeDropdown"
>
Your Profile
</RouterLink>
<RouterLink
to="/orders"
class="block px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700"
@click="closeDropdown"
>
Your Orders
</RouterLink>
<button
@click="handleLogout"
class="block w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700"
>
Sign out
</button>
</div>
</div>
</template>
<template v-else>
<RouterLink
to="/login"
class="text-gray-300 hover:text-indigo-400 px-3 py-2 rounded-md text-sm font-medium transition duration-150 ease-in-out"
>
Sign in
</RouterLink>
</template>
<RouterLink to="/cart" class="text-gray-300 hover:text-indigo-400 p-2 rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 relative">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
Expand Down Expand Up @@ -62,22 +111,61 @@
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { RouterLink } from 'vue-router'
import { ref, computed, onMounted } from 'vue'
import { RouterLink, useRouter } from 'vue-router'
import { useThemeStore } from '@/stores/theme'
import { useCartStore } from '@/stores/cart'
import { useUserStore } from '@/stores/user'
import GlobalSearch from '@/components/GlobalSearch.vue'
const themeStore = useThemeStore()
const cartStore = useCartStore()
const userStore = useUserStore()
const router = useRouter()
const mobileMenuOpen = ref(false)
const isDropdownOpen = ref(false)
const isLoading = ref(false)
const userAvatar = computed(() => {
if (userStore.user?.image) {
return userStore.user.image
} else {
// Return a default avatar image URL
return 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp&f=y'
}
})
const toggleMobileMenu = () => {
mobileMenuOpen.value = !mobileMenuOpen.value
}
const toggleDropdown = () => {
isDropdownOpen.value = !isDropdownOpen.value
}
const closeDropdown = () => {
// Use setTimeout to allow for click events on dropdown items to fire before closing
setTimeout(() => {
isDropdownOpen.value = false
}, 100)
}
const handleLogout = async () => {
isLoading.value = true
isDropdownOpen.value = false // Close the dropdown immediately
try {
await userStore.logout()
router.push('/')
} catch (error) {
console.error('Logout failed:', error)
} finally {
isLoading.value = false
}
}
onMounted(() => {
themeStore.initDarkMode()
userStore.checkAuth()
})
</script>

Expand All @@ -93,3 +181,9 @@ onMounted(() => {
}
}
</style>

<style scoped>
.group:hover .group-hover\:block {
display: block;
}
</style>
7 changes: 6 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'
import { useAuthStore } from './stores/auth'
import './assets/tailwind.css' // Make sure to create this file and import Tailwind CSS

const app = createApp(App)
const pinia = createPinia()

app.use(createPinia())
app.use(pinia)
app.use(router)

const authStore = useAuthStore()
authStore.initAuth()

app.mount('#app')
18 changes: 18 additions & 0 deletions src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import ProductsView from '../views/ProductsView.vue'
import CartView from '../views/CartView.vue'
import LoginView from '../views/LoginView.vue'
import ProductDetailView from '../views/ProductDetailView.vue'
import { useUserStore } from '@/stores/user'
import ProfileView from '../views/ProfileView.vue'

const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
Expand Down Expand Up @@ -49,9 +51,25 @@ const router = createRouter({
name: 'ProductDetail',
component: ProductDetailView,
props: true
},
{
path: '/profile',
name: 'profile',
component: ProfileView,
meta: { requiresAuth: true }
}
// ... other routes ...
]
})

router.beforeEach((to, from, next) => {
const userStore = useUserStore()

if (to.meta.requiresAuth && !userStore.isAuthenticated) {
next({ path: '/login', query: { redirect: to.fullPath } })
} else {
next()
}
})

export default router
130 changes: 130 additions & 0 deletions src/stores/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'

interface User {
id: number
username: string
email: string
firstName: string
lastName: string
gender: string
image: string
}

interface LoginResponse extends User {
token: string
refreshToken: string
}

export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null)
const isAuthenticated = ref(false)
const token = ref<string | null>(null)
const refreshToken = ref<string | null>(null)

const login = async (username: string, password: string) => {
try {
const response = await fetch('https://dummyjson.com/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password, expiresInMins: 30 })
})
const data: LoginResponse = await response.json()

user.value = {
id: data.id,
username: data.username,
email: data.email,
firstName: data.firstName,
lastName: data.lastName,
gender: data.gender,
image: data.image
}
isAuthenticated.value = true
token.value = data.token
refreshToken.value = data.refreshToken

// Store tokens in localStorage
localStorage.setItem('token', data.token)
localStorage.setItem('refreshToken', data.refreshToken)
} catch (error) {
console.error('Login failed:', error)
throw error
}
}

const logout = () => {
user.value = null
isAuthenticated.value = false
token.value = null
refreshToken.value = null
localStorage.removeItem('token')
localStorage.removeItem('refreshToken')
}

const refreshSession = async () => {
if (!refreshToken.value) {
throw new Error('No refresh token available')
}

try {
const response = await fetch('https://dummyjson.com/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: refreshToken.value, expiresInMins: 30 })
})
const data = await response.json()

token.value = data.token
refreshToken.value = data.refreshToken

// Update tokens in localStorage
localStorage.setItem('token', data.token)
localStorage.setItem('refreshToken', data.refreshToken)
} catch (error) {
console.error('Session refresh failed:', error)
throw error
}
}

const fetchUser = async () => {
if (!token.value) {
throw new Error('No token available')
}

try {
const response = await fetch('https://dummyjson.com/auth/me', {
method: 'GET',
headers: { 'Authorization': `Bearer ${token.value}` }
})
const data: User = await response.json()

user.value = data
isAuthenticated.value = true
} catch (error) {
console.error('Fetching user data failed:', error)
throw error
}
}

// Initialize auth state from localStorage
const initAuth = () => {
const storedToken = localStorage.getItem('token')
const storedRefreshToken = localStorage.getItem('refreshToken')
if (storedToken && storedRefreshToken) {
token.value = storedToken
refreshToken.value = storedRefreshToken
fetchUser() // Fetch user data using the stored token
}
}

return {
user,
isAuthenticated,
login,
logout,
refreshSession,
fetchUser,
initAuth
}
})
Loading

0 comments on commit d502493

Please sign in to comment.