v2.0.0: Multi-user authentication with JWT
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:
poignatov
2026-01-01 18:21:18 +03:00
parent 6015b62d29
commit 4a06ceb7f6
23 changed files with 1970 additions and 279 deletions

View File

@@ -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