diff --git a/my_website/accounts/__init__.py b/my_website/accounts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/my_website/accounts/admin.py b/my_website/accounts/admin.py new file mode 100644 index 0000000..21ada80 --- /dev/null +++ b/my_website/accounts/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin +from .managers import UserManager +from .models import User + +# Register your models here. +admin.site.register(User) diff --git a/my_website/accounts/apps.py b/my_website/accounts/apps.py new file mode 100644 index 0000000..3e3c765 --- /dev/null +++ b/my_website/accounts/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AccountsConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'accounts' diff --git a/my_website/accounts/managers.py b/my_website/accounts/managers.py new file mode 100644 index 0000000..10607c1 --- /dev/null +++ b/my_website/accounts/managers.py @@ -0,0 +1,16 @@ +from django.contrib.auth.models import BaseUserManager + +class UserManager(BaseUserManager): + def create_user(self, email, name, password=None, **extra_fields): + if not email: + raise ValueError("Users must have a valid email adress") + email = self.normalize_email(email) + user = self.model(email=email, name=name, **extra_fields) + user.set_unusable_password() + user.save() + return user + + def create_superuser(self, email, name, password=None, **extra_fields): + extra_fields.setdefault("is_staff", True) + extra_fields.setdefault("is_superuser", True) + return self.create_user(email, name, **extra_fields) diff --git a/my_website/accounts/migrations/0001_initial.py b/my_website/accounts/migrations/0001_initial.py new file mode 100644 index 0000000..2a8c7dd --- /dev/null +++ b/my_website/accounts/migrations/0001_initial.py @@ -0,0 +1,36 @@ +# Generated by Django 4.2.11 on 2025-11-19 21:32 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('email', models.EmailField(max_length=254, unique=True)), + ('name', models.CharField(max_length=255)), + ('is_verified', models.BooleanField(default=False)), + ('mailing_list', models.BooleanField(default=False)), + ('is_staff', models.BooleanField(default=False)), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/my_website/accounts/migrations/__init__.py b/my_website/accounts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/my_website/accounts/models.py b/my_website/accounts/models.py new file mode 100644 index 0000000..3aefba5 --- /dev/null +++ b/my_website/accounts/models.py @@ -0,0 +1,21 @@ +from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin +from django.db import models +from django.utils import timezone +from .managers import UserManager + +# Create your models here. +class User(AbstractBaseUser, PermissionsMixin): + email = models.EmailField(unique=True) + name = models.CharField(max_length=255) + is_verified = models.BooleanField(default=False) + mailing_list = models.BooleanField(default=False) + is_staff = models.BooleanField(default=False) + date_joined = models.DateTimeField(default=timezone.now) + + USERNAME_FIELD = "email" + REQUIRED_FIELDS = ["name"] + + objects = UserManager() + + def __str__(self): + return self.email diff --git a/my_website/accounts/templates/accounts/login.html b/my_website/accounts/templates/accounts/login.html new file mode 100644 index 0000000..bd628da --- /dev/null +++ b/my_website/accounts/templates/accounts/login.html @@ -0,0 +1,12 @@ +{% extends "core/layout.html" %} + {% block title %} + Login + {% endblock %} + {% block body %} +

Login

+
+ {% csrf_token %} + + +
+ {% endblock %} diff --git a/my_website/accounts/templates/accounts/register.html b/my_website/accounts/templates/accounts/register.html new file mode 100644 index 0000000..ca67c9d --- /dev/null +++ b/my_website/accounts/templates/accounts/register.html @@ -0,0 +1,13 @@ +{% extends "core/layout.html" %} + {% block title %} + Register + {% endblock %} + {% block body %} +

Register

+
+ {% csrf_token %} + + + +
+ {% endblock %} diff --git a/my_website/accounts/tests.py b/my_website/accounts/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/my_website/accounts/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/my_website/accounts/tokens.py b/my_website/accounts/tokens.py new file mode 100644 index 0000000..2a40875 --- /dev/null +++ b/my_website/accounts/tokens.py @@ -0,0 +1,29 @@ +from django.core import signing +import time + +def generate_login_token(data: dict, expires=1800): + data['exp'] = time.time() + expires + return signing.dumps(data, salt="magic-link") + +def generate_verification_token(data: dict, expires=60 * 60 * 24): + data['exp'] = time.time() + expires + return signing.dumps(data, salt="magic-link") + + +def verify_login_token(token, max_age=1800): + try: + data = signing.loads(token, salt="magic-link", max_age=max_age) + if data.get('exp', 0) < time.time(): + return None + return data + except Exception: + return None + +def verify_verification_token(token, max_age=60*60*24): + try: + data = signing.loads(token, salt="magic-link", max_age=max_age) + if data.get('exp', 0) < time.time(): + return None + return data + except Exception: + return None diff --git a/my_website/accounts/urls.py b/my_website/accounts/urls.py new file mode 100644 index 0000000..4127977 --- /dev/null +++ b/my_website/accounts/urls.py @@ -0,0 +1,13 @@ +from django.urls import path +from . import views + +app_name = 'accounts' + +urlpatterns = [ + path('register/', views.register, name='register'), + path('login/', views.login_request, name='login'), + path('verify/', views.verify_email, name='verify'), + path('login/confirm/', views.login_confirm, name='login_confirm'), + path('logout/', views.logout_view, name='logout'), + path('delete//', views.delete_user, name='delete_user') +] diff --git a/my_website/accounts/views.py b/my_website/accounts/views.py new file mode 100644 index 0000000..d05dc00 --- /dev/null +++ b/my_website/accounts/views.py @@ -0,0 +1,130 @@ +from django.shortcuts import render, redirect, HttpResponse, HttpResponseRedirect +from django.contrib.auth import login, logout, get_user_model +from django.urls import reverse +from django.core.mail import send_mail +from django.contrib import messages +from .tokens import generate_login_token, generate_verification_token, verify_login_token, verify_verification_token +from django.contrib.admin.views.decorators import staff_member_required +from dotenv import load_dotenv +import os + +load_dotenv() + +# Host Email +sender = os.getenv("EMAIL_HOST_USER") + +User = get_user_model() + +# Create your views here. +def register(request): + if request.method == "GET": + return render(request, "accounts/register.html") + # POST + email = request.POST.get("email") + name = request.POST.get("name") + # Gets whether the User want to be part of the mailing list + mailing_list = request.POST.get("mailing_list") + if mailing_list == None: + mailing_list = False + + if User.objects.filter(email=email).exists(): + if User.objects.get(email=email).is_verified != True: + token = generate_verification_token({"email": email}) + url = request.build_absolute_uri(reverse("accounts:verify") + f"?token={token}") + send_mail( + "Verify your account", + f"Click to verify your email:\n\n{url}", + sender, + [email], + ) + return HttpResponse("Email already exisits, Check your email to verify") + + messages.error(request, "Email already registered") + return redirect("accounts:register") + + user = User.objects.create_user( + email=email, + name=name, + mailing_list=mailing_list + ) + + # Send the Verification Email Might extract into a seperate Helper method + token = generate_verification_token({"email": email}) + url = request.build_absolute_uri(reverse("accounts:verify") + f"?token={token}") + send_mail( + "Verify your account", + f"Click to verify your email:\n\n{url}", + sender, + [email], + fail_silently=False + ) + return HttpResponse("Check your email") + +def login_request(request): + if request.method == "GET": + return render(request, "accounts/login.html") + # POST + email = request.POST.get("email") + try: + user = User.objects.get(email=email) + except User.DoesNotExist: + messages.error(request, "Account Doesn't exist") + return redirect("accounts:login") + + if not user.is_verified: + messages.error("Please verify your email address") + return redirect("accounts:login") + token = generate_login_token({"email": email}) + url = request.build_absolute_uri(reverse("accounts:login_confirm") + f"?token={token}") + send_mail( + "Your login link", + f"Click here to log in:\n\n{url}", + sender, + [email], + fail_silently=False + ) + return HttpResponse("login success, Check your Email") + +def verify_email(request): + token = request.GET.get("token") + data = verify_verification_token(token) + + if not data: + return HttpResponse("invalid token") + + user = User.objects.get(email=data["email"]) + user.is_verified = True + user.save() + + return redirect("core:landing") + +def login_confirm(request): + token = request.GET.get("token") + data = verify_login_token(token) + + if not data: + return render(request, "accounts/invalid_token.html") + user = User.objects.get(email=data["email"]) + login(request, user) + return redirect("core:landing") + +def logout_view(request): + logout(request) + return redirect("core:landing") + +# Endpoint to delete users +@staff_member_required +def delete_user(request, email): + try: + user = User.objects.get(email=email) + user.delete() + messages.success(request, "User deleted") + + except User.DoesNotExist: + messages.error(request, "User dosn't exist") + return render(request, 'core:landing') + + except Exception as e: + return render(request, 'core:landing',{'error':e.message}) + + return render(request, 'core:landing') diff --git a/my_website/core/__init__.py b/my_website/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/my_website/core/admin.py b/my_website/core/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/my_website/core/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/my_website/core/apps.py b/my_website/core/apps.py new file mode 100644 index 0000000..c0ce093 --- /dev/null +++ b/my_website/core/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class CoreConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "core" diff --git a/my_website/core/migrations/__init__.py b/my_website/core/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/my_website/core/models.py b/my_website/core/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/my_website/core/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/my_website/core/templates/core/about.html b/my_website/core/templates/core/about.html new file mode 100644 index 0000000..5689844 --- /dev/null +++ b/my_website/core/templates/core/about.html @@ -0,0 +1,7 @@ +{% extends 'core/layout.html' %} + {% block title %} + About + {% endblock %} + {% block body %} +

About

+ {% endblock %} diff --git a/my_website/core/templates/core/landing.html b/my_website/core/templates/core/landing.html new file mode 100644 index 0000000..0802bf2 --- /dev/null +++ b/my_website/core/templates/core/landing.html @@ -0,0 +1,76 @@ +{% extends "core/layout.html" %} + {% block title %} + Welcome! + {% endblock %} + {% block body %} +

Welcome!

+
+

About

+

I'm Mohamed, a computer science student currently studing in the University + of El Neelain in Sudan, I have a passion for Building things and tackling problems. + some of my projects include this website, A RAG chatbot and much more. + You can check my projects + here. +

+
+ +
+

Register

+

+ Register to Enter my mailing list and get updates about my Projects and offers, it's + quick and easy I promise. +

+

Note: By default new accounts aren't added to the mailing list unless they consent to it.

+
+ {% csrf_token %} + + + +
+
+
+ +

Login

+

+ Already have an account? login using your Email adress. +

+
+ {% csrf_token %} + + +
+
+ +
+ +
+ +
+

Projects

+

+ Some of the projects i've worked on, if you like what you see be sure to leave a nice comment + and a rating, you can find more in the Projects Section. +

+ +
+ +
+

Footer

+
+ {% endblock %} diff --git a/my_website/core/templates/core/layout.html b/my_website/core/templates/core/layout.html new file mode 100644 index 0000000..844e3db --- /dev/null +++ b/my_website/core/templates/core/layout.html @@ -0,0 +1,10 @@ + + + + {% block title %} {% endblock %} + + + {% block body %} + {% endblock %} + + diff --git a/my_website/core/tests.py b/my_website/core/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/my_website/core/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/my_website/core/urls.py b/my_website/core/urls.py new file mode 100644 index 0000000..a42388c --- /dev/null +++ b/my_website/core/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from . import views + +app_name = 'core' + +urlpatterns = [ + path('', views.landing_view, name='landing'), + path('about/', views.about_view, name='about'), + path('projects/', views.projects_view, name='projects'), +] diff --git a/my_website/core/views.py b/my_website/core/views.py new file mode 100644 index 0000000..416323a --- /dev/null +++ b/my_website/core/views.py @@ -0,0 +1,16 @@ +from django.contrib.auth import authenticate, login, logout +from django.contrib.auth.forms import UserCreationForm, AuthenticationForm +from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import render +from django.urls import reverse +from main.forms import RegisterForm + +# Create your views here. +def landing_view(request): + return render(request, "core/landing.html") + +def about_view(request): + return render(request, "core/about.html") + +def projects_view(request): + return render(request, "core/projects.html") diff --git a/my_website/main/views.py b/my_website/main/views.py index eeba087..da1caf8 100644 --- a/my_website/main/views.py +++ b/my_website/main/views.py @@ -12,7 +12,7 @@ def index(request): return render(request, 'main/users.html') def login_view(request): - if request.method == "POST": + if request.method == 'POST': form = AuthenticationForm(request, data=request.POST) if form.is_valid(): user = form.get_user() @@ -20,11 +20,11 @@ def login_view(request): return HttpResponseRedirect(reverse('index')) else: new_form = AuthenticationForm(request) - return render(request, "main/login.html", {"form": new_form, "message": "Invalid Credentials"}) + return render(request, 'main/login.html', {'form': new_form, 'message': 'Invalid Credentials'}) else: form = AuthenticationForm() - return render(request, "main/login.html", {"form": form}) + return render(request, 'main/login.html', {'form': form}) def logout_view(request): logout(request) diff --git a/my_website/my_website/settings.py b/my_website/my_website/settings.py index bb6d032..f2fb70d 100644 --- a/my_website/my_website/settings.py +++ b/my_website/my_website/settings.py @@ -10,8 +10,12 @@ https://docs.djangoproject.com/en/5.2/ref/settings/ """ +from dotenv import load_dotenv +import os from pathlib import Path +load_dotenv() + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -27,10 +31,14 @@ ALLOWED_HOSTS = [] +# Custom User Model +AUTH_USER_MODEL = 'accounts.User' # Application definition INSTALLED_APPS = [ + 'accounts', + 'core', 'main', 'django.contrib.admin', 'django.contrib.auth', @@ -50,6 +58,17 @@ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +EMAIL_HOST = "smtp.gmail.com" +EMAIL_PORT = 587 +EMAIL_USE_TLS = True +EMAIL_HOST_USER = os.getenv("EMAIL_HOST_USER") +EMAIL_HOST_PASSWORD = os.getenv("EMAIL_HOST_PASSWORD") + +AUTHENTICATION_BACKENDS = [ + 'django.contrib.auth.backends.ModelBackend', +] + ROOT_URLCONF = 'my_website.urls' TEMPLATES = [ @@ -116,6 +135,7 @@ # https://docs.djangoproject.com/en/5.2/howto/static-files/ STATIC_URL = 'static/' +STATICFILES_DIRS = [] # Default primary key field type # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field diff --git a/my_website/my_website/urls.py b/my_website/my_website/urls.py index 3c1b94a..895d961 100644 --- a/my_website/my_website/urls.py +++ b/my_website/my_website/urls.py @@ -19,5 +19,7 @@ urlpatterns = [ path('admin/', admin.site.urls), - path('main/', include("main.urls")) + path('main/', include("main.urls")), + path('core/', include("core.urls")), + path('accounts/', include("accounts.urls")) ]