From 6ee465e5254875fb9d9852a900f83bf93fd67252 Mon Sep 17 00:00:00 2001 From: Ananya Date: Fri, 20 Mar 2026 03:42:47 +0530 Subject: [PATCH 1/6] implement login with google --- web/settings.py | 18 +++++++++++++++++- web/templates/account/login.html | 20 ++++++++++++++++++++ web/templates/account/signup.html | 20 ++++++++++++++++++++ 3 files changed, 57 insertions(+), 1 deletion(-) diff --git a/web/settings.py b/web/settings.py index 4ddcae8bb..a7a48eed3 100644 --- a/web/settings.py +++ b/web/settings.py @@ -15,7 +15,7 @@ env = environ.Env() env_file = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env") - +environ.Env.read_env(env_file) # Set encryption key for secure messaging; in production, this must come from the environment MESSAGE_ENCRYPTION_KEY = env.str("MESSAGE_ENCRYPTION_KEY", default=Fernet.generate_key()).strip() @@ -131,6 +131,8 @@ SERVER_EMAIL = os.getenv("EMAIL_FROM") # Email address error messages come from INSTALLED_APPS = [ + "allauth.socialaccount", + "allauth.socialaccount.providers.google", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", @@ -533,3 +535,17 @@ CSRF_COOKIE_SECURE = env.bool("CSRF_COOKIE_SECURE", default=not DEBUG) SESSION_COOKIE_SECURE = env.bool("SESSION_COOKIE_SECURE", default=not DEBUG) GITHUB_WEBHOOK_SECRET = os.environ.get("GITHUB_WEBHOOK_SECRET", "") + +SOCIALACCOUNT_PROVIDERS = { + "google": { + "APP": { + "client_id": env.str("GOOGLE_CLIENT_ID", default=""), + "secret": env.str("GOOGLE_CLIENT_SECRET", default=""), + }, + "SCOPE": ["profile", "email"], + "AUTH_PARAMS": {"access_type": "online"}, + } +} + +SOCIALACCOUNT_EMAIL_VERIFICATION = "none" +SOCIALACCOUNT_EMAIL_REQUIRED = False diff --git a/web/templates/account/login.html b/web/templates/account/login.html index 38ac4da1e..7279dcdb2 100644 --- a/web/templates/account/login.html +++ b/web/templates/account/login.html @@ -2,6 +2,7 @@ {% load static %} {% load account %} +{% load socialaccount %} {% block extra_head %} {{ block.super }} @@ -86,6 +87,25 @@

+
+
+
+
+ Or continue with +
+ + + + Google + Sign in with Google +

Don't have an account? diff --git a/web/templates/account/signup.html b/web/templates/account/signup.html index 7720a6a97..e11cf7216 100644 --- a/web/templates/account/signup.html +++ b/web/templates/account/signup.html @@ -1,6 +1,7 @@ {% extends "allauth/layouts/base.html" %} {% load static %} +{% load socialaccount %} {% block extra_head %} {{ block.super }} @@ -248,6 +249,25 @@

+ +
+
+
+
+
+ Or sign up with +
+
+ + + Google + Sign up with Google + From ed52b829c6a4f54752a96542708461310e04e9b5 Mon Sep 17 00:00:00 2001 From: Ananya Date: Fri, 20 Mar 2026 04:08:11 +0530 Subject: [PATCH 2/6] fixes --- poetry.lock | 23 ++++++++++++++++++++++- pyproject.toml | 1 + web/settings.py | 29 +++++++++++++++++------------ web/static/images/google-icon.svg | 28 ++++++++++++++++++++++++++++ web/templates/account/login.html | 4 ++-- web/templates/account/signup.html | 4 ++-- 6 files changed, 72 insertions(+), 17 deletions(-) create mode 100644 web/static/images/google-icon.svg diff --git a/poetry.lock b/poetry.lock index 5fba1b224..23f738269 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1317,6 +1317,27 @@ files = [ {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] +[[package]] +name = "pyjwt" +version = "2.12.1" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c"}, + {file = "pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b"}, +] + +[package.dependencies] +typing_extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==7.10.7)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=8.4.2,<9.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==7.10.7)", "pytest (>=8.4.2,<9.0.0)"] + [[package]] name = "pyopenssl" version = "25.0.0" @@ -1951,4 +1972,4 @@ brotli = ["brotli"] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "637c808ca7e717a872f26b077fadf360e774dccb1b2944f66ee1c8ce2c5ec656" +content-hash = "6a241b148f5ffad2ce368107489fd86bd573ff7ec1219563d15c4e9a0313130a" diff --git a/pyproject.toml b/pyproject.toml index fde77eafb..d2d00776c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ django-redis = "^5.4.0" redis = "^6.4.0" mysqlclient = "^2.2.4" psutil = "^7.1.3" +pyjwt = "^2.12.1" [tool.poetry.group.dev.dependencies] djlint = "^1.34.1" diff --git a/web/settings.py b/web/settings.py index a7a48eed3..5d33843e4 100644 --- a/web/settings.py +++ b/web/settings.py @@ -6,7 +6,7 @@ import environ import sentry_sdk from cryptography.fernet import Fernet -from django.core.exceptions import DisallowedHost +from django.core.exceptions import DisallowedHost, ImproperlyConfigured from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.logging import LoggingIntegration @@ -15,17 +15,16 @@ env = environ.Env() env_file = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env") -environ.Env.read_env(env_file) - -# Set encryption key for secure messaging; in production, this must come from the environment -MESSAGE_ENCRYPTION_KEY = env.str("MESSAGE_ENCRYPTION_KEY", default=Fernet.generate_key()).strip() -SECURE_MESSAGE_KEY = MESSAGE_ENCRYPTION_KEY if os.path.exists(env_file): environ.Env.read_env(env_file) else: print("No .env file found.") +# Set encryption key for secure messaging; in production, this must come from the environment +MESSAGE_ENCRYPTION_KEY = env.str("MESSAGE_ENCRYPTION_KEY", default=Fernet.generate_key()).strip() +SECURE_MESSAGE_KEY = MESSAGE_ENCRYPTION_KEY + # Re-initialize / initialize Sentry AFTER environment variables are loaded so DSN is present here. SENTRY_DSN = env.str("SENTRY_DSN", default="") if SENTRY_DSN: @@ -131,8 +130,6 @@ SERVER_EMAIL = os.getenv("EMAIL_FROM") # Email address error messages come from INSTALLED_APPS = [ - "allauth.socialaccount", - "allauth.socialaccount.providers.google", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", @@ -144,6 +141,8 @@ "channels", "allauth", "allauth.account", + "allauth.socialaccount", + "allauth.socialaccount.providers.google", "captcha", "markdownx", "web", @@ -536,16 +535,22 @@ SESSION_COOKIE_SECURE = env.bool("SESSION_COOKIE_SECURE", default=not DEBUG) GITHUB_WEBHOOK_SECRET = os.environ.get("GITHUB_WEBHOOK_SECRET", "") +# Validate Google OAuth credentials +google_client_id = env.str("GOOGLE_CLIENT_ID", default="") +google_client_secret = env.str("GOOGLE_CLIENT_SECRET", default="") +if not DEBUG and (not google_client_id or not google_client_secret): + raise ImproperlyConfigured("GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must be set in production") + SOCIALACCOUNT_PROVIDERS = { "google": { "APP": { - "client_id": env.str("GOOGLE_CLIENT_ID", default=""), - "secret": env.str("GOOGLE_CLIENT_SECRET", default=""), + "client_id": google_client_id, + "secret": google_client_secret, }, "SCOPE": ["profile", "email"], "AUTH_PARAMS": {"access_type": "online"}, } } -SOCIALACCOUNT_EMAIL_VERIFICATION = "none" -SOCIALACCOUNT_EMAIL_REQUIRED = False +SOCIALACCOUNT_EMAIL_VERIFICATION = "mandatory" +SOCIALACCOUNT_EMAIL_REQUIRED = True diff --git a/web/static/images/google-icon.svg b/web/static/images/google-icon.svg new file mode 100644 index 000000000..80ab84bd7 --- /dev/null +++ b/web/static/images/google-icon.svg @@ -0,0 +1,28 @@ + + + + + Google-color + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + diff --git a/web/templates/account/login.html b/web/templates/account/login.html index 7279dcdb2..6dbaf1856 100644 --- a/web/templates/account/login.html +++ b/web/templates/account/login.html @@ -97,9 +97,9 @@

- Or continue with + Or continue with
diff --git a/web/templates/account/signup.html b/web/templates/account/signup.html index 1711c7e99..bf9bc54c2 100644 --- a/web/templates/account/signup.html +++ b/web/templates/account/signup.html @@ -255,7 +255,7 @@

- Or sign up with + Or sign up with
From 1bc2a30244f8501a495830ef1cd0ac52ae858911 Mon Sep 17 00:00:00 2001 From: Ananya Date: Sun, 22 Mar 2026 03:27:21 +0530 Subject: [PATCH 4/6] fixes --- web/settings.py | 6 ++++-- web/templates/account/login.html | 5 +++-- web/templates/account/signup.html | 5 +++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/web/settings.py b/web/settings.py index 557115f08..1e5cb681c 100644 --- a/web/settings.py +++ b/web/settings.py @@ -16,7 +16,6 @@ MESSAGE_ENCRYPTION_KEY_REQUIRED_MSG = "MESSAGE_ENCRYPTION_KEY must be set in production" GOOGLE_OAUTH_CREDENTIALS_REQUIRED_MSG = "GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET must be set in production" -EARLY_DEBUG = env.bool("DEBUG", default=False) env_file = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env") @@ -25,10 +24,13 @@ else: print("No .env file found.") +EARLY_ENVIRONMENT = env.str("ENVIRONMENT", default="development") +EARLY_DEBUG = EARLY_ENVIRONMENT == "development" or "test" in sys.argv + # Set encryption key for secure messaging; in production, this must come from the environment MESSAGE_ENCRYPTION_KEY = env.str("MESSAGE_ENCRYPTION_KEY", default="").strip() if not MESSAGE_ENCRYPTION_KEY: - if EARLY_DEBUG: + if EARLY_DEBUG or "collectstatic" in sys.argv: MESSAGE_ENCRYPTION_KEY = Fernet.generate_key().decode() else: raise ImproperlyConfigured(MESSAGE_ENCRYPTION_KEY_REQUIRED_MSG) diff --git a/web/templates/account/login.html b/web/templates/account/login.html index 3d74df691..6ed1cd023 100644 --- a/web/templates/account/login.html +++ b/web/templates/account/login.html @@ -1,6 +1,7 @@ {% extends "allauth/layouts/base.html" %} {% load static %} +{% load i18n %} {% load account %} {% load socialaccount %} @@ -93,7 +94,7 @@

- Or continue with + {% trans "Or continue with" %}
@@ -104,7 +105,7 @@

- Sign in with Google + {% trans "Sign in with Google" %}

diff --git a/web/templates/account/signup.html b/web/templates/account/signup.html index bf9bc54c2..6cccedd67 100644 --- a/web/templates/account/signup.html +++ b/web/templates/account/signup.html @@ -1,6 +1,7 @@ {% extends "allauth/layouts/base.html" %} {% load static %} +{% load i18n %} {% load socialaccount %} {% block extra_head %} @@ -255,7 +256,7 @@

- Or sign up with + {% trans "Or sign up with" %}
@@ -266,7 +267,7 @@

- Sign up with Google + {% trans "Sign up with Google" %} From 2f1ebcc0d4ad5cb4a53459c11d2d7b54994e40b0 Mon Sep 17 00:00:00 2001 From: Ananya Date: Sun, 22 Mar 2026 03:50:16 +0530 Subject: [PATCH 5/6] fixes --- web/forms.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++++ web/settings.py | 5 ++++ 2 files changed, 75 insertions(+) diff --git a/web/forms.py b/web/forms.py index 96489d524..f0d19d66f 100644 --- a/web/forms.py +++ b/web/forms.py @@ -4,6 +4,7 @@ import bleach from allauth.account.forms import LoginForm, SignupForm +from allauth.socialaccount.forms import SignupForm as SocialSignupForm from captcha.fields import CaptchaField from cryptography.fernet import Fernet from django import forms @@ -72,6 +73,7 @@ __all__ = [ "UserRegistrationForm", + "SocialUserRegistrationForm", "ProfileForm", "ChallengeSubmissionForm", "CourseCreationForm", @@ -307,6 +309,74 @@ def save(self, request): return user +class SocialUserRegistrationForm(SocialSignupForm): + first_name = forms.CharField( + max_length=30, + required=True, + widget=TailwindInput(attrs={"placeholder": "First Name"}), + ) + last_name = forms.CharField( + max_length=30, + required=True, + widget=TailwindInput(attrs={"placeholder": "Last Name"}), + ) + is_teacher = forms.BooleanField( + required=False, + label="Register as a teacher", + widget=TailwindCheckboxInput(), + ) + referral_code = forms.CharField( + max_length=20, + required=False, + widget=TailwindInput(attrs={"placeholder": "Enter referral code"}), + help_text="Optional - Enter a referral code if you have one", + ) + how_did_you_hear_about_us = forms.CharField( + max_length=500, + required=False, + widget=TailwindTextarea( + attrs={"rows": 2, "placeholder": "How did you hear about us? You can enter text or a link."} + ), + help_text="Optional - Tell us how you found us. You can enter text or a link.", + ) + captcha = CaptchaField(widget=TailwindCaptchaTextInput) + is_profile_public = forms.TypedChoiceField( + required=True, + choices=(("True", "Public"), ("False", "Private")), + coerce=lambda x: x == "True", + widget=forms.RadioSelect, + label="Profile Visibility", + help_text="Select whether your profile details will be public or private.", + ) + + def clean_referral_code(self): + referral_code = self.cleaned_data.get("referral_code") + if referral_code and not Profile.objects.filter(referral_code=referral_code).exists(): + raise forms.ValidationError("Invalid referral code. Please check and try again.") + return referral_code + + def save(self, request): + user = super().save(request) + + user.first_name = self.cleaned_data.get("first_name", "") + user.last_name = self.cleaned_data.get("last_name", "") + user.save() + + user.profile.is_profile_public = self.cleaned_data.get("is_profile_public") + user.profile.how_did_you_hear_about_us = self.cleaned_data.get("how_did_you_hear_about_us", "") + + if self.cleaned_data.get("is_teacher"): + user.profile.is_teacher = True + + user.profile.save() + + referral_code = self.cleaned_data.get("referral_code") + if referral_code: + handle_referral(user, referral_code) + + return user + + class TailwindInput(forms.widgets.Input, TailwindWidgetMixin): def __init__(self, *args, **kwargs): kwargs.setdefault("attrs", {}).update( diff --git a/web/settings.py b/web/settings.py index 1e5cb681c..45b009e58 100644 --- a/web/settings.py +++ b/web/settings.py @@ -316,6 +316,10 @@ "signup": "web.forms.UserRegistrationForm", "login": "web.forms.CustomLoginForm", } +SOCIALACCOUNT_FORMS = { + "signup": "web.forms.SocialUserRegistrationForm", +} +SOCIALACCOUNT_AUTO_SIGNUP = False LANGUAGE_CODE = "en" @@ -554,6 +558,7 @@ SOCIALACCOUNT_PROVIDERS = { "google": { + "EMAIL_AUTHENTICATION": True, "APP": { "client_id": google_client_id, "secret": google_client_secret, From cc94e2c6ada714e2fd632cef4306f70b9cbee66c Mon Sep 17 00:00:00 2001 From: Ananya Date: Sun, 22 Mar 2026 04:00:34 +0530 Subject: [PATCH 6/6] fixes --- web/forms.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/web/forms.py b/web/forms.py index f0d19d66f..98f70c5e3 100644 --- a/web/forms.py +++ b/web/forms.py @@ -1,3 +1,4 @@ +import logging import re from typing import ClassVar from urllib.parse import parse_qs, urlparse @@ -117,6 +118,7 @@ ] fernet = Fernet(settings.SECURE_MESSAGE_KEY) +INVALID_REFERRAL_CODE_MSG = "Invalid referral code. Please check and try again." class TailwindWidgetMixin: @@ -265,7 +267,7 @@ def clean_referral_code(self): referral_code = self.cleaned_data.get("referral_code") if referral_code: if not Profile.objects.filter(referral_code=referral_code).exists(): - raise forms.ValidationError("Invalid referral code. Please check and try again.") + raise forms.ValidationError(INVALID_REFERRAL_CODE_MSG) return referral_code def save(self, request): @@ -297,7 +299,10 @@ def save(self, request): # Handle referral code if provided. referral_code = self.cleaned_data.get("referral_code") if referral_code: - handle_referral(user, referral_code) + try: + handle_referral(user, referral_code) + except Exception: + logging.getLogger(__name__).exception("Failed to process referral for user %s", user.pk) # Ensure email verification is sent from allauth.account.models import EmailAddress @@ -310,6 +315,8 @@ def save(self, request): class SocialUserRegistrationForm(SocialSignupForm): + """Custom social signup form that collects onboarding fields used in standard registration.""" + first_name = forms.CharField( max_length=30, required=True, @@ -352,10 +359,10 @@ class SocialUserRegistrationForm(SocialSignupForm): def clean_referral_code(self): referral_code = self.cleaned_data.get("referral_code") if referral_code and not Profile.objects.filter(referral_code=referral_code).exists(): - raise forms.ValidationError("Invalid referral code. Please check and try again.") + raise forms.ValidationError(INVALID_REFERRAL_CODE_MSG) return referral_code - def save(self, request): + def save(self, request) -> User: user = super().save(request) user.first_name = self.cleaned_data.get("first_name", "") @@ -372,7 +379,10 @@ def save(self, request): referral_code = self.cleaned_data.get("referral_code") if referral_code: - handle_referral(user, referral_code) + try: + handle_referral(user, referral_code) + except Exception: + logging.getLogger(__name__).exception("Failed to process referral for user %s", user.pk) return user