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
+
+ {% 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
+
+ {% 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.
+
+
+
+
+
Login
+
+ Already have an account? login using your Email adress.
+
+
+
+
+
+
+ - TODO: Nice comment #1
+ - TODO: Nice comment #2
+ - TODO: Nice comment #3
+
+
+
+
+
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.
+
+
+ - TODO: project #1
+ - TODO: project #2
+ - TODO: project #3
+
+
+
+
+
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"))
]