CCRss commited on
Commit
7bc4c7e
·
verified ·
1 Parent(s): 17f79b3

Upload google_auth_spec.md

Browse files
Files changed (1) hide show
  1. google_auth_spec.md +619 -0
google_auth_spec.md ADDED
@@ -0,0 +1,619 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ЗАДАЧА: Реализовать авторизацию через Google OAuth 2.0
2
+
3
+ ## КОНТЕКСТ ПРОЕКТА
4
+
5
+ Проект **Steppe Sonorum: Piano** — цифровой каталог казахских фортепианных композиций.
6
+
7
+ Стек:
8
+ - Backend: Django 5.x + Django REST Framework, PostgreSQL
9
+ - Frontend: Next.js 14+ (App Router), Tailwind CSS
10
+ - Сервер: Ubuntu 24.04, Nginx, домен https://kitan-a.com/
11
+
12
+ Структура репозитория — монорепо:
13
+ ```
14
+ steppe-sonorum/
15
+ ├── backend/ # Django
16
+ │ ├── config/ # settings, urls
17
+ │ └── catalog/ # основное приложение каталога
18
+ ├── frontend/ # Next.js
19
+ └── ...
20
+ ```
21
+
22
+ Авторизация нужна для будущих фич: избранное, коллекции, комментарии, предложение правок. Сейчас сайт полностью публичный (каталог доступен без логина). Авторизация НЕ должна блокировать доступ к каталогу — только добавлять возможности залогиненным пользователям.
23
+
24
+ ---
25
+
26
+ ## ЧТО НУЖНО РЕАЛИЗОВАТЬ
27
+
28
+ ### 1. Backend (Django)
29
+
30
+ **Установить и настроить пакеты:**
31
+ ```
32
+ django-allauth[socialaccount]
33
+ dj-rest-auth[with_social]
34
+ djangorestframework-simplejwt
35
+ ```
36
+
37
+ **Создать новое Django приложение `accounts`:**
38
+ ```
39
+ backend/
40
+ ├── accounts/
41
+ │ ├── models.py # Расширенная модель пользователя
42
+ │ ├── serializers.py # User serializer
43
+ │ ├── views.py # Профиль, Google callback
44
+ │ ├── urls.py
45
+ │ └── admin.py
46
+ ```
47
+
48
+ **Модель пользователя — расширить стандартную:**
49
+ ```python
50
+ from django.contrib.auth.models import AbstractUser
51
+
52
+ class User(AbstractUser):
53
+ avatar_url = models.URLField(blank=True, default="") # Google profile photo
54
+ display_name = models.CharField(max_length=255, blank=True) # Имя для отображения
55
+ bio = models.TextField(blank=True, default="") # О себе (опционально)
56
+ created_at = models.DateTimeField(auto_now_add=True)
57
+ updated_at = models.DateTimeField(auto_now=True)
58
+
59
+ def __str__(self):
60
+ return self.display_name or self.username
61
+ ```
62
+
63
+ ВАЖНО: Задать `AUTH_USER_MODEL = 'accounts.User'` в settings.py ПЕРЕД первой миграцией. Если миграции уже существуют — нужно сбросить БД или мигрировать аккуратно.
64
+
65
+ **Настройки Django (config/settings/base.py):**
66
+ ```python
67
+ INSTALLED_APPS = [
68
+ # ... существующие приложения ...
69
+ 'django.contrib.sites',
70
+ 'allauth',
71
+ 'allauth.account',
72
+ 'allauth.socialaccount',
73
+ 'allauth.socialaccount.providers.google',
74
+ 'rest_framework',
75
+ 'rest_framework.authtoken',
76
+ 'dj_rest_auth',
77
+ 'dj_rest_auth.registration',
78
+ 'accounts',
79
+ ]
80
+
81
+ SITE_ID = 1
82
+
83
+ # REST Framework — JWT авторизация
84
+ REST_FRAMEWORK = {
85
+ 'DEFAULT_AUTHENTICATION_CLASSES': [
86
+ 'rest_framework_simplejwt.authentication.JWTAuthentication',
87
+ 'rest_framework.authentication.SessionAuthentication', # Для Django admin
88
+ ],
89
+ 'DEFAULT_PERMISSION_CLASSES': [
90
+ 'rest_framework.permissions.AllowAny', # Каталог публичный по умолчанию
91
+ ],
92
+ }
93
+
94
+ # JWT настройки
95
+ from datetime import timedelta
96
+ SIMPLE_JWT = {
97
+ 'ACCESS_TOKEN_LIFETIME': timedelta(hours=1),
98
+ 'REFRESH_TOKEN_LIFETIME': timedelta(days=30),
99
+ 'ROTATE_REFRESH_TOKENS': True,
100
+ 'AUTH_HEADER_TYPES': ('Bearer',),
101
+ }
102
+
103
+ # dj-rest-auth — использовать JWT вместо Token
104
+ REST_AUTH = {
105
+ 'USE_JWT': True,
106
+ 'JWT_AUTH_COOKIE': 'access_token',
107
+ 'JWT_AUTH_REFRESH_COOKIE': 'refresh_token',
108
+ 'JWT_AUTH_HTTPONLY': True, # HttpOnly cookie — безопасно
109
+ 'JWT_AUTH_SAMESITE': 'Lax',
110
+ 'JWT_AUTH_SECURE': True, # True для HTTPS (production)
111
+ 'USER_DETAILS_SERIALIZER': 'accounts.serializers.UserSerializer',
112
+ }
113
+
114
+ # allauth
115
+ ACCOUNT_EMAIL_REQUIRED = True
116
+ ACCOUNT_USERNAME_REQUIRED = False
117
+ ACCOUNT_AUTHENTICATION_METHOD = 'email'
118
+ ACCOUNT_EMAIL_VERIFICATION = 'none' # Не требовать верификацию email при входе через Google
119
+ SOCIALACCOUNT_AUTO_SIGNUP = True # Автоматически создавать юзера при первом входе
120
+ SOCIALACCOUNT_EMAIL_AUTHENTICATION_AUTO_CONNECT = True
121
+
122
+ # Google OAuth — значения из переменных окружения
123
+ SOCIALACCOUNT_PROVIDERS = {
124
+ 'google': {
125
+ 'SCOPE': ['profile', 'email'],
126
+ 'AUTH_PARAMS': {'access_type': 'online'},
127
+ 'APP': {
128
+ 'client_id': os.environ.get('GOOGLE_CLIENT_ID', ''),
129
+ 'secret': os.environ.get('GOOGLE_CLIENT_SECRET', ''),
130
+ },
131
+ }
132
+ }
133
+
134
+ # Для allauth
135
+ MIDDLEWARE = [
136
+ # ... существующие middleware ...
137
+ 'allauth.account.middleware.AccountMiddleware',
138
+ ]
139
+
140
+ # Куда редиректить после логина (allauth использует, но мы перехватим на фронте)
141
+ LOGIN_REDIRECT_URL = '/'
142
+ ```
143
+
144
+ **URL маршруты (config/urls.py):**
145
+ ```python
146
+ urlpatterns = [
147
+ path('admin/', admin.site.urls),
148
+ path('api/v1/', include('catalog.urls')),
149
+ path('api/v1/auth/', include('accounts.urls')),
150
+ ]
151
+ ```
152
+
153
+ **URL маршруты (accounts/urls.py):**
154
+ ```python
155
+ from django.urls import path, include
156
+ from .views import GoogleLogin, UserProfileView
157
+
158
+ urlpatterns = [
159
+ # dj-rest-auth базовые эндпоинты
160
+ path('', include('dj_rest_auth.urls')),
161
+ # Google OAuth
162
+ path('google/', GoogleLogin.as_view(), name='google_login'),
163
+ # Профиль текущего пользователя
164
+ path('profile/', UserProfileView.as_view(), name='user_profile'),
165
+ ]
166
+ ```
167
+
168
+ Итоговые эндпоинты:
169
+ ```
170
+ POST /api/v1/auth/google/ # Принимает Google access_token или code, возвращает JWT
171
+ GET /api/v1/auth/user/ # Текущий пользователь (dj-rest-auth built-in)
172
+ POST /api/v1/auth/token/refresh/ # Обновить JWT
173
+ POST /api/v1/auth/logout/ # Выход
174
+ GET /api/v1/auth/profile/ # Расширенный профиль
175
+ PUT /api/v1/auth/profile/ # Обновить профиль
176
+ ```
177
+
178
+ **Views (accounts/views.py):**
179
+ ```python
180
+ from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
181
+ from allauth.socialaccount.providers.oauth2.client import OAuth2Client
182
+ from dj_rest_auth.registration.views import SocialLoginView
183
+ from rest_framework.generics import RetrieveUpdateAPIView
184
+ from rest_framework.permissions import IsAuthenticated
185
+ from .serializers import UserSerializer
186
+
187
+ class GoogleLogin(SocialLoginView):
188
+ adapter_class = GoogleOAuth2Adapter
189
+ callback_url = "https://kitan-a.com/auth/callback" # URL фронтенда
190
+ client_class = OAuth2Client
191
+
192
+ class UserProfileView(RetrieveUpdateAPIView):
193
+ serializer_class = UserSerializer
194
+ permission_classes = [IsAuthenticated]
195
+
196
+ def get_object(self):
197
+ return self.request.user
198
+ ```
199
+
200
+ **Serializers (accounts/serializers.py):**
201
+ ```python
202
+ from rest_framework import serializers
203
+ from .models import User
204
+
205
+ class UserSerializer(serializers.ModelSerializer):
206
+ class Meta:
207
+ model = User
208
+ fields = ['id', 'email', 'display_name', 'avatar_url', 'bio', 'created_at']
209
+ read_only_fields = ['id', 'email', 'created_at']
210
+ ```
211
+
212
+ **Adapter для автозаполнения данных из Google (accounts/adapter.py):**
213
+ ```python
214
+ from allauth.socialaccount.adapter import DefaultSocialAccountAdapter
215
+
216
+ class CustomSocialAccountAdapter(DefaultSocialAccountAdapter):
217
+ def populate_user(self, request, sociallogin, data):
218
+ user = super().populate_user(request, sociallogin, data)
219
+ user.display_name = data.get('name', '')
220
+ extra_data = sociallogin.account.extra_data
221
+ user.avatar_url = extra_data.get('picture', '')
222
+ return user
223
+ ```
224
+
225
+ В settings.py добавить:
226
+ ```python
227
+ SOCIALACCOUNT_ADAPTER = 'accounts.adapter.CustomSocialAccountAdapter'
228
+ ```
229
+
230
+ ---
231
+
232
+ ### 2. Google Cloud Console — настройка OAuth
233
+
234
+ Это делается вручную (не кодом). Инструкции для владельца проекта:
235
+
236
+ 1. Перейти на https://console.cloud.google.com/
237
+ 2. Создать проект (или выбрать существующий)
238
+ 3. APIs & Services → Credentials → Create Credentials → OAuth 2.0 Client ID
239
+ 4. Application type: Web application
240
+ 5. Authorized JavaScript origins:
241
+ - https://kitan-a.com
242
+ - http://localhost:3000 (для разработки)
243
+ 6. Authorized redirect URIs:
244
+ - https://kitan-a.com/auth/callback
245
+ - http://localhost:3000/auth/callback (для разработки)
246
+ 7. Скопировать Client ID и Client Secret
247
+ 8. Добавить в .env файл на сервере:
248
+ ```
249
+ GOOGLE_CLIENT_ID=xxxxxxxxxxxxx.apps.googleusercontent.com
250
+ GOOGLE_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxx
251
+ ```
252
+
253
+ 9. APIs & Services → OAuth consent screen:
254
+ - User Type: External
255
+ - App name: Steppe Sonorum: Piano
256
+ - User support email: (ваш email)
257
+ - Authorized domains: kitan-a.com
258
+ - Scopes: email, profile
259
+
260
+ ---
261
+
262
+ ### 3. Frontend (Next.js)
263
+
264
+ **Установить пакеты:**
265
+ ```bash
266
+ npm install @react-oauth/google js-cookie
267
+ ```
268
+
269
+ **Переменные окружения (frontend/.env.local):**
270
+ ```
271
+ NEXT_PUBLIC_API_URL=https://kitan-a.com/api/v1
272
+ NEXT_PUBLIC_GOOGLE_CLIENT_ID=xxxxxxxxxxxxx.apps.googleusercontent.com
273
+ ```
274
+
275
+ **Auth контекст (src/context/AuthContext.tsx):**
276
+ ```tsx
277
+ 'use client';
278
+
279
+ import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
280
+ import Cookies from 'js-cookie';
281
+
282
+ interface User {
283
+ id: number;
284
+ email: string;
285
+ display_name: string;
286
+ avatar_url: string;
287
+ bio: string;
288
+ }
289
+
290
+ interface AuthContextType {
291
+ user: User | null;
292
+ isLoading: boolean;
293
+ isAuthenticated: boolean;
294
+ loginWithGoogle: (credential: string) => Promise<void>;
295
+ logout: () => Promise<void>;
296
+ refreshUser: () => Promise<void>;
297
+ }
298
+
299
+ const AuthContext = createContext<AuthContextType | undefined>(undefined);
300
+
301
+ export function AuthProvider({ children }: { children: React.ReactNode }) {
302
+ const [user, setUser] = useState<User | null>(null);
303
+ const [isLoading, setIsLoading] = useState(true);
304
+
305
+ const API_URL = process.env.NEXT_PUBLIC_API_URL;
306
+
307
+ // Загрузить данные пользователя при старте
308
+ const refreshUser = useCallback(async () => {
309
+ try {
310
+ const res = await fetch(`${API_URL}/auth/user/`, {
311
+ credentials: 'include', // Отправляет HttpOnly cookies
312
+ });
313
+ if (res.ok) {
314
+ const data = await res.json();
315
+ setUser(data);
316
+ } else {
317
+ setUser(null);
318
+ }
319
+ } catch {
320
+ setUser(null);
321
+ } finally {
322
+ setIsLoading(false);
323
+ }
324
+ }, [API_URL]);
325
+
326
+ useEffect(() => {
327
+ refreshUser();
328
+ }, [refreshUser]);
329
+
330
+ // Логин через Google — отправить credential (ID token) на бэкенд
331
+ const loginWithGoogle = async (credential: string) => {
332
+ setIsLoading(true);
333
+ try {
334
+ const res = await fetch(`${API_URL}/auth/google/`, {
335
+ method: 'POST',
336
+ headers: { 'Content-Type': 'application/json' },
337
+ credentials: 'include',
338
+ body: JSON.stringify({ access_token: credential }),
339
+ });
340
+ if (res.ok) {
341
+ await refreshUser();
342
+ } else {
343
+ const error = await res.json();
344
+ console.error('Google login failed:', error);
345
+ throw new Error('Login failed');
346
+ }
347
+ } finally {
348
+ setIsLoading(false);
349
+ }
350
+ };
351
+
352
+ // Выход
353
+ const logout = async () => {
354
+ try {
355
+ await fetch(`${API_URL}/auth/logout/`, {
356
+ method: 'POST',
357
+ credentials: 'include',
358
+ });
359
+ } finally {
360
+ setUser(null);
361
+ }
362
+ };
363
+
364
+ return (
365
+ <AuthContext.Provider value={{
366
+ user,
367
+ isLoading,
368
+ isAuthenticated: !!user,
369
+ loginWithGoogle,
370
+ logout,
371
+ refreshUser,
372
+ }}>
373
+ {children}
374
+ </AuthContext.Provider>
375
+ );
376
+ }
377
+
378
+ export function useAuth() {
379
+ const context = useContext(AuthContext);
380
+ if (!context) throw new Error('useAuth must be used within AuthProvider');
381
+ return context;
382
+ }
383
+ ```
384
+
385
+ **Обернуть приложение в AuthProvider (src/app/layout.tsx):**
386
+ ```tsx
387
+ import { AuthProvider } from '@/context/AuthContext';
388
+ import { GoogleOAuthProvider } from '@react-oauth/google';
389
+
390
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
391
+ return (
392
+ <html>
393
+ <body>
394
+ <GoogleOAuthProvider clientId={process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID!}>
395
+ <AuthProvider>
396
+ {/* Header, навигация и т.д. */}
397
+ {children}
398
+ </AuthProvider>
399
+ </GoogleOAuthProvider>
400
+ </body>
401
+ </html>
402
+ );
403
+ }
404
+ ```
405
+
406
+ **Кнопка входа в хедере (src/components/AuthButton.tsx):**
407
+ ```tsx
408
+ 'use client';
409
+
410
+ import { useAuth } from '@/context/AuthContext';
411
+ import { GoogleLogin } from '@react-oauth/google';
412
+ import { useState, useRef, useEffect } from 'react';
413
+
414
+ export default function AuthButton() {
415
+ const { user, isAuthenticated, isLoading, loginWithGoogle, logout } = useAuth();
416
+ const [showDropdown, setShowDropdown] = useState(false);
417
+ const dropdownRef = useRef<HTMLDivElement>(null);
418
+
419
+ // Закрыть dropdown при клике вне
420
+ useEffect(() => {
421
+ function handleClickOutside(event: MouseEvent) {
422
+ if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
423
+ setShowDropdown(false);
424
+ }
425
+ }
426
+ document.addEventListener('mousedown', handleClickOutside);
427
+ return () => document.removeEventListener('mousedown', handleClickOutside);
428
+ }, []);
429
+
430
+ if (isLoading) {
431
+ return <div className="w-8 h-8 rounded-full bg-gray-200 animate-pulse" />;
432
+ }
433
+
434
+ // Не авторизован — показать кнопку Google
435
+ if (!isAuthenticated) {
436
+ return (
437
+ <GoogleLogin
438
+ onSuccess={(credentialResponse) => {
439
+ if (credentialResponse.credential) {
440
+ loginWithGoogle(credentialResponse.credential);
441
+ }
442
+ }}
443
+ onError={() => console.error('Google login error')}
444
+ size="medium"
445
+ shape="pill"
446
+ text="signin"
447
+ theme="outline"
448
+ />
449
+ );
450
+ }
451
+
452
+ // Авторизован — показать аватар + dropdown
453
+ return (
454
+ <div className="relative" ref={dropdownRef}>
455
+ <button
456
+ onClick={() => setShowDropdown(!showDropdown)}
457
+ className="flex items-center gap-2 hover:opacity-80 transition"
458
+ >
459
+ {user?.avatar_url ? (
460
+ <img
461
+ src={user.avatar_url}
462
+ alt={user.display_name}
463
+ className="w-8 h-8 rounded-full border-2 border-white"
464
+ />
465
+ ) : (
466
+ <div className="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white text-sm font-medium">
467
+ {user?.display_name?.charAt(0) || '?'}
468
+ </div>
469
+ )}
470
+ </button>
471
+
472
+ {showDropdown && (
473
+ <div className="absolute right-0 top-full mt-2 w-56 bg-white rounded-lg shadow-lg border border-gray-200 py-2 z-50">
474
+ {/* Инфо о пользователе */}
475
+ <div className="px-4 py-2 border-b border-gray-100">
476
+ <p className="text-sm font-medium text-gray-900">{user?.display_name}</p>
477
+ <p className="text-xs text-gray-500">{user?.email}</p>
478
+ </div>
479
+
480
+ {/* Навигация */}
481
+ <a href="/profile" className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">
482
+ Мой профиль
483
+ </a>
484
+ <a href="/favorites" className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">
485
+ Избранное
486
+ </a>
487
+ <a href="/collections" className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50">
488
+ Коллекции
489
+ </a>
490
+
491
+ {/* Выход */}
492
+ <div className="border-t border-gray-100 mt-1">
493
+ <button
494
+ onClick={() => { logout(); setShowDropdown(false); }}
495
+ className="block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50"
496
+ >
497
+ Выйти
498
+ </button>
499
+ </div>
500
+ </div>
501
+ )}
502
+ </div>
503
+ );
504
+ }
505
+ ```
506
+
507
+ **Использование в хедере:**
508
+ ```tsx
509
+ // В компоненте Header, рядом с переключателем языка
510
+ import AuthButton from '@/components/AuthButton';
511
+
512
+ // ...внутри header JSX:
513
+ <div className="flex items-center gap-4">
514
+ <LanguageSwitcher />
515
+ <AuthButton />
516
+ </div>
517
+ ```
518
+
519
+ ---
520
+
521
+ ### 4. CORS настройка
522
+
523
+ В Django settings (backend/config/settings/base.py):
524
+
525
+ ```bash
526
+ pip install django-cors-headers
527
+ ```
528
+
529
+ ```python
530
+ INSTALLED_APPS = [
531
+ # ...
532
+ 'corsheaders',
533
+ ]
534
+
535
+ MIDDLEWARE = [
536
+ 'corsheaders.middleware.CorsMiddleware', # ПЕРВЫМ в списке!
537
+ 'django.middleware.common.CommonMiddleware',
538
+ # ...
539
+ ]
540
+
541
+ # Development
542
+ CORS_ALLOWED_ORIGINS = [
543
+ "http://localhost:3000",
544
+ "https://kitan-a.com",
545
+ ]
546
+ CORS_ALLOW_CREDENTIALS = True # Для HttpOnly cookies
547
+ ```
548
+
549
+ ---
550
+
551
+ ### 5. Миграции и инициализация
552
+
553
+ ```bash
554
+ cd backend
555
+ python manage.py makemigrations accounts
556
+ python manage.py migrate
557
+
558
+ # Создать Site object (нужен для allauth)
559
+ python manage.py shell -c "
560
+ from django.contrib.sites.models import Site
561
+ site = Site.objects.get_or_create(id=1, defaults={'domain': 'kitan-a.com', 'name': 'Steppe Sonorum'})
562
+ print('Site created:', site)
563
+ "
564
+ ```
565
+
566
+ ---
567
+
568
+ ## ВАЖНЫЕ ПРАВИЛА
569
+
570
+ 1. **Каталог остаётся публичным.** `DEFAULT_PERMISSION_CLASSES = ['AllowAny']`. Только эндпоинты профиля, избранного, комментариев требуют `IsAuthenticated`.
571
+
572
+ 2. **JWT хранится в HttpOnly cookie**, НЕ в localStorage. Это безопаснее — JavaScript на фронте не имеет доступа к токену.
573
+
574
+ 3. **CSRF**: При использовании cookies нужно учитывать CSRF. Django REST Framework + `SessionAuthentication` требует CSRF token. Для JWT в cookies — `dj-rest-auth` обрабатывает это автоматически через `JWT_AUTH_HTTPONLY` и `JWT_AUTH_SAMESITE`.
575
+
576
+ 4. **При первом входе через Google** автоматически создаётся User с данными из Google профиля (имя, email, фото). Юзеру НЕ нужно заполнять формы регистрации.
577
+
578
+ 5. **Не делать регистрацию по email/password.** Только Google OAuth. Это упрощает систему и избавляет от необходимости верификации email, сброса пароля, и т.д.
579
+
580
+ 6. **Аватар** берётся из Google profile photo URL и сохраняется в `avatar_url`. Не скачивать файл — просто хранить URL.
581
+
582
+ 7. **Стилизация кнопки Google** — использовать встроенный компонент `<GoogleLogin />` из `@react-oauth/google`. Он соответствует гайдлайнам Google и адаптивен. Не рисовать кастомную кнопку.
583
+
584
+ ---
585
+
586
+ ## ТЕСТИРОВАНИЕ
587
+
588
+ 1. Открыть сайт → в хедере должна быть кнопка "Sign in with Google"
589
+ 2. Нажать → появляется Google popup → выбрать аккаунт
590
+ 3. После успешного входа → кнопка заменяется на аватар пользователя
591
+ 4. Нажать на аватар → dropdown с именем, email, ссылками на профиль/избранное, кнопка выхода
592
+ 5. Обновить страницу → пользователь остаётся залогиненным (JWT в cookie)
593
+ 6. Нажать "Выйти" → возвращается кнопка "Sign in with Google"
594
+ 7. Каталог, страницы композиций, композиторов — всё доступно без логина
595
+
596
+ ---
597
+
598
+ ## ФАЙЛЫ КОТОРЫЕ НУЖНО СОЗДАТЬ/ИЗМЕНИТЬ
599
+
600
+ ### Создать:
601
+ - `backend/accounts/__init__.py`
602
+ - `backend/accounts/models.py`
603
+ - `backend/accounts/serializers.py`
604
+ - `backend/accounts/views.py`
605
+ - `backend/accounts/urls.py`
606
+ - `backend/accounts/admin.py`
607
+ - `backend/accounts/adapter.py`
608
+ - `backend/accounts/apps.py`
609
+ - `frontend/src/context/AuthContext.tsx`
610
+ - `frontend/src/components/AuthButton.tsx`
611
+
612
+ ### Изменить:
613
+ - `backend/config/settings/base.py` — добавить приложения, настройки allauth, JWT, CORS
614
+ - `backend/config/urls.py` — добавить маршрут auth
615
+ - `backend/requirements.txt` — добавить пакеты
616
+ - `frontend/package.json` — добавить зависимости
617
+ - `frontend/src/app/layout.tsx` — обернуть в провайдеры
618
+ - Компонент Header — добавить `<AuthButton />`
619
+ - `.env` — добавить GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET