feat(auth): implement login page and state management
This commit is contained in:
27
src/App.vue
27
src/App.vue
@@ -1,16 +1,31 @@
|
||||
<template>
|
||||
<header>
|
||||
<nav>
|
||||
<router-link to="/">Главная</router-link> | <router-link to="/login">Вход</router-link> |
|
||||
<router-link to="/register">Регистрация</router-link>
|
||||
<router-link to="/">Главная</router-link> |
|
||||
|
||||
<!-- Показываем, если пользователь НЕ залогинен -->
|
||||
<template v-if="!authStore.isAuthenticated">
|
||||
<router-link to="/login">Вход</router-link> |
|
||||
<router-link to="/register">Регистрация</router-link>
|
||||
</template>
|
||||
|
||||
<!-- Показываем, если пользователь залогинен -->
|
||||
<template v-else>
|
||||
<span>Привет, {{ authStore.user?.username }}!</span> |
|
||||
<a @click="authStore.handleLogout" href="#">Выход</a>
|
||||
</template>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<!-- Здесь будут отображаться компоненты-страницы -->
|
||||
<router-view />
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
const authStore = useAuthStore();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
header {
|
||||
background-color: #f0f0f0;
|
||||
@@ -29,4 +44,10 @@ nav a.router-link-exact-active {
|
||||
main {
|
||||
padding: 1rem;
|
||||
}
|
||||
span {
|
||||
margin: 0 1rem;
|
||||
}
|
||||
a {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import type { RegisterRequest } from "@/types"; // Мы создадим этот тип
|
||||
import type { RegisterRequest } from "@/types";
|
||||
import apiClient from "./axios";
|
||||
|
||||
export const registerUser = (userData: RegisterRequest) => {
|
||||
// apiClient уже настроен на /api, поэтому путь относительный
|
||||
return apiClient.post("/register", userData);
|
||||
};
|
||||
|
||||
export const loginUser = (credentials: LoginRequest) => {
|
||||
return apiClient.post<LoginResponse>("/login", credentials);
|
||||
};
|
||||
|
||||
@@ -1,26 +1,53 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { ref, computed } from "vue";
|
||||
import { registerUser as apiRegisterUser } from "@/api/auth";
|
||||
import type { RegisterRequest } from "@/types";
|
||||
import router from "@/router";
|
||||
import apiClient from "@/api/axios";
|
||||
|
||||
export const useAuthStore = defineStore("auth", () => {
|
||||
// State
|
||||
const user = ref<User | null>(null);
|
||||
const token = ref<string | null>(localStorage.getItem("authToken"));
|
||||
const isLoading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
// === Getters ===
|
||||
const isAuthenticated = computed(() => !!user.value && !!token.value);
|
||||
|
||||
// Actions
|
||||
function setAuthData(userData: User, authToken: string) {
|
||||
user.value = userData;
|
||||
token.value = authToken;
|
||||
localStorage.setItem("authToken", authToken);
|
||||
apiClient.defaults.headers.common["Authorization"] = `Bearer ${authToken}`;
|
||||
}
|
||||
|
||||
async function handleLogin(credentials: LoginRequest) {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
const response = await apiLoginUser(credentials);
|
||||
setAuthData(response.data.user, response.data.token);
|
||||
await router.push({ name: "home" });
|
||||
} catch (e: any) {
|
||||
if (e.response && e.response.data) {
|
||||
error.value = e.response.data;
|
||||
} else {
|
||||
error.value = "Произошла неизвестная ошибка при входе";
|
||||
}
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleRegister(userData: RegisterRequest) {
|
||||
isLoading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
await apiRegisterUser(userData);
|
||||
// После успешной регистрации перенаправляем на страницу входа
|
||||
await router.push({ name: "login" });
|
||||
// Можно добавить сообщение об успехе ("тост")
|
||||
// alert('Регистрация прошла успешно! Теперь вы можете войти.');
|
||||
} catch (e: any) {
|
||||
// Обрабатываем ошибки от Axios
|
||||
if (e.response && e.response.data) {
|
||||
error.value = e.response.data;
|
||||
} else {
|
||||
@@ -30,10 +57,26 @@ export const useAuthStore = defineStore("auth", () => {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
// Функция для выхода
|
||||
function handleLogout() {
|
||||
user.value = null;
|
||||
token.value = null;
|
||||
localStorage.removeItem("authToken");
|
||||
delete apiClient.defaults.headers.common["Authorization"];
|
||||
router.push({ name: "login" });
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
user,
|
||||
token,
|
||||
isLoading,
|
||||
error,
|
||||
// Getters
|
||||
isAuthenticated,
|
||||
// Actions
|
||||
handleLogin,
|
||||
handleRegister,
|
||||
handleLogout,
|
||||
};
|
||||
});
|
||||
|
||||
22
src/types.ts
22
src/types.ts
@@ -4,3 +4,25 @@ export interface RegisterRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
// Тип для запроса на логин
|
||||
export interface LoginRequest {
|
||||
login: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
// Тип для ответа с бэкенда
|
||||
export interface LoginResponse {
|
||||
token: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
// Тип для объекта пользователя
|
||||
export interface User {
|
||||
uuid: string;
|
||||
username: string;
|
||||
email: string;
|
||||
role: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,112 @@
|
||||
<template>
|
||||
<h1>Вход</h1>
|
||||
<div class="login-container">
|
||||
<form @submit.prevent="onSubmit" class="login-form">
|
||||
<h1>Вход</h1>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="login">Имя пользователя или Email</label>
|
||||
<input id="login" v-model="login" type="text" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Пароль</label>
|
||||
<input id="password" v-model="password" type="password" required />
|
||||
</div>
|
||||
|
||||
<div v-if="authStore.error" class="error-message">
|
||||
{{ authStore.error }}
|
||||
</div>
|
||||
|
||||
<button type="submit" :disabled="authStore.isLoading">
|
||||
{{ authStore.isLoading ? "Вход..." : "Войти" }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onUnmounted } from "vue";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
|
||||
const authStore = useAuthStore();
|
||||
|
||||
const login = ref("");
|
||||
const password = ref("");
|
||||
|
||||
const onSubmit = async () => {
|
||||
if (!login.value || !password.value) {
|
||||
return;
|
||||
}
|
||||
await authStore.handleLogin({
|
||||
login: login.value,
|
||||
password: password.value,
|
||||
});
|
||||
};
|
||||
|
||||
onUnmounted(() => {
|
||||
authStore.error = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Стили можно скопировать из RegisterView.vue или вынести в общий файл */
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-top: 50px;
|
||||
}
|
||||
.login-form {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
padding: 2rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.error-message {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #f5c6cb;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background-color: #42b983;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #36a476;
|
||||
}
|
||||
button:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user