231 lines
5.8 KiB
React
231 lines
5.8 KiB
React
|
|
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'
|
||
|
|
|
||
|
|
const AuthContext = createContext(null)
|
||
|
|
|
||
|
|
const TOKEN_KEY = 'access_token'
|
||
|
|
const REFRESH_TOKEN_KEY = 'refresh_token'
|
||
|
|
const USER_KEY = 'user'
|
||
|
|
|
||
|
|
export function AuthProvider({ children }) {
|
||
|
|
const [user, setUser] = useState(null)
|
||
|
|
const [loading, setLoading] = useState(true)
|
||
|
|
const [error, setError] = useState(null)
|
||
|
|
|
||
|
|
// Initialize from localStorage
|
||
|
|
useEffect(() => {
|
||
|
|
const initAuth = async () => {
|
||
|
|
const token = localStorage.getItem(TOKEN_KEY)
|
||
|
|
const savedUser = localStorage.getItem(USER_KEY)
|
||
|
|
|
||
|
|
if (token && savedUser) {
|
||
|
|
try {
|
||
|
|
setUser(JSON.parse(savedUser))
|
||
|
|
// Verify token is still valid
|
||
|
|
const response = await fetch('/api/auth/me', {
|
||
|
|
headers: {
|
||
|
|
'Authorization': `Bearer ${token}`,
|
||
|
|
'Content-Type': 'application/json'
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
if (response.ok) {
|
||
|
|
const data = await response.json()
|
||
|
|
setUser(data.user)
|
||
|
|
localStorage.setItem(USER_KEY, JSON.stringify(data.user))
|
||
|
|
} else if (response.status === 401) {
|
||
|
|
// Try to refresh token
|
||
|
|
const refreshed = await refreshToken()
|
||
|
|
if (!refreshed) {
|
||
|
|
logout()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Auth init error:', err)
|
||
|
|
logout()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
setLoading(false)
|
||
|
|
}
|
||
|
|
|
||
|
|
initAuth()
|
||
|
|
}, [])
|
||
|
|
|
||
|
|
const login = useCallback(async (email, password) => {
|
||
|
|
setError(null)
|
||
|
|
try {
|
||
|
|
const response = await fetch('/api/auth/login', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json'
|
||
|
|
},
|
||
|
|
body: JSON.stringify({ email, password })
|
||
|
|
})
|
||
|
|
|
||
|
|
const data = await response.json()
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error(data.error || 'Ошибка входа')
|
||
|
|
}
|
||
|
|
|
||
|
|
localStorage.setItem(TOKEN_KEY, data.access_token)
|
||
|
|
localStorage.setItem(REFRESH_TOKEN_KEY, data.refresh_token)
|
||
|
|
localStorage.setItem(USER_KEY, JSON.stringify(data.user))
|
||
|
|
setUser(data.user)
|
||
|
|
|
||
|
|
return true
|
||
|
|
} catch (err) {
|
||
|
|
setError(err.message)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}, [])
|
||
|
|
|
||
|
|
const register = useCallback(async (email, password, name) => {
|
||
|
|
setError(null)
|
||
|
|
try {
|
||
|
|
const response = await fetch('/api/auth/register', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json'
|
||
|
|
},
|
||
|
|
body: JSON.stringify({ email, password, name: name || undefined })
|
||
|
|
})
|
||
|
|
|
||
|
|
const data = await response.json()
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
throw new Error(data.error || 'Ошибка регистрации')
|
||
|
|
}
|
||
|
|
|
||
|
|
localStorage.setItem(TOKEN_KEY, data.access_token)
|
||
|
|
localStorage.setItem(REFRESH_TOKEN_KEY, data.refresh_token)
|
||
|
|
localStorage.setItem(USER_KEY, JSON.stringify(data.user))
|
||
|
|
setUser(data.user)
|
||
|
|
|
||
|
|
return true
|
||
|
|
} catch (err) {
|
||
|
|
setError(err.message)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}, [])
|
||
|
|
|
||
|
|
const logout = useCallback(async () => {
|
||
|
|
const token = localStorage.getItem(TOKEN_KEY)
|
||
|
|
|
||
|
|
if (token) {
|
||
|
|
try {
|
||
|
|
await fetch('/api/auth/logout', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Authorization': `Bearer ${token}`,
|
||
|
|
'Content-Type': 'application/json'
|
||
|
|
}
|
||
|
|
})
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Logout error:', err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
localStorage.removeItem(TOKEN_KEY)
|
||
|
|
localStorage.removeItem(REFRESH_TOKEN_KEY)
|
||
|
|
localStorage.removeItem(USER_KEY)
|
||
|
|
setUser(null)
|
||
|
|
}, [])
|
||
|
|
|
||
|
|
const refreshToken = useCallback(async () => {
|
||
|
|
const refresh = localStorage.getItem(REFRESH_TOKEN_KEY)
|
||
|
|
|
||
|
|
if (!refresh) {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const response = await fetch('/api/auth/refresh', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json'
|
||
|
|
},
|
||
|
|
body: JSON.stringify({ refresh_token: refresh })
|
||
|
|
})
|
||
|
|
|
||
|
|
if (!response.ok) {
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
const data = await response.json()
|
||
|
|
|
||
|
|
localStorage.setItem(TOKEN_KEY, data.access_token)
|
||
|
|
localStorage.setItem(REFRESH_TOKEN_KEY, data.refresh_token)
|
||
|
|
localStorage.setItem(USER_KEY, JSON.stringify(data.user))
|
||
|
|
setUser(data.user)
|
||
|
|
|
||
|
|
return true
|
||
|
|
} catch (err) {
|
||
|
|
console.error('Refresh token error:', err)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}, [])
|
||
|
|
|
||
|
|
const getToken = useCallback(() => {
|
||
|
|
return localStorage.getItem(TOKEN_KEY)
|
||
|
|
}, [])
|
||
|
|
|
||
|
|
// Fetch wrapper that handles auth
|
||
|
|
const authFetch = useCallback(async (url, options = {}) => {
|
||
|
|
const token = localStorage.getItem(TOKEN_KEY)
|
||
|
|
|
||
|
|
const headers = {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
...options.headers
|
||
|
|
}
|
||
|
|
|
||
|
|
if (token) {
|
||
|
|
headers['Authorization'] = `Bearer ${token}`
|
||
|
|
}
|
||
|
|
|
||
|
|
let response = await fetch(url, { ...options, headers })
|
||
|
|
|
||
|
|
// If 401, try to refresh token and retry
|
||
|
|
if (response.status === 401) {
|
||
|
|
const refreshed = await refreshToken()
|
||
|
|
if (refreshed) {
|
||
|
|
const newToken = localStorage.getItem(TOKEN_KEY)
|
||
|
|
headers['Authorization'] = `Bearer ${newToken}`
|
||
|
|
response = await fetch(url, { ...options, headers })
|
||
|
|
} else {
|
||
|
|
logout()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return response
|
||
|
|
}, [refreshToken, logout])
|
||
|
|
|
||
|
|
const value = {
|
||
|
|
user,
|
||
|
|
loading,
|
||
|
|
error,
|
||
|
|
login,
|
||
|
|
register,
|
||
|
|
logout,
|
||
|
|
getToken,
|
||
|
|
authFetch,
|
||
|
|
isAuthenticated: !!user
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<AuthContext.Provider value={value}>
|
||
|
|
{children}
|
||
|
|
</AuthContext.Provider>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
export function useAuth() {
|
||
|
|
const context = useContext(AuthContext)
|
||
|
|
if (!context) {
|
||
|
|
throw new Error('useAuth must be used within an AuthProvider')
|
||
|
|
}
|
||
|
|
return context
|
||
|
|
}
|
||
|
|
|
||
|
|
export default AuthContext
|
||
|
|
|