|
1 | 1 | # -*- coding: utf-8 -*- |
2 | 2 | from json import loads |
3 | 3 | from typing import Any |
| 4 | +import secrets |
| 5 | + |
| 6 | +from graphql_jwt.shortcuts import get_token |
4 | 7 |
|
5 | 8 | from django.conf import settings |
6 | 9 | from django.contrib.auth import get_user_model |
7 | 10 | from django.contrib.auth.views import PasswordResetConfirmView, PasswordResetView |
8 | 11 | from django.contrib.messages.views import SuccessMessageMixin |
9 | 12 | from django.core.mail import EmailMultiAlternatives |
10 | | -from django.http import JsonResponse |
| 13 | +from django.http import JsonResponse, HttpResponseRedirect |
11 | 14 | from django.template.loader import render_to_string |
12 | 15 | from django.urls import reverse_lazy as r |
13 | 16 | from django.utils.decorators import method_decorator |
|
16 | 19 | from django.views import View |
17 | 20 | from django.views.decorators.csrf import csrf_exempt |
18 | 21 | from loguru import logger |
| 22 | +import requests |
19 | 23 |
|
20 | 24 | from backend.apps.account.signals import send_activation_email |
21 | 25 | from backend.apps.account.token import token_generator |
@@ -139,3 +143,184 @@ def dispatch(self, request, uidb64, token): |
139 | 143 | return JsonResponse({}, status=200) |
140 | 144 | else: |
141 | 145 | return JsonResponse({}, status=422) |
| 146 | + |
| 147 | +class GoogleAuthView(View): |
| 148 | + """View para iniciar o fluxo de autenticação Google OAuth""" |
| 149 | + |
| 150 | + @method_decorator(csrf_exempt, name="dispatch") |
| 151 | + def dispatch(self, request, *args: Any, **kwargs: Any): |
| 152 | + return super().dispatch(request, *args, **kwargs) |
| 153 | + |
| 154 | + def get(self, request): |
| 155 | + """Inicia o fluxo de autenticação Google OAuth""" |
| 156 | + try: |
| 157 | + state = secrets.token_urlsafe(32) |
| 158 | + request.session['oauth_state'] = state |
| 159 | + |
| 160 | + auth_url = ( |
| 161 | + "https://accounts.google.com/o/oauth2/v2/auth?" |
| 162 | + f"client_id={settings.GOOGLE_OAUTH_CLIENT_ID}&" |
| 163 | + f"redirect_uri={settings.BACKEND_URL}/account/google/callback/&" |
| 164 | + f"scope=openid email profile&" |
| 165 | + f"response_type=code&" |
| 166 | + f"state={state}&" |
| 167 | + f"access_type=offline&" |
| 168 | + f"include_granted_scopes=true" |
| 169 | + ) |
| 170 | + return HttpResponseRedirect(auth_url) |
| 171 | + except Exception as e: |
| 172 | + logger.error(f"Erro ao iniciar autenticação Google: {e}") |
| 173 | + return JsonResponse({"error": "Erro interno do servidor"}, status=500) |
| 174 | + |
| 175 | + |
| 176 | +class GoogleCallbackView(View): |
| 177 | + """View para processar o callback do Google OAuth""" |
| 178 | + |
| 179 | + @method_decorator(csrf_exempt, name="dispatch") |
| 180 | + def dispatch(self, request, *args: Any, **kwargs: Any): |
| 181 | + return super().dispatch(request, *args, **kwargs) |
| 182 | + |
| 183 | + def get(self, request): |
| 184 | + """Processa o callback do Google OAuth""" |
| 185 | + try: |
| 186 | + auth_code = request.GET.get('code') |
| 187 | + state = request.GET.get('state') |
| 188 | + error = request.GET.get('error') |
| 189 | + |
| 190 | + if error: |
| 191 | + logger.error(f"Erro do Google OAuth: {error}") |
| 192 | + return JsonResponse({"error": f"Erro de autorização: {error}"}, status=400) |
| 193 | + |
| 194 | + if not auth_code: |
| 195 | + logger.error("Código de autorização não fornecido") |
| 196 | + return JsonResponse({"error": "Código de autorização não fornecido"}, status=400) |
| 197 | + |
| 198 | + if not state or state != request.session.get('oauth_state'): |
| 199 | + logger.error("Estado inválido - possível ataque CSRF") |
| 200 | + return JsonResponse({"error": "Estado inválido"}, status=400) |
| 201 | + |
| 202 | + if 'oauth_state' in request.session: |
| 203 | + del request.session['oauth_state'] |
| 204 | + |
| 205 | + token_data = self._exchange_code_for_token(auth_code) |
| 206 | + if not token_data: |
| 207 | + logger.error("Falha ao trocar código por token") |
| 208 | + error_url = f"{settings.FRONTEND_URL}/user/login?error=auth_failed" |
| 209 | + return HttpResponseRedirect(error_url) |
| 210 | + |
| 211 | + user_info = self._get_user_info(token_data['access_token']) |
| 212 | + if not user_info: |
| 213 | + logger.error("Não foi possível obter informações do usuário") |
| 214 | + error_url = f"{settings.FRONTEND_URL}/user/login?error=user_info_failed" |
| 215 | + return HttpResponseRedirect(error_url) |
| 216 | + |
| 217 | + account = self._create_or_update_account(user_info, token_data.get('id_token')) |
| 218 | + |
| 219 | + if account: |
| 220 | + jwt_token = get_token(account) |
| 221 | + frontend_url = f"{settings.FRONTEND_URL}/user/login?login=success&token={jwt_token}&id={account.id}" |
| 222 | + return HttpResponseRedirect(frontend_url) |
| 223 | + else: |
| 224 | + logger.error("Erro ao criar/atualizar conta") |
| 225 | + error_url = f"{settings.FRONTEND_URL}/user/login?error=account_creation_failed" |
| 226 | + return HttpResponseRedirect(error_url) |
| 227 | + |
| 228 | + except Exception as e: |
| 229 | + logger.error(f"Erro no callback Google OAuth: {e}") |
| 230 | + error_url = f"{settings.FRONTEND_URL}/user/login?error=internal_server_error" |
| 231 | + return HttpResponseRedirect(error_url) |
| 232 | + |
| 233 | + def _exchange_code_for_token(self, auth_code): |
| 234 | + """Troca código de autorização por token de acesso""" |
| 235 | + try: |
| 236 | + token_url = "https://oauth2.googleapis.com/token" |
| 237 | + |
| 238 | + data = { |
| 239 | + 'client_id': settings.GOOGLE_OAUTH_CLIENT_ID, |
| 240 | + 'client_secret': settings.GOOGLE_OAUTH_CLIENT_SECRET, |
| 241 | + 'code': auth_code, |
| 242 | + 'grant_type': 'authorization_code', |
| 243 | + 'redirect_uri': f"{settings.BACKEND_URL}/account/google/callback/" |
| 244 | + } |
| 245 | + |
| 246 | + response = requests.post(token_url, data=data) |
| 247 | + response.raise_for_status() |
| 248 | + |
| 249 | + token_data = response.json() |
| 250 | + logger.info("Token obtido com sucesso") |
| 251 | + |
| 252 | + return token_data |
| 253 | + |
| 254 | + except requests.RequestException as e: |
| 255 | + error_details = e.response.json() if e.response else str(e) |
| 256 | + logger.error(f"Erro ao trocar código por token: {error_details}") |
| 257 | + return None |
| 258 | + |
| 259 | + def _get_user_info(self, access_token): |
| 260 | + """Obtém informações do usuário do Google usando token de acesso""" |
| 261 | + try: |
| 262 | + userinfo_url = "https://www.googleapis.com/oauth2/v2/userinfo" |
| 263 | + headers = {'Authorization': f'Bearer {access_token}'} |
| 264 | + |
| 265 | + response = requests.get(userinfo_url, headers=headers) |
| 266 | + response.raise_for_status() |
| 267 | + |
| 268 | + user_info = response.json() |
| 269 | + logger.info(f"Informações do usuário obtidas: {user_info.get('email')}") |
| 270 | + |
| 271 | + return user_info |
| 272 | + |
| 273 | + except requests.RequestException as e: |
| 274 | + error_details = e.response.json() if e.response else str(e) |
| 275 | + logger.error(f"Erro ao obter informações do usuário: {error_details}") |
| 276 | + return None |
| 277 | + |
| 278 | + def _create_or_update_account(self, user_info, id_token=None): |
| 279 | + """Cria nova conta ou atualiza conta existente com dados do Google""" |
| 280 | + try: |
| 281 | + user_model = get_user_model() |
| 282 | + email = user_info.get('email') |
| 283 | + google_sub = user_info.get('id') |
| 284 | + |
| 285 | + if not email or not google_sub: |
| 286 | + logger.error("Email ou Google Sub não fornecidos") |
| 287 | + return None |
| 288 | + |
| 289 | + name_parts = user_info.get('name', '').split(' ', 1) |
| 290 | + first_name = name_parts[0] if name_parts else '' |
| 291 | + last_name = name_parts[1] if len(name_parts) > 1 else '' |
| 292 | + |
| 293 | + username = email.split('@')[0] |
| 294 | + counter = 1 |
| 295 | + original_username = username |
| 296 | + while user_model.objects.filter(username=username).exists(): |
| 297 | + username = f"{original_username}{counter}" |
| 298 | + counter += 1 |
| 299 | + |
| 300 | + account, created = user_model.objects.get_or_create( |
| 301 | + email=email, |
| 302 | + defaults={ |
| 303 | + 'username': username, |
| 304 | + 'first_name': first_name, |
| 305 | + 'last_name': last_name, |
| 306 | + 'google_sub': google_sub, |
| 307 | + 'is_active': True, |
| 308 | + } |
| 309 | + ) |
| 310 | + |
| 311 | + if created: |
| 312 | + logger.info(f"Nova conta criada para {email}") |
| 313 | + else: |
| 314 | + logger.info(f"Conta Existente encontrada: {email}") |
| 315 | + if not account.google_sub: |
| 316 | + account.google_sub = google_sub |
| 317 | + logger.info(f"Conta {email} vinculada ao Google Sub") |
| 318 | + |
| 319 | + account.is_active = True |
| 320 | + account.save() |
| 321 | + |
| 322 | + return account |
| 323 | + |
| 324 | + except Exception as e: |
| 325 | + logger.error(f"Erro ao criar/atualizar conta: {e}") |
| 326 | + return None |
0 commit comments