v2.0.0: Multi-user authentication with JWT
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 16s
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 16s
Features: - User registration and login with JWT tokens - All data is now user-specific (multi-tenancy) - Profile page with integrations and logout - Automatic migration of existing data to first user Backend changes: - Added users and refresh_tokens tables - Added user_id to all data tables (projects, entries, nodes, dictionaries, words, progress, configs, telegram_integrations, weekly_goals) - JWT authentication middleware - claimOrphanedData() for data migration Frontend changes: - AuthContext for state management - Login/Register forms - Profile page (replaced Integrations) - All API calls use authFetch with Bearer token Migration notes: - On first deploy, backend automatically adds user_id columns - First user to login claims all existing data
This commit is contained in:
@@ -7,13 +7,30 @@ import AddWords from './components/AddWords'
|
||||
import TestConfigSelection from './components/TestConfigSelection'
|
||||
import AddConfig from './components/AddConfig'
|
||||
import TestWords from './components/TestWords'
|
||||
import Integrations from './components/Integrations'
|
||||
import Profile from './components/Profile'
|
||||
import { AuthProvider, useAuth } from './components/auth/AuthContext'
|
||||
import AuthScreen from './components/auth/AuthScreen'
|
||||
|
||||
// API endpoints (используем относительные пути, проксирование настроено в nginx/vite)
|
||||
const CURRENT_WEEK_API_URL = '/playlife-feed'
|
||||
const FULL_STATISTICS_API_URL = '/d2dc349a-0d13-49b2-a8f0-1ab094bfba9b'
|
||||
|
||||
function App() {
|
||||
function AppContent() {
|
||||
const { authFetch, isAuthenticated, loading: authLoading } = useAuth()
|
||||
|
||||
// Show loading while checking auth
|
||||
if (authLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900">
|
||||
<div className="text-white text-xl">Загрузка...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Show auth screen if not authenticated
|
||||
if (!isAuthenticated) {
|
||||
return <AuthScreen />
|
||||
}
|
||||
const [activeTab, setActiveTab] = useState('current')
|
||||
const [selectedProject, setSelectedProject] = useState(null)
|
||||
const [loadedTabs, setLoadedTabs] = useState({
|
||||
@@ -25,7 +42,7 @@ function App() {
|
||||
'test-config': false,
|
||||
'add-config': false,
|
||||
test: false,
|
||||
integrations: false,
|
||||
profile: false,
|
||||
})
|
||||
|
||||
// Отслеживаем, какие табы уже были загружены (для предотвращения повторных загрузок)
|
||||
@@ -38,7 +55,7 @@ function App() {
|
||||
'test-config': false,
|
||||
'add-config': false,
|
||||
test: false,
|
||||
integrations: false,
|
||||
profile: false,
|
||||
})
|
||||
|
||||
// Параметры для навигации между вкладками
|
||||
@@ -77,7 +94,7 @@ function App() {
|
||||
|
||||
try {
|
||||
const savedTab = window.localStorage?.getItem('activeTab')
|
||||
const validTabs = ['current', 'priorities', 'full', 'words', 'add-words', 'test-config', 'add-config', 'test', 'integrations']
|
||||
const validTabs = ['current', 'priorities', 'full', 'words', 'add-words', 'test-config', 'add-config', 'test', 'profile']
|
||||
if (savedTab && validTabs.includes(savedTab)) {
|
||||
setActiveTab(savedTab)
|
||||
setLoadedTabs(prev => ({ ...prev, [savedTab]: true }))
|
||||
@@ -104,7 +121,7 @@ function App() {
|
||||
}
|
||||
setCurrentWeekError(null)
|
||||
console.log('Fetching current week data from:', CURRENT_WEEK_API_URL)
|
||||
const response = await fetch(CURRENT_WEEK_API_URL)
|
||||
const response = await authFetch(CURRENT_WEEK_API_URL)
|
||||
if (!response.ok) {
|
||||
throw new Error('Ошибка загрузки данных')
|
||||
}
|
||||
@@ -149,7 +166,7 @@ function App() {
|
||||
setCurrentWeekLoading(false)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
}, [authFetch])
|
||||
|
||||
const fetchFullStatisticsData = useCallback(async (isBackground = false) => {
|
||||
try {
|
||||
@@ -159,7 +176,7 @@ function App() {
|
||||
setFullStatisticsLoading(true)
|
||||
}
|
||||
setFullStatisticsError(null)
|
||||
const response = await fetch(FULL_STATISTICS_API_URL)
|
||||
const response = await authFetch(FULL_STATISTICS_API_URL)
|
||||
if (!response.ok) {
|
||||
throw new Error('Ошибка загрузки данных')
|
||||
}
|
||||
@@ -175,7 +192,7 @@ function App() {
|
||||
setFullStatisticsLoading(false)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
}, [authFetch])
|
||||
|
||||
// Используем ref для отслеживания инициализации табов (чтобы избежать лишних пересозданий функции)
|
||||
const tabsInitializedRef = useRef({
|
||||
@@ -187,7 +204,7 @@ function App() {
|
||||
'test-config': false,
|
||||
'add-config': false,
|
||||
test: false,
|
||||
integrations: false,
|
||||
profile: false,
|
||||
})
|
||||
|
||||
// Используем ref для отслеживания кеша (чтобы не зависеть от состояния в useCallback)
|
||||
@@ -476,9 +493,9 @@ function App() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loadedTabs.integrations && (
|
||||
<div className={activeTab === 'integrations' ? 'block' : 'hidden'}>
|
||||
<Integrations onNavigate={handleNavigate} />
|
||||
{loadedTabs.profile && (
|
||||
<div className={activeTab === 'profile' ? 'block' : 'hidden'}>
|
||||
<Profile onNavigate={handleNavigate} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -530,22 +547,21 @@ function App() {
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleTabChange('integrations')}
|
||||
onClick={() => handleTabChange('profile')}
|
||||
className={`min-w-max whitespace-nowrap px-4 py-4 transition-all duration-300 relative ${
|
||||
activeTab === 'integrations'
|
||||
activeTab === 'profile'
|
||||
? 'text-indigo-700 bg-white/50'
|
||||
: 'text-gray-600 hover:text-indigo-600 hover:bg-white/30'
|
||||
}`}
|
||||
title="Интеграции"
|
||||
title="Профиль"
|
||||
>
|
||||
<span className="relative z-10 flex items-center justify-center">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path>
|
||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline>
|
||||
<line x1="12" y1="22.08" x2="12" y2="12"></line>
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="12" cy="7" r="4"></circle>
|
||||
</svg>
|
||||
</span>
|
||||
{activeTab === 'integrations' && (
|
||||
{activeTab === 'profile' && (
|
||||
<div className="absolute top-0 left-0 right-0 h-1 bg-gradient-to-r from-indigo-500 to-purple-500"></div>
|
||||
)}
|
||||
</button>
|
||||
@@ -556,6 +572,14 @@ function App() {
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<AppContent />
|
||||
</AuthProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user