Skip to content
Open
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
23 changes: 22 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
84 changes: 82 additions & 2 deletions web/forms.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import logging
import re
from typing import ClassVar
from urllib.parse import parse_qs, urlparse

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
Expand Down Expand Up @@ -72,6 +74,7 @@

__all__ = [
"UserRegistrationForm",
"SocialUserRegistrationForm",
"ProfileForm",
"ChallengeSubmissionForm",
"CourseCreationForm",
Expand Down Expand Up @@ -115,6 +118,7 @@
]

fernet = Fernet(settings.SECURE_MESSAGE_KEY)
INVALID_REFERRAL_CODE_MSG = "Invalid referral code. Please check and try again."


class TailwindWidgetMixin:
Expand Down Expand Up @@ -263,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):
Expand Down Expand Up @@ -295,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
Expand All @@ -307,6 +314,79 @@ def save(self, request):
return user


class SocialUserRegistrationForm(SocialSignupForm):
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"""Custom social signup form that collects onboarding fields used in standard registration."""

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.",
)
Comment thread
Ananya44444 marked this conversation as resolved.

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_MSG)
return referral_code
Comment thread
coderabbitai[bot] marked this conversation as resolved.

def save(self, request) -> User:
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:
try:
handle_referral(user, referral_code)
except Exception:
logging.getLogger(__name__).exception("Failed to process referral for user %s", user.pk)

return user


class TailwindInput(forms.widgets.Input, TailwindWidgetMixin):
def __init__(self, *args, **kwargs):
kwargs.setdefault("attrs", {}).update(
Expand Down
49 changes: 43 additions & 6 deletions web/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,36 @@
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

BASE_DIR = Path(__file__).resolve().parent.parent

env = environ.Env()

env_file = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".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"

# 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
env_file = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), ".env")
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if os.path.exists(env_file):
environ.Env.read_env(env_file)
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 or "collectstatic" in sys.argv:
MESSAGE_ENCRYPTION_KEY = Fernet.generate_key().decode()
else:
raise ImproperlyConfigured(MESSAGE_ENCRYPTION_KEY_REQUIRED_MSG)
SECURE_MESSAGE_KEY = MESSAGE_ENCRYPTION_KEY
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Re-initialize / initialize Sentry AFTER environment variables are loaded so DSN is present here.
SENTRY_DSN = env.str("SENTRY_DSN", default="")
if SENTRY_DSN:
Expand Down Expand Up @@ -142,6 +152,8 @@
"channels",
"allauth",
"allauth.account",
"allauth.socialaccount",
"allauth.socialaccount.providers.google",
"captcha",
"markdownx",
"web",
Expand Down Expand Up @@ -304,6 +316,10 @@
"signup": "web.forms.UserRegistrationForm",
"login": "web.forms.CustomLoginForm",
}
SOCIALACCOUNT_FORMS = {
"signup": "web.forms.SocialUserRegistrationForm",
}
SOCIALACCOUNT_AUTO_SIGNUP = False

LANGUAGE_CODE = "en"

Expand Down Expand Up @@ -533,3 +549,24 @@
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", "")

# 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_OAUTH_CREDENTIALS_REQUIRED_MSG)

SOCIALACCOUNT_PROVIDERS = {
"google": {
"EMAIL_AUTHENTICATION": True,
"APP": {
"client_id": google_client_id,
"secret": google_client_secret,
},
"SCOPE": ["profile", "email"],
"AUTH_PARAMS": {"access_type": "online"},
}
Comment thread
Ananya44444 marked this conversation as resolved.
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

SOCIALACCOUNT_EMAIL_VERIFICATION = "mandatory"
SOCIALACCOUNT_EMAIL_REQUIRED = True
Comment thread
coderabbitai[bot] marked this conversation as resolved.
28 changes: 28 additions & 0 deletions web/static/images/google-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions web/templates/account/login.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
{% extends "allauth/layouts/base.html" %}

{% load static %}
{% load i18n %}
{% load account %}
{% load socialaccount %}
Comment thread
Ananya44444 marked this conversation as resolved.

{% block extra_head %}
{{ block.super }}
Expand Down Expand Up @@ -86,6 +88,25 @@ <h2 class="mt-10 text-center text-2xl font-bold leading-9 tracking-tight text-gr
Sign in
</button>
</div>
<!-- Divider -->
<div class="relative my-4">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-300 dark:border-gray-600"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="bg-white dark:bg-gray-800 px-2 text-gray-500 dark:text-gray-300">{% trans "Or continue with" %}</span>
</div>
</div>
<!-- Google Login Button -->
<a href="{% provider_login_url 'google' next=redirect_field_value %}"
class="flex w-full items-center justify-center gap-3 rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-4 py-2 text-base font-semibold text-gray-700 dark:text-white shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600">
<img src="{% static 'images/google-icon.svg' %}"
width="20"
height="20"
alt="Google"
class="h-5 w-5" />
{% trans "Sign in with Google" %}
</a>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</form>
<p class="mt-10 text-center text-sm text-gray-500 dark:text-gray-400">
Don't have an account?
Expand Down
21 changes: 21 additions & 0 deletions web/templates/account/signup.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{% extends "allauth/layouts/base.html" %}

{% load static %}
{% load i18n %}
{% load socialaccount %}

{% block extra_head %}
{{ block.super }}
Expand Down Expand Up @@ -248,6 +250,25 @@ <h2 class="text-xl font-bold text-gray-900 dark:text-white flex items-center jus
Sign in <i class="fas fa-arrow-right text-xs ml-1"></i>
</a>
</p>
<!-- Divider -->
<div class="relative my-4">
<div class="absolute inset-0 flex items-center">
<div class="w-full border-t border-gray-300 dark:border-gray-600"></div>
</div>
<div class="relative flex justify-center text-sm">
<span class="bg-white dark:bg-gray-800 px-2 text-gray-500 dark:text-gray-300">{% trans "Or sign up with" %}</span>
</div>
</div>
<!-- Google Signup Button -->
<a href="{% provider_login_url 'google' next=redirect_field_value %}"
class="flex w-full items-center justify-center gap-3 rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 px-4 py-2 text-base font-semibold text-gray-700 dark:text-white shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600">
<img src="{% static 'images/google-icon.svg' %}"
width="20"
height="20"
alt="Google"
class="h-5 w-5" />
{% trans "Sign up with Google" %}
</a>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</form>
</div>
</div>
Expand Down
Loading