modified: begushiybashkir/bbvue/src/main.js

modified:   begushiybashkir/bbvue/src/stores/auth.js
	new file:   begushiybashkir/bbvue/src/stores/helpers/api.js
	new file:   begushiybashkir/bbvue/src/stores/index.js
	new file:   begushiybashkir/bbvue/src/stores/plugins/persistence.js
	modified:   begushiybashkir/bbvue/src/stores/user.js
	modified:   serv_nginx/api_bb/internal/models/user.go
	new file:   serv_nginx/api_bb/internal/models/workout.go
fix save store, add helpers, add new models
This commit is contained in:
2025-10-12 06:11:54 +05:00
parent fd9be2199c
commit 237ee6742e
8 changed files with 229 additions and 201 deletions
+2 -32
View File
@@ -1,44 +1,14 @@
import './assets/main.css' import './assets/main.css'
import { createApp } from 'vue' import { createApp } from 'vue'
import { createPinia } from 'pinia' import pinia from './stores'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import axios from 'axios'
// Глобальная конфигурация axios
axios.defaults.baseURL = 'https://begushiybashkir.ru/api/v1'
axios.defaults.withCredentials = true // Для работы с куками
// Интерцептор для автоматического добавления токена
axios.interceptors.request.use((config) => {
const token = localStorage.getItem('auth_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// Интерцептор для обработки ошибок авторизации
axios.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
// Токен истек или невалиден
const authStore = useAuthStore()
authStore.clearToken()
authStore.clearUser()
window.location.href = '/login'
}
return Promise.reject(error)
}
)
const app = createApp(App) const app = createApp(App)
app.use(createPinia()) app.use(pinia)
app.use(router) app.use(router)
// Инициализация auth store после создания app // Инициализация auth store после создания app
+56 -95
View File
@@ -1,57 +1,45 @@
// stores/auth.js
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import axios from 'axios' import { apiClient, withLoading } from './helpers/api'
const AUTH_API_URL = 'https://begushiybashkir.ru/api/v1/auth'
export const useAuthStore = defineStore('auth', () => { export const useAuthStore = defineStore('auth', () => {
// State
const user = ref(null) const user = ref(null)
const token = ref(localStorage.getItem('auth_token') || '') const token = ref(localStorage.getItem('auth_token') || '')
const loading = ref(false) const loading = ref(false)
const error = ref('') const error = ref('')
const initialized = ref(false)
// Computed свойства // Getters
const isAuthenticated = computed(() => !!token.value && !!user.value) const isAuthenticated = computed(() => !!token.value && !!user.value)
const userFullName = computed(() => { const userFullName = computed(() =>
if (!user.value) return '' user.value ? `${user.value.firstName} ${user.value.lastName}` : ''
return `${user.value.firstName} ${user.value.lastName}` )
})
// Установка токена // Actions
const setToken = (newToken) => { const setToken = (newToken) => {
token.value = newToken token.value = newToken
localStorage.setItem('auth_token', newToken) localStorage.setItem('auth_token', newToken)
// Устанавливаем токен в заголовки axios по умолчанию
axios.defaults.headers.common['Authorization'] = `Bearer ${newToken}`
} }
// Очистка токена const clearAuth = () => {
const clearToken = () => {
token.value = '' token.value = ''
user.value = null
localStorage.removeItem('auth_token') localStorage.removeItem('auth_token')
delete axios.defaults.headers.common['Authorization']
} }
// Установка пользователя
const setUser = (userData) => { const setUser = (userData) => {
user.value = userData user.value = userData
} }
// Очистка пользователя
const clearUser = () => {
user.value = null
}
// Регистрация
const register = async (userData) => { const register = async (userData) => {
loading.value = true // Передаем store объект с loading и error
error.value = '' return withLoading({ loading, error }, async () => {
await apiClient.post('/auth/register', userData)
try {
const response = await axios.post(`${AUTH_API_URL}/register`, userData) // Auto-login after registration
const loginResponse = await apiClient.post('/auth/login', {
// После успешной регистрации автоматически логинимся
const loginResponse = await axios.post(`${AUTH_API_URL}/login`, {
email: userData.email, email: userData.email,
password: userData.password password: userData.password
}) })
@@ -60,89 +48,62 @@ export const useAuthStore = defineStore('auth', () => {
setToken(authToken) setToken(authToken)
setUser(userInfo) setUser(userInfo)
return { success: true, data: response.data } return { success: true }
} catch (err) { })
error.value = err.response?.data?.message || err.message || 'Ошибка регистрации'
return { success: false, error: error.value }
} finally {
loading.value = false
}
} }
// Логин
const login = async (credentials) => { const login = async (credentials) => {
loading.value = true return withLoading({ loading, error }, async () => {
error.value = '' const response = await apiClient.post('/auth/login', credentials)
try {
const response = await axios.post(`${AUTH_API_URL}/login`, credentials)
const { token: authToken, user: userInfo } = response.data const { token: authToken, user: userInfo } = response.data
console.log("authToken: " + authToken + "userInfo: " + userInfo)
setToken(authToken) setToken(authToken)
setUser(userInfo) setUser(userInfo)
return { success: true, data: response.data } return { success: true, data: response.data }
} catch (err) { })
error.value = err.response?.data?.message || 'Ошибка входа'
return { success: false, error: error.value }
} finally {
loading.value = false
}
} }
// Выход
const logout = async () => { const logout = async () => {
loading.value = true return withLoading({ loading, error }, async () => {
try {
try { await apiClient.post('/auth/logout')
await axios.post(`${AUTH_API_URL}/logout`, {}, { } catch (err) {
headers: { console.error('Logout error:', err)
'Authorization': `Bearer ${token.value}` } finally {
} clearAuth()
}) }
} catch (err) { return { success: true }
console.error('Ошибка при выходе:', err) })
} finally {
clearToken()
clearUser()
loading.value = false
}
} }
// Получение профиля
const fetchProfile = async () => { const fetchProfile = async () => {
loading.value = true return withLoading({ loading, error }, async () => {
error.value = '' const response = await apiClient.get('/user/profile')
try {
const response = await axios.get(`${AUTH_API_URL}/profile`)
setUser(response.data) setUser(response.data)
return { success: true, data: response.data } return { success: true, data: response.data }
} catch (err) { })
error.value = err.response?.data?.message || err.message || 'Ошибка загрузки профиля' }
clearToken()
clearUser() const updateProfile = async (profileData) => {
return { success: false, error: error.value } return withLoading({ loading, error }, async () => {
} finally { const response = await apiClient.post('/user/editProfile', profileData)
loading.value = false setUser(response.data)
} return { success: true, data: response.data }
})
} }
// Инициализация при загрузке приложения
const initializeAuth = async () => { const initializeAuth = async () => {
if (token.value) { if (initialized.value || !token.value) return
// Восстанавливаем заголовок авторизации
axios.defaults.headers.common['Authorization'] = `Bearer ${token.value}` initialized.value = true
try {
// Загружаем данные пользователя try {
await fetchProfile() await fetchProfile()
} catch (error) { console.log('Auth restored successfully')
console.error('Ошибка инициализации авторизации:', error) } catch (err) {
// Если токен невалидный, очищаем его console.error('Auth restoration failed:', err)
clearToken() clearAuth()
clearUser()
}
} }
} }
@@ -152,6 +113,7 @@ export const useAuthStore = defineStore('auth', () => {
token, token,
loading, loading,
error, error,
initialized,
// Getters // Getters
isAuthenticated, isAuthenticated,
@@ -162,9 +124,8 @@ export const useAuthStore = defineStore('auth', () => {
login, login,
logout, logout,
fetchProfile, fetchProfile,
updateProfile,
initializeAuth, initializeAuth,
setToken, clearAuth
clearToken,
clearUser
} }
}) })
@@ -0,0 +1,77 @@
// stores/helpers/api.js
import axios from 'axios'
const API_BASE_URL = 'https://begushiybashkir.ru/api/v1'
// Создаем экземпляр axios с базовой конфигурацией
export const apiClient = axios.create({
baseURL: API_BASE_URL,
withCredentials: true
})
// Интерцептор для автоматического добавления токена
apiClient.interceptors.request.use((config) => {
const token = localStorage.getItem('auth_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// Интерцептор для обработки ошибок
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem('auth_token')
window.location.href = '/login'
}
return Promise.reject(error)
}
)
// Утилита для обработки ошибок
export const handleApiError = (error) => {
const message = error.response?.data?.message || error.message || 'Произошла ошибка'
return { success: false, error: message }
}
// Утилита для выполнения запросов с loading state
export const withLoading = async (store, fn) => {
store.loading = true
store.error = ''
try {
return await fn()
} catch (error) {
const result = handleApiError(error)
store.error = result.error
return result
} finally {
store.loading = false
}
}
export const createLoadingHandler = (store) => {
return async (fn) => {
if (store && typeof store.loading !== 'undefined') {
store.loading = true
}
if (store && typeof store.error !== 'undefined') {
store.error = ''
}
try {
return await fn()
} catch (error) {
const result = handleApiError(error)
if (store && typeof store.error !== 'undefined') {
store.error = result.error
}
return result
} finally {
if (store && typeof store.loading !== 'undefined') {
store.loading = false
}
}
}
}
@@ -0,0 +1,6 @@
// stores/index.js
import { createPinia } from 'pinia'
const pinia = createPinia()
export default pinia
@@ -0,0 +1,7 @@
// stores/plugins/persistence.js
export const authPersistPlugin = ({ store }) => {
// Восстанавливаем состояние при инициализации
if (store.$id === 'auth') {
store.initializeAuth()
}
}
+60 -57
View File
@@ -1,26 +1,53 @@
// stores/user.js
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
// import axios from 'axios' import { handleApiError } from './helpers/api'
// const API_BASE_URL = 'https://begushiybashkir.ru/api/v1'
export const useUserStore = defineStore('user', () => { export const useUserStore = defineStore('user', () => {
// State
const userStats = ref(null) const userStats = ref(null)
const userTraining = ref(null) const userTraining = ref(null)
const userAchievements = ref([]) const userAchievements = ref([])
const loading = ref(false) const loading = ref(false)
const error = ref('') const error = ref('')
// Получение статистики пользователя // Getters
const fetchUserStats = async () => { const completedAchievements = computed(() =>
userAchievements.value.filter(achievement => achievement.achieved)
)
const pendingAchievements = computed(() =>
userAchievements.value.filter(achievement => !achievement.achieved)
)
const achievementProgress = computed(() => {
if (!userAchievements.value.length) return 0
return Math.round((completedAchievements.value.length / userAchievements.value.length) * 100)
})
// Вспомогательная функция для обработки loading/error
const withStoreLoading = async (fn) => {
loading.value = true loading.value = true
error.value = '' error.value = ''
try { try {
// const response = await axios.get(`${API_BASE_URL}/user/stats`) return await fn()
} catch (err) {
const result = handleApiError(err)
error.value = result.error
return result
} finally {
loading.value = false
}
}
// Actions
const fetchUserStats = async () => {
return withStoreLoading(async () => {
// TODO: Заменить на реальный endpoint когда будет готов
// const response = await apiClient.get('/user/stats')
// Временные данные для демонстрации // Временные мок данные
await new Promise(resolve => setTimeout(resolve, 1000)) await new Promise(resolve => setTimeout(resolve, 500))
userStats.value = { userStats.value = {
totalDistance: 245, totalDistance: 245,
@@ -33,24 +60,15 @@ export const useUserStore = defineStore('user', () => {
} }
return { success: true, data: userStats.value } return { success: true, data: userStats.value }
} catch (err) { })
error.value = err.response?.data?.message || 'Ошибка загрузки статистики'
return { success: false, error: error.value }
} finally {
loading.value = false
}
} }
// Получение плана тренировок
const fetchUserTraining = async () => { const fetchUserTraining = async () => {
loading.value = true return withStoreLoading(async () => {
error.value = '' // TODO: Заменить на реальный endpoint когда будет готов
// const response = await apiClient.get('/user/training')
try {
// TODO: Заменить на реальный endpoint
// const response = await axios.get(`${API_BASE_URL}/user/training`)
await new Promise(resolve => setTimeout(resolve, 1000)) await new Promise(resolve => setTimeout(resolve, 500))
userTraining.value = { userTraining.value = {
currentWeek: 4, currentWeek: 4,
@@ -64,24 +82,15 @@ export const useUserStore = defineStore('user', () => {
} }
return { success: true, data: userTraining.value } return { success: true, data: userTraining.value }
} catch (err) { })
error.value = err.response?.data?.message || 'Ошибка загрузки плана тренировок'
return { success: false, error: error.value }
} finally {
loading.value = false
}
} }
// Получение достижений
const fetchUserAchievements = async () => { const fetchUserAchievements = async () => {
loading.value = true return withStoreLoading(async () => {
error.value = '' // TODO: Заменить на реальный endpoint когда будет готов
// const response = await apiClient.get('/user/achievements')
try {
// TODO: Заменить на реальный endpoint
// const response = await axios.get(`${API_BASE_URL}/user/achievements`)
await new Promise(resolve => setTimeout(resolve, 1000)) await new Promise(resolve => setTimeout(resolve, 500))
userAchievements.value = [ userAchievements.value = [
{ id: 1, name: 'Первый забег', description: 'Пробежать первую 5км', achieved: true, date: '2024-01-20' }, { id: 1, name: 'Первый забег', description: 'Пробежать первую 5км', achieved: true, date: '2024-01-20' },
@@ -92,27 +101,20 @@ export const useUserStore = defineStore('user', () => {
] ]
return { success: true, data: userAchievements.value } return { success: true, data: userAchievements.value }
} catch (err) { })
error.value = err.response?.data?.message || 'Ошибка загрузки достижений'
return { success: false, error: error.value }
} finally {
loading.value = false
}
} }
// Computed свойства // Пакетная загрузка всех данных пользователя
const completedAchievements = computed(() => const fetchAllUserData = async () => {
userAchievements.value.filter(achievement => achievement.achieved) return withStoreLoading(async () => {
) await Promise.all([
fetchUserStats(),
const pendingAchievements = computed(() => fetchUserTraining(),
userAchievements.value.filter(achievement => !achievement.achieved) fetchUserAchievements()
) ])
return { success: true }
const achievementProgress = computed(() => { })
if (!userAchievements.value.length) return 0 }
return Math.round((completedAchievements.value.length / userAchievements.value.length) * 100)
})
return { return {
// State // State
@@ -130,6 +132,7 @@ export const useUserStore = defineStore('user', () => {
// Actions // Actions
fetchUserStats, fetchUserStats,
fetchUserTraining, fetchUserTraining,
fetchUserAchievements fetchUserAchievements,
fetchAllUserData
} }
}) })
+16 -17
View File
@@ -6,23 +6,22 @@ import (
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"gorm.io/gorm" "gorm.io/gorm"
) )
type User struct { type User struct {
ID uint `json:"id" gorm:"primaryKey"` ID uint `json:"id" gorm:"primaryKey"`
Email string `json:"email" gorm:"uniqueIndex;not null"` Email string `json:"email" gorm:"uniqueIndex;not null"`
Password string `json:"-" gorm:"not null"` Password string `json:"-" gorm:"not null"`
FirstName string `json:"first_name" gorm:"not null"` FirstName string `json:"first_name" gorm:"not null"`
LastName string `json:"last_name" gorm:"not null"` LastName string `json:"last_name" gorm:"not null"`
Phone string `json:"phone"` Phone string `json:"phone"`
Experience string `json:"experience"` Experience string `json:"experience"`
Goals string `json:"goals"` Goals string `json:"goals"`
Newsletter bool `json:"newsletter"` Newsletter bool `json:"newsletter"`
Role string `json:"role" gorm:"default:user"` Role string `json:"role" gorm:"default:user"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
} }
// HashPassword хеширует пароль перед сохранением // HashPassword хеширует пароль перед сохранением
@@ -37,8 +36,8 @@ func (u *User) HashPassword() error {
// CheckPassword проверяет пароль // CheckPassword проверяет пароль
func (u *User) CheckPassword(password string) bool { func (u *User) CheckPassword(password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password)) err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
return err == nil return err == nil
} }
// BeforeCreate hook для GORM // BeforeCreate hook для GORM
@@ -56,4 +55,4 @@ func (u *User) BeforeCreate(tx *gorm.DB) error {
func (u *User) BeforeUpdate(tx *gorm.DB) error { func (u *User) BeforeUpdate(tx *gorm.DB) error {
u.UpdatedAt = time.Now() u.UpdatedAt = time.Now()
return nil return nil
} }
@@ -0,0 +1,5 @@
package models
type Workout struct {
ID uint
}