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 @@
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
+
+
+
+
+
+ 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 @@
+
+
+
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 @@
-
-
Date: Fri, 20 Mar 2026 04:42:09 +0530
Subject: [PATCH 3/6] fixes
---
web/settings.py | 13 +++++++++++--
web/templates/account/login.html | 2 +-
web/templates/account/signup.html | 2 +-
3 files changed, 13 insertions(+), 4 deletions(-)
diff --git a/web/settings.py b/web/settings.py
index 5d33843e4..557115f08 100644
--- a/web/settings.py
+++ b/web/settings.py
@@ -14,6 +14,10 @@
env = environ.Env()
+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")
if os.path.exists(env_file):
@@ -22,7 +26,12 @@
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()
+MESSAGE_ENCRYPTION_KEY = env.str("MESSAGE_ENCRYPTION_KEY", default="").strip()
+if not MESSAGE_ENCRYPTION_KEY:
+ if EARLY_DEBUG:
+ MESSAGE_ENCRYPTION_KEY = Fernet.generate_key().decode()
+ else:
+ raise ImproperlyConfigured(MESSAGE_ENCRYPTION_KEY_REQUIRED_MSG)
SECURE_MESSAGE_KEY = MESSAGE_ENCRYPTION_KEY
# Re-initialize / initialize Sentry AFTER environment variables are loaded so DSN is present here.
@@ -539,7 +548,7 @@
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")
+ raise ImproperlyConfigured(GOOGLE_OAUTH_CREDENTIALS_REQUIRED_MSG)
SOCIALACCOUNT_PROVIDERS = {
"google": {
diff --git a/web/templates/account/login.html b/web/templates/account/login.html
index 6dbaf1856..3d74df691 100644
--- a/web/templates/account/login.html
+++ b/web/templates/account/login.html
@@ -93,7 +93,7 @@
- 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