Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,6 @@ REDIS_HOST="localhost"
REDIS_PORT="6379"
# Index
ELASTICSEARCH_URL=http://localhost:9200
# Google
GOOGLE_OAUTH_CLIENT_ID="google_key"
GOOGLE_OAUTH_CLIENT_SECRET="google_secret"
26 changes: 26 additions & 0 deletions backend/apps/account/migrations/0024_account_google_sub.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated manually for google_sub field

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("account", "0023_alter_career_role_old_alter_career_team_old"),
]

operations = [
migrations.RunSQL(
sql="ALTER TABLE account ADD COLUMN IF NOT EXISTS google_sub VARCHAR(255) NULL;",
reverse_sql="ALTER TABLE account DROP COLUMN google_sub;",
state_operations=[
migrations.AddField(
model_name="account",
name="google_sub",
field=models.CharField(
blank=True, max_length=255, null=True, unique=True, verbose_name="Google Sub"
),
),
],
),
]
2 changes: 2 additions & 0 deletions backend/apps/account/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ class Account(BaseModel, AbstractBaseUser, PermissionsMixin):

email = models.EmailField("Email", unique=True)
gcp_email = models.EmailField("GCP email", null=True, blank=True) # Google Cloud Platform email
google_sub = models.CharField("Google Sub", max_length=255, null=True, blank=True, unique=True) # Google OAuth subject identifier
username = models.CharField("Username", max_length=40, blank=True, null=True, unique=True)

first_name = models.CharField("Nome", max_length=40, blank=True)
Expand Down Expand Up @@ -331,6 +332,7 @@ class Account(BaseModel, AbstractBaseUser, PermissionsMixin):
"is_admin",
"is_superuser",
"staff_groups",
"google_sub",
*BaseModel.graphql_fields_blacklist,
]
graphql_filter_fields_blacklist = ["internal_subscription"]
Expand Down
12 changes: 12 additions & 0 deletions backend/apps/account/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from backend.apps.account.views import (
AccountActivateConfirmView,
AccountActivateView,
GoogleAuthView,
GoogleCallbackView,
PasswordResetConfirmView,
PasswordResetView,
)
Expand All @@ -29,4 +31,14 @@
PasswordResetConfirmView.as_view(),
name="password_reset_confirm",
),
path(
"account/google/login/",
GoogleAuthView.as_view(),
name="google_auth",
),
path(
"account/google/callback/",
GoogleCallbackView.as_view(),
name="google_callback",
),
]
187 changes: 186 additions & 1 deletion backend/apps/account/views.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
# -*- coding: utf-8 -*-
from json import loads
from typing import Any
import secrets

from graphql_jwt.shortcuts import get_token

from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.views import PasswordResetConfirmView, PasswordResetView
from django.contrib.messages.views import SuccessMessageMixin
from django.core.mail import EmailMultiAlternatives
from django.http import JsonResponse
from django.http import JsonResponse, HttpResponseRedirect
from django.template.loader import render_to_string
from django.urls import reverse_lazy as r
from django.utils.decorators import method_decorator
Expand All @@ -16,6 +19,7 @@
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from loguru import logger
import requests

from backend.apps.account.signals import send_activation_email
from backend.apps.account.token import token_generator
Expand Down Expand Up @@ -139,3 +143,184 @@ def dispatch(self, request, uidb64, token):
return JsonResponse({}, status=200)
else:
return JsonResponse({}, status=422)

class GoogleAuthView(View):
"""View para iniciar o fluxo de autenticação Google OAuth"""

@method_decorator(csrf_exempt, name="dispatch")
def dispatch(self, request, *args: Any, **kwargs: Any):
return super().dispatch(request, *args, **kwargs)

def get(self, request):
"""Inicia o fluxo de autenticação Google OAuth"""
try:
state = secrets.token_urlsafe(32)
request.session['oauth_state'] = state

auth_url = (
"https://accounts.google.com/o/oauth2/v2/auth?"
f"client_id={settings.GOOGLE_OAUTH_CLIENT_ID}&"
f"redirect_uri={settings.BACKEND_URL}/account/google/callback/&"
f"scope=openid email profile&"
f"response_type=code&"
f"state={state}&"
f"access_type=offline&"
f"include_granted_scopes=true"
)
return HttpResponseRedirect(auth_url)
except Exception as e:
logger.error(f"Erro ao iniciar autenticação Google: {e}")
return JsonResponse({"error": "Erro interno do servidor"}, status=500)


class GoogleCallbackView(View):
"""View para processar o callback do Google OAuth"""

@method_decorator(csrf_exempt, name="dispatch")
def dispatch(self, request, *args: Any, **kwargs: Any):
return super().dispatch(request, *args, **kwargs)

def get(self, request):
"""Processa o callback do Google OAuth"""
try:
auth_code = request.GET.get('code')
state = request.GET.get('state')
error = request.GET.get('error')

if error:
logger.error(f"Erro do Google OAuth: {error}")
return JsonResponse({"error": f"Erro de autorização: {error}"}, status=400)

if not auth_code:
logger.error("Código de autorização não fornecido")
return JsonResponse({"error": "Código de autorização não fornecido"}, status=400)

if not state or state != request.session.get('oauth_state'):
logger.error("Estado inválido - possível ataque CSRF")
return JsonResponse({"error": "Estado inválido"}, status=400)

if 'oauth_state' in request.session:
del request.session['oauth_state']

token_data = self._exchange_code_for_token(auth_code)
if not token_data:
logger.error("Falha ao trocar código por token")
error_url = f"{settings.FRONTEND_URL}/user/login?error=auth_failed"
return HttpResponseRedirect(error_url)

user_info = self._get_user_info(token_data['access_token'])
if not user_info:
logger.error("Não foi possível obter informações do usuário")
error_url = f"{settings.FRONTEND_URL}/user/login?error=user_info_failed"
return HttpResponseRedirect(error_url)

account = self._create_or_update_account(user_info, token_data.get('id_token'))

if account:
jwt_token = get_token(account)
frontend_url = f"{settings.FRONTEND_URL}/user/login?login=success&token={jwt_token}&id={account.id}"
return HttpResponseRedirect(frontend_url)
else:
logger.error("Erro ao criar/atualizar conta")
error_url = f"{settings.FRONTEND_URL}/user/login?error=account_creation_failed"
return HttpResponseRedirect(error_url)

except Exception as e:
logger.error(f"Erro no callback Google OAuth: {e}")
error_url = f"{settings.FRONTEND_URL}/user/login?error=internal_server_error"
return HttpResponseRedirect(error_url)

def _exchange_code_for_token(self, auth_code):
"""Troca código de autorização por token de acesso"""
try:
token_url = "https://oauth2.googleapis.com/token"

data = {
'client_id': settings.GOOGLE_OAUTH_CLIENT_ID,
'client_secret': settings.GOOGLE_OAUTH_CLIENT_SECRET,
'code': auth_code,
'grant_type': 'authorization_code',
'redirect_uri': f"{settings.BACKEND_URL}/account/google/callback/"
}

response = requests.post(token_url, data=data)
response.raise_for_status()

token_data = response.json()
logger.info("Token obtido com sucesso")

return token_data

except requests.RequestException as e:
error_details = e.response.json() if e.response else str(e)
logger.error(f"Erro ao trocar código por token: {error_details}")
return None

def _get_user_info(self, access_token):
"""Obtém informações do usuário do Google usando token de acesso"""
try:
userinfo_url = "https://www.googleapis.com/oauth2/v2/userinfo"
headers = {'Authorization': f'Bearer {access_token}'}

response = requests.get(userinfo_url, headers=headers)
response.raise_for_status()

user_info = response.json()
logger.info(f"Informações do usuário obtidas: {user_info.get('email')}")

return user_info

except requests.RequestException as e:
error_details = e.response.json() if e.response else str(e)
logger.error(f"Erro ao obter informações do usuário: {error_details}")
return None

def _create_or_update_account(self, user_info, id_token=None):
"""Cria nova conta ou atualiza conta existente com dados do Google"""
try:
user_model = get_user_model()
email = user_info.get('email')
google_sub = user_info.get('id')

if not email or not google_sub:
logger.error("Email ou Google Sub não fornecidos")
return None

name_parts = user_info.get('name', '').split(' ', 1)
first_name = name_parts[0] if name_parts else ''
last_name = name_parts[1] if len(name_parts) > 1 else ''

username = email.split('@')[0]
counter = 1
original_username = username
while user_model.objects.filter(username=username).exists():
username = f"{original_username}{counter}"
counter += 1

account, created = user_model.objects.get_or_create(
email=email,
defaults={
'username': username,
'first_name': first_name,
'last_name': last_name,
'google_sub': google_sub,
'is_active': True,
}
)

if created:
logger.info(f"Nova conta criada para {email}")
else:
logger.info(f"Conta Existente encontrada: {email}")
if not account.google_sub:
account.google_sub = google_sub
logger.info(f"Conta {email} vinculada ao Google Sub")

account.is_active = True
account.save()

return account

except Exception as e:
logger.error(f"Erro ao criar/atualizar conta: {e}")
return None
8 changes: 6 additions & 2 deletions backend/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,8 +301,8 @@
}

# URLs
BACKEND_URL = getenv("BASE_URL_BACKEND", "https://localhost:8080")
FRONTEND_URL = getenv("BASE_URL_FRONTEND", "https://localhost:3000")
BACKEND_URL = getenv("BASE_URL_BACKEND", "http://localhost:8000")
FRONTEND_URL = getenv("BASE_URL_FRONTEND", "http://localhost:3000")

# Discord
DISCORD_BACKEND_WEBHOOK_URL = getenv("DISCORD_BACKEND_WEBHOOK_URL")
Expand All @@ -313,3 +313,7 @@
# reCAPTCHA
RECAPTCHA_SITE_KEY = getenv("RECAPTCHA_SITE_KEY")
RECAPTCHA_SECRET_KEY = getenv("RECAPTCHA_SECRET_KEY")

# Google OAuth
GOOGLE_OAUTH_CLIENT_ID = getenv("GOOGLE_OAUTH_CLIENT_ID")
GOOGLE_OAUTH_CLIENT_SECRET = getenv("GOOGLE_OAUTH_CLIENT_SECRET")
4 changes: 4 additions & 0 deletions backend/settings/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,7 @@ def as_bool(var):
DJSTRIPE_WEBHOOK_SECRET = getenv("DJSTRIPE_WEBHOOK_SECRET")
DJSTRIPE_USE_NATIVE_JSONFIELD = True
DJSTRIPE_FOREIGN_KEY_TO_FIELD = "id"

# Google OAuth
GOOGLE_OAUTH_CLIENT_ID = getenv("GOOGLE_OAUTH_CLIENT_ID")
GOOGLE_OAUTH_CLIENT_SECRET = getenv("GOOGLE_OAUTH_CLIENT_SECRET")
4 changes: 4 additions & 0 deletions backend/settings/remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,7 @@ def as_bool(var):
DJSTRIPE_WEBHOOK_SECRET = getenv("DJSTRIPE_WEBHOOK_SECRET")
DJSTRIPE_USE_NATIVE_JSONFIELD = True
DJSTRIPE_FOREIGN_KEY_TO_FIELD = "id"

# Google OAuth
GOOGLE_OAUTH_CLIENT_ID = getenv("GOOGLE_OAUTH_CLIENT_ID")
GOOGLE_OAUTH_CLIENT_SECRET = getenv("GOOGLE_OAUTH_CLIENT_SECRET")
Loading