diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index d74a112..280db1b 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 3137147..03554d8 100644 --- a/config/settings.py +++ b/config/settings.py @@ -43,6 +43,9 @@ CSRF_COOKIE_SECURE = True SESSION_COOKIE_SAMESITE = "None" CSRF_COOKIE_SAMESITE = "None" +# IMPORTANT for reverse proxy (Apache) to correctly identify HTTPS +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') + # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ @@ -65,6 +68,7 @@ MIDDLEWARE = [ 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', + 'core.middleware.ReferralMiddleware', # Disable X-Frame-Options middleware to allow Flatlogic preview iframes. # 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] @@ -156,13 +160,13 @@ STATICFILES_DIRS = [ ] # Email -EMAIL_HOST = os.getenv("EMAIL_HOST", "127.0.0.1") -EMAIL_PORT = int(os.getenv("EMAIL_PORT", "587")) -EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER", "") -EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD", "") -EMAIL_USE_TLS = os.getenv("EMAIL_USE_TLS", "true").lower() == "true" -EMAIL_USE_SSL = os.getenv("EMAIL_USE_SSL", "false").lower() == "true" -DEFAULT_FROM_EMAIL = os.getenv("DEFAULT_FROM_EMAIL", "no-reply@example.com") +EMAIL_HOST = os.getenv("SMTP_HOST", "127.0.0.1") +EMAIL_PORT = int(os.getenv("SMTP_PORT", "587")) +EMAIL_HOST_USER = os.getenv("SMTP_USER", "") +EMAIL_HOST_PASSWORD = os.getenv("SMTP_PASS", "") +EMAIL_USE_TLS = os.getenv("SMTP_SECURE", "tls").lower() == "tls" +EMAIL_USE_SSL = os.getenv("SMTP_SECURE", "tls").lower() == "ssl" +DEFAULT_FROM_EMAIL = os.getenv("MAIL_FROM", "no-reply@example.com") CONTACT_EMAIL_TO = [ item.strip() for item in os.getenv("CONTACT_EMAIL_TO", DEFAULT_FROM_EMAIL).split(",") @@ -186,4 +190,4 @@ if EMAIL_USE_SSL: DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' LOGIN_REDIRECT_URL = 'dashboard' -LOGOUT_REDIRECT_URL = 'home' +LOGOUT_REDIRECT_URL = 'home' \ No newline at end of file diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index 92a2633..f707a2c 100644 Binary files a/core/__pycache__/forms.cpython-311.pyc and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/middleware.cpython-311.pyc b/core/__pycache__/middleware.cpython-311.pyc new file mode 100644 index 0000000..63574de Binary files /dev/null and b/core/__pycache__/middleware.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 63509f2..d91fb3a 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 46a6e0e..fa589af 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index e8747cf..4042592 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/forms.py b/core/forms.py index be81d44..6d4e283 100644 --- a/core/forms.py +++ b/core/forms.py @@ -20,3 +20,6 @@ class SignupForm(UserCreationForm): if commit: user.save() return user + +class ResendActivationForm(forms.Form): + email = forms.EmailField(required=True, widget=forms.EmailInput(attrs={'class': 'form-control', 'placeholder': 'Enter your email'})) \ No newline at end of file diff --git a/core/management/__init__.py b/core/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/management/__pycache__/__init__.cpython-311.pyc b/core/management/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..826e230 Binary files /dev/null and b/core/management/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/management/commands/__init__.py b/core/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/core/management/commands/__pycache__/__init__.cpython-311.pyc b/core/management/commands/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..66a7108 Binary files /dev/null and b/core/management/commands/__pycache__/__init__.cpython-311.pyc differ diff --git a/core/management/commands/__pycache__/resend_activation.cpython-311.pyc b/core/management/commands/__pycache__/resend_activation.cpython-311.pyc new file mode 100644 index 0000000..c7fa2ad Binary files /dev/null and b/core/management/commands/__pycache__/resend_activation.cpython-311.pyc differ diff --git a/core/management/commands/resend_activation.py b/core/management/commands/resend_activation.py new file mode 100644 index 0000000..88ac0ed --- /dev/null +++ b/core/management/commands/resend_activation.py @@ -0,0 +1,48 @@ +import os +from django.core.management.base import BaseCommand +from django.contrib.auth import get_user_model +from django.utils.encoding import force_bytes +from django.utils.http import urlsafe_base64_encode +from django.template.loader import render_to_string +from django.core.mail import EmailMessage +from django.conf import settings +from core.tokens import account_activation_token + +User = get_user_model() + +class Command(BaseCommand): + help = 'Resend activation email to a specific user' + + def add_arguments(self, parser): + parser.add_argument('username', type=str) + parser.add_argument('--domain', type=str, default=os.getenv("HOST_FQDN", "localhost:8000")) + + def handle(self, *args, **args_options): + username = args_options['username'] + domain = args_options['domain'] + + try: + user = User.objects.get(username=username) + if user.is_active: + self.stdout.write(self.style.WARNING(f"User '{username}' is already active.")) + return + + mail_subject = 'Activate your Referral Rewards account.' + message = render_to_string('core/emails/activation_email.html', { + 'user': user, + 'domain': domain, + 'protocol': 'https', # Defaulting to https for Flatlogic + 'uid': urlsafe_base64_encode(force_bytes(user.pk)), + 'token': account_activation_token.make_token(user), + }) + + email = EmailMessage( + mail_subject, message, to=[user.email] + ) + email.send() + self.stdout.write(self.style.SUCCESS(f"Successfully sent activation email to '{username}' ({user.email}).")) + + except User.DoesNotExist: + self.stdout.write(self.style.ERROR(f"User '{username}' does not exist.")) + except Exception as e: + self.stdout.write(self.style.ERROR(f"Error sending email: {str(e)}")) diff --git a/core/middleware.py b/core/middleware.py new file mode 100644 index 0000000..3681baa --- /dev/null +++ b/core/middleware.py @@ -0,0 +1,11 @@ +class ReferralMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + ref_code = request.GET.get('ref') + if ref_code: + request.session['ref'] = ref_code + + response = self.get_response(request) + return response diff --git a/core/migrations/0002_profile_referred_by_referral.py b/core/migrations/0002_profile_referred_by_referral.py new file mode 100644 index 0000000..c0e1f7e --- /dev/null +++ b/core/migrations/0002_profile_referred_by_referral.py @@ -0,0 +1,33 @@ +# Generated by Django 5.2.7 on 2026-02-28 22:03 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='profile', + name='referred_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='referrals_made', to=settings.AUTH_USER_MODEL), + ), + migrations.CreateModel( + name='Referral', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('referral_code', models.CharField(max_length=20)), + ('status', models.CharField(choices=[('pending', 'Pending'), ('successful', 'Successful'), ('rewarded', 'Rewarded')], default='pending', max_length=20)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('completed_at', models.DateTimeField(blank=True, null=True)), + ('referred_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='received_referrals', to=settings.AUTH_USER_MODEL)), + ('referrer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sent_referrals', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/core/migrations/__pycache__/0002_profile_referred_by_referral.cpython-311.pyc b/core/migrations/__pycache__/0002_profile_referred_by_referral.cpython-311.pyc new file mode 100644 index 0000000..7700c5f Binary files /dev/null and b/core/migrations/__pycache__/0002_profile_referred_by_referral.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 3b991e3..b53237a 100644 --- a/core/models.py +++ b/core/models.py @@ -7,6 +7,7 @@ import uuid class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') referral_code = models.CharField(max_length=20, unique=True, blank=True) + referred_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name='referrals_made') points = models.IntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True) @@ -18,6 +19,21 @@ class Profile(models.Model): self.referral_code = str(uuid.uuid4()).replace('-', '')[:8].upper() super().save(*args, **kwargs) +class Referral(models.Model): + referrer = models.ForeignKey(User, on_delete=models.CASCADE, related_name='sent_referrals') + referred_user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='received_referrals', null=True, blank=True) + referral_code = models.CharField(max_length=20) + status = models.CharField(max_length=20, default='pending', choices=[ + ('pending', 'Pending'), + ('successful', 'Successful'), + ('rewarded', 'Rewarded'), + ]) + created_at = models.DateTimeField(auto_now_add=True) + completed_at = models.DateTimeField(null=True, blank=True) + + def __str__(self): + return f"Referral from {self.referrer.username} (Code: {self.referral_code})" + @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): if created: diff --git a/core/templates/core/dashboard.html b/core/templates/core/dashboard.html index 874c0c3..a9e8f0c 100644 --- a/core/templates/core/dashboard.html +++ b/core/templates/core/dashboard.html @@ -25,9 +25,9 @@

Total Points

{{ profile.points }}

-

Points to next reward: 100

+

Total successful referrals: {{ referrals.count }}

-
+
@@ -36,16 +36,53 @@

Your Referral Link

-

Share this code with your friends and earn points when they sign up!

+

Share this link with your friends and earn points when they sign up and activate their account!

- - + +
- - + Share on Twitter + Share on WhatsApp +
+
+
+ + +
+

Successful Referrals

+
+
+ + + + + + + + + + + {% for referral in referrals %} + + + + + + + {% empty %} + + + + {% endfor %} + +
UsernameDate JoinedStatusReward
{{ referral.referred_user.username }}{{ referral.completed_at|date:"M d, Y" }} + {{ referral.get_status_display }} + +10 Points
+ No referrals yet. Share your link to start earning rewards! +
@@ -96,8 +133,8 @@ -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/login.html b/core/templates/core/login.html index 34416b3..aebd6bf 100644 --- a/core/templates/core/login.html +++ b/core/templates/core/login.html @@ -32,7 +32,8 @@ -
+
+ Resend activation? Forgot password?
diff --git a/core/templates/core/resend_activation.html b/core/templates/core/resend_activation.html new file mode 100644 index 0000000..d3f38f4 --- /dev/null +++ b/core/templates/core/resend_activation.html @@ -0,0 +1,45 @@ +{% extends 'base.html' %} + +{% block title %}Resend Activation - Referral Rewards{% endblock %} + +{% block content %} +
+
+
+
+
+

Resend Activation

+

Enter your email to receive a new activation link.

+
+ +
+ {% csrf_token %} + {% if form.non_field_errors %} +
+ {% for error in form.non_field_errors %} + {{ error }} + {% endfor %} +
+ {% endif %} + +
+ + {{ form.email }} + {% if form.email.errors %} +
+ {{ form.email.errors }} +
+ {% endif %} +
+ + + + +
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/urls.py b/core/urls.py index 3578e5a..6943a65 100644 --- a/core/urls.py +++ b/core/urls.py @@ -6,6 +6,7 @@ urlpatterns = [ path("", views.home, name="home"), path("signup/", views.signup, name="signup"), path("activate///", views.activate, name="activate"), + path("resend-activation/", views.resend_activation, name="resend_activation"), path("dashboard/", views.dashboard, name="dashboard"), path("login/", auth_views.LoginView.as_view(template_name='core/login.html'), name="login"), path("logout/", views.logout_view, name="logout"), diff --git a/core/views.py b/core/views.py index 1066a46..9a745bb 100644 --- a/core/views.py +++ b/core/views.py @@ -7,13 +7,30 @@ from django.utils.encoding import force_bytes, force_str from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode from django.template.loader import render_to_string from django.core.mail import EmailMessage +from django.utils import timezone -from .models import Profile -from .forms import SignupForm +from .models import Profile, Referral +from .forms import SignupForm, ResendActivationForm from .tokens import account_activation_token User = get_user_model() +def send_activation_email(request, user): + current_site = get_current_site(request) + mail_subject = 'Activate your Referral Rewards account.' + message = render_to_string('core/emails/activation_email.html', { + 'user': user, + 'domain': current_site.domain, + 'protocol': 'https' if request.is_secure() else 'http', + 'uid': urlsafe_base64_encode(force_bytes(user.pk)), + 'token': account_activation_token.make_token(user), + }) + to_email = user.email + email = EmailMessage( + mail_subject, message, to=[to_email] + ) + return email.send() + def home(request): if request.user.is_authenticated: return redirect('dashboard') @@ -29,22 +46,30 @@ def signup(request): user.is_active = False user.save() + # Referral logic + ref_code = request.session.get('ref') + if ref_code: + try: + referrer_profile = Profile.objects.get(referral_code=ref_code) + referrer = referrer_profile.user + + # Update user's profile with referrer + user.profile.referred_by = referrer + user.profile.save() + + # Create Referral record + Referral.objects.create( + referrer=referrer, + referred_user=user, + referral_code=ref_code, + status='pending' + ) + except Profile.DoesNotExist: + pass # Invalid referral code, just ignore + # Send activation email - current_site = get_current_site(request) - mail_subject = 'Activate your Referral Rewards account.' - message = render_to_string('core/emails/activation_email.html', { - 'user': user, - 'domain': current_site.domain, - 'protocol': 'https' if request.is_secure() else 'http', - 'uid': urlsafe_base64_encode(force_bytes(user.pk)), - 'token': account_activation_token.make_token(user), - }) - to_email = form.cleaned_data.get('email') - email = EmailMessage( - mail_subject, message, to=[to_email] - ) try: - email.send() + send_activation_email(request, user) messages.success(request, 'Please confirm your email address to complete the registration. Check your inbox.') except Exception as e: messages.error(request, f'Error sending email: {str(e)}. Please contact support.') @@ -54,6 +79,34 @@ def signup(request): form = SignupForm() return render(request, 'core/signup.html', {'form': form}) +def resend_activation(request): + if request.user.is_authenticated: + return redirect('dashboard') + + if request.method == 'POST': + form = ResendActivationForm(request.POST) + if form.is_valid(): + email_addr = form.cleaned_data.get('email') + try: + user = User.objects.get(email=email_addr) + if not user.is_active: + send_activation_email(request, user) + messages.success(request, 'A new activation email has been sent. Please check your inbox.') + return redirect('login') + else: + messages.info(request, 'This account is already active. Please log in.') + return redirect('login') + except User.DoesNotExist: + # Security: Don't reveal if email exists, just say it's sent if it's inactive + messages.success(request, 'If an inactive account with that email exists, an activation email has been sent.') + return redirect('login') + except Exception as e: + messages.error(request, f'Error sending email: {str(e)}. Please contact support.') + else: + form = ResendActivationForm() + + return render(request, 'core/resend_activation.html', {'form': form}) + def activate(request, uidb64, token): try: uid = force_str(urlsafe_base64_decode(uidb64)) @@ -63,6 +116,21 @@ def activate(request, uidb64, token): if user is not None and account_activation_token.check_token(user, token): user.is_active = True user.save() + + # Complete referral if exists + referral = Referral.objects.filter(referred_user=user, status='pending').first() + if referral: + referral.status = 'successful' + referral.completed_at = timezone.now() + referral.save() + + # Reward referrer (e.g. add 10 points) + referrer_profile = referral.referrer.profile + referrer_profile.points += 10 + referrer_profile.save() + + messages.info(request, f'You were successfully referred by {referral.referrer.username}!') + login(request, user) messages.success(request, 'Thank you for your email confirmation. Now you can enjoy our services.') return redirect('dashboard') @@ -72,10 +140,23 @@ def activate(request, uidb64, token): @login_required def dashboard(request): - # Ensure profile exists (though signal should handle it) profile, created = Profile.objects.get_or_create(user=request.user) - return render(request, 'core/dashboard.html', {'profile': profile}) + + # Get successful referrals + referrals = Referral.objects.filter(referrer=request.user, status__in=['successful', 'rewarded']).order_by('-completed_at') + + current_site = get_current_site(request) + domain = current_site.domain + protocol = 'https' if request.is_secure() else 'http' + referral_link = f"{protocol}://{domain}/?ref={profile.referral_code}" + + context = { + 'profile': profile, + 'referrals': referrals, + 'referral_link': referral_link, + } + return render(request, 'core/dashboard.html', context) def logout_view(request): logout(request) - return redirect('home') + return redirect('home') \ No newline at end of file