kodlarımı bir kez daha paylaşayım tümüyle
store/authStore.ts
import { defineStore } from 'pinia'
export const useAuthStore = defineStore({
id: 'AuthStore',
state: () => ({
user: null,
}),
getters: {
_isLoggedIn: (state) => !!state.user
},
actions: {
async register(newUser) {
try {
const data = await $fetch('/api/auth/register', {
method: 'POST',
body: JSON.stringify(newUser),
headers: {
'Content-Type': 'application/json'
}
});
this.user = data;
} catch (error) {
throw error.data;
}
},
async login(newUser) {
try {
const data = await $fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify(newUser),
headers: {
'Content-Type': 'application/json'
}
});
this.user = data.user;
localStorage.setItem('user', JSON.stringify(data.user));
} catch (error) {
throw error.data;
}
},
logout() {
this.user = null;
localStorage.removeItem('user');
},
},
persist: true
});
NavBar.vue
<script setup>
import { useAuthStore } from "~/store/authStore";
const authStore = useAuthStore();
// Computed
const isLoggedIn = computed(() => authStore._isLoggedIn);
watch(() => isLoggedIn.value, (newValue, oldValue) => {
console.log('Eski değer:', oldValue);
console.log('Yeni değer:', newValue);
}, {
immediate: true
});
// Methods
const logout = () => {
authStore.logout();
return navigateTo("/");
};
</script>
<template>
<nav class="navbar navbar-expand-md custom-nav">
<div class="container">
<NuxtLink class="navbar-brand" :to="`/`">Bostorek</NuxtLink>
<ul class="navbar-nav">
<li class="nav-item active">
<NuxtLink class="nav-link pl-lg-0" :to="`/`"
>Home <span class="sr-only">(current)</span></NuxtLink
>
</li>
<li class="nav-item">
<NuxtLink class="nav-link" to="/books">Books</NuxtLink>
</li>
<li v-if="isLoggedIn" class="nav-item">
<NuxtLink class="nav-link" to="/dashboard">Dashboard</NuxtLink>
</li>
<li v-if="!isLoggedIn" class="nav-item">
<NuxtLink class="nav-link" to="/login">Login</NuxtLink>
</li>
<li v-if="!isLoggedIn" class="nav-item">
<NuxtLink class="nav-link" to="/register">Register</NuxtLink>
</li>
<li v-if="isLoggedIn" class="nav-item">
<button class="nav-link" @click="logout">Logout</button>
</li>
</ul>
</div>
</nav>
</template>
dashboard.vue
<script setup>
import { useAuthStore } from "~/store/authStore";
const userStore = useAuthStore();
const activeTab = ref("general");
const editMode = ref(false);
const userInfo = reactive({
username: "",
email: "",
password: "",
});
const toggleEditMode = () => {
editMode.value = !editMode.value;
};
const cancelEditMode = () => {
editMode.value = false;
userInfo.username = userStore.user.username;
userInfo.email = userStore.user.email;
userInfo.password = userStore.user.password;
};
onMounted(() => {
userInfo.username = userStore.user.username;
userInfo.email = userStore.user.email;
});
</script>
<template>
<section style="min-height: calc(100vh - 130px)">
<div class="container py-5">
<ul class="nav nav-tabs" id="dashboardTab" role="tablist">
<li class="nav-item" role="presentation" @click="activeTab = 'general'">
<button
class="nav-link"
:class="{ active: activeTab === 'general' }"
id="general-tab"
data-bs-toggle="tab"
data-bs-target="#general-tab-pane"
type="button"
role="tab"
aria-controls="general-tab-pane"
aria-selected="true"
>
General
</button>
</li>
<li class="nav-item" role="presentation" @click="activeTab = 'books'">
<button
class="nav-link"
:class="{ active: activeTab === 'books' }"
id="books-tab"
data-bs-toggle="tab"
data-bs-target="#books-tab-pane"
type="button"
role="tab"
aria-controls="books-tab-pane"
aria-selected="false"
>
Books
</button>
</li>
</ul>
<div class="tab-content py-4" id="dashboardContent">
<div
class="tab-pane fade"
:class="{ 'active show': activeTab === 'general' }"
id="general-tab-pane"
role="tabpanel"
aria-labelledby="general-tab"
tabindex="0"
>
<div class="row">
<div class="col-lg-6">
<h2 class="mb-4">User Information</h2>
<form>
<div class="mb-3">
<label for="username">Username</label>
<input
v-model="userInfo.username"
type="text"
id="username"
class="form-control"
:disabled="!editMode"
/>
</div>
<div class="mb-3">
<label for="email">Email</label>
<input
v-model="userInfo.email"
type="email"
id="email"
class="form-control"
:disabled="!editMode"
/>
</div>
<div class="mb-3">
<label for="password">Password</label>
<input
v-model="userInfo.password"
type="password"
id="password"
class="form-control"
:disabled="!editMode"
/>
</div>
<button
@click="!editMode ? toggleEditMode() : saveUserInfo()"
type="button"
class="btn btn-primary"
>
{{ editMode ? "Save" : "Edit" }}
</button>
<button
v-if="editMode"
style="
background-color: #ecc73c !important;
border-color: #ecc73c !important;
"
@click="cancelEditMode"
type="button"
class="btn btn-primary ms-3"
>
Cancel
</button>
</form>
</div>
<div class="col-lg-6"></div>
</div>
</div>
<div
class="tab-pane fade"
:class="{ 'active show': activeTab === 'books' }"
id="books-tab-pane"
role="tabpanel"
aria-labelledby="books-tab"
tabindex="0"
>
{{ userStore.user.username }} books
</div>
</div>
</div>
</section>
</template>
login.vue
<script setup>
import { useAuthStore } from "~/store/authStore";
import { useToast } from "vue-toastification";
const authStore = useAuthStore();
const router = useRouter();
const formData = reactive({
email: "",
password: "",
});
const showEmailWarningMessage = ref(false);
const showPasswordWarningMessage = ref(false);
const notFoundEmail = ref(null);
const isPasswordMatch = ref(true);
const toast = useToast();
const isEmailValid = computed(() => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email);
});
const isPasswordValid = computed(() => {
return formData.password.length >= 4 && formData.password.length <= 10;
});
const isFormValid = computed(() => {
return isEmailValid.value && isPasswordValid.value;
});
const submitForm = async () => {
try {
await authStore.login(formData);
toast.success("You will be redirected to the dashboard page", {
position: "top-right",
timeout: 3500,
closeButton: "button",
icon: true,
rtl: false,
});
setTimeout(() => {
router.push("/dashboard");
}, 4000);
} catch (errors) {
const { error } = errors;
if (error === "User not found!") {
notFoundEmail.value = this.formData.email;
} else if (error === "Your password is not true") {
isPasswordMatch.value = false;
}
}
};
</script>
<template>
<section class="full-section-height">
<Head>
<Title>Login</Title>
<Meta name="description" content="Login" />
</Head>
<div class="container">
<div class="row justify-content-center mb-2 text-center">
<div class="d-flex justify-content-center">
<h1 class="display-3">Login</h1>
</div>
</div>
<form @submit.prevent="submitForm">
<div class="row justify-content-center">
<!-- Email Field (Medium and Larger Screens) -->
<div class="col-md-6 col-8 mb-3">
<label for="email" class="form-label">Email</label>
<input
type="email"
class="form-control"
id="email"
name="email"
v-model.trim="formData.email"
autocomplete="on"
:class="{
'is-valid': isEmailValid,
'is-invalid':
(!isEmailValid && showEmailWarningMessage) ||
notFoundEmail === formData.email,
}"
@focus="showEmailWarningMessage = true"
@blur="showEmailWarningMessage = false"
required
/>
<span
v-if="showEmailWarningMessage && !isEmailValid"
class="text-danger small"
>Please provide a valid email!</span
>
<span
v-if="notFoundEmail === formData.email"
class="text-danger small"
>{{ `${notFoundEmail} is not found!` }}</span
>
</div>
</div>
<!-- Password Field -->
<div class="row justify-content-center">
<div class="col-md-6 col-8 mb-3">
<label for="password" class="form-label">Password</label>
<input
type="password"
class="form-control"
id="password"
name="password"
v-model.trim="formData.password"
:class="{
'is-valid': isPasswordValid,
'is-invalid':
(!isPasswordValid && showPasswordWarningMessage) ||
!isPasswordMatch,
}"
@focus="showPasswordWarningMessage = true"
@blur="showPasswordWarningMessage = false"
@input="isPasswordMatch = true"
required
/>
<span
v-if="showPasswordWarningMessage && !isPasswordValid"
class="text-danger small"
>Password must be between 4 and 10 characters!</span
>
<span v-if="!isPasswordMatch" class="text-danger small"
>Your password is not true!</span
>
</div>
</div>
<!-- Submit Button -->
<div class="row justify-content-center">
<div class="col-md-6 col-8 mb-3">
<button type="submit" class="btn btn-primary w-100">Login</button>
</div>
</div>
</form>
</div>
</section>
</template>
register.vue
<script setup>
import { useAuthStore } from "~/store/authStore";
import { useToast } from "vue-toastification";
const authStore = useAuthStore();
const router = useRouter();
const formData = reactive({
username: "",
email: "",
password: "",
});
const showUsernameWarningMessage = ref(false);
const showEmailWarningMessage = ref(false);
const showPasswordWarningMessage = ref(false);
const existingEmail = ref(null);
const showGenericWarningMessage = ref(false);
const isUsernameValid = computed(() => {
return formData.username.length >= 5 && formData.username.length <= 20;
});
const isEmailValid = computed(() => {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email);
});
const isPasswordValid = computed(() => {
return formData.password.length >= 4 && formData.password.length <= 10;
});
const isFormValid = computed(() => {
return isUsernameValid.value && isEmailValid.value && isPasswordValid.value;
});
const submitForm = async () => {
try {
await authStore.register(formData);
const toast = useToast();
toast.success("You will be redirected to the login page", {
position: "top-right",
timeout: 3500,
closeButton: "button",
icon: true,
rtl: false,
});
setTimeout(() => {
router.push("/login");
}, 4000);
} catch (errors) {
const { error } = errors;
if (error == "The Email is already exists!") {
existingEmail.value = formData.email;
} else {
showGenericWarningMessage.value = true;
formData.username = "";
formData.email = "";
formData.password = "";
}
}
};
</script>
<template>
<section class="full-section-height">
<Head>
<Title>Register</Title>
<Meta name="description" content="Register" />
</Head>
<div class="container">
<div class="row justify-content-center mb-2 text-center">
<div class="d-flex justify-content-center">
<h1 class="display-3">Register</h1>
</div>
</div>
<form @submit.prevent="submitForm">
<div v-if="showGenericWarningMessage" class="text-center">
<span class="text-danger small"
>Something happened, please try again later!</span
>
</div>
<div class="row justify-content-center">
<!-- Username Field (Medium and Larger Screens) -->
<div class="col-md-6 col-8 mb-3">
<label for="username" class="form-label"
>Username
<span class="text-danger">*</span>
</label>
<input
type="text"
class="form-control"
id="username"
name="username"
v-model.trim="formData.username"
autocomplete="off"
:class="{
'is-valid': isUsernameValid,
'is-invalid': !isUsernameValid && showUsernameWarningMessage,
}"
@focus="showUsernameWarningMessage = true"
@blur="showUsernameWarningMessage = false"
required
/>
<span
v-if="showUsernameWarningMessage && !isUsernameValid"
class="text-danger small"
>Username must be between 5 and 20 characters!</span
>
</div>
</div>
<div class="row justify-content-center">
<!-- Email Field (Medium and Larger Screens) -->
<div class="col-md-6 col-8 mb-3">
<label for="email" class="form-label"
>Email
<span class="text-danger">*</span>
</label>
<input
type="email"
class="form-control"
id="email"
name="email"
v-model.trim="formData.email"
autocomplete="off"
:class="{
'is-valid': isEmailValid,
'is-invalid':
(!isEmailValid && showEmailWarningMessage) ||
existingEmail === formData.email,
}"
@focus="showEmailWarningMessage = true"
@blur="showEmailWarningMessage = false"
required
/>
<span
v-if="showEmailWarningMessage && !isEmailValid"
class="text-danger small"
>Please provide a valid email!</span
>
<span
v-if="existingEmail === formData.email"
class="text-danger small"
>
{{ `${existingEmail} is already exist!` }}</span
>
</div>
</div>
<!-- Password Field -->
<div class="row justify-content-center">
<div class="col-md-6 col-8 mb-3">
<label for="password" class="form-label"
>Password
<span class="text-danger">*</span>
</label>
<input
type="password"
class="form-control"
id="password"
name="password"
v-model.trim="formData.password"
:class="{
'is-valid': isPasswordValid,
'is-invalid': !isPasswordValid && showPasswordWarningMessage,
}"
@focus="showPasswordWarningMessage = true"
@blur="showPasswordWarningMessage = false"
required
/>
<span
v-if="showPasswordWarningMessage && !isPasswordValid"
class="text-danger small"
>Password must be between 4 and 10 characters!</span
>
</div>
</div>
<!-- Submit Button -->
<div class="row justify-content-center">
<div class="col-md-6 col-8 mb-3">
<button
:disabled="!isFormValid"
type="submit"
class="btn btn-primary w-100"
>
Register
</button>
<span v-if="!isFormValid" class="text-danger small"
>* Please complete all of the required fields!</span
>
</div>
</div>
</form>
</div>
</section>
</template>
middleware/auth.global.ts
import { useAuthStore } from "~/store/authStore";
export default defineNuxtRouteMiddleware((to, from) => {
if (process.client) {
const authStore = useAuthStore();
if (!authStore.user) {
if (to.path === '/dashboard') {
return navigateTo('/login');
}
}
if (authStore.user) {
if (to.path === '/login' || to.path === '/register') {
return navigateTo({ name: 'dashboard' });
}
}
}
});
Hatalar
1 - Giriş yapmama ve middleware tanımlamama rağmen url kısmından login/register
yazıpta enter dediğimde beni login sayfasına atıyor ve login sayfası 1 saniyelik gözüküyor sonra hemen dashboar
da atıyor beni ve attığı zaman şöyle hatalar gözüküyor console
da.
[Vue warn]: Hydration class mismatch on <a href="/dashboard" class="nav-link" data-v-2202255b>Dashboard</a>
- rendered on server: class="nav-link"
- expected on client: class="router-link-active router-link-exact-active nav-link"
Note: this mismatch is check-only. The DOM will not be rectified in production due to performance overhead.
You should fix the source of the mismatch.
at <RouterLink ref=fn to="/dashboard" activeClass=undefined ... >
at <NuxtLink class="nav-link" to="/dashboard" >
at <NavBar>
at <Default ref=Ref< undefined > >
at <LayoutLoader key="default" layoutProps= {ref: RefImpl} name="default" >
at <NuxtLayoutProvider layoutProps= {ref: RefImpl} key="default" name="default" ... >
at <NuxtLayout>
at <App key=4 >
at <NuxtRoot>
runtime-core.esm-bundler.js?v=912f7bb0? [sm]:3641 Hydration completed but contains mismatches.
runtime-core.esm-bundler.js?v=912f7bb0? [sm]:51 [Vue warn]: Hydration node mismatch:
- rendered on server: <!--[--> (start of fragment)
- expected on client: div
at <Dashboard onVnodeUnmounted=fn<onVnodeUnmounted> ref=Ref< undefined > >
at <Anonymous key="/dashboard" vnode= {__v_isVNode: true, __v_skip: true, type: {…}, props: {…}, key: null, …} route= {fullPath: '/dashboard', hash: '', query: {…}, name: 'dashboard', path: '/dashboard', …} ... >
at <RouterView name=undefined route=undefined >
at <NuxtPage>
at <Default ref=Ref< undefined > >
at <LayoutLoader key="default" layoutProps= {ref: RefImpl} name="default" >
at <NuxtLayoutProvider layoutProps= {ref: RefImpl} key="default" name="default" ... >
at <NuxtLayout>
at <App key=4 >
at <NuxtRoot>
[Vue warn]: Hydration children mismatch on <section class="full-section-height" data-v-inspector="pages/login.vue:56:3">…</section> flex
Server rendered element contains more child nodes than client vdom.
at <Dashboard onVnodeUnmounted=fn<onVnodeUnmounted> ref=Ref< undefined > >
at <Anonymous key="/dashboard" vnode= {__v_isVNode: true, __v_skip: true, type: {…}, props: {…}, key: null, …} route= {fullPath: '/dashboard', hash: '', query: {…}, name: 'dashboard', path: '/dashboard', …} ... >
at <RouterView name=undefined route=undefined >
at <NuxtPage>
at <Default ref=Ref< undefined > >
at <LayoutLoader key="default" layoutProps= {ref: RefImpl} name="default" >
at <NuxtLayoutProvider layoutProps= {ref: RefImpl} key="default" name="default" ... >
at <NuxtLayout>
at <App key=4 >
at <NuxtRoot>
runtime-core.esm-bundler.js?v=912f7bb0? [sm]:51 [Vue warn]: Hydration style mismatch on <section class="full-section-height" data-v-inspector="pages/login.vue:56:3">…</section> flex
- rendered on server: style=""
- expected on client: style="min-height:calc(100vh - 130px);"
Note: this mismatch is check-only. The DOM will not be rectified in production due to performance overhead.
You should fix the source of the mismatch.
at <Dashboard onVnodeUnmounted=fn<onVnodeUnmounted> ref=Ref< undefined > >
at <Anonymous key="/dashboard" vnode= {__v_isVNode: true, __v_skip: true, type: {…}, props: {…}, key: null, …} route= {fullPath: '/dashboard', hash: '', query: {…}, name: 'dashboard', path: '/dashboard', …} ... >
at <RouterView name=undefined route=undefined >
at <NuxtPage>
at <Default ref=Ref< undefined > >
at <LayoutLoader key="default" layoutProps= {ref: RefImpl} name="default" >
at <NuxtLayoutProvider layoutProps= {ref: RefImpl} key="default" name="default" ... >
at <NuxtLayout>
at <App key=4 >
at <NuxtRoot>
Bunun sebebinin store
da userin başta null olması ve sonradan atanması olduğunu düşünüyorum. Böyle düşünmemin sebebi watch
ile baktığımda
watch(() => isLoggedIn.value, (newValue, oldValue) => {
console.log('Eski değer:', oldValue);
console.log('Yeni değer:', newValue);
}, {
immediate: true
});
ve sayfayı yenilediğimde
Eski değer: undefined
Yeni değer: true
gözüküyor yani bence login sayfasının 1 saniye gözüküp sonra beni dashobarda atmasının nedeni userin başta undefined olup sonra true olması.