add storage

This commit is contained in:
2026-07-18 16:16:10 +07:00
parent ad68ccc297
commit 0731123fbb
7 changed files with 375 additions and 23 deletions
@@ -0,0 +1,60 @@
import { createEveningDetectiveServerClient, type EveningDetectiveServer } from ".";
type RequestType = {
path: string;
method: string;
body: string | null;
};
function createHttpHandler(
baseURL: string,
getToken?: () => string | null,
): (
request: { path: string, method: string, body: string | null },
) => Promise<unknown> {
return async (
request: { path: string, method: string, body: string | null },
) => {
// Формируем полный URL
const url = `${baseURL}/${request.path}`;
// Подготавливаем заголовки
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
// Добавляем токен, если он есть
const token = getToken?.();
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
// Делаем запрос
const response = await fetch(url, {
method: request.method,
headers,
body: request.body, // body уже JSON-строка из генератора
});
// Парсим ответ
const data = await response.json();
// Возвращаем в формате, который ожидает генератор
return data;
};
}
export function getURL(): string {
return "http://localhost:8090"
}
export function buildClient(
getToken?: () => string | null,
): EveningDetectiveServer {
return createEveningDetectiveServerClient(
createHttpHandler(
getURL(),
getToken,
)
)
}
+44 -11
View File
@@ -12,7 +12,14 @@
<n-input v-model:value="password" type="password" placeholder="********" /> <n-input v-model:value="password" type="password" placeholder="********" />
<div class="form-button-wrapper"> <div class="form-button-wrapper">
<div class="form-label"> <div class="form-label">
<n-button @click="signin" :disabled="email.length == 0 || password.length == 0">Вход</n-button> <n-button @click="signin" :disabled="email.length == 0 || password.length == 0">
<span v-if="!authStore.isLoading">
Вход
</span>
<span v-else>
Подождите...
</span>
</n-button>
</div> </div>
</div> </div>
</n-space> </n-space>
@@ -36,8 +43,14 @@
</div> </div>
<div class="form-button-wrapper"> <div class="form-button-wrapper">
<div class="form-label"> <div class="form-label">
<n-button @click="signup" <n-button @click="signup" :disabled="username.length == 0 || password.length == 0 || !approval">
:disabled="username.length == 0 || password.length == 0 || !approval">Регистрация</n-button> <span v-if="!authStore.isLoading">
Регистрация
</span>
<span v-else>
Подождите...
</span>
</n-button>
</div> </div>
</div> </div>
</n-space> </n-space>
@@ -50,7 +63,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { NCard, NTabs, NTabPane, NCheckbox, NButton, NSpace, NInput, NAutoComplete } from 'naive-ui' import { NCard, NTabs, NTabPane, NCheckbox, NButton, NSpace, NInput, NAutoComplete, useMessage } from 'naive-ui'
import { useAuthStore } from '@/stores/auth';
import { useRouter } from 'vue-router';
const authStore = useAuthStore();
const router = useRouter()
const message = useMessage()
const username = ref('') const username = ref('')
const email = ref('') const email = ref('')
@@ -68,15 +89,27 @@ const options = computed(() => {
}) })
}) })
function signin() { async function signin() {
console.log(email.value) const res = await authStore.login(email.value, password.value)
console.log(password.value)
if (!res && authStore.error != null) {
message.error(authStore.error)
return
} }
function signup() { router.push('/office')
console.log(username.value) }
console.log(email.value)
console.log(approval.value) async function signup() {
if (!approval.value) {
return
}
const res = await authStore.signup(username.value, email.value)
if (!res && authStore.error != null) {
message.error(authStore.error)
return
}
message.error("Пароль отправлен на почту")
} }
</script> </script>
+29
View File
@@ -0,0 +1,29 @@
<template>
<header class="header">
<p @click="goToLogout" class="btn-login">
Выйти
</p>
</header>
<p>
Доброго времени суток, {{ authStore.username }}.
</p>
<p>
{{ authStore.userRoles }}
</p>
</template>
<script setup lang="ts">
import router from '@/router';
import { useAuthStore } from '@/stores/auth';
const authStore = useAuthStore();
const goToLogout = () => {
authStore.logout()
router.push('/login')
}
</script>
<style scoped></style>
+6
View File
@@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router' import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue' import HomeView from '../views/HomeView.vue'
import LoginView from '../views/LoginView.vue' import LoginView from '../views/LoginView.vue'
import OfficeView from '../views/OfficeView.vue'
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
@@ -15,6 +16,11 @@ const router = createRouter({
name: 'login', name: 'login',
component: LoginView, component: LoginView,
}, },
{
path: '/office',
name: 'office',
component: OfficeView,
},
], ],
}) })
+226
View File
@@ -0,0 +1,226 @@
// stores/auth.ts
import { type User } from '@/api/generated/crabs/evening_detective_server';
import { buildClient, getURL } from '@/api/generated/crabs/evening_detective_server/client';
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useAuthStore = defineStore('auth', () => {
// ===== STATE =====
const accessToken = ref<string | null>(null);
const refreshToken = ref<string | null>(null);
const user = ref<User | null>(null);
const isLoading = ref(false);
const error = ref<string | null>(null);
// ===== GETTERS =====
const isAuthenticated = computed(() => !!accessToken.value);
const username = computed(() => user.value?.username || '');
const userRoles = computed(() => user.value?.roles || []);
// ===== ACTIONS =====
// Инициализация из localStorage при загрузке
function initFromStorage() {
const token = localStorage.getItem('accessToken');
const refresh = localStorage.getItem('refreshToken');
if (token) {
accessToken.value = token;
refreshToken.value = refresh;
// Получаем данные пользователя
fetchUser();
}
}
function getToken(): string | null {
return accessToken.value
}
// Получение данных пользователя
async function fetchUser() {
if (!accessToken.value) return;
try {
const client = buildClient(getToken);
const response = await client.GetMe({});
if (response.error) {
error.value = response.error;
return;
}
if (response.user) {
user.value = response.user;
}
} catch (err) {
console.error('Failed to fetch user:', err);
// Если 401 — токен протух
if (err instanceof Error && err.message.includes('401')) {
await logout();
}
}
}
// Логин
async function login(email: string, password: string) {
isLoading.value = true;
error.value = null;
try {
const client = buildClient(getToken);
const response = await client.Login({ email, password });
if (response.error) {
error.value = response.error;
return false;
}
// Сохраняем токены
if (response.accessToken) {
accessToken.value = response.accessToken;
refreshToken.value = response.refreshToken || null;
// Сохраняем в localStorage
localStorage.setItem('accessToken', response.accessToken);
if (response.refreshToken) {
localStorage.setItem('refreshToken', response.refreshToken);
}
// Получаем данные пользователя
await fetchUser();
return true;
}
return false;
} catch (err) {
error.value = err instanceof Error ? err.message : 'Login failed';
return false;
} finally {
isLoading.value = false;
}
}
// Обновление токена
async function refreshTokenAction() {
if (!refreshToken.value) return false;
try {
const client = buildClient(getToken);
const response = await client.Refresh({
refreshToken: refreshToken.value
});
if (response.error || !response.accessToken) {
return false;
}
// Обновляем токены
accessToken.value = response.accessToken;
refreshToken.value = response.refreshToken || null;
localStorage.setItem('accessToken', response.accessToken);
if (response.refreshToken) {
localStorage.setItem('refreshToken', response.refreshToken);
}
return true;
} catch {
return false;
}
}
// Регистрация
async function signup(username: string, email: string) {
isLoading.value = true;
error.value = null;
try {
const client = buildClient(getToken);
const response = await client.Signup({ username, email });
if (response.error) {
error.value = response.error;
return false;
}
return true
} catch (err) {
error.value = err instanceof Error ? err.message : 'Signup failed';
return false;
} finally {
isLoading.value = false;
}
}
// Восстановление пароля
async function refreshPassword(email: string) {
isLoading.value = true;
error.value = null;
try {
const client = buildClient(getToken);
const response = await client.RefreshPassword({ email });
if (response.error) {
error.value = response.error;
return false;
}
return true;
} catch (err) {
error.value = err instanceof Error ? err.message : 'Password reset failed';
return false;
} finally {
isLoading.value = false;
}
}
// Выход
async function logout() {
accessToken.value = null;
refreshToken.value = null;
user.value = null;
error.value = null;
localStorage.removeItem('accessToken');
localStorage.removeItem('refreshToken');
}
// Проверка ролей
function hasRole(role: string): boolean {
return userRoles.value.includes(role);
}
function hasAnyRole(roles: string[]): boolean {
return roles.some(role => userRoles.value.includes(role));
}
// ===== RETURN =====
return {
// State
accessToken,
refreshToken,
user,
isLoading,
error,
// Getters
isAuthenticated,
username,
userRoles,
// Actions
initFromStorage,
getToken,
login,
signup,
refreshPassword,
logout,
fetchUser,
refreshTokenAction,
hasRole,
hasAnyRole,
};
});
-12
View File
@@ -1,12 +0,0 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
+10
View File
@@ -0,0 +1,10 @@
<script setup lang="ts">
import OfficePage from '@/components/OfficePage.vue';
</script>
<!-- Страница авторизации -->
<template>
<main>
<OfficePage />
</main>
</template>